From ca08333d4df281b5e7a25a83ce7adc896f70e728 Mon Sep 17 00:00:00 2001 From: Vlad Vitan Date: Tue, 27 Aug 2024 16:32:08 +0200 Subject: [PATCH 1/6] api: Add gateway qr code service --- api/ttn/lorawan/v3/qrcodegenerator.proto | 36 ++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/api/ttn/lorawan/v3/qrcodegenerator.proto b/api/ttn/lorawan/v3/qrcodegenerator.proto index 27bff44b03..3233cbf3e5 100644 --- a/api/ttn/lorawan/v3/qrcodegenerator.proto +++ b/api/ttn/lorawan/v3/qrcodegenerator.proto @@ -130,3 +130,39 @@ service EndDeviceQRCodeGenerator { }; } } + +message ParseGatewayQRCodeRequest { + // QR code format identifier. + // If this field is not specified, the server will default to ttigw001. + string format_id = 1 [(validate.rules).string = { + pattern: "^[a-z0-9](?:[-]?[a-z0-9]){2,}$|^$", + max_len: 36 + }]; + // Raw QR code contents. + bytes qr_code = 2 [(validate.rules).bytes = { + min_len: 10, + max_len: 1024 + }]; +} + +message ParseGatewayQRCodeResponse { + // Identifier of the format used to successfully parse the QR code data. + string format_id = 1; + ClaimGatewayRequest claim_gateway_request = 2; +} + + +// The GatewayQRCodeGenerator service provides functionality to generate and parse QR codes for gateways. +service GatewayQRCodeGenerator { + // Parse QR Codes of known formats and return the information contained within. + rpc Parse(ParseGatewayQRCodeRequest) returns (ParseGatewayQRCodeResponse) { + option (google.api.http) = { + post: "/qr-codes/gateways/parse", + body: "*" + additional_bindings { + post: "/qr-codes/gateways/{format_id}/parse" + body: "*" + } + }; + } +} From b107d7d325145654f86f3b96d2532a60348d32ad Mon Sep 17 00:00:00 2001 From: Vlad Vitan Date: Wed, 28 Aug 2024 15:26:59 +0200 Subject: [PATCH 2/6] qrg: Implement gateway server for qrcodegenerator --- api/ttn/lorawan/v3/qrcodegenerator.proto | 3 +- pkg/qrcodegenerator/grpc_gateways.go | 66 +++ pkg/qrcodegenerator/grpc_gateways_test.go | 130 +++++ .../qrcode/gateways/gateways.go | 127 +++++ .../qrcode/gateways/gateways_test.go | 53 ++ .../qrcode/gateways/ttigpro1.go | 94 ++++ .../qrcode/gateways/ttigpro1_test.go | 100 ++++ pkg/qrcodegenerator/qrcodegenerator.go | 14 + pkg/ttnpb/qrcodegenerator.pb.go | 470 ++++++++++++------ pkg/ttnpb/qrcodegenerator.pb.gw.go | 242 +++++++++ pkg/ttnpb/qrcodegenerator.pb.paths.fm.go | 43 ++ pkg/ttnpb/qrcodegenerator.pb.setters.fm.go | 76 +++ pkg/ttnpb/qrcodegenerator.pb.validate.go | 205 ++++++++ pkg/ttnpb/qrcodegenerator_grpc.pb.go | 93 ++++ pkg/ttnpb/qrcodegenerator_json.pb.go | 54 ++ 15 files changed, 1623 insertions(+), 147 deletions(-) create mode 100644 pkg/qrcodegenerator/grpc_gateways.go create mode 100644 pkg/qrcodegenerator/grpc_gateways_test.go create mode 100644 pkg/qrcodegenerator/qrcode/gateways/gateways.go create mode 100644 pkg/qrcodegenerator/qrcode/gateways/gateways_test.go create mode 100644 pkg/qrcodegenerator/qrcode/gateways/ttigpro1.go create mode 100644 pkg/qrcodegenerator/qrcode/gateways/ttigpro1_test.go diff --git a/api/ttn/lorawan/v3/qrcodegenerator.proto b/api/ttn/lorawan/v3/qrcodegenerator.proto index 3233cbf3e5..be548b626c 100644 --- a/api/ttn/lorawan/v3/qrcodegenerator.proto +++ b/api/ttn/lorawan/v3/qrcodegenerator.proto @@ -23,6 +23,7 @@ import "protoc-gen-openapiv2/options/annotations.proto"; import "ttn/lorawan/v3/end_device.proto"; import "ttn/lorawan/v3/picture.proto"; import "validate/validate.proto"; +import "ttn/lorawan/v3/deviceclaimingserver.proto"; option go_package = "go.thethings.network/lorawan-stack/v3/pkg/ttnpb"; @@ -133,7 +134,7 @@ service EndDeviceQRCodeGenerator { message ParseGatewayQRCodeRequest { // QR code format identifier. - // If this field is not specified, the server will default to ttigw001. + // If this field is not specified, the server will default to ttigpro1. string format_id = 1 [(validate.rules).string = { pattern: "^[a-z0-9](?:[-]?[a-z0-9]){2,}$|^$", max_len: 36 diff --git a/pkg/qrcodegenerator/grpc_gateways.go b/pkg/qrcodegenerator/grpc_gateways.go new file mode 100644 index 0000000000..1135045aa4 --- /dev/null +++ b/pkg/qrcodegenerator/grpc_gateways.go @@ -0,0 +1,66 @@ +// Copyright © 2024 The Things Network Foundation, The Things Industries B.V. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package qrcodegenerator + +import ( + "context" + + "go.thethings.network/lorawan-stack/v3/pkg/rpcmetadata" + "go.thethings.network/lorawan-stack/v3/pkg/ttnpb" +) + +type gatewayQRCodeGeneratorServer struct { + ttnpb.UnimplementedGatewayQRCodeGeneratorServer + + QRG *QRCodeGenerator +} + +// GetFormat implements EndDeviceQRCodeGenerator. +func (s *gatewayQRCodeGeneratorServer) GetFormat(ctx context.Context, req *ttnpb.GetQRCodeFormatRequest) (*ttnpb.QRCodeFormat, error) { + _, err := rpcmetadata.WithForwardedAuth(ctx, s.QRG.AllowInsecureForCredentials()) + if err != nil { + return nil, err + } + format := s.QRG.gateways.GetGatewayFormat(req.FormatId) + if format == nil { + return nil, errFormatNotFound.New() + } + return format.Format(), nil +} + +// Parse implements EndDeviceQRCodeGenerator. +func (s *gatewayQRCodeGeneratorServer) Parse(ctx context.Context, req *ttnpb.ParseGatewayQRCodeRequest) (*ttnpb.ParseGatewayQRCodeResponse, error) { + _, err := rpcmetadata.WithForwardedAuth(ctx, s.QRG.AllowInsecureForCredentials()) + if err != nil { + return nil, err + } + + data, err := s.QRG.gateways.Parse(req.FormatId, req.QrCode) + if err != nil { + return nil, err + } + + return &ttnpb.ParseGatewayQRCodeResponse{ + FormatId: data.FormatID(), + ClaimGatewayRequest: &ttnpb.ClaimGatewayRequest{ + SourceGateway: &ttnpb.ClaimGatewayRequest_AuthenticatedIdentifiers_{ + AuthenticatedIdentifiers: &ttnpb.ClaimGatewayRequest_AuthenticatedIdentifiers{ + GatewayEui: []byte(data.GatewayEUI()), + AuthenticationCode: []byte(data.OwnerToken()), + }, + }, + }, + }, nil +} diff --git a/pkg/qrcodegenerator/grpc_gateways_test.go b/pkg/qrcodegenerator/grpc_gateways_test.go new file mode 100644 index 0000000000..48bdbabc5c --- /dev/null +++ b/pkg/qrcodegenerator/grpc_gateways_test.go @@ -0,0 +1,130 @@ +// Copyright © 2024 The Things Network Foundation, The Things Industries B.V. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package qrcodegenerator_test + +import ( + "testing" + + "github.com/smarty/assertions" + "go.thethings.network/lorawan-stack/v3/pkg/component" + componenttest "go.thethings.network/lorawan-stack/v3/pkg/component/test" + "go.thethings.network/lorawan-stack/v3/pkg/errors" + "go.thethings.network/lorawan-stack/v3/pkg/log" + . "go.thethings.network/lorawan-stack/v3/pkg/qrcodegenerator" + "go.thethings.network/lorawan-stack/v3/pkg/qrcodegenerator/qrcode/gateways" + "go.thethings.network/lorawan-stack/v3/pkg/ttnpb" + "go.thethings.network/lorawan-stack/v3/pkg/util/test" + "go.thethings.network/lorawan-stack/v3/pkg/util/test/assertions/should" +) + +func TestGatewayQRCodeParsing(t *testing.T) { + a := assertions.New(t) + ctx := log.NewContext(test.Context(), test.GetLogger(t)) + + c := componenttest.NewComponent(t, &component.Config{}) + ttigpro1 := new(gateways.TTIGPRO1Format) + qrg, err := New(c, &Config{}, WithGatewayFormat(ttigpro1.ID(), ttigpro1)) + test.Must(qrg, err) + + componenttest.StartComponent(t, c) + defer c.Close() + + mustHavePeer(ctx, c, ttnpb.ClusterRole_QR_CODE_GENERATOR) + + client := ttnpb.NewGatewayQRCodeGeneratorClient(c.LoopbackConn()) + + for _, tc := range []struct { + Name string + FormatID string + GetQRData func() []byte + Assertion func(*assertions.Assertion, *ttnpb.ParseGatewayQRCodeResponse, error) bool + }{ + { + Name: "EmptyData", + GetQRData: func() []byte { + return []byte{} + }, + Assertion: func(a *assertions.Assertion, resp *ttnpb.ParseGatewayQRCodeResponse, err error) bool { + if !a.So(resp, should.BeNil) { + return false + } + if !a.So(errors.IsInvalidArgument(err), should.BeTrue) { + return false + } + return true + }, + }, + { + Name: "UnknownFormat", + FormatID: "unknown", + GetQRData: func() []byte { + return []byte(`https://ttig.pro/c/ec656efffe000128/abcdef123456`) + }, + Assertion: func(a *assertions.Assertion, resp *ttnpb.ParseGatewayQRCodeResponse, err error) bool { + if !a.So(resp, should.BeNil) { + return false + } + if !a.So(errors.IsInvalidArgument(err), should.BeTrue) { + return false + } + return true + }, + }, + { + Name: "InvalidFormat", + FormatID: "tr005", + GetQRData: func() []byte { + return []byte(`https://ttig.pro/c/ec656efffe000128/abcdef123456`) + }, + Assertion: func(a *assertions.Assertion, resp *ttnpb.ParseGatewayQRCodeResponse, err error) bool { + if !a.So(resp, should.BeNil) { + return false + } + if !a.So(errors.IsInvalidArgument(err), should.BeTrue) { + return false + } + return true + }, + }, + { + Name: "ValidTTIGPRO1", + FormatID: ttigpro1.ID(), + GetQRData: func() []byte { + return []byte(`https://ttig.pro/c/ec656efffe000128/abcdef123456`) + }, + Assertion: func(a *assertions.Assertion, resp *ttnpb.ParseGatewayQRCodeResponse, err error) bool { + if !a.So(resp, should.NotBeNil) { + return false + } + if !a.So(err, should.BeNil) { + return false + } + a.So(resp.FormatId, should.Equal, ttigpro1.ID()) + + return true + }, + }, + } { + t.Run(tc.Name, func(t *testing.T) { + resp, err := client.Parse(ctx, &ttnpb.ParseGatewayQRCodeRequest{ + FormatId: tc.FormatID, + QrCode: tc.GetQRData(), + }, c.WithClusterAuth()) + if !a.So(tc.Assertion(a, resp, err), should.BeTrue) { + t.FailNow() + } + }) + } +} diff --git a/pkg/qrcodegenerator/qrcode/gateways/gateways.go b/pkg/qrcodegenerator/qrcode/gateways/gateways.go new file mode 100644 index 0000000000..19c774d05a --- /dev/null +++ b/pkg/qrcodegenerator/qrcode/gateways/gateways.go @@ -0,0 +1,127 @@ +// Copyright © 2024 The Things Network Foundation, The Things Industries B.V. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gateways + +import ( + "context" + "encoding" + + "go.thethings.network/lorawan-stack/v3/pkg/errors" + "go.thethings.network/lorawan-stack/v3/pkg/ttnpb" +) + +var ( + errCharacter = errors.DefineInvalidArgument("character", "invalid character `{c}`") + errUnknownFormat = errors.DefineInvalidArgument("unknown_format", "format unknown") + errInvalidLength = errors.DefineInvalidArgument("length", "invalid length") + errFormat = errors.DefineInvalidArgument("format", "invalid format") +) + +// Format is a gateway QR code format. +type Format interface { + Format() *ttnpb.QRCodeFormat + New() Data +} + +// Data represents gateway QR code data. +type Data interface { + // FormatID returns the ID of the format used to parse the QR Code data. + FormatID() string + GatewayEUI() string + OwnerToken() string + encoding.TextUnmarshaler +} + +type gatewayFormat struct { + id string + format Format +} + +// Server provides methods for gateways QR codes. +type Server struct { + gatewayFormats []gatewayFormat +} + +// New returns a new Server. +func New(ctx context.Context) *Server { + s := &Server{ + // Newer formats should be added to this slice first to + // preferentially match with those first. + gatewayFormats: []gatewayFormat{ + { + id: formatIDttigpro1, + format: new(TTIGPRO1Format), + }, + }, + } + return s +} + +// RegisterGatewayFormat registers the given gateway QR code format. +func (s *Server) RegisterGatewayFormat(id string, f Format) { + s.gatewayFormats = append(s.gatewayFormats, gatewayFormat{ + id: id, + format: f, + }) +} + +// GetGatewayFormats returns the registered gateway QR code formats. +func (s *Server) GetGatewayFormats() map[string]Format { + ret := make(map[string]Format) + for _, gtwFormat := range s.gatewayFormats { + ret[gtwFormat.id] = gtwFormat.format + } + return ret +} + +// GetGatewayFormat returns the format by ID. +func (s *Server) GetGatewayFormat(id string) Format { + for _, gtwFormat := range s.gatewayFormats { + if gtwFormat.id == id { + return gtwFormat.format + } + } + return nil +} + +// Formats returns the registered gateway QR code formats. +func (s *Server) Formats() []*ttnpb.QRCodeFormat { + formats := make([]*ttnpb.QRCodeFormat, 0, len(s.gatewayFormats)) + for _, gtwFormat := range s.gatewayFormats { + formats = append(formats, gtwFormat.format.Format()) + } + return formats +} + +// Parse the given QR code data. If formatID is provided, only that format is used. +// Otherwise, the first format registered will be used. +func (s *Server) Parse(formatID string, data []byte) (ret Data, err error) { + for _, gtwFormat := range s.gatewayFormats { + // If format ID is provided, use only that. Otherwise, + // default to the first format listed in gatewayFormats. + if formatID != "" && formatID != gtwFormat.id { + continue + } + + f := gtwFormat.format.New() + if err := f.UnmarshalText(data); err != nil { + return nil, err + } + + return f, nil + } + + return nil, errUnknownFormat +} diff --git a/pkg/qrcodegenerator/qrcode/gateways/gateways_test.go b/pkg/qrcodegenerator/qrcode/gateways/gateways_test.go new file mode 100644 index 0000000000..b362ff3e24 --- /dev/null +++ b/pkg/qrcodegenerator/qrcode/gateways/gateways_test.go @@ -0,0 +1,53 @@ +// Copyright © 2024 The Things Network Foundation, The Things Industries B.V. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gateways_test + +import ( + "context" + "strconv" + "testing" + + "github.com/smarty/assertions" + . "go.thethings.network/lorawan-stack/v3/pkg/qrcodegenerator/qrcode/gateways" + "go.thethings.network/lorawan-stack/v3/pkg/util/test" + "go.thethings.network/lorawan-stack/v3/pkg/util/test/assertions/should" +) + +func TestParseGatewaysAuthenticationCodes(t *testing.T) { + for i, tc := range []struct { + FormatID string + Data []byte + ExpectedEUI, + ExpectedOwnerToken string + }{ + { + FormatID: "ttigpro1", + Data: []byte("https://ttig.pro/c/ec656efffe000128/abcdef123456"), + ExpectedEUI: "ec656efffe000128", + ExpectedOwnerToken: "abcdef123456", + }, + } { + t.Run(strconv.Itoa(i), func(t *testing.T) { + a := assertions.New(t) + + qrCode := New(context.Background()) + + d, err := qrCode.Parse(tc.FormatID, tc.Data) + data := test.Must(d, err) + + a.So(data, should.NotBeNil) + }) + } +} diff --git a/pkg/qrcodegenerator/qrcode/gateways/ttigpro1.go b/pkg/qrcodegenerator/qrcode/gateways/ttigpro1.go new file mode 100644 index 0000000000..1b592f67e2 --- /dev/null +++ b/pkg/qrcodegenerator/qrcode/gateways/ttigpro1.go @@ -0,0 +1,94 @@ +// Copyright © 2024 The Things Network Foundation, The Things Industries B.V. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gateways + +import ( + "regexp" + + "go.thethings.network/lorawan-stack/v3/pkg/ttnpb" + "go.thethings.network/lorawan-stack/v3/pkg/types" +) + +const ( + formatIDttigpro1 = "ttigpro1" + + euiLength = 16 + ownerTokenLength = 12 +) + +// TTIGPRO1 is a format for gateway identification QR codes. +type ttigpro1 struct { + gatewayEUI types.EUI64 + ownerToken string +} + +// ttigpro1Regex is the regular expression to match the TTIGPRO1 format. +// The format is as follows: https://ttig.pro/c/{16 lowercase base16 chars}/{12 base62 chars} +var ttigpro1Regex = regexp.MustCompile(`^https://ttig\.pro/c/([a-f0-9]{16})/([a-z0-9]{12})$`) + +// UnmarshalText implements the TextUnmarshaler interface. +func (m *ttigpro1) UnmarshalText(text []byte) error { + // Match the URL against the pattern + matches := ttigpro1Regex.FindStringSubmatch(string(text)) + if matches == nil { + return errFormat + } + + if err := m.gatewayEUI.UnmarshalText([]byte(matches[1])); err != nil { + return err + } + + m.ownerToken = matches[2] + + return nil +} + +// FormatID implements the Data interface. +func (m *ttigpro1) FormatID() string { + return formatIDttigpro1 +} + +func (m *ttigpro1) GatewayEUI() string { + return m.gatewayEUI.String() +} + +func (m *ttigpro1) OwnerToken() string { + return string(m.ownerToken) +} + +// TTIGPRO1Format implements the TTIGPRO1 Format. +type TTIGPRO1Format struct{} + +// Format implements the Format interface. +func (TTIGPRO1Format) Format() *ttnpb.QRCodeFormat { + return &ttnpb.QRCodeFormat{ + Name: "TTIGPRO1", + Description: "TTI QR code format for gateway devices.", + FieldMask: ttnpb.FieldMask( + "ids.eui", + "claim_authentication_code.secret.value", + ), + } +} + +// ID is the identifier of the format as a string. +func (TTIGPRO1Format) ID() string { + return formatIDttigpro1 +} + +// New implements the Format interface. +func (TTIGPRO1Format) New() Data { + return new(ttigpro1) +} diff --git a/pkg/qrcodegenerator/qrcode/gateways/ttigpro1_test.go b/pkg/qrcodegenerator/qrcode/gateways/ttigpro1_test.go new file mode 100644 index 0000000000..77de18bfe9 --- /dev/null +++ b/pkg/qrcodegenerator/qrcode/gateways/ttigpro1_test.go @@ -0,0 +1,100 @@ +// Copyright © 2024 The Things Network Foundation, The Things Industries B.V. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gateways + +import ( + "testing" + + "github.com/smarty/assertions" + "go.thethings.network/lorawan-stack/v3/pkg/errors" + "go.thethings.network/lorawan-stack/v3/pkg/types" + "go.thethings.network/lorawan-stack/v3/pkg/util/test/assertions/should" +) + +func TestTTIGPRO1(t *testing.T) { + t.Run("Decode", func(t *testing.T) { + for _, tc := range []struct { + Name string + Data []byte + Expected ttigpro1 + ErrorAssertion func(t *testing.T, err error) bool + }{ + { + Name: "CorrectQRCode", + Data: []byte("https://ttig.pro/c/ec656efffe000128/abcdef123456"), + Expected: ttigpro1{ + gatewayEUI: types.EUI64{0xec, 0x65, 0x6e, 0xff, 0xfe, 0x00, 0x01, 0x28}, + ownerToken: "abcdef123456", + }, + }, + { + Name: "InvalidURLPrefix", + Data: []byte("https://example.com/c/ec656efffe000128/abcdef12"), + ErrorAssertion: func(t *testing.T, err error) bool { + return assertions.New(t).So(errors.IsInvalidArgument(err), should.BeTrue) + }, + }, + { + Name: "Invalid/EUINotLowercase", + Data: []byte("https://ttig.pro/c/EC656effFe000128/abcdef12"), + ErrorAssertion: func(t *testing.T, err error) bool { + return assertions.New(t).So(errors.IsInvalidArgument(err), should.BeTrue) + }, + }, + { + Name: "Invalid/EUILength", + Data: []byte("https://ttig.pro/c/ec656efffe00012/abcdef12"), + ErrorAssertion: func(t *testing.T, err error) bool { + return assertions.New(t).So(errors.IsInvalidArgument(err), should.BeTrue) + }, + }, + { + Name: "Invalid/EUINotBase16", + Data: []byte("https://ttig.pro/c/ec656efffe00012g/abcdef12"), + ErrorAssertion: func(t *testing.T, err error) bool { + return assertions.New(t).So(errors.IsInvalidArgument(err), should.BeTrue) + }, + }, + { + Name: "Invalid/OwnerTokenLength", + Data: []byte("https://ttig.pro/c/ec656efffe000128/abcdef123"), + ErrorAssertion: func(t *testing.T, err error) bool { + return assertions.New(t).So(errors.IsInvalidArgument(err), should.BeTrue) + }, + }, + { + Name: "Invalid/OwnerTokenNotBase62", + Data: []byte("https://ttig.pro/c/ec656efffe000128/abcdef12!"), + ErrorAssertion: func(t *testing.T, err error) bool { + return assertions.New(t).So(errors.IsInvalidArgument(err), should.BeTrue) + }, + }, + } { + t.Run(tc.Name, func(t *testing.T) { + a := assertions.New(t) + + var data ttigpro1 + err := data.UnmarshalText(tc.Data) + if tc.ErrorAssertion != nil { + a.So(tc.ErrorAssertion(t, err), should.BeTrue) + return + } + if !a.So(err, should.BeNil) || !a.So(data, should.Resemble, tc.Expected) { + t.FailNow() + } + }) + } + }) +} diff --git a/pkg/qrcodegenerator/qrcodegenerator.go b/pkg/qrcodegenerator/qrcodegenerator.go index 6cd66e13d8..ed52d3a26a 100644 --- a/pkg/qrcodegenerator/qrcodegenerator.go +++ b/pkg/qrcodegenerator/qrcodegenerator.go @@ -23,6 +23,7 @@ import ( "go.thethings.network/lorawan-stack/v3/pkg/errors" "go.thethings.network/lorawan-stack/v3/pkg/log" "go.thethings.network/lorawan-stack/v3/pkg/qrcodegenerator/qrcode/enddevices" + "go.thethings.network/lorawan-stack/v3/pkg/qrcodegenerator/qrcode/gateways" "go.thethings.network/lorawan-stack/v3/pkg/ttnpb" "google.golang.org/grpc" ) @@ -35,9 +36,11 @@ type QRCodeGenerator struct { ctx context.Context endDevices *enddevices.Server + gateways *gateways.Server grpc struct { endDeviceQRCodeGenerator *endDeviceQRCodeGeneratorServer + gatewayQRCodeGenerator *gatewayQRCodeGeneratorServer } } @@ -53,6 +56,9 @@ func New(c *component.Component, conf *Config, opts ...Option) (*QRCodeGenerator qrg.grpc.endDeviceQRCodeGenerator = &endDeviceQRCodeGeneratorServer{QRG: qrg} qrg.endDevices = enddevices.New(ctx) + qrg.grpc.gatewayQRCodeGenerator = &gatewayQRCodeGeneratorServer{QRG: qrg} + qrg.gateways = gateways.New(ctx) + c.RegisterGRPC(qrg) for _, opt := range opts { @@ -72,6 +78,13 @@ func WithEndDeviceFormat(id string, f enddevices.Format) Option { } } +// WithGatewayFormat configures QRCodeGenerator with a GatewayFormat. +func WithGatewayFormat(id string, f gateways.Format) Option { + return func(qrg *QRCodeGenerator) { + qrg.gateways.RegisterGatewayFormat(id, f) + } +} + // Context returns the context of the QR Code Generator. func (qrg *QRCodeGenerator) Context() context.Context { return qrg.ctx @@ -85,6 +98,7 @@ func (qrg *QRCodeGenerator) Roles() []ttnpb.ClusterRole { // RegisterServices registers services provided by qrg at s. func (qrg *QRCodeGenerator) RegisterServices(s *grpc.Server) { ttnpb.RegisterEndDeviceQRCodeGeneratorServer(s, qrg.grpc.endDeviceQRCodeGenerator) + ttnpb.RegisterGatewayQRCodeGeneratorServer(s, qrg.grpc.gatewayQRCodeGenerator) } // RegisterHandlers registers gRPC handlers. diff --git a/pkg/ttnpb/qrcodegenerator.pb.go b/pkg/ttnpb/qrcodegenerator.pb.go index 4897d4334b..a50ffef04e 100644 --- a/pkg/ttnpb/qrcodegenerator.pb.go +++ b/pkg/ttnpb/qrcodegenerator.pb.go @@ -437,6 +437,120 @@ func (x *ParseEndDeviceQRCodeResponse) GetEndDeviceTemplate() *EndDeviceTemplate return nil } +type ParseGatewayQRCodeRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // QR code format identifier. + // If this field is not specified, the server will default to ttigpro1. + FormatId string `protobuf:"bytes,1,opt,name=format_id,json=formatId,proto3" json:"format_id,omitempty"` + // Raw QR code contents. + QrCode []byte `protobuf:"bytes,2,opt,name=qr_code,json=qrCode,proto3" json:"qr_code,omitempty"` +} + +func (x *ParseGatewayQRCodeRequest) Reset() { + *x = ParseGatewayQRCodeRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_ttn_lorawan_v3_qrcodegenerator_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ParseGatewayQRCodeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ParseGatewayQRCodeRequest) ProtoMessage() {} + +func (x *ParseGatewayQRCodeRequest) ProtoReflect() protoreflect.Message { + mi := &file_ttn_lorawan_v3_qrcodegenerator_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ParseGatewayQRCodeRequest.ProtoReflect.Descriptor instead. +func (*ParseGatewayQRCodeRequest) Descriptor() ([]byte, []int) { + return file_ttn_lorawan_v3_qrcodegenerator_proto_rawDescGZIP(), []int{7} +} + +func (x *ParseGatewayQRCodeRequest) GetFormatId() string { + if x != nil { + return x.FormatId + } + return "" +} + +func (x *ParseGatewayQRCodeRequest) GetQrCode() []byte { + if x != nil { + return x.QrCode + } + return nil +} + +type ParseGatewayQRCodeResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Identifier of the format used to successfully parse the QR code data. + FormatId string `protobuf:"bytes,1,opt,name=format_id,json=formatId,proto3" json:"format_id,omitempty"` + ClaimGatewayRequest *ClaimGatewayRequest `protobuf:"bytes,2,opt,name=claim_gateway_request,json=claimGatewayRequest,proto3" json:"claim_gateway_request,omitempty"` +} + +func (x *ParseGatewayQRCodeResponse) Reset() { + *x = ParseGatewayQRCodeResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_ttn_lorawan_v3_qrcodegenerator_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ParseGatewayQRCodeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ParseGatewayQRCodeResponse) ProtoMessage() {} + +func (x *ParseGatewayQRCodeResponse) ProtoReflect() protoreflect.Message { + mi := &file_ttn_lorawan_v3_qrcodegenerator_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ParseGatewayQRCodeResponse.ProtoReflect.Descriptor instead. +func (*ParseGatewayQRCodeResponse) Descriptor() ([]byte, []int) { + return file_ttn_lorawan_v3_qrcodegenerator_proto_rawDescGZIP(), []int{8} +} + +func (x *ParseGatewayQRCodeResponse) GetFormatId() string { + if x != nil { + return x.FormatId + } + return "" +} + +func (x *ParseGatewayQRCodeResponse) GetClaimGatewayRequest() *ClaimGatewayRequest { + if x != nil { + return x.ClaimGatewayRequest + } + return nil +} + type GenerateEndDeviceQRCodeRequest_Image struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -449,7 +563,7 @@ type GenerateEndDeviceQRCodeRequest_Image struct { func (x *GenerateEndDeviceQRCodeRequest_Image) Reset() { *x = GenerateEndDeviceQRCodeRequest_Image{} if protoimpl.UnsafeEnabled { - mi := &file_ttn_lorawan_v3_qrcodegenerator_proto_msgTypes[8] + mi := &file_ttn_lorawan_v3_qrcodegenerator_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -462,7 +576,7 @@ func (x *GenerateEndDeviceQRCodeRequest_Image) String() string { func (*GenerateEndDeviceQRCodeRequest_Image) ProtoMessage() {} func (x *GenerateEndDeviceQRCodeRequest_Image) ProtoReflect() protoreflect.Message { - mi := &file_ttn_lorawan_v3_qrcodegenerator_proto_msgTypes[8] + mi := &file_ttn_lorawan_v3_qrcodegenerator_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -505,123 +619,156 @@ var file_ttn_lorawan_v3_qrcodegenerator_proto_rawDesc = []byte{ 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1c, 0x74, 0x74, 0x6e, 0x2f, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2f, 0x76, 0x33, 0x2f, 0x70, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x17, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x61, 0x6c, - 0x69, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x92, 0x01, 0x0a, 0x0c, - 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x1b, 0x0a, 0x04, - 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, - 0x02, 0x18, 0x64, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2a, 0x0a, 0x0b, 0x64, 0x65, 0x73, - 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, - 0xfa, 0x42, 0x05, 0x72, 0x03, 0x18, 0xc8, 0x01, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x39, 0x0a, 0x0a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x6d, - 0x61, 0x73, 0x6b, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x46, 0x69, 0x65, 0x6c, - 0x64, 0x4d, 0x61, 0x73, 0x6b, 0x52, 0x09, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x4d, 0x61, 0x73, 0x6b, - 0x22, 0xdd, 0x01, 0x0a, 0x0d, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, - 0x74, 0x73, 0x12, 0x72, 0x0a, 0x07, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x73, 0x18, 0x01, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, - 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, - 0x74, 0x73, 0x2e, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x42, - 0x2c, 0xfa, 0x42, 0x29, 0x9a, 0x01, 0x26, 0x22, 0x24, 0x72, 0x22, 0x18, 0x24, 0x32, 0x1e, 0x5e, - 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x2d, 0x5d, 0x3f, 0x5b, - 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x7b, 0x32, 0x2c, 0x7d, 0x24, 0x52, 0x07, 0x66, - 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x73, 0x1a, 0x58, 0x0a, 0x0c, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, - 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x32, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, - 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x46, - 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, - 0x22, 0x5e, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x46, 0x6f, 0x72, - 0x6d, 0x61, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x44, 0x0a, 0x09, 0x66, 0x6f, - 0x72, 0x6d, 0x61, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x27, 0xfa, - 0x42, 0x24, 0x72, 0x22, 0x18, 0x24, 0x32, 0x1e, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, - 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x2d, 0x5d, 0x3f, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, - 0x29, 0x7b, 0x32, 0x2c, 0x7d, 0x24, 0x52, 0x08, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x49, 0x64, - 0x22, 0xaa, 0x02, 0x0a, 0x1e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, - 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x44, 0x0a, 0x09, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x5f, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x27, 0xfa, 0x42, 0x24, 0x72, 0x22, 0x18, 0x24, 0x32, - 0x1e, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x2d, 0x5d, - 0x3f, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x7b, 0x32, 0x2c, 0x7d, 0x24, 0x52, - 0x08, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x49, 0x64, 0x12, 0x42, 0x0a, 0x0a, 0x65, 0x6e, 0x64, - 0x5f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, - 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x45, - 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x8a, 0x01, 0x02, - 0x10, 0x01, 0x52, 0x09, 0x65, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x12, 0x4a, 0x0a, - 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x74, - 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x47, 0x65, - 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x51, - 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x49, 0x6d, 0x61, - 0x67, 0x65, 0x52, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x1a, 0x32, 0x0a, 0x05, 0x49, 0x6d, 0x61, - 0x67, 0x65, 0x12, 0x29, 0x0a, 0x0a, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x42, 0x0a, 0xfa, 0x42, 0x07, 0x2a, 0x05, 0x18, 0xe8, 0x07, - 0x28, 0x0a, 0x52, 0x09, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x22, 0x5b, 0x0a, - 0x16, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x78, 0x74, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x65, 0x78, 0x74, 0x12, 0x2d, 0x0a, 0x05, 0x69, - 0x6d, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x74, 0x6e, - 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x50, 0x69, 0x63, 0x74, - 0x75, 0x72, 0x65, 0x52, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x22, 0x8b, 0x01, 0x0a, 0x1b, 0x50, - 0x61, 0x72, 0x73, 0x65, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x51, 0x52, 0x43, - 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x47, 0x0a, 0x09, 0x66, 0x6f, - 0x72, 0x6d, 0x61, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x2a, 0xfa, - 0x42, 0x27, 0x72, 0x25, 0x18, 0x24, 0x32, 0x21, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, - 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x2d, 0x5d, 0x3f, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, - 0x29, 0x7b, 0x32, 0x2c, 0x7d, 0x24, 0x7c, 0x5e, 0x24, 0x52, 0x08, 0x66, 0x6f, 0x72, 0x6d, 0x61, - 0x74, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x07, 0x71, 0x72, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0c, 0x42, 0x0a, 0xfa, 0x42, 0x07, 0x7a, 0x05, 0x10, 0x0a, 0x18, 0x80, 0x08, - 0x52, 0x06, 0x71, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x22, 0x8e, 0x01, 0x0a, 0x1c, 0x50, 0x61, 0x72, - 0x73, 0x65, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, - 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x6f, 0x72, - 0x6d, 0x61, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x6f, - 0x72, 0x6d, 0x61, 0x74, 0x49, 0x64, 0x12, 0x51, 0x0a, 0x13, 0x65, 0x6e, 0x64, 0x5f, 0x64, 0x65, - 0x76, 0x69, 0x63, 0x65, 0x5f, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, - 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x54, 0x65, - 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x52, 0x11, 0x65, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, - 0x65, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x32, 0xfe, 0x04, 0x0a, 0x18, 0x45, 0x6e, - 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x47, 0x65, 0x6e, - 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x84, 0x01, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x46, 0x6f, - 0x72, 0x6d, 0x61, 0x74, 0x12, 0x26, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, - 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x47, 0x65, 0x74, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x46, - 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x74, - 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x51, 0x52, - 0x43, 0x6f, 0x64, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x22, 0x31, 0x82, 0xd3, 0xe4, 0x93, - 0x02, 0x2b, 0x12, 0x29, 0x2f, 0x71, 0x72, 0x2d, 0x63, 0x6f, 0x64, 0x65, 0x73, 0x2f, 0x65, 0x6e, - 0x64, 0x2d, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, - 0x73, 0x2f, 0x7b, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x5f, 0x69, 0x64, 0x7d, 0x12, 0x6b, 0x0a, - 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x73, 0x12, 0x16, 0x2e, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, - 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, - 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x46, 0x6f, 0x72, 0x6d, - 0x61, 0x74, 0x73, 0x22, 0x25, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1f, 0x12, 0x1d, 0x2f, 0x71, 0x72, - 0x2d, 0x63, 0x6f, 0x64, 0x65, 0x73, 0x2f, 0x65, 0x6e, 0x64, 0x2d, 0x64, 0x65, 0x76, 0x69, 0x63, - 0x65, 0x73, 0x2f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x73, 0x12, 0x84, 0x01, 0x0a, 0x08, 0x47, - 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x12, 0x2e, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, + 0x69, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x29, 0x74, 0x74, 0x6e, + 0x2f, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2f, 0x76, 0x33, 0x2f, 0x64, 0x65, 0x76, 0x69, + 0x63, 0x65, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x92, 0x01, 0x0a, 0x0c, 0x51, 0x52, 0x43, 0x6f, 0x64, + 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x1b, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x18, 0x64, 0x52, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2a, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x72, 0x03, + 0x18, 0xc8, 0x01, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0x39, 0x0a, 0x0a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4d, 0x61, 0x73, 0x6b, + 0x52, 0x09, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x4d, 0x61, 0x73, 0x6b, 0x22, 0xdd, 0x01, 0x0a, 0x0d, + 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x73, 0x12, 0x72, 0x0a, + 0x07, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2a, + 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, + 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x73, 0x2e, 0x46, 0x6f, + 0x72, 0x6d, 0x61, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x42, 0x2c, 0xfa, 0x42, 0x29, 0x9a, + 0x01, 0x26, 0x22, 0x24, 0x72, 0x22, 0x18, 0x24, 0x32, 0x1e, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x30, + 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x2d, 0x5d, 0x3f, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, + 0x39, 0x5d, 0x29, 0x7b, 0x32, 0x2c, 0x7d, 0x24, 0x52, 0x07, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, + 0x73, 0x1a, 0x58, 0x0a, 0x0c, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, + 0x6b, 0x65, 0x79, 0x12, 0x32, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, + 0x2e, 0x76, 0x33, 0x2e, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, + 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x5e, 0x0a, 0x16, 0x47, + 0x65, 0x74, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x44, 0x0a, 0x09, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x27, 0xfa, 0x42, 0x24, 0x72, 0x22, 0x18, + 0x24, 0x32, 0x1e, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, + 0x2d, 0x5d, 0x3f, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x7b, 0x32, 0x2c, 0x7d, + 0x24, 0x52, 0x08, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x49, 0x64, 0x22, 0xaa, 0x02, 0x0a, 0x1e, + 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x44, + 0x0a, 0x09, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x42, 0x27, 0xfa, 0x42, 0x24, 0x72, 0x22, 0x18, 0x24, 0x32, 0x1e, 0x5e, 0x5b, 0x61, 0x2d, + 0x7a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x2d, 0x5d, 0x3f, 0x5b, 0x61, 0x2d, 0x7a, + 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x7b, 0x32, 0x2c, 0x7d, 0x24, 0x52, 0x08, 0x66, 0x6f, 0x72, 0x6d, + 0x61, 0x74, 0x49, 0x64, 0x12, 0x42, 0x0a, 0x0a, 0x65, 0x6e, 0x64, 0x5f, 0x64, 0x65, 0x76, 0x69, + 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, + 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, + 0x69, 0x63, 0x65, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x8a, 0x01, 0x02, 0x10, 0x01, 0x52, 0x09, 0x65, + 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x12, 0x4a, 0x0a, 0x05, 0x69, 0x6d, 0x61, 0x67, + 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, - 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, - 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x3a, 0x01, 0x2a, 0x22, 0x15, 0x2f, 0x71, 0x72, 0x2d, - 0x63, 0x6f, 0x64, 0x65, 0x73, 0x2f, 0x65, 0x6e, 0x64, 0x2d, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, - 0x73, 0x12, 0xb8, 0x01, 0x0a, 0x05, 0x50, 0x61, 0x72, 0x73, 0x65, 0x12, 0x2b, 0x2e, 0x74, 0x74, - 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x50, 0x61, 0x72, - 0x73, 0x65, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, - 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x45, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x05, 0x69, + 0x6d, 0x61, 0x67, 0x65, 0x1a, 0x32, 0x0a, 0x05, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x29, 0x0a, + 0x0a, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0d, 0x42, 0x0a, 0xfa, 0x42, 0x07, 0x2a, 0x05, 0x18, 0xe8, 0x07, 0x28, 0x0a, 0x52, 0x09, 0x69, + 0x6d, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x22, 0x5b, 0x0a, 0x16, 0x47, 0x65, 0x6e, 0x65, + 0x72, 0x61, 0x74, 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x78, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x74, 0x65, 0x78, 0x74, 0x12, 0x2d, 0x0a, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, + 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x50, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x52, 0x05, + 0x69, 0x6d, 0x61, 0x67, 0x65, 0x22, 0x8b, 0x01, 0x0a, 0x1b, 0x50, 0x61, 0x72, 0x73, 0x65, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x54, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x4e, 0x3a, 0x01, - 0x2a, 0x5a, 0x2c, 0x3a, 0x01, 0x2a, 0x22, 0x27, 0x2f, 0x71, 0x72, 0x2d, 0x63, 0x6f, 0x64, 0x65, - 0x73, 0x2f, 0x65, 0x6e, 0x64, 0x2d, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x7b, 0x66, - 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x70, 0x61, 0x72, 0x73, 0x65, 0x22, - 0x1b, 0x2f, 0x71, 0x72, 0x2d, 0x63, 0x6f, 0x64, 0x65, 0x73, 0x2f, 0x65, 0x6e, 0x64, 0x2d, 0x64, - 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x70, 0x61, 0x72, 0x73, 0x65, 0x1a, 0x2c, 0x92, 0x41, - 0x29, 0x12, 0x27, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x20, 0x61, 0x6e, 0x64, 0x20, - 0x70, 0x61, 0x72, 0x73, 0x65, 0x20, 0x65, 0x6e, 0x64, 0x20, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, - 0x20, 0x51, 0x52, 0x20, 0x63, 0x6f, 0x64, 0x65, 0x73, 0x2e, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x6f, - 0x2e, 0x74, 0x68, 0x65, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, - 0x72, 0x6b, 0x2f, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2d, 0x73, 0x74, 0x61, 0x63, 0x6b, - 0x2f, 0x76, 0x33, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x74, 0x74, 0x6e, 0x70, 0x62, 0x62, 0x06, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x47, 0x0a, 0x09, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x2a, 0xfa, 0x42, 0x27, 0x72, 0x25, 0x18, + 0x24, 0x32, 0x21, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, + 0x2d, 0x5d, 0x3f, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x7b, 0x32, 0x2c, 0x7d, + 0x24, 0x7c, 0x5e, 0x24, 0x52, 0x08, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x49, 0x64, 0x12, 0x23, + 0x0a, 0x07, 0x71, 0x72, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x42, + 0x0a, 0xfa, 0x42, 0x07, 0x7a, 0x05, 0x10, 0x0a, 0x18, 0x80, 0x08, 0x52, 0x06, 0x71, 0x72, 0x43, + 0x6f, 0x64, 0x65, 0x22, 0x8e, 0x01, 0x0a, 0x1c, 0x50, 0x61, 0x72, 0x73, 0x65, 0x45, 0x6e, 0x64, + 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x5f, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x49, + 0x64, 0x12, 0x51, 0x0a, 0x13, 0x65, 0x6e, 0x64, 0x5f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, + 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, + 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, + 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, + 0x65, 0x52, 0x11, 0x65, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x54, 0x65, 0x6d, 0x70, + 0x6c, 0x61, 0x74, 0x65, 0x22, 0x89, 0x01, 0x0a, 0x19, 0x50, 0x61, 0x72, 0x73, 0x65, 0x47, 0x61, + 0x74, 0x65, 0x77, 0x61, 0x79, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x47, 0x0a, 0x09, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x2a, 0xfa, 0x42, 0x27, 0x72, 0x25, 0x18, 0x24, 0x32, 0x21, + 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x2d, 0x5d, 0x3f, + 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x7b, 0x32, 0x2c, 0x7d, 0x24, 0x7c, 0x5e, + 0x24, 0x52, 0x08, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x07, 0x71, + 0x72, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x0a, 0xfa, 0x42, + 0x07, 0x7a, 0x05, 0x10, 0x0a, 0x18, 0x80, 0x08, 0x52, 0x06, 0x71, 0x72, 0x43, 0x6f, 0x64, 0x65, + 0x22, 0x92, 0x01, 0x0a, 0x1a, 0x50, 0x61, 0x72, 0x73, 0x65, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, + 0x79, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x1b, 0x0a, 0x09, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x49, 0x64, 0x12, 0x57, 0x0a, 0x15, + 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x5f, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x5f, 0x72, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x74, 0x74, + 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x43, 0x6c, 0x61, + 0x69, 0x6d, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x52, 0x13, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x32, 0xfe, 0x04, 0x0a, 0x18, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, + 0x69, 0x63, 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, + 0x6f, 0x72, 0x12, 0x84, 0x01, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, + 0x12, 0x26, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, + 0x33, 0x2e, 0x47, 0x65, 0x74, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, + 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, + 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, + 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x22, 0x31, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x2b, 0x12, 0x29, + 0x2f, 0x71, 0x72, 0x2d, 0x63, 0x6f, 0x64, 0x65, 0x73, 0x2f, 0x65, 0x6e, 0x64, 0x2d, 0x64, 0x65, + 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x73, 0x2f, 0x7b, 0x66, + 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x5f, 0x69, 0x64, 0x7d, 0x12, 0x6b, 0x0a, 0x0b, 0x4c, 0x69, 0x73, + 0x74, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x1a, 0x1d, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, + 0x33, 0x2e, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x73, 0x22, + 0x25, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1f, 0x12, 0x1d, 0x2f, 0x71, 0x72, 0x2d, 0x63, 0x6f, 0x64, + 0x65, 0x73, 0x2f, 0x65, 0x6e, 0x64, 0x2d, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x66, + 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x73, 0x12, 0x84, 0x01, 0x0a, 0x08, 0x47, 0x65, 0x6e, 0x65, 0x72, + 0x61, 0x74, 0x65, 0x12, 0x2e, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, + 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, + 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, + 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x51, 0x52, 0x43, + 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x20, 0x82, 0xd3, 0xe4, + 0x93, 0x02, 0x1a, 0x3a, 0x01, 0x2a, 0x22, 0x15, 0x2f, 0x71, 0x72, 0x2d, 0x63, 0x6f, 0x64, 0x65, + 0x73, 0x2f, 0x65, 0x6e, 0x64, 0x2d, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x12, 0xb8, 0x01, + 0x0a, 0x05, 0x50, 0x61, 0x72, 0x73, 0x65, 0x12, 0x2b, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, + 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x45, 0x6e, + 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, + 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x45, 0x6e, 0x64, 0x44, 0x65, + 0x76, 0x69, 0x63, 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x54, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x4e, 0x3a, 0x01, 0x2a, 0x5a, 0x2c, 0x3a, + 0x01, 0x2a, 0x22, 0x27, 0x2f, 0x71, 0x72, 0x2d, 0x63, 0x6f, 0x64, 0x65, 0x73, 0x2f, 0x65, 0x6e, + 0x64, 0x2d, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x7b, 0x66, 0x6f, 0x72, 0x6d, 0x61, + 0x74, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x70, 0x61, 0x72, 0x73, 0x65, 0x22, 0x1b, 0x2f, 0x71, 0x72, + 0x2d, 0x63, 0x6f, 0x64, 0x65, 0x73, 0x2f, 0x65, 0x6e, 0x64, 0x2d, 0x64, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x73, 0x2f, 0x70, 0x61, 0x72, 0x73, 0x65, 0x1a, 0x2c, 0x92, 0x41, 0x29, 0x12, 0x27, 0x47, + 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x70, 0x61, 0x72, 0x73, + 0x65, 0x20, 0x65, 0x6e, 0x64, 0x20, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x20, 0x51, 0x52, 0x20, + 0x63, 0x6f, 0x64, 0x65, 0x73, 0x2e, 0x32, 0xc9, 0x01, 0x0a, 0x16, 0x47, 0x61, 0x74, 0x65, 0x77, + 0x61, 0x79, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x6f, + 0x72, 0x12, 0xae, 0x01, 0x0a, 0x05, 0x50, 0x61, 0x72, 0x73, 0x65, 0x12, 0x29, 0x2e, 0x74, 0x74, + 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x50, 0x61, 0x72, + 0x73, 0x65, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, + 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x47, 0x61, 0x74, + 0x65, 0x77, 0x61, 0x79, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x4e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x48, 0x3a, 0x01, 0x2a, 0x5a, 0x29, 0x3a, + 0x01, 0x2a, 0x22, 0x24, 0x2f, 0x71, 0x72, 0x2d, 0x63, 0x6f, 0x64, 0x65, 0x73, 0x2f, 0x67, 0x61, + 0x74, 0x65, 0x77, 0x61, 0x79, 0x73, 0x2f, 0x7b, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x5f, 0x69, + 0x64, 0x7d, 0x2f, 0x70, 0x61, 0x72, 0x73, 0x65, 0x22, 0x18, 0x2f, 0x71, 0x72, 0x2d, 0x63, 0x6f, + 0x64, 0x65, 0x73, 0x2f, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x73, 0x2f, 0x70, 0x61, 0x72, + 0x73, 0x65, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x6f, 0x2e, 0x74, 0x68, 0x65, 0x74, 0x68, 0x69, 0x6e, + 0x67, 0x73, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x6c, 0x6f, 0x72, 0x61, 0x77, + 0x61, 0x6e, 0x2d, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2f, 0x76, 0x33, 0x2f, 0x70, 0x6b, 0x67, 0x2f, + 0x74, 0x74, 0x6e, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -636,7 +783,7 @@ func file_ttn_lorawan_v3_qrcodegenerator_proto_rawDescGZIP() []byte { return file_ttn_lorawan_v3_qrcodegenerator_proto_rawDescData } -var file_ttn_lorawan_v3_qrcodegenerator_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_ttn_lorawan_v3_qrcodegenerator_proto_msgTypes = make([]protoimpl.MessageInfo, 11) var file_ttn_lorawan_v3_qrcodegenerator_proto_goTypes = []interface{}{ (*QRCodeFormat)(nil), // 0: ttn.lorawan.v3.QRCodeFormat (*QRCodeFormats)(nil), // 1: ttn.lorawan.v3.QRCodeFormats @@ -645,35 +792,41 @@ var file_ttn_lorawan_v3_qrcodegenerator_proto_goTypes = []interface{}{ (*GenerateQRCodeResponse)(nil), // 4: ttn.lorawan.v3.GenerateQRCodeResponse (*ParseEndDeviceQRCodeRequest)(nil), // 5: ttn.lorawan.v3.ParseEndDeviceQRCodeRequest (*ParseEndDeviceQRCodeResponse)(nil), // 6: ttn.lorawan.v3.ParseEndDeviceQRCodeResponse - nil, // 7: ttn.lorawan.v3.QRCodeFormats.FormatsEntry - (*GenerateEndDeviceQRCodeRequest_Image)(nil), // 8: ttn.lorawan.v3.GenerateEndDeviceQRCodeRequest.Image - (*fieldmaskpb.FieldMask)(nil), // 9: google.protobuf.FieldMask - (*EndDevice)(nil), // 10: ttn.lorawan.v3.EndDevice - (*Picture)(nil), // 11: ttn.lorawan.v3.Picture - (*EndDeviceTemplate)(nil), // 12: ttn.lorawan.v3.EndDeviceTemplate - (*emptypb.Empty)(nil), // 13: google.protobuf.Empty + (*ParseGatewayQRCodeRequest)(nil), // 7: ttn.lorawan.v3.ParseGatewayQRCodeRequest + (*ParseGatewayQRCodeResponse)(nil), // 8: ttn.lorawan.v3.ParseGatewayQRCodeResponse + nil, // 9: ttn.lorawan.v3.QRCodeFormats.FormatsEntry + (*GenerateEndDeviceQRCodeRequest_Image)(nil), // 10: ttn.lorawan.v3.GenerateEndDeviceQRCodeRequest.Image + (*fieldmaskpb.FieldMask)(nil), // 11: google.protobuf.FieldMask + (*EndDevice)(nil), // 12: ttn.lorawan.v3.EndDevice + (*Picture)(nil), // 13: ttn.lorawan.v3.Picture + (*EndDeviceTemplate)(nil), // 14: ttn.lorawan.v3.EndDeviceTemplate + (*ClaimGatewayRequest)(nil), // 15: ttn.lorawan.v3.ClaimGatewayRequest + (*emptypb.Empty)(nil), // 16: google.protobuf.Empty } var file_ttn_lorawan_v3_qrcodegenerator_proto_depIdxs = []int32{ - 9, // 0: ttn.lorawan.v3.QRCodeFormat.field_mask:type_name -> google.protobuf.FieldMask - 7, // 1: ttn.lorawan.v3.QRCodeFormats.formats:type_name -> ttn.lorawan.v3.QRCodeFormats.FormatsEntry - 10, // 2: ttn.lorawan.v3.GenerateEndDeviceQRCodeRequest.end_device:type_name -> ttn.lorawan.v3.EndDevice - 8, // 3: ttn.lorawan.v3.GenerateEndDeviceQRCodeRequest.image:type_name -> ttn.lorawan.v3.GenerateEndDeviceQRCodeRequest.Image - 11, // 4: ttn.lorawan.v3.GenerateQRCodeResponse.image:type_name -> ttn.lorawan.v3.Picture - 12, // 5: ttn.lorawan.v3.ParseEndDeviceQRCodeResponse.end_device_template:type_name -> ttn.lorawan.v3.EndDeviceTemplate - 0, // 6: ttn.lorawan.v3.QRCodeFormats.FormatsEntry.value:type_name -> ttn.lorawan.v3.QRCodeFormat - 2, // 7: ttn.lorawan.v3.EndDeviceQRCodeGenerator.GetFormat:input_type -> ttn.lorawan.v3.GetQRCodeFormatRequest - 13, // 8: ttn.lorawan.v3.EndDeviceQRCodeGenerator.ListFormats:input_type -> google.protobuf.Empty - 3, // 9: ttn.lorawan.v3.EndDeviceQRCodeGenerator.Generate:input_type -> ttn.lorawan.v3.GenerateEndDeviceQRCodeRequest - 5, // 10: ttn.lorawan.v3.EndDeviceQRCodeGenerator.Parse:input_type -> ttn.lorawan.v3.ParseEndDeviceQRCodeRequest - 0, // 11: ttn.lorawan.v3.EndDeviceQRCodeGenerator.GetFormat:output_type -> ttn.lorawan.v3.QRCodeFormat - 1, // 12: ttn.lorawan.v3.EndDeviceQRCodeGenerator.ListFormats:output_type -> ttn.lorawan.v3.QRCodeFormats - 4, // 13: ttn.lorawan.v3.EndDeviceQRCodeGenerator.Generate:output_type -> ttn.lorawan.v3.GenerateQRCodeResponse - 6, // 14: ttn.lorawan.v3.EndDeviceQRCodeGenerator.Parse:output_type -> ttn.lorawan.v3.ParseEndDeviceQRCodeResponse - 11, // [11:15] is the sub-list for method output_type - 7, // [7:11] is the sub-list for method input_type - 7, // [7:7] is the sub-list for extension type_name - 7, // [7:7] is the sub-list for extension extendee - 0, // [0:7] is the sub-list for field type_name + 11, // 0: ttn.lorawan.v3.QRCodeFormat.field_mask:type_name -> google.protobuf.FieldMask + 9, // 1: ttn.lorawan.v3.QRCodeFormats.formats:type_name -> ttn.lorawan.v3.QRCodeFormats.FormatsEntry + 12, // 2: ttn.lorawan.v3.GenerateEndDeviceQRCodeRequest.end_device:type_name -> ttn.lorawan.v3.EndDevice + 10, // 3: ttn.lorawan.v3.GenerateEndDeviceQRCodeRequest.image:type_name -> ttn.lorawan.v3.GenerateEndDeviceQRCodeRequest.Image + 13, // 4: ttn.lorawan.v3.GenerateQRCodeResponse.image:type_name -> ttn.lorawan.v3.Picture + 14, // 5: ttn.lorawan.v3.ParseEndDeviceQRCodeResponse.end_device_template:type_name -> ttn.lorawan.v3.EndDeviceTemplate + 15, // 6: ttn.lorawan.v3.ParseGatewayQRCodeResponse.claim_gateway_request:type_name -> ttn.lorawan.v3.ClaimGatewayRequest + 0, // 7: ttn.lorawan.v3.QRCodeFormats.FormatsEntry.value:type_name -> ttn.lorawan.v3.QRCodeFormat + 2, // 8: ttn.lorawan.v3.EndDeviceQRCodeGenerator.GetFormat:input_type -> ttn.lorawan.v3.GetQRCodeFormatRequest + 16, // 9: ttn.lorawan.v3.EndDeviceQRCodeGenerator.ListFormats:input_type -> google.protobuf.Empty + 3, // 10: ttn.lorawan.v3.EndDeviceQRCodeGenerator.Generate:input_type -> ttn.lorawan.v3.GenerateEndDeviceQRCodeRequest + 5, // 11: ttn.lorawan.v3.EndDeviceQRCodeGenerator.Parse:input_type -> ttn.lorawan.v3.ParseEndDeviceQRCodeRequest + 7, // 12: ttn.lorawan.v3.GatewayQRCodeGenerator.Parse:input_type -> ttn.lorawan.v3.ParseGatewayQRCodeRequest + 0, // 13: ttn.lorawan.v3.EndDeviceQRCodeGenerator.GetFormat:output_type -> ttn.lorawan.v3.QRCodeFormat + 1, // 14: ttn.lorawan.v3.EndDeviceQRCodeGenerator.ListFormats:output_type -> ttn.lorawan.v3.QRCodeFormats + 4, // 15: ttn.lorawan.v3.EndDeviceQRCodeGenerator.Generate:output_type -> ttn.lorawan.v3.GenerateQRCodeResponse + 6, // 16: ttn.lorawan.v3.EndDeviceQRCodeGenerator.Parse:output_type -> ttn.lorawan.v3.ParseEndDeviceQRCodeResponse + 8, // 17: ttn.lorawan.v3.GatewayQRCodeGenerator.Parse:output_type -> ttn.lorawan.v3.ParseGatewayQRCodeResponse + 13, // [13:18] is the sub-list for method output_type + 8, // [8:13] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name } func init() { file_ttn_lorawan_v3_qrcodegenerator_proto_init() } @@ -683,6 +836,7 @@ func file_ttn_lorawan_v3_qrcodegenerator_proto_init() { } file_ttn_lorawan_v3_end_device_proto_init() file_ttn_lorawan_v3_picture_proto_init() + file_ttn_lorawan_v3_deviceclaimingserver_proto_init() if !protoimpl.UnsafeEnabled { file_ttn_lorawan_v3_qrcodegenerator_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*QRCodeFormat); i { @@ -768,7 +922,31 @@ func file_ttn_lorawan_v3_qrcodegenerator_proto_init() { return nil } } + file_ttn_lorawan_v3_qrcodegenerator_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ParseGatewayQRCodeRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } file_ttn_lorawan_v3_qrcodegenerator_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ParseGatewayQRCodeResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_ttn_lorawan_v3_qrcodegenerator_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*GenerateEndDeviceQRCodeRequest_Image); i { case 0: return &v.state @@ -787,9 +965,9 @@ func file_ttn_lorawan_v3_qrcodegenerator_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_ttn_lorawan_v3_qrcodegenerator_proto_rawDesc, NumEnums: 0, - NumMessages: 9, + NumMessages: 11, NumExtensions: 0, - NumServices: 1, + NumServices: 2, }, GoTypes: file_ttn_lorawan_v3_qrcodegenerator_proto_goTypes, DependencyIndexes: file_ttn_lorawan_v3_qrcodegenerator_proto_depIdxs, diff --git a/pkg/ttnpb/qrcodegenerator.pb.gw.go b/pkg/ttnpb/qrcodegenerator.pb.gw.go index 60faa7ba36..cffa22581f 100644 --- a/pkg/ttnpb/qrcodegenerator.pb.gw.go +++ b/pkg/ttnpb/qrcodegenerator.pb.gw.go @@ -214,6 +214,92 @@ func local_request_EndDeviceQRCodeGenerator_Parse_1(ctx context.Context, marshal } +func request_GatewayQRCodeGenerator_Parse_0(ctx context.Context, marshaler runtime.Marshaler, client GatewayQRCodeGeneratorClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ParseGatewayQRCodeRequest + var metadata runtime.ServerMetadata + + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.Parse(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GatewayQRCodeGenerator_Parse_0(ctx context.Context, marshaler runtime.Marshaler, server GatewayQRCodeGeneratorServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ParseGatewayQRCodeRequest + var metadata runtime.ServerMetadata + + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.Parse(ctx, &protoReq) + return msg, metadata, err + +} + +func request_GatewayQRCodeGenerator_Parse_1(ctx context.Context, marshaler runtime.Marshaler, client GatewayQRCodeGeneratorClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ParseGatewayQRCodeRequest + var metadata runtime.ServerMetadata + + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["format_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "format_id") + } + + protoReq.FormatId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "format_id", err) + } + + msg, err := client.Parse(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GatewayQRCodeGenerator_Parse_1(ctx context.Context, marshaler runtime.Marshaler, server GatewayQRCodeGeneratorServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ParseGatewayQRCodeRequest + var metadata runtime.ServerMetadata + + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["format_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "format_id") + } + + protoReq.FormatId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "format_id", err) + } + + msg, err := server.Parse(ctx, &protoReq) + return msg, metadata, err + +} + // RegisterEndDeviceQRCodeGeneratorHandlerServer registers the http handlers for service EndDeviceQRCodeGenerator to "mux". // UnaryRPC :call EndDeviceQRCodeGeneratorServer directly. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. @@ -348,6 +434,65 @@ func RegisterEndDeviceQRCodeGeneratorHandlerServer(ctx context.Context, mux *run return nil } +// RegisterGatewayQRCodeGeneratorHandlerServer registers the http handlers for service GatewayQRCodeGenerator to "mux". +// UnaryRPC :call GatewayQRCodeGeneratorServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterGatewayQRCodeGeneratorHandlerFromEndpoint instead. +func RegisterGatewayQRCodeGeneratorHandlerServer(ctx context.Context, mux *runtime.ServeMux, server GatewayQRCodeGeneratorServer) error { + + mux.Handle("POST", pattern_GatewayQRCodeGenerator_Parse_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/ttn.lorawan.v3.GatewayQRCodeGenerator/Parse", runtime.WithHTTPPathPattern("/qr-codes/gateways/parse")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GatewayQRCodeGenerator_Parse_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_GatewayQRCodeGenerator_Parse_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GatewayQRCodeGenerator_Parse_1, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/ttn.lorawan.v3.GatewayQRCodeGenerator/Parse", runtime.WithHTTPPathPattern("/qr-codes/gateways/{format_id}/parse")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GatewayQRCodeGenerator_Parse_1(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_GatewayQRCodeGenerator_Parse_1(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + return nil +} + // RegisterEndDeviceQRCodeGeneratorHandlerFromEndpoint is same as RegisterEndDeviceQRCodeGeneratorHandler but // automatically dials to "endpoint" and closes the connection when "ctx" gets done. func RegisterEndDeviceQRCodeGeneratorHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { @@ -522,3 +667,100 @@ var ( forward_EndDeviceQRCodeGenerator_Parse_1 = runtime.ForwardResponseMessage ) + +// RegisterGatewayQRCodeGeneratorHandlerFromEndpoint is same as RegisterGatewayQRCodeGeneratorHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterGatewayQRCodeGeneratorHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.DialContext(ctx, endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + + return RegisterGatewayQRCodeGeneratorHandler(ctx, mux, conn) +} + +// RegisterGatewayQRCodeGeneratorHandler registers the http handlers for service GatewayQRCodeGenerator to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterGatewayQRCodeGeneratorHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterGatewayQRCodeGeneratorHandlerClient(ctx, mux, NewGatewayQRCodeGeneratorClient(conn)) +} + +// RegisterGatewayQRCodeGeneratorHandlerClient registers the http handlers for service GatewayQRCodeGenerator +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "GatewayQRCodeGeneratorClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "GatewayQRCodeGeneratorClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "GatewayQRCodeGeneratorClient" to call the correct interceptors. +func RegisterGatewayQRCodeGeneratorHandlerClient(ctx context.Context, mux *runtime.ServeMux, client GatewayQRCodeGeneratorClient) error { + + mux.Handle("POST", pattern_GatewayQRCodeGenerator_Parse_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/ttn.lorawan.v3.GatewayQRCodeGenerator/Parse", runtime.WithHTTPPathPattern("/qr-codes/gateways/parse")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GatewayQRCodeGenerator_Parse_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_GatewayQRCodeGenerator_Parse_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GatewayQRCodeGenerator_Parse_1, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/ttn.lorawan.v3.GatewayQRCodeGenerator/Parse", runtime.WithHTTPPathPattern("/qr-codes/gateways/{format_id}/parse")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GatewayQRCodeGenerator_Parse_1(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_GatewayQRCodeGenerator_Parse_1(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + return nil +} + +var ( + pattern_GatewayQRCodeGenerator_Parse_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"qr-codes", "gateways", "parse"}, "")) + + pattern_GatewayQRCodeGenerator_Parse_1 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3}, []string{"qr-codes", "gateways", "format_id", "parse"}, "")) +) + +var ( + forward_GatewayQRCodeGenerator_Parse_0 = runtime.ForwardResponseMessage + + forward_GatewayQRCodeGenerator_Parse_1 = runtime.ForwardResponseMessage +) diff --git a/pkg/ttnpb/qrcodegenerator.pb.paths.fm.go b/pkg/ttnpb/qrcodegenerator.pb.paths.fm.go index 8f1eb3ddd5..a96f377fd2 100644 --- a/pkg/ttnpb/qrcodegenerator.pb.paths.fm.go +++ b/pkg/ttnpb/qrcodegenerator.pb.paths.fm.go @@ -1579,6 +1579,49 @@ var ParseEndDeviceQRCodeResponseFieldPathsTopLevel = []string{ "end_device_template", "format_id", } +var ParseGatewayQRCodeRequestFieldPathsNested = []string{ + "format_id", + "qr_code", +} + +var ParseGatewayQRCodeRequestFieldPathsTopLevel = []string{ + "format_id", + "qr_code", +} +var ParseGatewayQRCodeResponseFieldPathsNested = []string{ + "claim_gateway_request", + "claim_gateway_request.collaborator", + "claim_gateway_request.collaborator.ids", + "claim_gateway_request.collaborator.ids.organization_ids", + "claim_gateway_request.collaborator.ids.organization_ids.organization_id", + "claim_gateway_request.collaborator.ids.user_ids", + "claim_gateway_request.collaborator.ids.user_ids.email", + "claim_gateway_request.collaborator.ids.user_ids.user_id", + "claim_gateway_request.cups_redirection", + "claim_gateway_request.cups_redirection.current_gateway_key", + "claim_gateway_request.cups_redirection.gateway_credentials", + "claim_gateway_request.cups_redirection.gateway_credentials.auth_token", + "claim_gateway_request.cups_redirection.gateway_credentials.client_tls", + "claim_gateway_request.cups_redirection.gateway_credentials.client_tls.cert", + "claim_gateway_request.cups_redirection.gateway_credentials.client_tls.key", + "claim_gateway_request.cups_redirection.target_cups_trust", + "claim_gateway_request.cups_redirection.target_cups_uri", + "claim_gateway_request.source_gateway", + "claim_gateway_request.source_gateway.authenticated_identifiers", + "claim_gateway_request.source_gateway.authenticated_identifiers.authentication_code", + "claim_gateway_request.source_gateway.authenticated_identifiers.gateway_eui", + "claim_gateway_request.source_gateway.qr_code", + "claim_gateway_request.target_frequency_plan_id", + "claim_gateway_request.target_frequency_plan_ids", + "claim_gateway_request.target_gateway_id", + "claim_gateway_request.target_gateway_server_address", + "format_id", +} + +var ParseGatewayQRCodeResponseFieldPathsTopLevel = []string{ + "claim_gateway_request", + "format_id", +} var GenerateEndDeviceQRCodeRequest_ImageFieldPathsNested = []string{ "image_size", } diff --git a/pkg/ttnpb/qrcodegenerator.pb.setters.fm.go b/pkg/ttnpb/qrcodegenerator.pb.setters.fm.go index 8de3a0bcc7..18b7cd41d7 100644 --- a/pkg/ttnpb/qrcodegenerator.pb.setters.fm.go +++ b/pkg/ttnpb/qrcodegenerator.pb.setters.fm.go @@ -278,6 +278,82 @@ func (dst *ParseEndDeviceQRCodeResponse) SetFields(src *ParseEndDeviceQRCodeResp return nil } +func (dst *ParseGatewayQRCodeRequest) SetFields(src *ParseGatewayQRCodeRequest, paths ...string) error { + for name, subs := range _processPaths(paths) { + switch name { + case "format_id": + if len(subs) > 0 { + return fmt.Errorf("'format_id' has no subfields, but %s were specified", subs) + } + if src != nil { + dst.FormatId = src.FormatId + } else { + var zero string + dst.FormatId = zero + } + case "qr_code": + if len(subs) > 0 { + return fmt.Errorf("'qr_code' has no subfields, but %s were specified", subs) + } + if src != nil { + dst.QrCode = src.QrCode + } else { + dst.QrCode = nil + } + + default: + return fmt.Errorf("invalid field: '%s'", name) + } + } + return nil +} + +func (dst *ParseGatewayQRCodeResponse) SetFields(src *ParseGatewayQRCodeResponse, paths ...string) error { + for name, subs := range _processPaths(paths) { + switch name { + case "format_id": + if len(subs) > 0 { + return fmt.Errorf("'format_id' has no subfields, but %s were specified", subs) + } + if src != nil { + dst.FormatId = src.FormatId + } else { + var zero string + dst.FormatId = zero + } + case "claim_gateway_request": + if len(subs) > 0 { + var newDst, newSrc *ClaimGatewayRequest + if (src == nil || src.ClaimGatewayRequest == nil) && dst.ClaimGatewayRequest == nil { + continue + } + if src != nil { + newSrc = src.ClaimGatewayRequest + } + if dst.ClaimGatewayRequest != nil { + newDst = dst.ClaimGatewayRequest + } else { + newDst = &ClaimGatewayRequest{} + dst.ClaimGatewayRequest = newDst + } + if err := newDst.SetFields(newSrc, subs...); err != nil { + return err + } + } else { + if src != nil { + dst.ClaimGatewayRequest = src.ClaimGatewayRequest + } else { + dst.ClaimGatewayRequest = nil + } + } + + default: + return fmt.Errorf("invalid field: '%s'", name) + } + } + return nil +} + func (dst *GenerateEndDeviceQRCodeRequest_Image) SetFields(src *GenerateEndDeviceQRCodeRequest_Image, paths ...string) error { for name, subs := range _processPaths(paths) { switch name { diff --git a/pkg/ttnpb/qrcodegenerator.pb.validate.go b/pkg/ttnpb/qrcodegenerator.pb.validate.go index 2d6bce669e..c126659205 100644 --- a/pkg/ttnpb/qrcodegenerator.pb.validate.go +++ b/pkg/ttnpb/qrcodegenerator.pb.validate.go @@ -783,6 +783,211 @@ var _ interface { ErrorName() string } = ParseEndDeviceQRCodeResponseValidationError{} +// ValidateFields checks the field values on ParseGatewayQRCodeRequest with the +// rules defined in the proto definition for this message. If any rules are +// violated, an error is returned. +func (m *ParseGatewayQRCodeRequest) ValidateFields(paths ...string) error { + if m == nil { + return nil + } + + if len(paths) == 0 { + paths = ParseGatewayQRCodeRequestFieldPathsNested + } + + for name, subs := range _processPaths(append(paths[:0:0], paths...)) { + _ = subs + switch name { + case "format_id": + + if utf8.RuneCountInString(m.GetFormatId()) > 36 { + return ParseGatewayQRCodeRequestValidationError{ + field: "format_id", + reason: "value length must be at most 36 runes", + } + } + + if !_ParseGatewayQRCodeRequest_FormatId_Pattern.MatchString(m.GetFormatId()) { + return ParseGatewayQRCodeRequestValidationError{ + field: "format_id", + reason: "value does not match regex pattern \"^[a-z0-9](?:[-]?[a-z0-9]){2,}$|^$\"", + } + } + + case "qr_code": + + if l := len(m.GetQrCode()); l < 10 || l > 1024 { + return ParseGatewayQRCodeRequestValidationError{ + field: "qr_code", + reason: "value length must be between 10 and 1024 bytes, inclusive", + } + } + + default: + return ParseGatewayQRCodeRequestValidationError{ + field: name, + reason: "invalid field path", + } + } + } + return nil +} + +// ParseGatewayQRCodeRequestValidationError is the validation error returned by +// ParseGatewayQRCodeRequest.ValidateFields if the designated constraints +// aren't met. +type ParseGatewayQRCodeRequestValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e ParseGatewayQRCodeRequestValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e ParseGatewayQRCodeRequestValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e ParseGatewayQRCodeRequestValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e ParseGatewayQRCodeRequestValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e ParseGatewayQRCodeRequestValidationError) ErrorName() string { + return "ParseGatewayQRCodeRequestValidationError" +} + +// Error satisfies the builtin error interface +func (e ParseGatewayQRCodeRequestValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sParseGatewayQRCodeRequest.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = ParseGatewayQRCodeRequestValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = ParseGatewayQRCodeRequestValidationError{} + +var _ParseGatewayQRCodeRequest_FormatId_Pattern = regexp.MustCompile("^[a-z0-9](?:[-]?[a-z0-9]){2,}$|^$") + +// ValidateFields checks the field values on ParseGatewayQRCodeResponse with +// the rules defined in the proto definition for this message. If any rules +// are violated, an error is returned. +func (m *ParseGatewayQRCodeResponse) ValidateFields(paths ...string) error { + if m == nil { + return nil + } + + if len(paths) == 0 { + paths = ParseGatewayQRCodeResponseFieldPathsNested + } + + for name, subs := range _processPaths(append(paths[:0:0], paths...)) { + _ = subs + switch name { + case "format_id": + // no validation rules for FormatId + case "claim_gateway_request": + + if v, ok := interface{}(m.GetClaimGatewayRequest()).(interface{ ValidateFields(...string) error }); ok { + if err := v.ValidateFields(subs...); err != nil { + return ParseGatewayQRCodeResponseValidationError{ + field: "claim_gateway_request", + reason: "embedded message failed validation", + cause: err, + } + } + } + + default: + return ParseGatewayQRCodeResponseValidationError{ + field: name, + reason: "invalid field path", + } + } + } + return nil +} + +// ParseGatewayQRCodeResponseValidationError is the validation error returned +// by ParseGatewayQRCodeResponse.ValidateFields if the designated constraints +// aren't met. +type ParseGatewayQRCodeResponseValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e ParseGatewayQRCodeResponseValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e ParseGatewayQRCodeResponseValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e ParseGatewayQRCodeResponseValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e ParseGatewayQRCodeResponseValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e ParseGatewayQRCodeResponseValidationError) ErrorName() string { + return "ParseGatewayQRCodeResponseValidationError" +} + +// Error satisfies the builtin error interface +func (e ParseGatewayQRCodeResponseValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sParseGatewayQRCodeResponse.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = ParseGatewayQRCodeResponseValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = ParseGatewayQRCodeResponseValidationError{} + // ValidateFields checks the field values on // GenerateEndDeviceQRCodeRequest_Image with the rules defined in the proto // definition for this message. If any rules are violated, an error is returned. diff --git a/pkg/ttnpb/qrcodegenerator_grpc.pb.go b/pkg/ttnpb/qrcodegenerator_grpc.pb.go index b7c74458b5..2ef92b5fa0 100644 --- a/pkg/ttnpb/qrcodegenerator_grpc.pb.go +++ b/pkg/ttnpb/qrcodegenerator_grpc.pb.go @@ -242,3 +242,96 @@ var EndDeviceQRCodeGenerator_ServiceDesc = grpc.ServiceDesc{ Streams: []grpc.StreamDesc{}, Metadata: "ttn/lorawan/v3/qrcodegenerator.proto", } + +const ( + GatewayQRCodeGenerator_Parse_FullMethodName = "/ttn.lorawan.v3.GatewayQRCodeGenerator/Parse" +) + +// GatewayQRCodeGeneratorClient is the client API for GatewayQRCodeGenerator service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type GatewayQRCodeGeneratorClient interface { + // Parse QR Codes of known formats and return the information contained within. + Parse(ctx context.Context, in *ParseGatewayQRCodeRequest, opts ...grpc.CallOption) (*ParseGatewayQRCodeResponse, error) +} + +type gatewayQRCodeGeneratorClient struct { + cc grpc.ClientConnInterface +} + +func NewGatewayQRCodeGeneratorClient(cc grpc.ClientConnInterface) GatewayQRCodeGeneratorClient { + return &gatewayQRCodeGeneratorClient{cc} +} + +func (c *gatewayQRCodeGeneratorClient) Parse(ctx context.Context, in *ParseGatewayQRCodeRequest, opts ...grpc.CallOption) (*ParseGatewayQRCodeResponse, error) { + out := new(ParseGatewayQRCodeResponse) + err := c.cc.Invoke(ctx, GatewayQRCodeGenerator_Parse_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// GatewayQRCodeGeneratorServer is the server API for GatewayQRCodeGenerator service. +// All implementations must embed UnimplementedGatewayQRCodeGeneratorServer +// for forward compatibility +type GatewayQRCodeGeneratorServer interface { + // Parse QR Codes of known formats and return the information contained within. + Parse(context.Context, *ParseGatewayQRCodeRequest) (*ParseGatewayQRCodeResponse, error) + mustEmbedUnimplementedGatewayQRCodeGeneratorServer() +} + +// UnimplementedGatewayQRCodeGeneratorServer must be embedded to have forward compatible implementations. +type UnimplementedGatewayQRCodeGeneratorServer struct { +} + +func (UnimplementedGatewayQRCodeGeneratorServer) Parse(context.Context, *ParseGatewayQRCodeRequest) (*ParseGatewayQRCodeResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Parse not implemented") +} +func (UnimplementedGatewayQRCodeGeneratorServer) mustEmbedUnimplementedGatewayQRCodeGeneratorServer() { +} + +// UnsafeGatewayQRCodeGeneratorServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to GatewayQRCodeGeneratorServer will +// result in compilation errors. +type UnsafeGatewayQRCodeGeneratorServer interface { + mustEmbedUnimplementedGatewayQRCodeGeneratorServer() +} + +func RegisterGatewayQRCodeGeneratorServer(s grpc.ServiceRegistrar, srv GatewayQRCodeGeneratorServer) { + s.RegisterService(&GatewayQRCodeGenerator_ServiceDesc, srv) +} + +func _GatewayQRCodeGenerator_Parse_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ParseGatewayQRCodeRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GatewayQRCodeGeneratorServer).Parse(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: GatewayQRCodeGenerator_Parse_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GatewayQRCodeGeneratorServer).Parse(ctx, req.(*ParseGatewayQRCodeRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// GatewayQRCodeGenerator_ServiceDesc is the grpc.ServiceDesc for GatewayQRCodeGenerator service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var GatewayQRCodeGenerator_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "ttn.lorawan.v3.GatewayQRCodeGenerator", + HandlerType: (*GatewayQRCodeGeneratorServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Parse", + Handler: _GatewayQRCodeGenerator_Parse_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "ttn/lorawan/v3/qrcodegenerator.proto", +} diff --git a/pkg/ttnpb/qrcodegenerator_json.pb.go b/pkg/ttnpb/qrcodegenerator_json.pb.go index 5dc86c424a..9aabcda718 100644 --- a/pkg/ttnpb/qrcodegenerator_json.pb.go +++ b/pkg/ttnpb/qrcodegenerator_json.pb.go @@ -204,3 +204,57 @@ func (x *ParseEndDeviceQRCodeResponse) UnmarshalProtoJSON(s *jsonplugin.Unmarsha func (x *ParseEndDeviceQRCodeResponse) UnmarshalJSON(b []byte) error { return jsonplugin.DefaultUnmarshalerConfig.Unmarshal(b, x) } + +// MarshalProtoJSON marshals the ParseGatewayQRCodeResponse message to JSON. +func (x *ParseGatewayQRCodeResponse) MarshalProtoJSON(s *jsonplugin.MarshalState) { + if x == nil { + s.WriteNil() + return + } + s.WriteObjectStart() + var wroteField bool + if x.FormatId != "" || s.HasField("format_id") { + s.WriteMoreIf(&wroteField) + s.WriteObjectField("format_id") + s.WriteString(x.FormatId) + } + if x.ClaimGatewayRequest != nil || s.HasField("claim_gateway_request") { + s.WriteMoreIf(&wroteField) + s.WriteObjectField("claim_gateway_request") + x.ClaimGatewayRequest.MarshalProtoJSON(s.WithField("claim_gateway_request")) + } + s.WriteObjectEnd() +} + +// MarshalJSON marshals the ParseGatewayQRCodeResponse to JSON. +func (x *ParseGatewayQRCodeResponse) MarshalJSON() ([]byte, error) { + return jsonplugin.DefaultMarshalerConfig.Marshal(x) +} + +// UnmarshalProtoJSON unmarshals the ParseGatewayQRCodeResponse message from JSON. +func (x *ParseGatewayQRCodeResponse) UnmarshalProtoJSON(s *jsonplugin.UnmarshalState) { + if s.ReadNil() { + return + } + s.ReadObject(func(key string) { + switch key { + default: + s.ReadAny() // ignore unknown field + case "format_id", "formatId": + s.AddField("format_id") + x.FormatId = s.ReadString() + case "claim_gateway_request", "claimGatewayRequest": + if s.ReadNil() { + x.ClaimGatewayRequest = nil + return + } + x.ClaimGatewayRequest = &ClaimGatewayRequest{} + x.ClaimGatewayRequest.UnmarshalProtoJSON(s.WithField("claim_gateway_request", true)) + } + }) +} + +// UnmarshalJSON unmarshals the ParseGatewayQRCodeResponse from JSON. +func (x *ParseGatewayQRCodeResponse) UnmarshalJSON(b []byte) error { + return jsonplugin.DefaultUnmarshalerConfig.Unmarshal(b, x) +} From ca0c286181e7e51167dd7fbd0232ca48dc42aafb Mon Sep 17 00:00:00 2001 From: Vlad Vitan Date: Mon, 2 Sep 2024 17:18:23 +0200 Subject: [PATCH 3/6] qrg: Update error messages, api and translations --- api/ttn/lorawan/v3/api.md | 39 +++++ api/ttn/lorawan/v3/api.swagger.json | 134 ++++++++++++++++-- api/ttn/lorawan/v3/qrcodegenerator.proto | 3 +- config/messages.json | 27 ++++ .../qrcode/gateways/gateways.go | 5 +- .../qrcode/gateways/ttigpro1.go | 8 +- pkg/ttnpb/qrcodegenerator.pb.go | 18 +-- pkg/webui/locales/ja.json | 3 + sdk/js/generated/api-definition.json | 21 +++ sdk/js/generated/api.json | 132 +++++++++++++++++ 10 files changed, 363 insertions(+), 27 deletions(-) diff --git a/api/ttn/lorawan/v3/api.md b/api/ttn/lorawan/v3/api.md index 6386fa8517..0d8e50162a 100644 --- a/api/ttn/lorawan/v3/api.md +++ b/api/ttn/lorawan/v3/api.md @@ -743,10 +743,13 @@ - [Message `GetQRCodeFormatRequest`](#ttn.lorawan.v3.GetQRCodeFormatRequest) - [Message `ParseEndDeviceQRCodeRequest`](#ttn.lorawan.v3.ParseEndDeviceQRCodeRequest) - [Message `ParseEndDeviceQRCodeResponse`](#ttn.lorawan.v3.ParseEndDeviceQRCodeResponse) + - [Message `ParseGatewayQRCodeRequest`](#ttn.lorawan.v3.ParseGatewayQRCodeRequest) + - [Message `ParseGatewayQRCodeResponse`](#ttn.lorawan.v3.ParseGatewayQRCodeResponse) - [Message `QRCodeFormat`](#ttn.lorawan.v3.QRCodeFormat) - [Message `QRCodeFormats`](#ttn.lorawan.v3.QRCodeFormats) - [Message `QRCodeFormats.FormatsEntry`](#ttn.lorawan.v3.QRCodeFormats.FormatsEntry) - [Service `EndDeviceQRCodeGenerator`](#ttn.lorawan.v3.EndDeviceQRCodeGenerator) + - [Service `GatewayQRCodeGenerator`](#ttn.lorawan.v3.GatewayQRCodeGenerator) - [File `ttn/lorawan/v3/regional.proto`](#ttn/lorawan/v3/regional.proto) - [Message `ConcentratorConfig`](#ttn.lorawan.v3.ConcentratorConfig) - [Message `ConcentratorConfig.Channel`](#ttn.lorawan.v3.ConcentratorConfig.Channel) @@ -10542,6 +10545,27 @@ The Pba service allows clients to manage peering through Packet Broker. | `format_id` | [`string`](#string) | | Identifier of the format used to successfully parse the QR code data. | | `end_device_template` | [`EndDeviceTemplate`](#ttn.lorawan.v3.EndDeviceTemplate) | | | +### Message `ParseGatewayQRCodeRequest` + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| `format_id` | [`string`](#string) | | QR code format identifier. If this field is not specified, the server will default to ttigpro1. | +| `qr_code` | [`bytes`](#bytes) | | Raw QR code contents. | + +#### Field Rules + +| Field | Validations | +| ----- | ----------- | +| `format_id` |

`string.max_len`: `36`

`string.pattern`: `^[a-z0-9](?:[-]?[a-z0-9]){2,}$|^$`

| +| `qr_code` |

`bytes.min_len`: `10`

`bytes.max_len`: `1024`

| + +### Message `ParseGatewayQRCodeResponse` + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| `format_id` | [`string`](#string) | | Identifier of the format used to successfully parse the QR code data. | +| `claim_gateway_request` | [`ClaimGatewayRequest`](#ttn.lorawan.v3.ClaimGatewayRequest) | | | + ### Message `QRCodeFormat` | Field | Type | Label | Description | @@ -10597,6 +10621,21 @@ The EndDeviceQRCodeGenerator service provides functionality to generate and pars | `Parse` | `POST` | `/api/v3/qr-codes/end-devices/parse` | `*` | | `Parse` | `POST` | `/api/v3/qr-codes/end-devices/{format_id}/parse` | `*` | +### Service `GatewayQRCodeGenerator` + +The GatewayQRCodeGenerator service provides functionality to generate and parse QR codes for gateways. + +| Method Name | Request Type | Response Type | Description | +| ----------- | ------------ | ------------- | ------------| +| `Parse` | [`ParseGatewayQRCodeRequest`](#ttn.lorawan.v3.ParseGatewayQRCodeRequest) | [`ParseGatewayQRCodeResponse`](#ttn.lorawan.v3.ParseGatewayQRCodeResponse) | Parse QR Codes of known formats and return the information contained within. | + +#### HTTP bindings + +| Method Name | Method | Pattern | Body | +| ----------- | ------ | ------- | ---- | +| `Parse` | `POST` | `/api/v3/qr-codes/gateways/parse` | `*` | +| `Parse` | `POST` | `/api/v3/qr-codes/gateways/{format_id}/parse` | `*` | + ## File `ttn/lorawan/v3/regional.proto` ### Message `ConcentratorConfig` diff --git a/api/ttn/lorawan/v3/api.swagger.json b/api/ttn/lorawan/v3/api.swagger.json index dfdc12b208..290e3ad963 100644 --- a/api/ttn/lorawan/v3/api.swagger.json +++ b/api/ttn/lorawan/v3/api.swagger.json @@ -245,6 +245,9 @@ "name": "EndDeviceQRCodeGenerator", "description": "Generate and parse end device QR codes." }, + { + "name": "GatewayQRCodeGenerator" + }, { "name": "EntityRegistrySearch", "description": "Search for entities in the Entity Registry." @@ -13142,7 +13145,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/EndDeviceQRCodeGeneratorParseBody" + "$ref": "#/definitions/v3EndDeviceQRCodeGeneratorParseBody" } } ], @@ -13151,6 +13154,79 @@ ] } }, + "/qr-codes/gateways/parse": { + "post": { + "summary": "Parse QR Codes of known formats and return the information contained within.", + "operationId": "GatewayQRCodeGenerator_Parse", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v3ParseGatewayQRCodeResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/googlerpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v3ParseGatewayQRCodeRequest" + } + } + ], + "tags": [ + "GatewayQRCodeGenerator" + ] + } + }, + "/qr-codes/gateways/{format_id}/parse": { + "post": { + "summary": "Parse QR Codes of known formats and return the information contained within.", + "operationId": "GatewayQRCodeGenerator_Parse2", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v3ParseGatewayQRCodeResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/googlerpcStatus" + } + } + }, + "parameters": [ + { + "name": "format_id", + "description": "QR code format identifier.\nIf this field is not specified, the server will default to ttigpro1.", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v3GatewayQRCodeGeneratorParseBody" + } + } + ], + "tags": [ + "GatewayQRCodeGenerator" + ] + } + }, "/search/accounts": { "get": { "summary": "Search for accounts that match the conditions specified in the request.", @@ -17413,16 +17489,6 @@ } } }, - "EndDeviceQRCodeGeneratorParseBody": { - "type": "object", - "properties": { - "qr_code": { - "type": "string", - "format": "byte", - "description": "Raw QR code contents." - } - } - }, "EventAuthentication": { "type": "object", "properties": { @@ -23054,6 +23120,16 @@ } } }, + "v3EndDeviceQRCodeGeneratorParseBody": { + "type": "object", + "properties": { + "qr_code": { + "type": "string", + "format": "byte", + "description": "Raw QR code contents." + } + } + }, "v3EndDeviceRegistryCreateBody": { "type": "object", "properties": { @@ -24281,6 +24357,16 @@ }, "description": "GatewayDown contains downlink messages for the gateway." }, + "v3GatewayQRCodeGeneratorParseBody": { + "type": "object", + "properties": { + "qr_code": { + "type": "string", + "format": "byte", + "description": "Raw QR code contents." + } + } + }, "v3GatewayRadio": { "type": "object", "properties": { @@ -27953,6 +28039,32 @@ } } }, + "v3ParseGatewayQRCodeRequest": { + "type": "object", + "properties": { + "format_id": { + "type": "string", + "description": "QR code format identifier.\nIf this field is not specified, the server will default to ttigpro1." + }, + "qr_code": { + "type": "string", + "format": "byte", + "description": "Raw QR code contents." + } + } + }, + "v3ParseGatewayQRCodeResponse": { + "type": "object", + "properties": { + "format_id": { + "type": "string", + "description": "Identifier of the format used to successfully parse the QR code data." + }, + "claim_gateway_request": { + "$ref": "#/definitions/v3ClaimGatewayRequest" + } + } + }, "v3PayloadFormatter": { "type": "string", "enum": [ diff --git a/api/ttn/lorawan/v3/qrcodegenerator.proto b/api/ttn/lorawan/v3/qrcodegenerator.proto index be548b626c..97d7dce97b 100644 --- a/api/ttn/lorawan/v3/qrcodegenerator.proto +++ b/api/ttn/lorawan/v3/qrcodegenerator.proto @@ -20,10 +20,10 @@ import "google/api/annotations.proto"; import "google/protobuf/empty.proto"; import "google/protobuf/field_mask.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; +import "ttn/lorawan/v3/deviceclaimingserver.proto"; import "ttn/lorawan/v3/end_device.proto"; import "ttn/lorawan/v3/picture.proto"; import "validate/validate.proto"; -import "ttn/lorawan/v3/deviceclaimingserver.proto"; option go_package = "go.thethings.network/lorawan-stack/v3/pkg/ttnpb"; @@ -152,7 +152,6 @@ message ParseGatewayQRCodeResponse { ClaimGatewayRequest claim_gateway_request = 2; } - // The GatewayQRCodeGenerator service provides functionality to generate and parse QR codes for gateways. service GatewayQRCodeGenerator { // Parse QR Codes of known formats and return the information contained within. diff --git a/config/messages.json b/config/messages.json index b766109e77..d7461ca92f 100644 --- a/config/messages.json +++ b/config/messages.json @@ -9026,6 +9026,33 @@ "file": "enddevices.go" } }, + "error:pkg/qrcodegenerator/qrcode/gateways:invalid_format": { + "translations": { + "en": "invalid format" + }, + "description": { + "package": "pkg/qrcodegenerator/qrcode/gateways", + "file": "gateways.go" + } + }, + "error:pkg/qrcodegenerator/qrcode/gateways:invalid_length": { + "translations": { + "en": "invalid length" + }, + "description": { + "package": "pkg/qrcodegenerator/qrcode/gateways", + "file": "gateways.go" + } + }, + "error:pkg/qrcodegenerator/qrcode/gateways:unknown_format": { + "translations": { + "en": "format unknown" + }, + "description": { + "package": "pkg/qrcodegenerator/qrcode/gateways", + "file": "gateways.go" + } + }, "error:pkg/qrcodegenerator:format_not_found": { "translations": { "en": "format `{id}` not found" diff --git a/pkg/qrcodegenerator/qrcode/gateways/gateways.go b/pkg/qrcodegenerator/qrcode/gateways/gateways.go index 19c774d05a..6e64a6021f 100644 --- a/pkg/qrcodegenerator/qrcode/gateways/gateways.go +++ b/pkg/qrcodegenerator/qrcode/gateways/gateways.go @@ -23,10 +23,9 @@ import ( ) var ( - errCharacter = errors.DefineInvalidArgument("character", "invalid character `{c}`") errUnknownFormat = errors.DefineInvalidArgument("unknown_format", "format unknown") - errInvalidLength = errors.DefineInvalidArgument("length", "invalid length") - errFormat = errors.DefineInvalidArgument("format", "invalid format") + errInvalidLength = errors.DefineInvalidArgument("invalid_length", "invalid length") + errInvalidFormat = errors.DefineInvalidArgument("invalid_format", "invalid format") ) // Format is a gateway QR code format. diff --git a/pkg/qrcodegenerator/qrcode/gateways/ttigpro1.go b/pkg/qrcodegenerator/qrcode/gateways/ttigpro1.go index 1b592f67e2..535ad78ef9 100644 --- a/pkg/qrcodegenerator/qrcode/gateways/ttigpro1.go +++ b/pkg/qrcodegenerator/qrcode/gateways/ttigpro1.go @@ -42,8 +42,8 @@ var ttigpro1Regex = regexp.MustCompile(`^https://ttig\.pro/c/([a-f0-9]{16})/([a- func (m *ttigpro1) UnmarshalText(text []byte) error { // Match the URL against the pattern matches := ttigpro1Regex.FindStringSubmatch(string(text)) - if matches == nil { - return errFormat + if matches == nil || len(matches) != 3 { + return errInvalidFormat } if err := m.gatewayEUI.UnmarshalText([]byte(matches[1])); err != nil { @@ -52,6 +52,10 @@ func (m *ttigpro1) UnmarshalText(text []byte) error { m.ownerToken = matches[2] + if len(m.ownerToken) != ownerTokenLength { + return errInvalidLength + } + return nil } diff --git a/pkg/ttnpb/qrcodegenerator.pb.go b/pkg/ttnpb/qrcodegenerator.pb.go index a50ffef04e..c063ccd948 100644 --- a/pkg/ttnpb/qrcodegenerator.pb.go +++ b/pkg/ttnpb/qrcodegenerator.pb.go @@ -614,14 +614,14 @@ var file_ttn_lorawan_v3_qrcodegenerator_proto_rawDesc = []byte{ 0x6f, 0x74, 0x6f, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x2d, 0x67, 0x65, 0x6e, 0x2d, 0x6f, 0x70, 0x65, 0x6e, 0x61, 0x70, 0x69, 0x76, 0x32, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x74, 0x74, 0x6e, 0x2f, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, - 0x2f, 0x76, 0x33, 0x2f, 0x65, 0x6e, 0x64, 0x5f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1c, 0x74, 0x74, 0x6e, 0x2f, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, - 0x6e, 0x2f, 0x76, 0x33, 0x2f, 0x70, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x1a, 0x17, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x61, 0x6c, - 0x69, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x29, 0x74, 0x74, 0x6e, - 0x2f, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2f, 0x76, 0x33, 0x2f, 0x64, 0x65, 0x76, 0x69, - 0x63, 0x65, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x6f, 0x74, 0x6f, 0x1a, 0x29, 0x74, 0x74, 0x6e, 0x2f, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, + 0x2f, 0x76, 0x33, 0x2f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x69, + 0x6e, 0x67, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, + 0x74, 0x74, 0x6e, 0x2f, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2f, 0x76, 0x33, 0x2f, 0x65, + 0x6e, 0x64, 0x5f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, + 0x1c, 0x74, 0x74, 0x6e, 0x2f, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2f, 0x76, 0x33, 0x2f, + 0x70, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x17, 0x76, + 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x92, 0x01, 0x0a, 0x0c, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x1b, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x18, 0x64, 0x52, 0x04, @@ -834,9 +834,9 @@ func file_ttn_lorawan_v3_qrcodegenerator_proto_init() { if File_ttn_lorawan_v3_qrcodegenerator_proto != nil { return } + file_ttn_lorawan_v3_deviceclaimingserver_proto_init() file_ttn_lorawan_v3_end_device_proto_init() file_ttn_lorawan_v3_picture_proto_init() - file_ttn_lorawan_v3_deviceclaimingserver_proto_init() if !protoimpl.UnsafeEnabled { file_ttn_lorawan_v3_qrcodegenerator_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*QRCodeFormat); i { diff --git a/pkg/webui/locales/ja.json b/pkg/webui/locales/ja.json index 8e18e357fe..9bea83f15d 100644 --- a/pkg/webui/locales/ja.json +++ b/pkg/webui/locales/ja.json @@ -2792,6 +2792,9 @@ "error:pkg/qrcodegenerator/qrcode/enddevices:no_dev_eui": "DevEUIがありません", "error:pkg/qrcodegenerator/qrcode/enddevices:no_join_eui": "JoinEUIがありません", "error:pkg/qrcodegenerator/qrcode/enddevices:unknown_format": "形式が不明", + "error:pkg/qrcodegenerator/qrcode/gateways:invalid_format": "", + "error:pkg/qrcodegenerator/qrcode/gateways:invalid_length": "", + "error:pkg/qrcodegenerator/qrcode/gateways:unknown_format": "", "error:pkg/qrcodegenerator:format_not_found": "フォーマット `{id}` が見つかりません", "error:pkg/qrcodegenerator:unauthenticated": "呼び出しが認証されていません", "error:pkg/ratelimit:invalid_rate": "プロファイル `{name}` の無効なレート `{rate}`", diff --git a/sdk/js/generated/api-definition.json b/sdk/js/generated/api-definition.json index e554911d9f..bffb562105 100644 --- a/sdk/js/generated/api-definition.json +++ b/sdk/js/generated/api-definition.json @@ -7147,6 +7147,27 @@ ] } }, + "GatewayQRCodeGenerator": { + "Parse": { + "file": "ttn/lorawan/v3/qrcodegenerator.proto", + "http": [ + { + "method": "post", + "pattern": "/qr-codes/gateways/parse", + "body": "*", + "parameters": [] + }, + { + "method": "post", + "pattern": "/qr-codes/gateways/{format_id}/parse", + "body": "*", + "parameters": [ + "format_id" + ] + } + ] + } + }, "EndDeviceRegistrySearch": { "SearchEndDevices": { "file": "ttn/lorawan/v3/search_services.proto", diff --git a/sdk/js/generated/api.json b/sdk/js/generated/api.json index 7aa60fc4e2..83ef9e1986 100644 --- a/sdk/js/generated/api.json +++ b/sdk/js/generated/api.json @@ -48254,6 +48254,102 @@ } ] }, + { + "name": "ParseGatewayQRCodeRequest", + "longName": "ParseGatewayQRCodeRequest", + "fullName": "ttn.lorawan.v3.ParseGatewayQRCodeRequest", + "description": "", + "hasExtensions": false, + "hasFields": true, + "hasOneofs": false, + "extensions": [], + "fields": [ + { + "name": "format_id", + "description": "QR code format identifier.\nIf this field is not specified, the server will default to ttigpro1.", + "label": "", + "type": "string", + "longType": "string", + "fullType": "string", + "ismap": false, + "isoneof": false, + "oneofdecl": "", + "defaultValue": "", + "options": { + "validate.rules": [ + { + "name": "string.max_len", + "value": 36 + }, + { + "name": "string.pattern", + "value": "^[a-z0-9](?:[-]?[a-z0-9]){2,}$|^$" + } + ] + } + }, + { + "name": "qr_code", + "description": "Raw QR code contents.", + "label": "", + "type": "bytes", + "longType": "bytes", + "fullType": "bytes", + "ismap": false, + "isoneof": false, + "oneofdecl": "", + "defaultValue": "", + "options": { + "validate.rules": [ + { + "name": "bytes.min_len", + "value": 10 + }, + { + "name": "bytes.max_len", + "value": 1024 + } + ] + } + } + ] + }, + { + "name": "ParseGatewayQRCodeResponse", + "longName": "ParseGatewayQRCodeResponse", + "fullName": "ttn.lorawan.v3.ParseGatewayQRCodeResponse", + "description": "", + "hasExtensions": false, + "hasFields": true, + "hasOneofs": false, + "extensions": [], + "fields": [ + { + "name": "format_id", + "description": "Identifier of the format used to successfully parse the QR code data.", + "label": "", + "type": "string", + "longType": "string", + "fullType": "string", + "ismap": false, + "isoneof": false, + "oneofdecl": "", + "defaultValue": "" + }, + { + "name": "claim_gateway_request", + "description": "", + "label": "", + "type": "ClaimGatewayRequest", + "longType": "ClaimGatewayRequest", + "fullType": "ttn.lorawan.v3.ClaimGatewayRequest", + "ismap": false, + "isoneof": false, + "oneofdecl": "", + "defaultValue": "" + } + ] + }, { "name": "QRCodeFormat", "longName": "QRCodeFormat", @@ -48494,6 +48590,42 @@ } } ] + }, + { + "name": "GatewayQRCodeGenerator", + "longName": "GatewayQRCodeGenerator", + "fullName": "ttn.lorawan.v3.GatewayQRCodeGenerator", + "description": "The GatewayQRCodeGenerator service provides functionality to generate and parse QR codes for gateways.", + "methods": [ + { + "name": "Parse", + "description": "Parse QR Codes of known formats and return the information contained within.", + "requestType": "ParseGatewayQRCodeRequest", + "requestLongType": "ParseGatewayQRCodeRequest", + "requestFullType": "ttn.lorawan.v3.ParseGatewayQRCodeRequest", + "requestStreaming": false, + "responseType": "ParseGatewayQRCodeResponse", + "responseLongType": "ParseGatewayQRCodeResponse", + "responseFullType": "ttn.lorawan.v3.ParseGatewayQRCodeResponse", + "responseStreaming": false, + "options": { + "google.api.http": { + "rules": [ + { + "method": "POST", + "pattern": "/qr-codes/gateways/parse", + "body": "*" + }, + { + "method": "POST", + "pattern": "/qr-codes/gateways/{format_id}/parse", + "body": "*" + } + ] + } + } + } + ] } ] }, From dcd2455f52cb8a18cde37cec0fd5cd288c3a71fa Mon Sep 17 00:00:00 2001 From: Vlad Vitan Date: Tue, 3 Sep 2024 15:43:37 +0200 Subject: [PATCH 4/6] qrg: Register gateway qr service grpc handler --- .../grpc_gateways_test.go | 2 +- pkg/qrcodegenerator/grpc_gateways.go | 12 +++++++--- pkg/qrcodegenerator/grpc_gateways_test.go | 14 +++++++++--- .../qrcode/gateways/gateways.go | 8 ++++--- .../qrcode/gateways/gateways_test.go | 22 +++++++++++++------ .../qrcode/gateways/ttigpro1.go | 18 +++++++-------- .../qrcode/gateways/ttigpro1_test.go | 13 +++++++++++ pkg/qrcodegenerator/qrcodegenerator.go | 11 +++++----- 8 files changed, 69 insertions(+), 31 deletions(-) diff --git a/pkg/deviceclaimingserver/grpc_gateways_test.go b/pkg/deviceclaimingserver/grpc_gateways_test.go index baf104822a..2abf729324 100644 --- a/pkg/deviceclaimingserver/grpc_gateways_test.go +++ b/pkg/deviceclaimingserver/grpc_gateways_test.go @@ -48,7 +48,7 @@ var ( authorizedCallOpt = grpc.PerRPCCredentials(authorizedMD) ) -func TestGatewayClaimingServer(t *testing.T) { // nolint:paralleltest +func TestGatewayClaimingServer(t *testing.T) { //nolint:paralleltest a := assertions.New(t) ctx := log.NewContext(test.Context(), test.GetLogger(t)) ctx, cancelCtx := context.WithCancel(ctx) diff --git a/pkg/qrcodegenerator/grpc_gateways.go b/pkg/qrcodegenerator/grpc_gateways.go index 1135045aa4..0d71101cf7 100644 --- a/pkg/qrcodegenerator/grpc_gateways.go +++ b/pkg/qrcodegenerator/grpc_gateways.go @@ -28,7 +28,10 @@ type gatewayQRCodeGeneratorServer struct { } // GetFormat implements EndDeviceQRCodeGenerator. -func (s *gatewayQRCodeGeneratorServer) GetFormat(ctx context.Context, req *ttnpb.GetQRCodeFormatRequest) (*ttnpb.QRCodeFormat, error) { +func (s *gatewayQRCodeGeneratorServer) GetFormat( + ctx context.Context, + req *ttnpb.GetQRCodeFormatRequest, +) (*ttnpb.QRCodeFormat, error) { _, err := rpcmetadata.WithForwardedAuth(ctx, s.QRG.AllowInsecureForCredentials()) if err != nil { return nil, err @@ -41,7 +44,10 @@ func (s *gatewayQRCodeGeneratorServer) GetFormat(ctx context.Context, req *ttnpb } // Parse implements EndDeviceQRCodeGenerator. -func (s *gatewayQRCodeGeneratorServer) Parse(ctx context.Context, req *ttnpb.ParseGatewayQRCodeRequest) (*ttnpb.ParseGatewayQRCodeResponse, error) { +func (s *gatewayQRCodeGeneratorServer) Parse( + ctx context.Context, + req *ttnpb.ParseGatewayQRCodeRequest, +) (*ttnpb.ParseGatewayQRCodeResponse, error) { _, err := rpcmetadata.WithForwardedAuth(ctx, s.QRG.AllowInsecureForCredentials()) if err != nil { return nil, err @@ -57,7 +63,7 @@ func (s *gatewayQRCodeGeneratorServer) Parse(ctx context.Context, req *ttnpb.Par ClaimGatewayRequest: &ttnpb.ClaimGatewayRequest{ SourceGateway: &ttnpb.ClaimGatewayRequest_AuthenticatedIdentifiers_{ AuthenticatedIdentifiers: &ttnpb.ClaimGatewayRequest_AuthenticatedIdentifiers{ - GatewayEui: []byte(data.GatewayEUI()), + GatewayEui: data.GatewayEUI().Bytes(), AuthenticationCode: []byte(data.OwnerToken()), }, }, diff --git a/pkg/qrcodegenerator/grpc_gateways_test.go b/pkg/qrcodegenerator/grpc_gateways_test.go index 48bdbabc5c..a5876a5d47 100644 --- a/pkg/qrcodegenerator/grpc_gateways_test.go +++ b/pkg/qrcodegenerator/grpc_gateways_test.go @@ -22,7 +22,7 @@ import ( componenttest "go.thethings.network/lorawan-stack/v3/pkg/component/test" "go.thethings.network/lorawan-stack/v3/pkg/errors" "go.thethings.network/lorawan-stack/v3/pkg/log" - . "go.thethings.network/lorawan-stack/v3/pkg/qrcodegenerator" + "go.thethings.network/lorawan-stack/v3/pkg/qrcodegenerator" "go.thethings.network/lorawan-stack/v3/pkg/qrcodegenerator/qrcode/gateways" "go.thethings.network/lorawan-stack/v3/pkg/ttnpb" "go.thethings.network/lorawan-stack/v3/pkg/util/test" @@ -30,16 +30,21 @@ import ( ) func TestGatewayQRCodeParsing(t *testing.T) { + t.Parallel() + a := assertions.New(t) ctx := log.NewContext(test.Context(), test.GetLogger(t)) c := componenttest.NewComponent(t, &component.Config{}) ttigpro1 := new(gateways.TTIGPRO1Format) - qrg, err := New(c, &Config{}, WithGatewayFormat(ttigpro1.ID(), ttigpro1)) + qrg, err := qrcodegenerator.New(c, + &qrcodegenerator.Config{}, + qrcodegenerator.WithGatewayFormat(ttigpro1.ID(), ttigpro1), + ) test.Must(qrg, err) componenttest.StartComponent(t, c) - defer c.Close() + t.Cleanup(func() { c.Close() }) mustHavePeer(ctx, c, ttnpb.ClusterRole_QR_CODE_GENERATOR) @@ -117,7 +122,10 @@ func TestGatewayQRCodeParsing(t *testing.T) { }, }, } { + tc := tc t.Run(tc.Name, func(t *testing.T) { + t.Parallel() + resp, err := client.Parse(ctx, &ttnpb.ParseGatewayQRCodeRequest{ FormatId: tc.FormatID, QrCode: tc.GetQRData(), diff --git a/pkg/qrcodegenerator/qrcode/gateways/gateways.go b/pkg/qrcodegenerator/qrcode/gateways/gateways.go index 6e64a6021f..b50d3d4ddd 100644 --- a/pkg/qrcodegenerator/qrcode/gateways/gateways.go +++ b/pkg/qrcodegenerator/qrcode/gateways/gateways.go @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package gateways provides a QR code parser for gateways. package gateways import ( @@ -20,6 +21,7 @@ import ( "go.thethings.network/lorawan-stack/v3/pkg/errors" "go.thethings.network/lorawan-stack/v3/pkg/ttnpb" + "go.thethings.network/lorawan-stack/v3/pkg/types" ) var ( @@ -38,7 +40,7 @@ type Format interface { type Data interface { // FormatID returns the ID of the format used to parse the QR Code data. FormatID() string - GatewayEUI() string + GatewayEUI() types.EUI64 OwnerToken() string encoding.TextUnmarshaler } @@ -54,13 +56,13 @@ type Server struct { } // New returns a new Server. -func New(ctx context.Context) *Server { +func New(_ context.Context) *Server { s := &Server{ // Newer formats should be added to this slice first to // preferentially match with those first. gatewayFormats: []gatewayFormat{ { - id: formatIDttigpro1, + id: formatIDTTIGPRO1, format: new(TTIGPRO1Format), }, }, diff --git a/pkg/qrcodegenerator/qrcode/gateways/gateways_test.go b/pkg/qrcodegenerator/qrcode/gateways/gateways_test.go index b362ff3e24..c5d152ee4b 100644 --- a/pkg/qrcodegenerator/qrcode/gateways/gateways_test.go +++ b/pkg/qrcodegenerator/qrcode/gateways/gateways_test.go @@ -20,34 +20,42 @@ import ( "testing" "github.com/smarty/assertions" - . "go.thethings.network/lorawan-stack/v3/pkg/qrcodegenerator/qrcode/gateways" + "go.thethings.network/lorawan-stack/v3/pkg/qrcodegenerator/qrcode/gateways" + "go.thethings.network/lorawan-stack/v3/pkg/types" "go.thethings.network/lorawan-stack/v3/pkg/util/test" "go.thethings.network/lorawan-stack/v3/pkg/util/test/assertions/should" ) func TestParseGatewaysAuthenticationCodes(t *testing.T) { + t.Parallel() + for i, tc := range []struct { - FormatID string - Data []byte - ExpectedEUI, + FormatID string + Data []byte + ExpectedEUI types.EUI64 ExpectedOwnerToken string }{ { FormatID: "ttigpro1", Data: []byte("https://ttig.pro/c/ec656efffe000128/abcdef123456"), - ExpectedEUI: "ec656efffe000128", + ExpectedEUI: types.EUI64{0xec, 0x65, 0x6e, 0xff, 0xfe, 0x00, 0x01, 0x28}, ExpectedOwnerToken: "abcdef123456", }, } { + tc := tc + t.Run(strconv.Itoa(i), func(t *testing.T) { + t.Parallel() a := assertions.New(t) - qrCode := New(context.Background()) + qrCode := gateways.New(context.Background()) d, err := qrCode.Parse(tc.FormatID, tc.Data) data := test.Must(d, err) - a.So(data, should.NotBeNil) + a.So(data.FormatID(), should.Equal, tc.FormatID) + a.So(data.GatewayEUI(), should.Resemble, tc.ExpectedEUI) + a.So(data.OwnerToken(), should.Equal, tc.ExpectedOwnerToken) }) } } diff --git a/pkg/qrcodegenerator/qrcode/gateways/ttigpro1.go b/pkg/qrcodegenerator/qrcode/gateways/ttigpro1.go index 535ad78ef9..e42284121f 100644 --- a/pkg/qrcodegenerator/qrcode/gateways/ttigpro1.go +++ b/pkg/qrcodegenerator/qrcode/gateways/ttigpro1.go @@ -22,7 +22,7 @@ import ( ) const ( - formatIDttigpro1 = "ttigpro1" + formatIDTTIGPRO1 = "ttigpro1" euiLength = 16 ownerTokenLength = 12 @@ -35,7 +35,7 @@ type ttigpro1 struct { } // ttigpro1Regex is the regular expression to match the TTIGPRO1 format. -// The format is as follows: https://ttig.pro/c/{16 lowercase base16 chars}/{12 base62 chars} +// The format is as follows: https://ttig.pro/c/{16 lowercase base16 chars}/{12 base62 chars}. var ttigpro1Regex = regexp.MustCompile(`^https://ttig\.pro/c/([a-f0-9]{16})/([a-z0-9]{12})$`) // UnmarshalText implements the TextUnmarshaler interface. @@ -60,16 +60,16 @@ func (m *ttigpro1) UnmarshalText(text []byte) error { } // FormatID implements the Data interface. -func (m *ttigpro1) FormatID() string { - return formatIDttigpro1 +func (*ttigpro1) FormatID() string { + return formatIDTTIGPRO1 } -func (m *ttigpro1) GatewayEUI() string { - return m.gatewayEUI.String() +func (m *ttigpro1) GatewayEUI() types.EUI64 { + return m.gatewayEUI } func (m *ttigpro1) OwnerToken() string { - return string(m.ownerToken) + return m.ownerToken } // TTIGPRO1Format implements the TTIGPRO1 Format. @@ -79,7 +79,7 @@ type TTIGPRO1Format struct{} func (TTIGPRO1Format) Format() *ttnpb.QRCodeFormat { return &ttnpb.QRCodeFormat{ Name: "TTIGPRO1", - Description: "TTI QR code format for gateway devices.", + Description: "QR code format for The Things Indoor Gateway Pro.", FieldMask: ttnpb.FieldMask( "ids.eui", "claim_authentication_code.secret.value", @@ -89,7 +89,7 @@ func (TTIGPRO1Format) Format() *ttnpb.QRCodeFormat { // ID is the identifier of the format as a string. func (TTIGPRO1Format) ID() string { - return formatIDttigpro1 + return formatIDTTIGPRO1 } // New implements the Format interface. diff --git a/pkg/qrcodegenerator/qrcode/gateways/ttigpro1_test.go b/pkg/qrcodegenerator/qrcode/gateways/ttigpro1_test.go index 77de18bfe9..2d404a7495 100644 --- a/pkg/qrcodegenerator/qrcode/gateways/ttigpro1_test.go +++ b/pkg/qrcodegenerator/qrcode/gateways/ttigpro1_test.go @@ -24,7 +24,11 @@ import ( ) func TestTTIGPRO1(t *testing.T) { + t.Parallel() + t.Run("Decode", func(t *testing.T) { + t.Parallel() + for _, tc := range []struct { Name string Data []byte @@ -43,6 +47,7 @@ func TestTTIGPRO1(t *testing.T) { Name: "InvalidURLPrefix", Data: []byte("https://example.com/c/ec656efffe000128/abcdef12"), ErrorAssertion: func(t *testing.T, err error) bool { + t.Helper() return assertions.New(t).So(errors.IsInvalidArgument(err), should.BeTrue) }, }, @@ -50,6 +55,7 @@ func TestTTIGPRO1(t *testing.T) { Name: "Invalid/EUINotLowercase", Data: []byte("https://ttig.pro/c/EC656effFe000128/abcdef12"), ErrorAssertion: func(t *testing.T, err error) bool { + t.Helper() return assertions.New(t).So(errors.IsInvalidArgument(err), should.BeTrue) }, }, @@ -57,6 +63,7 @@ func TestTTIGPRO1(t *testing.T) { Name: "Invalid/EUILength", Data: []byte("https://ttig.pro/c/ec656efffe00012/abcdef12"), ErrorAssertion: func(t *testing.T, err error) bool { + t.Helper() return assertions.New(t).So(errors.IsInvalidArgument(err), should.BeTrue) }, }, @@ -64,6 +71,7 @@ func TestTTIGPRO1(t *testing.T) { Name: "Invalid/EUINotBase16", Data: []byte("https://ttig.pro/c/ec656efffe00012g/abcdef12"), ErrorAssertion: func(t *testing.T, err error) bool { + t.Helper() return assertions.New(t).So(errors.IsInvalidArgument(err), should.BeTrue) }, }, @@ -71,6 +79,7 @@ func TestTTIGPRO1(t *testing.T) { Name: "Invalid/OwnerTokenLength", Data: []byte("https://ttig.pro/c/ec656efffe000128/abcdef123"), ErrorAssertion: func(t *testing.T, err error) bool { + t.Helper() return assertions.New(t).So(errors.IsInvalidArgument(err), should.BeTrue) }, }, @@ -78,11 +87,15 @@ func TestTTIGPRO1(t *testing.T) { Name: "Invalid/OwnerTokenNotBase62", Data: []byte("https://ttig.pro/c/ec656efffe000128/abcdef12!"), ErrorAssertion: func(t *testing.T, err error) bool { + t.Helper() return assertions.New(t).So(errors.IsInvalidArgument(err), should.BeTrue) }, }, } { + tc := tc t.Run(tc.Name, func(t *testing.T) { + t.Parallel() + a := assertions.New(t) var data ttigpro1 diff --git a/pkg/qrcodegenerator/qrcodegenerator.go b/pkg/qrcodegenerator/qrcodegenerator.go index ed52d3a26a..78874dd3b9 100644 --- a/pkg/qrcodegenerator/qrcodegenerator.go +++ b/pkg/qrcodegenerator/qrcodegenerator.go @@ -47,16 +47,16 @@ type QRCodeGenerator struct { var errFormatNotFound = errors.DefineNotFound("format_not_found", "format `{id}` not found") // New returns a new *QRCodeGenerator. -func New(c *component.Component, conf *Config, opts ...Option) (*QRCodeGenerator, error) { +func New(c *component.Component, _ *Config, opts ...Option) (*QRCodeGenerator, error) { ctx := log.NewContextWithField(c.Context(), "namespace", "qrcodegenerator") qrg := &QRCodeGenerator{ Component: c, ctx: ctx, } qrg.grpc.endDeviceQRCodeGenerator = &endDeviceQRCodeGeneratorServer{QRG: qrg} - qrg.endDevices = enddevices.New(ctx) - qrg.grpc.gatewayQRCodeGenerator = &gatewayQRCodeGeneratorServer{QRG: qrg} + + qrg.endDevices = enddevices.New(ctx) qrg.gateways = gateways.New(ctx) c.RegisterGRPC(qrg) @@ -91,7 +91,7 @@ func (qrg *QRCodeGenerator) Context() context.Context { } // Roles returns the roles that the QR Code Generator fulfills. -func (qrg *QRCodeGenerator) Roles() []ttnpb.ClusterRole { +func (*QRCodeGenerator) Roles() []ttnpb.ClusterRole { return []ttnpb.ClusterRole{ttnpb.ClusterRole_QR_CODE_GENERATOR} } @@ -103,5 +103,6 @@ func (qrg *QRCodeGenerator) RegisterServices(s *grpc.Server) { // RegisterHandlers registers gRPC handlers. func (qrg *QRCodeGenerator) RegisterHandlers(s *runtime.ServeMux, conn *grpc.ClientConn) { - ttnpb.RegisterEndDeviceQRCodeGeneratorHandler(qrg.Context(), s, conn) + ttnpb.RegisterEndDeviceQRCodeGeneratorHandler(qrg.Context(), s, conn) //nolint:errcheck + ttnpb.RegisterGatewayQRCodeGeneratorHandler(qrg.Context(), s, conn) //nolint:errcheck } From d780ef204cbc053880a8d0a89fd5d417f925ae72 Mon Sep 17 00:00:00 2001 From: Vlad Vitan Date: Mon, 9 Sep 2024 13:56:40 +0200 Subject: [PATCH 5/6] qrg: Update parse gateway qr response --- api/ttn/lorawan/v3/api.md | 11 +- api/ttn/lorawan/v3/api.swagger.json | 11 +- api/ttn/lorawan/v3/qrcodegenerator.proto | 21 +- pkg/qrcodegenerator/grpc_gateways.go | 12 +- pkg/qrcodegenerator/grpc_gateways_test.go | 2 + .../qrcode/gateways/gateways.go | 5 +- .../qrcode/gateways/gateways_test.go | 3 +- .../qrcode/gateways/ttigpro1.go | 19 +- pkg/qrcodegenerator/qrcodegenerator.go | 2 +- pkg/ttnpb/qrcodegenerator.pb.go | 360 +++++++++--------- pkg/ttnpb/qrcodegenerator.pb.paths.fm.go | 31 +- pkg/ttnpb/qrcodegenerator.pb.setters.fm.go | 38 +- pkg/ttnpb/qrcodegenerator.pb.validate.go | 15 +- pkg/ttnpb/qrcodegenerator_json.pb.go | 25 +- sdk/js/generated/api.json | 30 +- 15 files changed, 309 insertions(+), 276 deletions(-) diff --git a/api/ttn/lorawan/v3/api.md b/api/ttn/lorawan/v3/api.md index 0d8e50162a..cba83db331 100644 --- a/api/ttn/lorawan/v3/api.md +++ b/api/ttn/lorawan/v3/api.md @@ -10563,8 +10563,15 @@ The Pba service allows clients to manage peering through Packet Broker. | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | -| `format_id` | [`string`](#string) | | Identifier of the format used to successfully parse the QR code data. | -| `claim_gateway_request` | [`ClaimGatewayRequest`](#ttn.lorawan.v3.ClaimGatewayRequest) | | | +| `format_id` | [`string`](#string) | | Identifier of the format used to parse the QR code data. | +| `gateway_eui` | [`bytes`](#bytes) | | | +| `owner_token` | [`string`](#string) | | | + +#### Field Rules + +| Field | Validations | +| ----- | ----------- | +| `gateway_eui` |

`bytes.len`: `8`

| ### Message `QRCodeFormat` diff --git a/api/ttn/lorawan/v3/api.swagger.json b/api/ttn/lorawan/v3/api.swagger.json index 290e3ad963..77b09c66c7 100644 --- a/api/ttn/lorawan/v3/api.swagger.json +++ b/api/ttn/lorawan/v3/api.swagger.json @@ -28058,10 +28058,15 @@ "properties": { "format_id": { "type": "string", - "description": "Identifier of the format used to successfully parse the QR code data." + "description": "Identifier of the format used to parse the QR code data." }, - "claim_gateway_request": { - "$ref": "#/definitions/v3ClaimGatewayRequest" + "gateway_eui": { + "type": "string", + "format": "string", + "example": "70B3D57ED000ABCD" + }, + "owner_token": { + "type": "string" } } }, diff --git a/api/ttn/lorawan/v3/qrcodegenerator.proto b/api/ttn/lorawan/v3/qrcodegenerator.proto index 97d7dce97b..820a858e25 100644 --- a/api/ttn/lorawan/v3/qrcodegenerator.proto +++ b/api/ttn/lorawan/v3/qrcodegenerator.proto @@ -20,7 +20,7 @@ import "google/api/annotations.proto"; import "google/protobuf/empty.proto"; import "google/protobuf/field_mask.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; -import "ttn/lorawan/v3/deviceclaimingserver.proto"; +import "thethings/json/annotations.proto"; import "ttn/lorawan/v3/end_device.proto"; import "ttn/lorawan/v3/picture.proto"; import "validate/validate.proto"; @@ -147,9 +147,24 @@ message ParseGatewayQRCodeRequest { } message ParseGatewayQRCodeResponse { - // Identifier of the format used to successfully parse the QR code data. + // Identifier of the format used to parse the QR code data. string format_id = 1; - ClaimGatewayRequest claim_gateway_request = 2; + bytes gateway_eui = 2 [ + (validate.rules).bytes = { + len: 8, + ignore_empty: true + }, + (thethings.json.field) = { + marshaler_func: "go.thethings.network/lorawan-stack/v3/pkg/types.MarshalHEXBytes", + unmarshaler_func: "go.thethings.network/lorawan-stack/v3/pkg/types.Unmarshal8Bytes" + }, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + type: STRING, + format: "string", + example: "\"70B3D57ED000ABCD\"" + } + ]; + string owner_token = 3; } // The GatewayQRCodeGenerator service provides functionality to generate and parse QR codes for gateways. diff --git a/pkg/qrcodegenerator/grpc_gateways.go b/pkg/qrcodegenerator/grpc_gateways.go index 0d71101cf7..4b61de1ea1 100644 --- a/pkg/qrcodegenerator/grpc_gateways.go +++ b/pkg/qrcodegenerator/grpc_gateways.go @@ -59,14 +59,8 @@ func (s *gatewayQRCodeGeneratorServer) Parse( } return &ttnpb.ParseGatewayQRCodeResponse{ - FormatId: data.FormatID(), - ClaimGatewayRequest: &ttnpb.ClaimGatewayRequest{ - SourceGateway: &ttnpb.ClaimGatewayRequest_AuthenticatedIdentifiers_{ - AuthenticatedIdentifiers: &ttnpb.ClaimGatewayRequest_AuthenticatedIdentifiers{ - GatewayEui: data.GatewayEUI().Bytes(), - AuthenticationCode: []byte(data.OwnerToken()), - }, - }, - }, + FormatId: data.FormatID(), + GatewayEui: data.GatewayEUI().Bytes(), + OwnerToken: data.OwnerToken(), }, nil } diff --git a/pkg/qrcodegenerator/grpc_gateways_test.go b/pkg/qrcodegenerator/grpc_gateways_test.go index a5876a5d47..0c6971fdad 100644 --- a/pkg/qrcodegenerator/grpc_gateways_test.go +++ b/pkg/qrcodegenerator/grpc_gateways_test.go @@ -117,6 +117,8 @@ func TestGatewayQRCodeParsing(t *testing.T) { return false } a.So(resp.FormatId, should.Equal, ttigpro1.ID()) + a.So(resp.GatewayEui, should.Resemble, []uint8{0xec, 0x65, 0x6e, 0xff, 0xfe, 0x00, 0x01, 0x28}) + a.So(resp.OwnerToken, should.Equal, "abcdef123456") return true }, diff --git a/pkg/qrcodegenerator/qrcode/gateways/gateways.go b/pkg/qrcodegenerator/qrcode/gateways/gateways.go index b50d3d4ddd..06570c6ef4 100644 --- a/pkg/qrcodegenerator/qrcode/gateways/gateways.go +++ b/pkg/qrcodegenerator/qrcode/gateways/gateways.go @@ -16,7 +16,6 @@ package gateways import ( - "context" "encoding" "go.thethings.network/lorawan-stack/v3/pkg/errors" @@ -56,13 +55,13 @@ type Server struct { } // New returns a new Server. -func New(_ context.Context) *Server { +func New() *Server { s := &Server{ // Newer formats should be added to this slice first to // preferentially match with those first. gatewayFormats: []gatewayFormat{ { - id: formatIDTTIGPRO1, + id: ttigpro1FormatID, format: new(TTIGPRO1Format), }, }, diff --git a/pkg/qrcodegenerator/qrcode/gateways/gateways_test.go b/pkg/qrcodegenerator/qrcode/gateways/gateways_test.go index c5d152ee4b..17d1439b4e 100644 --- a/pkg/qrcodegenerator/qrcode/gateways/gateways_test.go +++ b/pkg/qrcodegenerator/qrcode/gateways/gateways_test.go @@ -15,7 +15,6 @@ package gateways_test import ( - "context" "strconv" "testing" @@ -48,7 +47,7 @@ func TestParseGatewaysAuthenticationCodes(t *testing.T) { t.Parallel() a := assertions.New(t) - qrCode := gateways.New(context.Background()) + qrCode := gateways.New() d, err := qrCode.Parse(tc.FormatID, tc.Data) data := test.Must(d, err) diff --git a/pkg/qrcodegenerator/qrcode/gateways/ttigpro1.go b/pkg/qrcodegenerator/qrcode/gateways/ttigpro1.go index e42284121f..96f5e1dccb 100644 --- a/pkg/qrcodegenerator/qrcode/gateways/ttigpro1.go +++ b/pkg/qrcodegenerator/qrcode/gateways/ttigpro1.go @@ -22,22 +22,19 @@ import ( ) const ( - formatIDTTIGPRO1 = "ttigpro1" - - euiLength = 16 - ownerTokenLength = 12 + ttigpro1FormatID = "ttigpro1" ) +// ttigpro1Regex is the regular expression to match the TTIGPRO1 format. +// The format is as follows: https://ttig.pro/c/{16 lowercase base16 chars}/{12 base62 chars}. +var ttigpro1Regex = regexp.MustCompile(`^https://ttig\.pro/c/([a-f0-9]{16})/([a-z0-9]{12})$`) + // TTIGPRO1 is a format for gateway identification QR codes. type ttigpro1 struct { gatewayEUI types.EUI64 ownerToken string } -// ttigpro1Regex is the regular expression to match the TTIGPRO1 format. -// The format is as follows: https://ttig.pro/c/{16 lowercase base16 chars}/{12 base62 chars}. -var ttigpro1Regex = regexp.MustCompile(`^https://ttig\.pro/c/([a-f0-9]{16})/([a-z0-9]{12})$`) - // UnmarshalText implements the TextUnmarshaler interface. func (m *ttigpro1) UnmarshalText(text []byte) error { // Match the URL against the pattern @@ -52,7 +49,7 @@ func (m *ttigpro1) UnmarshalText(text []byte) error { m.ownerToken = matches[2] - if len(m.ownerToken) != ownerTokenLength { + if len(m.ownerToken) != 12 /* owner token length */ { return errInvalidLength } @@ -61,7 +58,7 @@ func (m *ttigpro1) UnmarshalText(text []byte) error { // FormatID implements the Data interface. func (*ttigpro1) FormatID() string { - return formatIDTTIGPRO1 + return ttigpro1FormatID } func (m *ttigpro1) GatewayEUI() types.EUI64 { @@ -89,7 +86,7 @@ func (TTIGPRO1Format) Format() *ttnpb.QRCodeFormat { // ID is the identifier of the format as a string. func (TTIGPRO1Format) ID() string { - return formatIDTTIGPRO1 + return ttigpro1FormatID } // New implements the Format interface. diff --git a/pkg/qrcodegenerator/qrcodegenerator.go b/pkg/qrcodegenerator/qrcodegenerator.go index 78874dd3b9..334a7f6d61 100644 --- a/pkg/qrcodegenerator/qrcodegenerator.go +++ b/pkg/qrcodegenerator/qrcodegenerator.go @@ -57,7 +57,7 @@ func New(c *component.Component, _ *Config, opts ...Option) (*QRCodeGenerator, e qrg.grpc.gatewayQRCodeGenerator = &gatewayQRCodeGeneratorServer{QRG: qrg} qrg.endDevices = enddevices.New(ctx) - qrg.gateways = gateways.New(ctx) + qrg.gateways = gateways.New() c.RegisterGRPC(qrg) diff --git a/pkg/ttnpb/qrcodegenerator.pb.go b/pkg/ttnpb/qrcodegenerator.pb.go index c063ccd948..ac6ea52b6d 100644 --- a/pkg/ttnpb/qrcodegenerator.pb.go +++ b/pkg/ttnpb/qrcodegenerator.pb.go @@ -21,6 +21,7 @@ package ttnpb import ( + _ "github.com/TheThingsIndustries/protoc-gen-go-json/annotations" _ "github.com/envoyproxy/protoc-gen-validate/validate" _ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2/options" _ "google.golang.org/genproto/googleapis/api/annotations" @@ -500,9 +501,10 @@ type ParseGatewayQRCodeResponse struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // Identifier of the format used to successfully parse the QR code data. - FormatId string `protobuf:"bytes,1,opt,name=format_id,json=formatId,proto3" json:"format_id,omitempty"` - ClaimGatewayRequest *ClaimGatewayRequest `protobuf:"bytes,2,opt,name=claim_gateway_request,json=claimGatewayRequest,proto3" json:"claim_gateway_request,omitempty"` + // Identifier of the format used to parse the QR code data. + FormatId string `protobuf:"bytes,1,opt,name=format_id,json=formatId,proto3" json:"format_id,omitempty"` + GatewayEui []byte `protobuf:"bytes,2,opt,name=gateway_eui,json=gatewayEui,proto3" json:"gateway_eui,omitempty"` + OwnerToken string `protobuf:"bytes,3,opt,name=owner_token,json=ownerToken,proto3" json:"owner_token,omitempty"` } func (x *ParseGatewayQRCodeResponse) Reset() { @@ -544,13 +546,20 @@ func (x *ParseGatewayQRCodeResponse) GetFormatId() string { return "" } -func (x *ParseGatewayQRCodeResponse) GetClaimGatewayRequest() *ClaimGatewayRequest { +func (x *ParseGatewayQRCodeResponse) GetGatewayEui() []byte { if x != nil { - return x.ClaimGatewayRequest + return x.GatewayEui } return nil } +func (x *ParseGatewayQRCodeResponse) GetOwnerToken() string { + if x != nil { + return x.OwnerToken + } + return "" +} + type GenerateEndDeviceQRCodeRequest_Image struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -614,161 +623,171 @@ var file_ttn_lorawan_v3_qrcodegenerator_proto_rawDesc = []byte{ 0x6f, 0x74, 0x6f, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x2d, 0x67, 0x65, 0x6e, 0x2d, 0x6f, 0x70, 0x65, 0x6e, 0x61, 0x70, 0x69, 0x76, 0x32, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x1a, 0x29, 0x74, 0x74, 0x6e, 0x2f, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, - 0x2f, 0x76, 0x33, 0x2f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x69, - 0x6e, 0x67, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, - 0x74, 0x74, 0x6e, 0x2f, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2f, 0x76, 0x33, 0x2f, 0x65, - 0x6e, 0x64, 0x5f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, - 0x1c, 0x74, 0x74, 0x6e, 0x2f, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2f, 0x76, 0x33, 0x2f, - 0x70, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x17, 0x76, - 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x92, 0x01, 0x0a, 0x0c, 0x51, 0x52, 0x43, 0x6f, 0x64, - 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x1b, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x18, 0x64, 0x52, 0x04, - 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2a, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x72, 0x03, - 0x18, 0xc8, 0x01, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, - 0x12, 0x39, 0x0a, 0x0a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4d, 0x61, 0x73, 0x6b, - 0x52, 0x09, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x4d, 0x61, 0x73, 0x6b, 0x22, 0xdd, 0x01, 0x0a, 0x0d, - 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x73, 0x12, 0x72, 0x0a, - 0x07, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2a, - 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, - 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x73, 0x2e, 0x46, 0x6f, - 0x72, 0x6d, 0x61, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x42, 0x2c, 0xfa, 0x42, 0x29, 0x9a, - 0x01, 0x26, 0x22, 0x24, 0x72, 0x22, 0x18, 0x24, 0x32, 0x1e, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x30, + 0x6f, 0x74, 0x6f, 0x1a, 0x20, 0x74, 0x68, 0x65, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x73, 0x2f, 0x6a, + 0x73, 0x6f, 0x6e, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x74, 0x74, 0x6e, 0x2f, 0x6c, 0x6f, 0x72, 0x61, 0x77, + 0x61, 0x6e, 0x2f, 0x76, 0x33, 0x2f, 0x65, 0x6e, 0x64, 0x5f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1c, 0x74, 0x74, 0x6e, 0x2f, 0x6c, 0x6f, 0x72, 0x61, + 0x77, 0x61, 0x6e, 0x2f, 0x76, 0x33, 0x2f, 0x70, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x17, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x76, + 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x92, 0x01, + 0x0a, 0x0c, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x1b, + 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, + 0x04, 0x72, 0x02, 0x18, 0x64, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2a, 0x0a, 0x0b, 0x64, + 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x42, 0x08, 0xfa, 0x42, 0x05, 0x72, 0x03, 0x18, 0xc8, 0x01, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x39, 0x0a, 0x0a, 0x66, 0x69, 0x65, 0x6c, 0x64, + 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x46, 0x69, + 0x65, 0x6c, 0x64, 0x4d, 0x61, 0x73, 0x6b, 0x52, 0x09, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x4d, 0x61, + 0x73, 0x6b, 0x22, 0xdd, 0x01, 0x0a, 0x0d, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x46, 0x6f, 0x72, + 0x6d, 0x61, 0x74, 0x73, 0x12, 0x72, 0x0a, 0x07, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, + 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x46, 0x6f, 0x72, + 0x6d, 0x61, 0x74, 0x73, 0x2e, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x42, 0x2c, 0xfa, 0x42, 0x29, 0x9a, 0x01, 0x26, 0x22, 0x24, 0x72, 0x22, 0x18, 0x24, 0x32, + 0x1e, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x2d, 0x5d, + 0x3f, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x7b, 0x32, 0x2c, 0x7d, 0x24, 0x52, + 0x07, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x73, 0x1a, 0x58, 0x0a, 0x0c, 0x46, 0x6f, 0x72, 0x6d, + 0x61, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x32, 0x0a, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x74, 0x74, 0x6e, 0x2e, + 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x51, 0x52, 0x43, 0x6f, 0x64, + 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, + 0x38, 0x01, 0x22, 0x5e, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x46, + 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x44, 0x0a, 0x09, + 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x27, 0xfa, 0x42, 0x24, 0x72, 0x22, 0x18, 0x24, 0x32, 0x1e, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x2d, 0x5d, 0x3f, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, - 0x39, 0x5d, 0x29, 0x7b, 0x32, 0x2c, 0x7d, 0x24, 0x52, 0x07, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, - 0x73, 0x1a, 0x58, 0x0a, 0x0c, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, - 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, - 0x6b, 0x65, 0x79, 0x12, 0x32, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, - 0x2e, 0x76, 0x33, 0x2e, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, - 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x5e, 0x0a, 0x16, 0x47, - 0x65, 0x74, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x52, 0x65, + 0x39, 0x5d, 0x29, 0x7b, 0x32, 0x2c, 0x7d, 0x24, 0x52, 0x08, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, + 0x49, 0x64, 0x22, 0xaa, 0x02, 0x0a, 0x1e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x45, + 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x44, 0x0a, 0x09, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x27, 0xfa, 0x42, 0x24, 0x72, 0x22, 0x18, 0x24, 0x32, 0x1e, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x2d, 0x5d, 0x3f, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x7b, 0x32, 0x2c, 0x7d, - 0x24, 0x52, 0x08, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x49, 0x64, 0x22, 0xaa, 0x02, 0x0a, 0x1e, - 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, - 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x44, - 0x0a, 0x09, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x42, 0x27, 0xfa, 0x42, 0x24, 0x72, 0x22, 0x18, 0x24, 0x32, 0x1e, 0x5e, 0x5b, 0x61, 0x2d, - 0x7a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x2d, 0x5d, 0x3f, 0x5b, 0x61, 0x2d, 0x7a, - 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x7b, 0x32, 0x2c, 0x7d, 0x24, 0x52, 0x08, 0x66, 0x6f, 0x72, 0x6d, - 0x61, 0x74, 0x49, 0x64, 0x12, 0x42, 0x0a, 0x0a, 0x65, 0x6e, 0x64, 0x5f, 0x64, 0x65, 0x76, 0x69, - 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, - 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, - 0x69, 0x63, 0x65, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x8a, 0x01, 0x02, 0x10, 0x01, 0x52, 0x09, 0x65, - 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x12, 0x4a, 0x0a, 0x05, 0x69, 0x6d, 0x61, 0x67, - 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, - 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, - 0x65, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x05, 0x69, - 0x6d, 0x61, 0x67, 0x65, 0x1a, 0x32, 0x0a, 0x05, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x29, 0x0a, - 0x0a, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0d, 0x42, 0x0a, 0xfa, 0x42, 0x07, 0x2a, 0x05, 0x18, 0xe8, 0x07, 0x28, 0x0a, 0x52, 0x09, 0x69, - 0x6d, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x22, 0x5b, 0x0a, 0x16, 0x47, 0x65, 0x6e, 0x65, - 0x72, 0x61, 0x74, 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x78, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x74, 0x65, 0x78, 0x74, 0x12, 0x2d, 0x0a, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, - 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x50, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x52, 0x05, - 0x69, 0x6d, 0x61, 0x67, 0x65, 0x22, 0x8b, 0x01, 0x0a, 0x1b, 0x50, 0x61, 0x72, 0x73, 0x65, 0x45, - 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x47, 0x0a, 0x09, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x5f, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x2a, 0xfa, 0x42, 0x27, 0x72, 0x25, 0x18, - 0x24, 0x32, 0x21, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, - 0x2d, 0x5d, 0x3f, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x7b, 0x32, 0x2c, 0x7d, - 0x24, 0x7c, 0x5e, 0x24, 0x52, 0x08, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x49, 0x64, 0x12, 0x23, - 0x0a, 0x07, 0x71, 0x72, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x42, - 0x0a, 0xfa, 0x42, 0x07, 0x7a, 0x05, 0x10, 0x0a, 0x18, 0x80, 0x08, 0x52, 0x06, 0x71, 0x72, 0x43, - 0x6f, 0x64, 0x65, 0x22, 0x8e, 0x01, 0x0a, 0x1c, 0x50, 0x61, 0x72, 0x73, 0x65, 0x45, 0x6e, 0x64, - 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x5f, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x49, - 0x64, 0x12, 0x51, 0x0a, 0x13, 0x65, 0x6e, 0x64, 0x5f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, - 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, + 0x24, 0x52, 0x08, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x49, 0x64, 0x12, 0x42, 0x0a, 0x0a, 0x65, + 0x6e, 0x64, 0x5f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x19, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, + 0x2e, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x8a, + 0x01, 0x02, 0x10, 0x01, 0x52, 0x09, 0x65, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x12, + 0x4a, 0x0a, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, - 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, - 0x65, 0x52, 0x11, 0x65, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x54, 0x65, 0x6d, 0x70, - 0x6c, 0x61, 0x74, 0x65, 0x22, 0x89, 0x01, 0x0a, 0x19, 0x50, 0x61, 0x72, 0x73, 0x65, 0x47, 0x61, - 0x74, 0x65, 0x77, 0x61, 0x79, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x47, 0x0a, 0x09, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x5f, 0x69, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x2a, 0xfa, 0x42, 0x27, 0x72, 0x25, 0x18, 0x24, 0x32, 0x21, - 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x2d, 0x5d, 0x3f, - 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x7b, 0x32, 0x2c, 0x7d, 0x24, 0x7c, 0x5e, - 0x24, 0x52, 0x08, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x07, 0x71, - 0x72, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x0a, 0xfa, 0x42, - 0x07, 0x7a, 0x05, 0x10, 0x0a, 0x18, 0x80, 0x08, 0x52, 0x06, 0x71, 0x72, 0x43, 0x6f, 0x64, 0x65, - 0x22, 0x92, 0x01, 0x0a, 0x1a, 0x50, 0x61, 0x72, 0x73, 0x65, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, - 0x79, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x1b, 0x0a, 0x09, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x08, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x49, 0x64, 0x12, 0x57, 0x0a, 0x15, - 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x5f, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x5f, 0x72, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x74, 0x74, - 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x43, 0x6c, 0x61, - 0x69, 0x6d, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x52, 0x13, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x32, 0xfe, 0x04, 0x0a, 0x18, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, - 0x69, 0x63, 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, - 0x6f, 0x72, 0x12, 0x84, 0x01, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, - 0x12, 0x26, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, - 0x33, 0x2e, 0x47, 0x65, 0x74, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, - 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, + 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x49, + 0x6d, 0x61, 0x67, 0x65, 0x52, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x1a, 0x32, 0x0a, 0x05, 0x49, + 0x6d, 0x61, 0x67, 0x65, 0x12, 0x29, 0x0a, 0x0a, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, + 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x42, 0x0a, 0xfa, 0x42, 0x07, 0x2a, 0x05, 0x18, + 0xe8, 0x07, 0x28, 0x0a, 0x52, 0x09, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x22, + 0x5b, 0x0a, 0x16, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x78, + 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x65, 0x78, 0x74, 0x12, 0x2d, 0x0a, + 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, + 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x50, 0x69, + 0x63, 0x74, 0x75, 0x72, 0x65, 0x52, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x22, 0x8b, 0x01, 0x0a, + 0x1b, 0x50, 0x61, 0x72, 0x73, 0x65, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x51, + 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x47, 0x0a, 0x09, + 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x2a, 0xfa, 0x42, 0x27, 0x72, 0x25, 0x18, 0x24, 0x32, 0x21, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x30, + 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x2d, 0x5d, 0x3f, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, + 0x39, 0x5d, 0x29, 0x7b, 0x32, 0x2c, 0x7d, 0x24, 0x7c, 0x5e, 0x24, 0x52, 0x08, 0x66, 0x6f, 0x72, + 0x6d, 0x61, 0x74, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x07, 0x71, 0x72, 0x5f, 0x63, 0x6f, 0x64, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x0a, 0xfa, 0x42, 0x07, 0x7a, 0x05, 0x10, 0x0a, 0x18, + 0x80, 0x08, 0x52, 0x06, 0x71, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x22, 0x8e, 0x01, 0x0a, 0x1c, 0x50, + 0x61, 0x72, 0x73, 0x65, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x51, 0x52, 0x43, + 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x66, + 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x49, 0x64, 0x12, 0x51, 0x0a, 0x13, 0x65, 0x6e, 0x64, 0x5f, + 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, + 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, + 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x52, 0x11, 0x65, 0x6e, 0x64, 0x44, 0x65, 0x76, + 0x69, 0x63, 0x65, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x22, 0x89, 0x01, 0x0a, 0x19, + 0x50, 0x61, 0x72, 0x73, 0x65, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x51, 0x52, 0x43, 0x6f, + 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x47, 0x0a, 0x09, 0x66, 0x6f, 0x72, + 0x6d, 0x61, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x2a, 0xfa, 0x42, + 0x27, 0x72, 0x25, 0x18, 0x24, 0x32, 0x21, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, + 0x28, 0x3f, 0x3a, 0x5b, 0x2d, 0x5d, 0x3f, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, 0x29, + 0x7b, 0x32, 0x2c, 0x7d, 0x24, 0x7c, 0x5e, 0x24, 0x52, 0x08, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, + 0x49, 0x64, 0x12, 0x23, 0x0a, 0x07, 0x71, 0x72, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0c, 0x42, 0x0a, 0xfa, 0x42, 0x07, 0x7a, 0x05, 0x10, 0x0a, 0x18, 0x80, 0x08, 0x52, + 0x06, 0x71, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x22, 0xb3, 0x02, 0x0a, 0x1a, 0x50, 0x61, 0x72, 0x73, + 0x65, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x6f, 0x72, 0x6d, 0x61, + 0x74, 0x49, 0x64, 0x12, 0xd6, 0x01, 0x0a, 0x0b, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x5f, + 0x65, 0x75, 0x69, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x42, 0xb4, 0x01, 0x92, 0x41, 0x21, 0x4a, + 0x12, 0x22, 0x37, 0x30, 0x42, 0x33, 0x44, 0x35, 0x37, 0x45, 0x44, 0x30, 0x30, 0x30, 0x41, 0x42, + 0x43, 0x44, 0x22, 0x9a, 0x02, 0x01, 0x07, 0xa2, 0x02, 0x06, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, + 0xfa, 0x42, 0x06, 0x7a, 0x04, 0x68, 0x08, 0x70, 0x01, 0xea, 0xaa, 0x19, 0x82, 0x01, 0x0a, 0x3f, + 0x67, 0x6f, 0x2e, 0x74, 0x68, 0x65, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x6e, 0x65, 0x74, + 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2d, 0x73, 0x74, 0x61, + 0x63, 0x6b, 0x2f, 0x76, 0x33, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, + 0x4d, 0x61, 0x72, 0x73, 0x68, 0x61, 0x6c, 0x48, 0x45, 0x58, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, + 0x3f, 0x67, 0x6f, 0x2e, 0x74, 0x68, 0x65, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x6e, 0x65, + 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2d, 0x73, 0x74, + 0x61, 0x63, 0x6b, 0x2f, 0x76, 0x33, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x73, + 0x2e, 0x55, 0x6e, 0x6d, 0x61, 0x72, 0x73, 0x68, 0x61, 0x6c, 0x38, 0x42, 0x79, 0x74, 0x65, 0x73, + 0x52, 0x0a, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x45, 0x75, 0x69, 0x12, 0x1f, 0x0a, 0x0b, + 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0a, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x32, 0xfe, 0x04, + 0x0a, 0x18, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, + 0x65, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x84, 0x01, 0x0a, 0x09, 0x47, + 0x65, 0x74, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x26, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, + 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x47, 0x65, 0x74, 0x51, 0x52, 0x43, + 0x6f, 0x64, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1c, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, + 0x33, 0x2e, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x22, 0x31, + 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x2b, 0x12, 0x29, 0x2f, 0x71, 0x72, 0x2d, 0x63, 0x6f, 0x64, 0x65, + 0x73, 0x2f, 0x65, 0x6e, 0x64, 0x2d, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x66, 0x6f, + 0x72, 0x6d, 0x61, 0x74, 0x73, 0x2f, 0x7b, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x5f, 0x69, 0x64, + 0x7d, 0x12, 0x6b, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x73, + 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, - 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x22, 0x31, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x2b, 0x12, 0x29, + 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x73, 0x22, 0x25, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1f, 0x12, + 0x1d, 0x2f, 0x71, 0x72, 0x2d, 0x63, 0x6f, 0x64, 0x65, 0x73, 0x2f, 0x65, 0x6e, 0x64, 0x2d, 0x64, + 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x73, 0x12, 0x84, + 0x01, 0x0a, 0x08, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x12, 0x2e, 0x2e, 0x74, 0x74, + 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x47, 0x65, 0x6e, + 0x65, 0x72, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x51, 0x52, + 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x74, 0x74, + 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x47, 0x65, 0x6e, + 0x65, 0x72, 0x61, 0x74, 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x3a, 0x01, 0x2a, 0x22, 0x15, 0x2f, 0x71, 0x72, 0x2d, 0x63, 0x6f, 0x64, 0x65, 0x73, 0x2f, 0x65, 0x6e, 0x64, 0x2d, 0x64, 0x65, - 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x73, 0x2f, 0x7b, 0x66, - 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x5f, 0x69, 0x64, 0x7d, 0x12, 0x6b, 0x0a, 0x0b, 0x4c, 0x69, 0x73, - 0x74, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x1a, 0x1d, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, - 0x33, 0x2e, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x73, 0x22, - 0x25, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1f, 0x12, 0x1d, 0x2f, 0x71, 0x72, 0x2d, 0x63, 0x6f, 0x64, - 0x65, 0x73, 0x2f, 0x65, 0x6e, 0x64, 0x2d, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x66, - 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x73, 0x12, 0x84, 0x01, 0x0a, 0x08, 0x47, 0x65, 0x6e, 0x65, 0x72, - 0x61, 0x74, 0x65, 0x12, 0x2e, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, - 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, - 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, - 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x51, 0x52, 0x43, - 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x20, 0x82, 0xd3, 0xe4, - 0x93, 0x02, 0x1a, 0x3a, 0x01, 0x2a, 0x22, 0x15, 0x2f, 0x71, 0x72, 0x2d, 0x63, 0x6f, 0x64, 0x65, - 0x73, 0x2f, 0x65, 0x6e, 0x64, 0x2d, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x12, 0xb8, 0x01, - 0x0a, 0x05, 0x50, 0x61, 0x72, 0x73, 0x65, 0x12, 0x2b, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, - 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x45, 0x6e, - 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, - 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x45, 0x6e, 0x64, 0x44, 0x65, - 0x76, 0x69, 0x63, 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x54, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x4e, 0x3a, 0x01, 0x2a, 0x5a, 0x2c, 0x3a, - 0x01, 0x2a, 0x22, 0x27, 0x2f, 0x71, 0x72, 0x2d, 0x63, 0x6f, 0x64, 0x65, 0x73, 0x2f, 0x65, 0x6e, - 0x64, 0x2d, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x7b, 0x66, 0x6f, 0x72, 0x6d, 0x61, - 0x74, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x70, 0x61, 0x72, 0x73, 0x65, 0x22, 0x1b, 0x2f, 0x71, 0x72, - 0x2d, 0x63, 0x6f, 0x64, 0x65, 0x73, 0x2f, 0x65, 0x6e, 0x64, 0x2d, 0x64, 0x65, 0x76, 0x69, 0x63, - 0x65, 0x73, 0x2f, 0x70, 0x61, 0x72, 0x73, 0x65, 0x1a, 0x2c, 0x92, 0x41, 0x29, 0x12, 0x27, 0x47, - 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x70, 0x61, 0x72, 0x73, - 0x65, 0x20, 0x65, 0x6e, 0x64, 0x20, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x20, 0x51, 0x52, 0x20, - 0x63, 0x6f, 0x64, 0x65, 0x73, 0x2e, 0x32, 0xc9, 0x01, 0x0a, 0x16, 0x47, 0x61, 0x74, 0x65, 0x77, - 0x61, 0x79, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x6f, - 0x72, 0x12, 0xae, 0x01, 0x0a, 0x05, 0x50, 0x61, 0x72, 0x73, 0x65, 0x12, 0x29, 0x2e, 0x74, 0x74, - 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x50, 0x61, 0x72, - 0x73, 0x65, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, - 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x47, 0x61, 0x74, - 0x65, 0x77, 0x61, 0x79, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x4e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x48, 0x3a, 0x01, 0x2a, 0x5a, 0x29, 0x3a, - 0x01, 0x2a, 0x22, 0x24, 0x2f, 0x71, 0x72, 0x2d, 0x63, 0x6f, 0x64, 0x65, 0x73, 0x2f, 0x67, 0x61, - 0x74, 0x65, 0x77, 0x61, 0x79, 0x73, 0x2f, 0x7b, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x5f, 0x69, - 0x64, 0x7d, 0x2f, 0x70, 0x61, 0x72, 0x73, 0x65, 0x22, 0x18, 0x2f, 0x71, 0x72, 0x2d, 0x63, 0x6f, - 0x64, 0x65, 0x73, 0x2f, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x73, 0x2f, 0x70, 0x61, 0x72, - 0x73, 0x65, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x6f, 0x2e, 0x74, 0x68, 0x65, 0x74, 0x68, 0x69, 0x6e, - 0x67, 0x73, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x6c, 0x6f, 0x72, 0x61, 0x77, - 0x61, 0x6e, 0x2d, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2f, 0x76, 0x33, 0x2f, 0x70, 0x6b, 0x67, 0x2f, - 0x74, 0x74, 0x6e, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x76, 0x69, 0x63, 0x65, 0x73, 0x12, 0xb8, 0x01, 0x0a, 0x05, 0x50, 0x61, 0x72, 0x73, 0x65, 0x12, + 0x2b, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, + 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x51, + 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x74, + 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x50, 0x61, + 0x72, 0x73, 0x65, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x51, 0x52, 0x43, 0x6f, + 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x54, 0x82, 0xd3, 0xe4, 0x93, + 0x02, 0x4e, 0x3a, 0x01, 0x2a, 0x5a, 0x2c, 0x3a, 0x01, 0x2a, 0x22, 0x27, 0x2f, 0x71, 0x72, 0x2d, + 0x63, 0x6f, 0x64, 0x65, 0x73, 0x2f, 0x65, 0x6e, 0x64, 0x2d, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, + 0x73, 0x2f, 0x7b, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x70, 0x61, + 0x72, 0x73, 0x65, 0x22, 0x1b, 0x2f, 0x71, 0x72, 0x2d, 0x63, 0x6f, 0x64, 0x65, 0x73, 0x2f, 0x65, + 0x6e, 0x64, 0x2d, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x70, 0x61, 0x72, 0x73, 0x65, + 0x1a, 0x2c, 0x92, 0x41, 0x29, 0x12, 0x27, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x20, + 0x61, 0x6e, 0x64, 0x20, 0x70, 0x61, 0x72, 0x73, 0x65, 0x20, 0x65, 0x6e, 0x64, 0x20, 0x64, 0x65, + 0x76, 0x69, 0x63, 0x65, 0x20, 0x51, 0x52, 0x20, 0x63, 0x6f, 0x64, 0x65, 0x73, 0x2e, 0x32, 0xc9, + 0x01, 0x0a, 0x16, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, + 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x12, 0xae, 0x01, 0x0a, 0x05, 0x50, 0x61, + 0x72, 0x73, 0x65, 0x12, 0x29, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, + 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, + 0x79, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, + 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, + 0x50, 0x61, 0x72, 0x73, 0x65, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x51, 0x52, 0x43, 0x6f, + 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x4e, 0x82, 0xd3, 0xe4, 0x93, + 0x02, 0x48, 0x3a, 0x01, 0x2a, 0x5a, 0x29, 0x3a, 0x01, 0x2a, 0x22, 0x24, 0x2f, 0x71, 0x72, 0x2d, + 0x63, 0x6f, 0x64, 0x65, 0x73, 0x2f, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x73, 0x2f, 0x7b, + 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x70, 0x61, 0x72, 0x73, 0x65, + 0x22, 0x18, 0x2f, 0x71, 0x72, 0x2d, 0x63, 0x6f, 0x64, 0x65, 0x73, 0x2f, 0x67, 0x61, 0x74, 0x65, + 0x77, 0x61, 0x79, 0x73, 0x2f, 0x70, 0x61, 0x72, 0x73, 0x65, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x6f, + 0x2e, 0x74, 0x68, 0x65, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, + 0x72, 0x6b, 0x2f, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2d, 0x73, 0x74, 0x61, 0x63, 0x6b, + 0x2f, 0x76, 0x33, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x74, 0x74, 0x6e, 0x70, 0x62, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -800,8 +819,7 @@ var file_ttn_lorawan_v3_qrcodegenerator_proto_goTypes = []interface{}{ (*EndDevice)(nil), // 12: ttn.lorawan.v3.EndDevice (*Picture)(nil), // 13: ttn.lorawan.v3.Picture (*EndDeviceTemplate)(nil), // 14: ttn.lorawan.v3.EndDeviceTemplate - (*ClaimGatewayRequest)(nil), // 15: ttn.lorawan.v3.ClaimGatewayRequest - (*emptypb.Empty)(nil), // 16: google.protobuf.Empty + (*emptypb.Empty)(nil), // 15: google.protobuf.Empty } var file_ttn_lorawan_v3_qrcodegenerator_proto_depIdxs = []int32{ 11, // 0: ttn.lorawan.v3.QRCodeFormat.field_mask:type_name -> google.protobuf.FieldMask @@ -810,23 +828,22 @@ var file_ttn_lorawan_v3_qrcodegenerator_proto_depIdxs = []int32{ 10, // 3: ttn.lorawan.v3.GenerateEndDeviceQRCodeRequest.image:type_name -> ttn.lorawan.v3.GenerateEndDeviceQRCodeRequest.Image 13, // 4: ttn.lorawan.v3.GenerateQRCodeResponse.image:type_name -> ttn.lorawan.v3.Picture 14, // 5: ttn.lorawan.v3.ParseEndDeviceQRCodeResponse.end_device_template:type_name -> ttn.lorawan.v3.EndDeviceTemplate - 15, // 6: ttn.lorawan.v3.ParseGatewayQRCodeResponse.claim_gateway_request:type_name -> ttn.lorawan.v3.ClaimGatewayRequest - 0, // 7: ttn.lorawan.v3.QRCodeFormats.FormatsEntry.value:type_name -> ttn.lorawan.v3.QRCodeFormat - 2, // 8: ttn.lorawan.v3.EndDeviceQRCodeGenerator.GetFormat:input_type -> ttn.lorawan.v3.GetQRCodeFormatRequest - 16, // 9: ttn.lorawan.v3.EndDeviceQRCodeGenerator.ListFormats:input_type -> google.protobuf.Empty - 3, // 10: ttn.lorawan.v3.EndDeviceQRCodeGenerator.Generate:input_type -> ttn.lorawan.v3.GenerateEndDeviceQRCodeRequest - 5, // 11: ttn.lorawan.v3.EndDeviceQRCodeGenerator.Parse:input_type -> ttn.lorawan.v3.ParseEndDeviceQRCodeRequest - 7, // 12: ttn.lorawan.v3.GatewayQRCodeGenerator.Parse:input_type -> ttn.lorawan.v3.ParseGatewayQRCodeRequest - 0, // 13: ttn.lorawan.v3.EndDeviceQRCodeGenerator.GetFormat:output_type -> ttn.lorawan.v3.QRCodeFormat - 1, // 14: ttn.lorawan.v3.EndDeviceQRCodeGenerator.ListFormats:output_type -> ttn.lorawan.v3.QRCodeFormats - 4, // 15: ttn.lorawan.v3.EndDeviceQRCodeGenerator.Generate:output_type -> ttn.lorawan.v3.GenerateQRCodeResponse - 6, // 16: ttn.lorawan.v3.EndDeviceQRCodeGenerator.Parse:output_type -> ttn.lorawan.v3.ParseEndDeviceQRCodeResponse - 8, // 17: ttn.lorawan.v3.GatewayQRCodeGenerator.Parse:output_type -> ttn.lorawan.v3.ParseGatewayQRCodeResponse - 13, // [13:18] is the sub-list for method output_type - 8, // [8:13] is the sub-list for method input_type - 8, // [8:8] is the sub-list for extension type_name - 8, // [8:8] is the sub-list for extension extendee - 0, // [0:8] is the sub-list for field type_name + 0, // 6: ttn.lorawan.v3.QRCodeFormats.FormatsEntry.value:type_name -> ttn.lorawan.v3.QRCodeFormat + 2, // 7: ttn.lorawan.v3.EndDeviceQRCodeGenerator.GetFormat:input_type -> ttn.lorawan.v3.GetQRCodeFormatRequest + 15, // 8: ttn.lorawan.v3.EndDeviceQRCodeGenerator.ListFormats:input_type -> google.protobuf.Empty + 3, // 9: ttn.lorawan.v3.EndDeviceQRCodeGenerator.Generate:input_type -> ttn.lorawan.v3.GenerateEndDeviceQRCodeRequest + 5, // 10: ttn.lorawan.v3.EndDeviceQRCodeGenerator.Parse:input_type -> ttn.lorawan.v3.ParseEndDeviceQRCodeRequest + 7, // 11: ttn.lorawan.v3.GatewayQRCodeGenerator.Parse:input_type -> ttn.lorawan.v3.ParseGatewayQRCodeRequest + 0, // 12: ttn.lorawan.v3.EndDeviceQRCodeGenerator.GetFormat:output_type -> ttn.lorawan.v3.QRCodeFormat + 1, // 13: ttn.lorawan.v3.EndDeviceQRCodeGenerator.ListFormats:output_type -> ttn.lorawan.v3.QRCodeFormats + 4, // 14: ttn.lorawan.v3.EndDeviceQRCodeGenerator.Generate:output_type -> ttn.lorawan.v3.GenerateQRCodeResponse + 6, // 15: ttn.lorawan.v3.EndDeviceQRCodeGenerator.Parse:output_type -> ttn.lorawan.v3.ParseEndDeviceQRCodeResponse + 8, // 16: ttn.lorawan.v3.GatewayQRCodeGenerator.Parse:output_type -> ttn.lorawan.v3.ParseGatewayQRCodeResponse + 12, // [12:17] is the sub-list for method output_type + 7, // [7:12] is the sub-list for method input_type + 7, // [7:7] is the sub-list for extension type_name + 7, // [7:7] is the sub-list for extension extendee + 0, // [0:7] is the sub-list for field type_name } func init() { file_ttn_lorawan_v3_qrcodegenerator_proto_init() } @@ -834,7 +851,6 @@ func file_ttn_lorawan_v3_qrcodegenerator_proto_init() { if File_ttn_lorawan_v3_qrcodegenerator_proto != nil { return } - file_ttn_lorawan_v3_deviceclaimingserver_proto_init() file_ttn_lorawan_v3_end_device_proto_init() file_ttn_lorawan_v3_picture_proto_init() if !protoimpl.UnsafeEnabled { diff --git a/pkg/ttnpb/qrcodegenerator.pb.paths.fm.go b/pkg/ttnpb/qrcodegenerator.pb.paths.fm.go index a96f377fd2..0c6cc9e100 100644 --- a/pkg/ttnpb/qrcodegenerator.pb.paths.fm.go +++ b/pkg/ttnpb/qrcodegenerator.pb.paths.fm.go @@ -1589,38 +1589,15 @@ var ParseGatewayQRCodeRequestFieldPathsTopLevel = []string{ "qr_code", } var ParseGatewayQRCodeResponseFieldPathsNested = []string{ - "claim_gateway_request", - "claim_gateway_request.collaborator", - "claim_gateway_request.collaborator.ids", - "claim_gateway_request.collaborator.ids.organization_ids", - "claim_gateway_request.collaborator.ids.organization_ids.organization_id", - "claim_gateway_request.collaborator.ids.user_ids", - "claim_gateway_request.collaborator.ids.user_ids.email", - "claim_gateway_request.collaborator.ids.user_ids.user_id", - "claim_gateway_request.cups_redirection", - "claim_gateway_request.cups_redirection.current_gateway_key", - "claim_gateway_request.cups_redirection.gateway_credentials", - "claim_gateway_request.cups_redirection.gateway_credentials.auth_token", - "claim_gateway_request.cups_redirection.gateway_credentials.client_tls", - "claim_gateway_request.cups_redirection.gateway_credentials.client_tls.cert", - "claim_gateway_request.cups_redirection.gateway_credentials.client_tls.key", - "claim_gateway_request.cups_redirection.target_cups_trust", - "claim_gateway_request.cups_redirection.target_cups_uri", - "claim_gateway_request.source_gateway", - "claim_gateway_request.source_gateway.authenticated_identifiers", - "claim_gateway_request.source_gateway.authenticated_identifiers.authentication_code", - "claim_gateway_request.source_gateway.authenticated_identifiers.gateway_eui", - "claim_gateway_request.source_gateway.qr_code", - "claim_gateway_request.target_frequency_plan_id", - "claim_gateway_request.target_frequency_plan_ids", - "claim_gateway_request.target_gateway_id", - "claim_gateway_request.target_gateway_server_address", "format_id", + "gateway_eui", + "owner_token", } var ParseGatewayQRCodeResponseFieldPathsTopLevel = []string{ - "claim_gateway_request", "format_id", + "gateway_eui", + "owner_token", } var GenerateEndDeviceQRCodeRequest_ImageFieldPathsNested = []string{ "image_size", diff --git a/pkg/ttnpb/qrcodegenerator.pb.setters.fm.go b/pkg/ttnpb/qrcodegenerator.pb.setters.fm.go index 18b7cd41d7..1e7032e28e 100644 --- a/pkg/ttnpb/qrcodegenerator.pb.setters.fm.go +++ b/pkg/ttnpb/qrcodegenerator.pb.setters.fm.go @@ -321,30 +321,24 @@ func (dst *ParseGatewayQRCodeResponse) SetFields(src *ParseGatewayQRCodeResponse var zero string dst.FormatId = zero } - case "claim_gateway_request": + case "gateway_eui": if len(subs) > 0 { - var newDst, newSrc *ClaimGatewayRequest - if (src == nil || src.ClaimGatewayRequest == nil) && dst.ClaimGatewayRequest == nil { - continue - } - if src != nil { - newSrc = src.ClaimGatewayRequest - } - if dst.ClaimGatewayRequest != nil { - newDst = dst.ClaimGatewayRequest - } else { - newDst = &ClaimGatewayRequest{} - dst.ClaimGatewayRequest = newDst - } - if err := newDst.SetFields(newSrc, subs...); err != nil { - return err - } + return fmt.Errorf("'gateway_eui' has no subfields, but %s were specified", subs) + } + if src != nil { + dst.GatewayEui = src.GatewayEui } else { - if src != nil { - dst.ClaimGatewayRequest = src.ClaimGatewayRequest - } else { - dst.ClaimGatewayRequest = nil - } + dst.GatewayEui = nil + } + case "owner_token": + if len(subs) > 0 { + return fmt.Errorf("'owner_token' has no subfields, but %s were specified", subs) + } + if src != nil { + dst.OwnerToken = src.OwnerToken + } else { + var zero string + dst.OwnerToken = zero } default: diff --git a/pkg/ttnpb/qrcodegenerator.pb.validate.go b/pkg/ttnpb/qrcodegenerator.pb.validate.go index c126659205..ef5173d639 100644 --- a/pkg/ttnpb/qrcodegenerator.pb.validate.go +++ b/pkg/ttnpb/qrcodegenerator.pb.validate.go @@ -909,18 +909,21 @@ func (m *ParseGatewayQRCodeResponse) ValidateFields(paths ...string) error { switch name { case "format_id": // no validation rules for FormatId - case "claim_gateway_request": + case "gateway_eui": - if v, ok := interface{}(m.GetClaimGatewayRequest()).(interface{ ValidateFields(...string) error }); ok { - if err := v.ValidateFields(subs...); err != nil { + if len(m.GetGatewayEui()) > 0 { + + if len(m.GetGatewayEui()) != 8 { return ParseGatewayQRCodeResponseValidationError{ - field: "claim_gateway_request", - reason: "embedded message failed validation", - cause: err, + field: "gateway_eui", + reason: "value length must be 8 bytes", } } + } + case "owner_token": + // no validation rules for OwnerToken default: return ParseGatewayQRCodeResponseValidationError{ field: name, diff --git a/pkg/ttnpb/qrcodegenerator_json.pb.go b/pkg/ttnpb/qrcodegenerator_json.pb.go index 9aabcda718..56dd3ecb7c 100644 --- a/pkg/ttnpb/qrcodegenerator_json.pb.go +++ b/pkg/ttnpb/qrcodegenerator_json.pb.go @@ -9,6 +9,7 @@ package ttnpb import ( golang "github.com/TheThingsIndustries/protoc-gen-go-json/golang" jsonplugin "github.com/TheThingsIndustries/protoc-gen-go-json/jsonplugin" + types "go.thethings.network/lorawan-stack/v3/pkg/types" ) // MarshalProtoJSON marshals the QRCodeFormat message to JSON. @@ -218,10 +219,15 @@ func (x *ParseGatewayQRCodeResponse) MarshalProtoJSON(s *jsonplugin.MarshalState s.WriteObjectField("format_id") s.WriteString(x.FormatId) } - if x.ClaimGatewayRequest != nil || s.HasField("claim_gateway_request") { + if len(x.GatewayEui) > 0 || s.HasField("gateway_eui") { s.WriteMoreIf(&wroteField) - s.WriteObjectField("claim_gateway_request") - x.ClaimGatewayRequest.MarshalProtoJSON(s.WithField("claim_gateway_request")) + s.WriteObjectField("gateway_eui") + types.MarshalHEXBytes(s.WithField("gateway_eui"), x.GatewayEui) + } + if x.OwnerToken != "" || s.HasField("owner_token") { + s.WriteMoreIf(&wroteField) + s.WriteObjectField("owner_token") + s.WriteString(x.OwnerToken) } s.WriteObjectEnd() } @@ -243,13 +249,12 @@ func (x *ParseGatewayQRCodeResponse) UnmarshalProtoJSON(s *jsonplugin.UnmarshalS case "format_id", "formatId": s.AddField("format_id") x.FormatId = s.ReadString() - case "claim_gateway_request", "claimGatewayRequest": - if s.ReadNil() { - x.ClaimGatewayRequest = nil - return - } - x.ClaimGatewayRequest = &ClaimGatewayRequest{} - x.ClaimGatewayRequest.UnmarshalProtoJSON(s.WithField("claim_gateway_request", true)) + case "gateway_eui", "gatewayEui": + s.AddField("gateway_eui") + x.GatewayEui = types.Unmarshal8Bytes(s.WithField("gateway_eui", false)) + case "owner_token", "ownerToken": + s.AddField("owner_token") + x.OwnerToken = s.ReadString() } }) } diff --git a/sdk/js/generated/api.json b/sdk/js/generated/api.json index 83ef9e1986..f85d137f0e 100644 --- a/sdk/js/generated/api.json +++ b/sdk/js/generated/api.json @@ -48326,7 +48326,7 @@ "fields": [ { "name": "format_id", - "description": "Identifier of the format used to successfully parse the QR code data.", + "description": "Identifier of the format used to parse the QR code data.", "label": "", "type": "string", "longType": "string", @@ -48337,12 +48337,32 @@ "defaultValue": "" }, { - "name": "claim_gateway_request", + "name": "gateway_eui", + "description": "", + "label": "", + "type": "bytes", + "longType": "bytes", + "fullType": "bytes", + "ismap": false, + "isoneof": false, + "oneofdecl": "", + "defaultValue": "", + "options": { + "validate.rules": [ + { + "name": "bytes.len", + "value": 8 + } + ] + } + }, + { + "name": "owner_token", "description": "", "label": "", - "type": "ClaimGatewayRequest", - "longType": "ClaimGatewayRequest", - "fullType": "ttn.lorawan.v3.ClaimGatewayRequest", + "type": "string", + "longType": "string", + "fullType": "string", "ismap": false, "isoneof": false, "oneofdecl": "", From 57ddabccf015b13e13503d0950cb2183926efdd6 Mon Sep 17 00:00:00 2001 From: Vlad Vitan Date: Tue, 10 Sep 2024 15:26:25 +0200 Subject: [PATCH 6/6] qrg: Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce900d58e0..f05d774e61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ For details about compatibility between different releases, see the **Commitment - Option to filter out non-gateway related frequency plans. - `ListFrequencyPlans` RPC has a new `gateways-only` flag. - Option to pause application webhooks. +- Endpoint for claiming gateways using a qr code ### Changed