Sync installation messages & contact requests (#1791)
This commit is contained in:
parent
eb93bab35d
commit
c569d8a4ed
9
go.mod
9
go.mod
|
@ -8,7 +8,7 @@ replace github.com/Sirupsen/logrus v1.4.2 => github.com/sirupsen/logrus v1.4.2
|
|||
|
||||
replace github.com/docker/docker => github.com/docker/engine v1.4.2-0.20190717161051-705d9623b7c1
|
||||
|
||||
replace github.com/gomarkdown/markdown v0.0.0-20191104174740-4d42851d4d5a => github.com/status-im/markdown v0.0.0-20191209105822-e3ba6c6109ba
|
||||
replace github.com/gomarkdown/markdown v0.0.0-20191209105822-e3ba6c6109ba => github.com/status-im/markdown v0.0.0-20191209105822-e3ba6c6109ba
|
||||
|
||||
replace github.com/status-im/status-go/protocol => ./protocol
|
||||
|
||||
|
@ -23,7 +23,9 @@ replace github.com/status-im/status-go/waku => ./waku
|
|||
require (
|
||||
github.com/beevik/ntp v0.2.0
|
||||
github.com/ethereum/go-ethereum v1.9.5
|
||||
github.com/go-playground/universal-translator v0.17.0 // indirect
|
||||
github.com/golang/mock v1.3.1
|
||||
github.com/leodido/go-urn v1.2.0 // indirect
|
||||
github.com/lib/pq v1.2.0
|
||||
github.com/libp2p/go-libp2p v0.4.2 // indirect
|
||||
github.com/libp2p/go-libp2p-core v0.2.4
|
||||
|
@ -33,6 +35,8 @@ require (
|
|||
github.com/pborman/uuid v1.2.0
|
||||
github.com/pkg/errors v0.8.1
|
||||
github.com/prometheus/client_golang v1.2.1
|
||||
github.com/russolsen/ohyeah v0.0.0-20160324131710-f4938c005315 // indirect
|
||||
github.com/russolsen/same v0.0.0-20160222130632-f089df61f51d // indirect
|
||||
github.com/russolsen/transit v0.0.0-20180705123435-0794b4c4505a
|
||||
github.com/status-im/migrate/v4 v4.6.2-status.2
|
||||
github.com/status-im/rendezvous v1.3.0
|
||||
|
@ -46,6 +50,7 @@ require (
|
|||
github.com/syndtr/goleveldb v1.0.0
|
||||
go.uber.org/zap v1.13.0
|
||||
golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c
|
||||
gopkg.in/go-playground/validator.v9 v9.29.1
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
|
||||
gopkg.in/go-playground/validator.v9 v9.31.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||
)
|
||||
|
|
15
go.sum
15
go.sum
|
@ -343,7 +343,6 @@ github.com/libp2p/go-flow-metrics v0.0.1 h1:0gxuFd2GuK7IIP5pKljLwps6TvcuYgvG7Atq
|
|||
github.com/libp2p/go-flow-metrics v0.0.1/go.mod h1:Iv1GH0sG8DtYN3SVJ2eG221wMiNpZxBdp967ls1g+k8=
|
||||
github.com/libp2p/go-libp2p v0.1.1 h1:52sB0TJuDk2nYMcMfHOKaPoaayDZjaYVCq6Vk1ejUTk=
|
||||
github.com/libp2p/go-libp2p v0.1.1/go.mod h1:I00BRo1UuUSdpuc8Q2mN7yDF/oTUTRAX6JWpTiK9Rp8=
|
||||
github.com/libp2p/go-libp2p v0.4.0/go.mod h1:9EsEIf9p2UDuwtPd0DwJsAl0qXVxgAnuDGRvHbfATfI=
|
||||
github.com/libp2p/go-libp2p v0.4.2 h1:p0cthB0jDNHO4gH2HzS8/nAMMXbfUlFHs0jwZ4U+F2g=
|
||||
github.com/libp2p/go-libp2p v0.4.2/go.mod h1:MNmgUxUw5pMsdOzMlT0EE7oKjRasl+WyVwM0IBlpKgQ=
|
||||
github.com/libp2p/go-libp2p-autonat v0.1.0 h1:aCWAu43Ri4nU0ZPO7NyLzUvvfqd0nE3dX0R/ZGYVgOU=
|
||||
|
@ -356,7 +355,6 @@ github.com/libp2p/go-libp2p-blankhost v0.1.4 h1:I96SWjR4rK9irDHcHq3XHN6hawCRTPUA
|
|||
github.com/libp2p/go-libp2p-blankhost v0.1.4/go.mod h1:oJF0saYsAXQCSfDq254GMNmLNz6ZTHTOvtF4ZydUvwU=
|
||||
github.com/libp2p/go-libp2p-circuit v0.1.0 h1:eniLL3Y9aq/sryfyV1IAHj5rlvuyj3b7iz8tSiZpdhY=
|
||||
github.com/libp2p/go-libp2p-circuit v0.1.0/go.mod h1:Ahq4cY3V9VJcHcn1SBXjr78AbFkZeIRmfunbA7pmFh8=
|
||||
github.com/libp2p/go-libp2p-circuit v0.1.3/go.mod h1:Xqh2TjSy8DD5iV2cCOMzdynd6h8OTBGoV1AWbWor3qM=
|
||||
github.com/libp2p/go-libp2p-circuit v0.1.4 h1:Phzbmrg3BkVzbqd4ZZ149JxCuUWu2wZcXf/Kr6hZJj8=
|
||||
github.com/libp2p/go-libp2p-circuit v0.1.4/go.mod h1:CY67BrEjKNDhdTk8UgBX1Y/H5c3xkAcs3gnksxY7osU=
|
||||
github.com/libp2p/go-libp2p-core v0.0.1/go.mod h1:g/VxnTZ/1ygHxH3dKok7Vno1VfpvGcGip57wjTU4fco=
|
||||
|
@ -364,7 +362,6 @@ github.com/libp2p/go-libp2p-core v0.0.3/go.mod h1:j+YQMNz9WNSkNezXOsahp9kwZBKBvx
|
|||
github.com/libp2p/go-libp2p-core v0.0.4/go.mod h1:jyuCQP356gzfCFtRKyvAbNkyeuxb7OlyhWZ3nls5d2I=
|
||||
github.com/libp2p/go-libp2p-core v0.2.0/go.mod h1:X0eyB0Gy93v0DZtSYbEM7RnMChm9Uv3j7yRXjO77xSI=
|
||||
github.com/libp2p/go-libp2p-core v0.2.2/go.mod h1:8fcwTbsG2B+lTgRJ1ICZtiM5GWCWZVoVrLaDRvIRng0=
|
||||
github.com/libp2p/go-libp2p-core v0.2.3/go.mod h1:GqhyQqyIAPsxFYXHMjfXgMv03lxsvM0mFzuYA9Ib42A=
|
||||
github.com/libp2p/go-libp2p-core v0.2.4 h1:Et6ykkTwI6PU44tr8qUF9k43vP0aduMNniShAbUJJw8=
|
||||
github.com/libp2p/go-libp2p-core v0.2.4/go.mod h1:STh4fdfa5vDYr0/SzYYeqnt+E6KfEV5VxfIrm0bcI0g=
|
||||
github.com/libp2p/go-libp2p-crypto v0.1.0 h1:k9MFy+o2zGDNGsaoZl0MA3iZ75qXxr9OOoAZF+sD5OQ=
|
||||
|
@ -504,7 +501,6 @@ github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU
|
|||
github.com/multiformats/go-multiaddr-dns v0.0.1/go.mod h1:9kWcqw/Pj6FwxAwW38n/9403szc57zJPs45fmnznu3Q=
|
||||
github.com/multiformats/go-multiaddr-dns v0.0.2 h1:/Bbsgsy3R6e3jf2qBahzNHzww6usYaZ0NhNH3sqdFS8=
|
||||
github.com/multiformats/go-multiaddr-dns v0.0.2/go.mod h1:9kWcqw/Pj6FwxAwW38n/9403szc57zJPs45fmnznu3Q=
|
||||
github.com/multiformats/go-multiaddr-dns v0.1.0/go.mod h1:01k2RAqtoXIuPa3DCavAE9/6jc6nM0H3EgZyfUhN2oY=
|
||||
github.com/multiformats/go-multiaddr-dns v0.2.0 h1:YWJoIDwLePniH7OU5hBnDZV6SWuvJqJ0YtN6pLeH9zA=
|
||||
github.com/multiformats/go-multiaddr-dns v0.2.0/go.mod h1:TJ5pr5bBO7Y1B18djPuRsVkduhQH2YqYSbxWJzYGdK0=
|
||||
github.com/multiformats/go-multiaddr-fmt v0.0.1 h1:5YjeOIzbX8OTKVaN72aOzGIYW7PnrZrnkDyOfAWRSMA=
|
||||
|
@ -659,7 +655,6 @@ github.com/status-im/migrate/v4 v4.6.2-status.2 h1:SdC+sMDl/aI7vUlwD2qj2p7KsK4T6
|
|||
github.com/status-im/migrate/v4 v4.6.2-status.2/go.mod h1:c/kc90n47GZu/58nnz1OMLTf7uE4Da4gZP5qmU+A/v8=
|
||||
github.com/status-im/rendezvous v1.3.0 h1:7RK/MXXW+tlm0asKm1u7Qp7Yni6AO29a7j8+E4Lbjg4=
|
||||
github.com/status-im/rendezvous v1.3.0/go.mod h1:+hzjuP+j/XzLPeF6E50b88pWOTLdTcwjvNYt+Gh1W1s=
|
||||
github.com/status-im/status-go v0.38.5/go.mod h1:UKxySGdqFuVPvCyPSYD0+zKeV2OzDijZqICvR+tZ3uM=
|
||||
github.com/status-im/tcp-shaker v0.0.0-20191114194237-215893130501 h1:oa0KU5jJRNtXaM/P465MhvSFo/HM2O8qi2DDuPcd7ro=
|
||||
github.com/status-im/tcp-shaker v0.0.0-20191114194237-215893130501/go.mod h1:RYo/itke1oU5k/6sj9DNM3QAwtE5rZSgg5JnkOv83hk=
|
||||
github.com/steakknife/bloomfilter v0.0.0-20180922174646-6819c0d2a570 h1:gIlAHnH1vJb5vwEjIp5kBj/eu99p/bl0Ay2goiPe5xE=
|
||||
|
@ -754,7 +749,6 @@ golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8U
|
|||
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190618222545-ea8f1a30c443/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191119213627-4f8c1d86b1ba h1:9bFeDpN3gTqNanMVqNcoR/pJQuP5uroC3t1D7eXozTE=
|
||||
golang.org/x/crypto v0.0.0-20191119213627-4f8c1d86b1ba/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
|
@ -787,9 +781,8 @@ golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6/go.mod h1:t9HGtf8HONx5eT2rtn
|
|||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190912160710-24e19bdeb0f2 h1:4dVFTC832rPn4pomLSz1vA+are2+dU19w1H8OngV7nc=
|
||||
golang.org/x/net v0.0.0-20190912160710-24e19bdeb0f2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190930134127-c5a3c61f89f3 h1:6KET3Sqa7fkVfD63QnAM81ZeYg5n4HwApOJkufONnHA=
|
||||
golang.org/x/net v0.0.0-20190930134127-c5a3c61f89f3/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
|
@ -851,8 +844,6 @@ golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtn
|
|||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191109212701-97ad0ed33101 h1:LCmXVkvpQCDj724eX6irUTPCJP5GelFHxqGSWL2D1R0=
|
||||
golang.org/x/tools v0.0.0-20191109212701-97ad0ed33101/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191213032237-7093a17b0467 h1:Jybbe55FT+YYZIJGWmJIA4ZGcglFuZOduakIW3+gHXY=
|
||||
golang.org/x/tools v0.0.0-20191213032237-7093a17b0467/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||
google.golang.org/api v0.3.2/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||
|
@ -885,8 +876,8 @@ gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
|||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||
gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc=
|
||||
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
|
||||
gopkg.in/go-playground/validator.v9 v9.31.0 h1:bmXmP2RSNtFES+bn4uYuHT7iJFJv7Vj+an+ZQdDaD1M=
|
||||
gopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/jcmturner/aescts.v1 v1.0.1/go.mod h1:nsR8qBOg+OucoIW+WMhB3GspUQXq9XorLnQb9XtvcOo=
|
||||
gopkg.in/jcmturner/dnsutils.v1 v1.0.1/go.mod h1:m3v+5svpVOhtFAP/wSz+yzh4Mc0Fg7eRhxkJMWSIz9Q=
|
||||
|
|
|
@ -75,6 +75,10 @@ func (c *Chat) PublicKey() (*ecdsa.PublicKey, error) {
|
|||
|
||||
}
|
||||
|
||||
func (c *Chat) Public() bool {
|
||||
return c.ChatType == ChatTypePublic
|
||||
}
|
||||
|
||||
func (c *Chat) MarshalJSON() ([]byte, error) {
|
||||
type ChatAlias Chat
|
||||
item := struct {
|
||||
|
@ -212,20 +216,22 @@ func OneToOneFromPublicKey(pk *ecdsa.PublicKey) *Chat {
|
|||
|
||||
func CreateOneToOneChat(name string, publicKey *ecdsa.PublicKey) Chat {
|
||||
return Chat{
|
||||
ID: oneToOneChatID(publicKey),
|
||||
Name: name,
|
||||
Active: true,
|
||||
ChatType: ChatTypeOneToOne,
|
||||
ID: oneToOneChatID(publicKey),
|
||||
Name: name,
|
||||
Timestamp: int64(timestampInMs()),
|
||||
Active: true,
|
||||
ChatType: ChatTypeOneToOne,
|
||||
}
|
||||
}
|
||||
|
||||
func CreatePublicChat(name string) Chat {
|
||||
return Chat{
|
||||
ID: name,
|
||||
Name: name,
|
||||
Active: true,
|
||||
Color: chatColors[rand.Intn(len(chatColors))],
|
||||
ChatType: ChatTypePublic,
|
||||
ID: name,
|
||||
Name: name,
|
||||
Active: true,
|
||||
Timestamp: int64(timestampInMs()),
|
||||
Color: chatColors[rand.Intn(len(chatColors))],
|
||||
ChatType: ChatTypePublic,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -106,3 +106,8 @@ func buildContact(publicKey *ecdsa.PublicKey) (*Contact, error) {
|
|||
|
||||
return contact, nil
|
||||
}
|
||||
|
||||
func contactIDFromPublicKey(key *ecdsa.PublicKey) string {
|
||||
return types.EncodeHex(crypto.FromECDSAPub(key))
|
||||
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ go 1.13
|
|||
|
||||
replace github.com/ethereum/go-ethereum v1.9.5 => github.com/status-im/go-ethereum v1.9.5-status.7
|
||||
|
||||
replace github.com/gomarkdown/markdown v0.0.0-20191104174740-4d42851d4d5a => github.com/status-im/markdown v0.0.0-20191209105822-e3ba6c6109ba
|
||||
replace github.com/gomarkdown/markdown v0.0.0-20191209105822-e3ba6c6109ba => github.com/status-im/markdown v0.0.0-20191209105822-e3ba6c6109ba
|
||||
|
||||
replace github.com/status-im/status-go/eth-node => ../eth-node
|
||||
|
||||
|
@ -14,7 +14,7 @@ require (
|
|||
github.com/cenkalti/backoff/v3 v3.0.0
|
||||
github.com/ethereum/go-ethereum v1.9.5
|
||||
github.com/golang/protobuf v1.3.2
|
||||
github.com/gomarkdown/markdown v0.0.0-20191104174740-4d42851d4d5a
|
||||
github.com/gomarkdown/markdown v0.0.0-20191209105822-e3ba6c6109ba
|
||||
github.com/google/uuid v1.1.1
|
||||
github.com/jinzhu/copier v0.0.0-20190625015134-976e0346caa8
|
||||
github.com/lucasb-eyer/go-colorful v1.0.2
|
||||
|
@ -23,9 +23,8 @@ require (
|
|||
github.com/russolsen/transit v0.0.0-20180705123435-0794b4c4505a
|
||||
github.com/status-im/doubleratchet v3.0.0+incompatible
|
||||
github.com/status-im/migrate/v4 v4.6.2-status.2
|
||||
github.com/status-im/status-go v0.38.5 // indirect
|
||||
github.com/status-im/status-go/eth-node v1.0.0
|
||||
github.com/status-im/status-go/whisper/v6 v6.0.1
|
||||
github.com/status-im/status-go/eth-node v1.1.0
|
||||
github.com/status-im/status-go/whisper/v6 v6.1.0
|
||||
github.com/stretchr/testify v1.4.0
|
||||
github.com/vacp2p/mvds v0.0.23
|
||||
go.uber.org/zap v1.13.0
|
||||
|
|
|
@ -156,9 +156,11 @@ github.com/gizak/termui v0.0.0-20170117222342-991cd3d38091/go.mod h1:PkJoWUt/zac
|
|||
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
|
||||
github.com/go-kit/kit v0.8.0 h1:Wz+5lgoB0kkuqLEc6NVmwRknTKP6dTGbSqvhZtBI/j0=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0 h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0 h1:8HUsc87TaSWLKwrnumgC8/YconD2fJQsRJAsWaPg2ic=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
|
||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||
|
@ -196,6 +198,7 @@ github.com/gomarkdown/markdown v0.0.0-20191104174740-4d42851d4d5a h1:DsPLKbIJTzH
|
|||
github.com/gomarkdown/markdown v0.0.0-20191104174740-4d42851d4d5a/go.mod h1:aii0r/K0ZnHv7G0KF7xy1v0A7s2Ljrb5byB7MO5p6TU=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
|
@ -289,6 +292,7 @@ github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6
|
|||
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/reedsolomon v1.9.2/go.mod h1:CwCi+NUr9pqSVktrkN+Ondf06rkhYZ/pcNv7fu+8Un4=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/koron/go-ssdp v0.0.0-20180514024734-4a0ed625a78b/go.mod h1:5Ky9EC2xfoUKUor0Hjgi2BJhCSXJfMOFlmyYrVKGQMk=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=
|
||||
|
@ -763,6 +767,7 @@ golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtn
|
|||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191109212701-97ad0ed33101 h1:LCmXVkvpQCDj724eX6irUTPCJP5GelFHxqGSWL2D1R0=
|
||||
golang.org/x/tools v0.0.0-20191109212701-97ad0ed33101/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191213032237-7093a17b0467 h1:Jybbe55FT+YYZIJGWmJIA4ZGcglFuZOduakIW3+gHXY=
|
||||
golang.org/x/tools v0.0.0-20191213032237-7093a17b0467/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||
|
|
|
@ -113,6 +113,10 @@ type RawMessage struct {
|
|||
|
||||
func (m *Message) MarshalJSON() ([]byte, error) {
|
||||
type MessageAlias Message
|
||||
type StickerAlias struct {
|
||||
Hash string `json:"hash"`
|
||||
Pack int32 `json:"pack"`
|
||||
}
|
||||
item := struct {
|
||||
ID string `json:"id"`
|
||||
WhisperTimestamp uint64 `json:"whisperTimestamp"`
|
||||
|
@ -132,7 +136,7 @@ func (m *Message) MarshalJSON() ([]byte, error) {
|
|||
Replace string `json:"replace,omitEmpty"`
|
||||
ResponseTo string `json:"responseTo"`
|
||||
EnsName string `json:"ensName"`
|
||||
Sticker *protobuf.StickerMessage `json:"sticker"`
|
||||
Sticker *StickerAlias `json:"sticker"`
|
||||
CommandParameters *CommandParameters `json:"commandParameters"`
|
||||
Timestamp uint64 `json:"timestamp"`
|
||||
ContentType protobuf.ChatMessage_ContentType `json:"contentType"`
|
||||
|
@ -159,10 +163,15 @@ func (m *Message) MarshalJSON() ([]byte, error) {
|
|||
Timestamp: m.Timestamp,
|
||||
ContentType: m.ContentType,
|
||||
MessageType: m.MessageType,
|
||||
Sticker: m.GetSticker(),
|
||||
CommandParameters: m.CommandParameters,
|
||||
}
|
||||
|
||||
if sticker := m.GetSticker(); sticker != nil {
|
||||
item.Sticker = &StickerAlias{
|
||||
Pack: sticker.Pack,
|
||||
Hash: sticker.Hash,
|
||||
}
|
||||
}
|
||||
return json.Marshal(item)
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@ import (
|
|||
"go.uber.org/zap"
|
||||
|
||||
"github.com/status-im/status-go/eth-node/crypto"
|
||||
"github.com/status-im/status-go/eth-node/types"
|
||||
"github.com/status-im/status-go/protocol/encryption/multidevice"
|
||||
"github.com/status-im/status-go/protocol/protobuf"
|
||||
v1protocol "github.com/status-im/status-go/protocol/v1"
|
||||
|
@ -49,7 +48,7 @@ func (m *MessageHandler) HandleMembershipUpdate(messageState *ReceivedMessageSta
|
|||
}
|
||||
|
||||
// A new chat must contain us
|
||||
if !group.IsMember(types.EncodeHex(crypto.FromECDSAPub(&m.identity.PublicKey))) {
|
||||
if !group.IsMember(contactIDFromPublicKey(&m.identity.PublicKey)) {
|
||||
return errors.New("can't create a new group chat without us being a member")
|
||||
}
|
||||
newChat := createGroupChat()
|
||||
|
@ -198,6 +197,21 @@ func (m *MessageHandler) HandleSyncInstallationContact(state *ReceivedMessageSta
|
|||
return nil
|
||||
}
|
||||
|
||||
func (m *MessageHandler) HandleSyncInstallationPublicChat(state *ReceivedMessageState, message protobuf.SyncInstallationPublicChat) error {
|
||||
chatID := message.Id
|
||||
_, ok := state.AllChats[chatID]
|
||||
if ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
chat := CreatePublicChat(chatID)
|
||||
|
||||
state.AllChats[chat.ID] = &chat
|
||||
state.ModifiedChats[chat.ID] = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MessageHandler) HandleContactUpdate(state *ReceivedMessageState, message protobuf.ContactUpdate) error {
|
||||
logger := m.logger.With(zap.String("site", "HandleContactUpdate"))
|
||||
contact := state.CurrentMessageState.Contact
|
||||
|
@ -212,7 +226,7 @@ func (m *MessageHandler) HandleContactUpdate(state *ReceivedMessageState, messag
|
|||
|
||||
if contact.LastUpdated < message.Clock {
|
||||
logger.Info("Updating contact")
|
||||
if !contact.HasBeenAdded() {
|
||||
if !contact.HasBeenAdded() && contact.ID != contactIDFromPublicKey(&m.identity.PublicKey) {
|
||||
contact.SystemTags = append(contact.SystemTags, contactRequestReceived)
|
||||
}
|
||||
if contact.Name != message.EnsName {
|
||||
|
@ -333,7 +347,7 @@ func (m *MessageHandler) HandleRequestAddressForTransaction(messageState *Receiv
|
|||
Clock: command.Clock,
|
||||
Timestamp: messageState.CurrentMessageState.WhisperTimestamp,
|
||||
Text: "Request address for transaction",
|
||||
ChatId: types.EncodeHex(crypto.FromECDSAPub(&m.identity.PublicKey)),
|
||||
ChatId: contactIDFromPublicKey(&m.identity.PublicKey),
|
||||
MessageType: protobuf.ChatMessage_ONE_TO_ONE,
|
||||
ContentType: protobuf.ChatMessage_TRANSACTION_COMMAND,
|
||||
},
|
||||
|
@ -357,7 +371,7 @@ func (m *MessageHandler) HandleRequestTransaction(messageState *ReceivedMessageS
|
|||
Clock: command.Clock,
|
||||
Timestamp: messageState.CurrentMessageState.WhisperTimestamp,
|
||||
Text: "Request transaction",
|
||||
ChatId: types.EncodeHex(crypto.FromECDSAPub(&m.identity.PublicKey)),
|
||||
ChatId: contactIDFromPublicKey(&m.identity.PublicKey),
|
||||
MessageType: protobuf.ChatMessage_ONE_TO_ONE,
|
||||
ContentType: protobuf.ChatMessage_TRANSACTION_COMMAND,
|
||||
},
|
||||
|
@ -563,7 +577,7 @@ func (m *MessageHandler) matchMessage(message *Message, chats map[string]*Chat)
|
|||
case message.MessageType == protobuf.ChatMessage_ONE_TO_ONE:
|
||||
// It's an incoming private message. ChatID is calculated from the signature.
|
||||
// If a chat does not exist, a new one is created and saved.
|
||||
chatID := types.EncodeHex(crypto.FromECDSAPub(message.SigPubKey))
|
||||
chatID := contactIDFromPublicKey(message.SigPubKey)
|
||||
chat := chats[chatID]
|
||||
if chat == nil {
|
||||
// TODO: this should be a three-word name used in the mobile client
|
||||
|
@ -580,8 +594,8 @@ func (m *MessageHandler) matchMessage(message *Message, chats map[string]*Chat)
|
|||
return nil, errors.New("received group chat message for non-existing chat")
|
||||
}
|
||||
|
||||
theirKeyHex := types.EncodeHex(crypto.FromECDSAPub(message.SigPubKey))
|
||||
myKeyHex := types.EncodeHex(crypto.FromECDSAPub(&m.identity.PublicKey))
|
||||
theirKeyHex := contactIDFromPublicKey(message.SigPubKey)
|
||||
myKeyHex := contactIDFromPublicKey(&m.identity.PublicKey)
|
||||
var theyJoined bool
|
||||
var iJoined bool
|
||||
for _, member := range chat.Members {
|
||||
|
|
|
@ -83,7 +83,7 @@ type MessengerResponse struct {
|
|||
}
|
||||
|
||||
func (m *MessengerResponse) IsEmpty() bool {
|
||||
return len(m.Chats) == 0 && len(m.Messages) == 0 && len(m.Contacts) == 0 && len(m.RawMessages) == 0
|
||||
return len(m.Chats) == 0 && len(m.Messages) == 0 && len(m.Contacts) == 0 && len(m.RawMessages) == 0 && len(m.Installations) == 0
|
||||
}
|
||||
|
||||
type featureFlags struct {
|
||||
|
@ -216,7 +216,7 @@ func NewMessenger(
|
|||
onNewInstallationsHandler := func(installations []*multidevice.Installation) {
|
||||
|
||||
for _, installation := range installations {
|
||||
if installation.Identity == types.EncodeHex(crypto.FromECDSAPub(&messenger.identity.PublicKey)) {
|
||||
if installation.Identity == contactIDFromPublicKey(&messenger.identity.PublicKey) {
|
||||
if _, ok := messenger.allInstallations[installation.ID]; !ok {
|
||||
messenger.allInstallations[installation.ID] = installation
|
||||
messenger.modifiedInstallations[installation.ID] = true
|
||||
|
@ -595,7 +595,7 @@ func (m *Messenger) Join(chat Chat) error {
|
|||
}
|
||||
return m.transport.JoinGroup(members)
|
||||
case ChatTypePublic:
|
||||
return m.transport.JoinPublic(chat.Name)
|
||||
return m.transport.JoinPublic(chat.ID)
|
||||
default:
|
||||
return errors.New("chat is neither public nor private")
|
||||
}
|
||||
|
@ -638,8 +638,9 @@ func (m *Messenger) CreateGroupChatWithMembers(ctx context.Context, name string,
|
|||
}
|
||||
chat.updateChatFromProtocolGroup(group)
|
||||
|
||||
clock, _ := chat.NextClockAndTimestamp()
|
||||
// Add members
|
||||
event := v1protocol.NewMembersAddedEvent(members, group.NextClockValue())
|
||||
event := v1protocol.NewMembersAddedEvent(members, clock)
|
||||
event.ChatID = chat.ID
|
||||
err = event.Sign(m.identity)
|
||||
if err != nil {
|
||||
|
@ -704,8 +705,9 @@ func (m *Messenger) RemoveMemberFromGroupChat(ctx context.Context, chatID string
|
|||
return nil, err
|
||||
}
|
||||
|
||||
clock, _ := chat.NextClockAndTimestamp()
|
||||
// Remove member
|
||||
event := v1protocol.NewMemberRemovedEvent(member, group.NextClockValue())
|
||||
event := v1protocol.NewMemberRemovedEvent(member, clock)
|
||||
event.ChatID = chat.ID
|
||||
err = event.Sign(m.identity)
|
||||
if err != nil {
|
||||
|
@ -755,8 +757,9 @@ func (m *Messenger) AddMembersToGroupChat(ctx context.Context, chatID string, me
|
|||
return nil, err
|
||||
}
|
||||
|
||||
clock, _ := chat.NextClockAndTimestamp()
|
||||
// Add members
|
||||
event := v1protocol.NewMembersAddedEvent(members, group.NextClockValue())
|
||||
event := v1protocol.NewMembersAddedEvent(members, clock)
|
||||
event.ChatID = chat.ID
|
||||
err = event.Sign(m.identity)
|
||||
if err != nil {
|
||||
|
@ -815,8 +818,9 @@ func (m *Messenger) AddAdminsToGroupChat(ctx context.Context, chatID string, mem
|
|||
return nil, err
|
||||
}
|
||||
|
||||
clock, _ := chat.NextClockAndTimestamp()
|
||||
// Add members
|
||||
event := v1protocol.NewAdminsAddedEvent(members, group.NextClockValue())
|
||||
event := v1protocol.NewAdminsAddedEvent(members, clock)
|
||||
event.ChatID = chat.ID
|
||||
err = event.Sign(m.identity)
|
||||
if err != nil {
|
||||
|
@ -877,8 +881,9 @@ func (m *Messenger) ConfirmJoiningGroup(ctx context.Context, chatID string) (*Me
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clock, _ := chat.NextClockAndTimestamp()
|
||||
event := v1protocol.NewMemberJoinedEvent(
|
||||
group.NextClockValue(),
|
||||
clock,
|
||||
)
|
||||
event.ChatID = chat.ID
|
||||
err = event.Sign(m.identity)
|
||||
|
@ -939,9 +944,10 @@ func (m *Messenger) LeaveGroupChat(ctx context.Context, chatID string) (*Messeng
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clock, _ := chat.NextClockAndTimestamp()
|
||||
event := v1protocol.NewMemberRemovedEvent(
|
||||
types.EncodeHex(crypto.FromECDSAPub(&m.identity.PublicKey)),
|
||||
group.NextClockValue(),
|
||||
contactIDFromPublicKey(&m.identity.PublicKey),
|
||||
clock,
|
||||
)
|
||||
event.ChatID = chat.ID
|
||||
err = event.Sign(m.identity)
|
||||
|
@ -983,6 +989,14 @@ func (m *Messenger) LeaveGroupChat(ctx context.Context, chatID string) (*Messeng
|
|||
}
|
||||
|
||||
func (m *Messenger) saveChat(chat *Chat) error {
|
||||
_, ok := m.allChats[chat.ID]
|
||||
// Sync chat if it's a new active public chat
|
||||
if !ok && chat.Active && chat.Public() {
|
||||
if err := m.syncPublicChat(context.Background(), chat); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err := m.persistence.SaveChat(*chat)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -1008,12 +1022,7 @@ func (m *Messenger) saveChats(chats []*Chat) error {
|
|||
func (m *Messenger) SaveChat(chat *Chat) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
err := m.saveChat(chat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return m.saveChat(chat)
|
||||
}
|
||||
|
||||
func (m *Messenger) Chats() []*Chat {
|
||||
|
@ -1345,6 +1354,13 @@ func (m *Messenger) SendContactUpdates(ctx context.Context, ensName, profileImag
|
|||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
myID := contactIDFromPublicKey(&m.identity.PublicKey)
|
||||
|
||||
if _, err := m.sendContactUpdate(ctx, myID, ensName, profileImage); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: This should not be sending paired messages, as we do it above
|
||||
for _, contact := range m.allContacts {
|
||||
if contact.IsAdded() {
|
||||
if _, err := m.sendContactUpdate(ctx, contact.ID, ensName, profileImage); err != nil {
|
||||
|
@ -1353,7 +1369,6 @@ func (m *Messenger) SendContactUpdates(ctx context.Context, ensName, profileImag
|
|||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// SendContactUpdate sends a contact update to a user and adds the user to contacts
|
||||
|
@ -1417,7 +1432,7 @@ func (m *Messenger) sendContactUpdate(ctx context.Context, chatID, ensName, prof
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if !contact.IsAdded() {
|
||||
if !contact.IsAdded() && contact.ID != contactIDFromPublicKey(&m.identity.PublicKey) {
|
||||
contact.SystemTags = append(contact.SystemTags, contactAdded)
|
||||
}
|
||||
|
||||
|
@ -1432,6 +1447,36 @@ func (m *Messenger) sendContactUpdate(ctx context.Context, chatID, ensName, prof
|
|||
return &response, m.saveContact(contact)
|
||||
}
|
||||
|
||||
// SyncDevices sends all public chats and contacts to paired devices
|
||||
func (m *Messenger) SyncDevices(ctx context.Context, ensName, photoPath string) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
myID := contactIDFromPublicKey(&m.identity.PublicKey)
|
||||
|
||||
if _, err := m.sendContactUpdate(ctx, myID, ensName, photoPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, chat := range m.allChats {
|
||||
if chat.Public() && chat.Active {
|
||||
if err := m.syncPublicChat(ctx, chat); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, contact := range m.allContacts {
|
||||
if contact.IsAdded() && contact.ID != myID {
|
||||
if err := m.syncContact(ctx, contact); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendPairInstallation sends a pair installation message
|
||||
func (m *Messenger) SendPairInstallation(ctx context.Context) (*MessengerResponse, error) {
|
||||
m.mutex.Lock()
|
||||
|
@ -1449,7 +1494,7 @@ func (m *Messenger) SendPairInstallation(ctx context.Context) (*MessengerRespons
|
|||
return nil, errors.New("no installation metadata")
|
||||
}
|
||||
|
||||
chatID := types.EncodeHex(crypto.FromECDSAPub(&m.identity.PublicKey))
|
||||
chatID := contactIDFromPublicKey(&m.identity.PublicKey)
|
||||
|
||||
chat, ok := m.allChats[chatID]
|
||||
if !ok {
|
||||
|
@ -1491,13 +1536,54 @@ func (m *Messenger) SendPairInstallation(ctx context.Context) (*MessengerRespons
|
|||
return &response, nil
|
||||
}
|
||||
|
||||
// syncPublicChat sync a public chat with paired devices
|
||||
func (m *Messenger) syncPublicChat(ctx context.Context, publicChat *Chat) error {
|
||||
var err error
|
||||
if !m.hasPairedDevices() {
|
||||
return nil
|
||||
}
|
||||
chatID := contactIDFromPublicKey(&m.identity.PublicKey)
|
||||
|
||||
chat, ok := m.allChats[chatID]
|
||||
if !ok {
|
||||
chat = OneToOneFromPublicKey(&m.identity.PublicKey)
|
||||
// We don't want to show the chat to the user
|
||||
chat.Active = false
|
||||
}
|
||||
|
||||
m.allChats[chat.ID] = chat
|
||||
clock, _ := chat.NextClockAndTimestamp()
|
||||
|
||||
syncMessage := &protobuf.SyncInstallationPublicChat{
|
||||
Clock: clock,
|
||||
Id: publicChat.ID,
|
||||
}
|
||||
encodedMessage, err := proto.Marshal(syncMessage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = m.dispatchMessage(ctx, &RawMessage{
|
||||
LocalChatID: chatID,
|
||||
Payload: encodedMessage,
|
||||
MessageType: protobuf.ApplicationMetadataMessage_SYNC_INSTALLATION_PUBLIC_CHAT,
|
||||
ResendAutomatically: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
chat.LastClockValue = clock
|
||||
return m.saveChat(chat)
|
||||
}
|
||||
|
||||
// syncContact sync as contact with paired devices
|
||||
func (m *Messenger) syncContact(ctx context.Context, contact *Contact) error {
|
||||
var err error
|
||||
if !m.hasPairedDevices() {
|
||||
return nil
|
||||
}
|
||||
chatID := types.EncodeHex(crypto.FromECDSAPub(&m.identity.PublicKey))
|
||||
chatID := contactIDFromPublicKey(&m.identity.PublicKey)
|
||||
|
||||
chat, ok := m.allChats[chatID]
|
||||
if !ok {
|
||||
|
@ -1624,7 +1710,7 @@ func (m *Messenger) handleRetrievedMessages(chatWithMessages map[transport.Filte
|
|||
publicKey := msg.SigPubKey()
|
||||
|
||||
// Check for messages from blocked users
|
||||
senderID := types.EncodeHex(crypto.FromECDSAPub(publicKey))
|
||||
senderID := contactIDFromPublicKey(publicKey)
|
||||
if _, ok := messageState.AllContacts[senderID]; ok && messageState.AllContacts[senderID].IsBlocked() {
|
||||
continue
|
||||
}
|
||||
|
@ -1706,6 +1792,19 @@ func (m *Messenger) handleRetrievedMessages(chatWithMessages map[transport.Filte
|
|||
logger.Warn("failed to handle SyncInstallationContact", zap.Error(err))
|
||||
continue
|
||||
}
|
||||
case protobuf.SyncInstallationPublicChat:
|
||||
if !isPubKeyEqual(messageState.CurrentMessageState.PublicKey, &m.identity.PublicKey) {
|
||||
logger.Warn("not coming from us, ignoring")
|
||||
continue
|
||||
}
|
||||
|
||||
p := msg.ParsedMessage.(protobuf.SyncInstallationPublicChat)
|
||||
logger.Debug("Handling SyncInstallationPublicChat", zap.Any("message", p))
|
||||
err = m.handler.HandleSyncInstallationPublicChat(messageState, p)
|
||||
if err != nil {
|
||||
logger.Warn("failed to handle SyncInstallationPublicChat", zap.Error(err))
|
||||
continue
|
||||
}
|
||||
case protobuf.RequestAddressForTransaction:
|
||||
command := msg.ParsedMessage.(protobuf.RequestAddressForTransaction)
|
||||
logger.Debug("Handling RequestAddressForTransaction", zap.Any("message", command))
|
||||
|
@ -2479,7 +2578,7 @@ func (m *Messenger) AcceptRequestTransaction(ctx context.Context, transactionHas
|
|||
return &response, m.saveChat(chat)
|
||||
}
|
||||
|
||||
func (m *Messenger) SendTransaction(ctx context.Context, chatID, transactionHash string, signature []byte) (*MessengerResponse, error) {
|
||||
func (m *Messenger) SendTransaction(ctx context.Context, chatID, value, contract, transactionHash string, signature []byte) (*MessengerResponse, error) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
|
@ -2534,6 +2633,8 @@ func (m *Messenger) SendTransaction(ctx context.Context, chatID, transactionHash
|
|||
message.ID = types.EncodeHex(newMessageID)
|
||||
message.CommandParameters = &CommandParameters{
|
||||
TransactionHash: transactionHash,
|
||||
Value: value,
|
||||
Contract: contract,
|
||||
Signature: signature,
|
||||
CommandState: CommandStateTransactionSent,
|
||||
}
|
||||
|
@ -2584,7 +2685,7 @@ func (m *Messenger) ValidateTransactions(ctx context.Context, addresses []types.
|
|||
}
|
||||
for _, validationResult := range responses {
|
||||
var message *Message
|
||||
chatID := types.EncodeHex(crypto.FromECDSAPub(validationResult.Transaction.From))
|
||||
chatID := contactIDFromPublicKey(validationResult.Transaction.From)
|
||||
chat, ok := m.allChats[chatID]
|
||||
if !ok {
|
||||
chat = OneToOneFromPublicKey(validationResult.Transaction.From)
|
||||
|
|
|
@ -141,7 +141,135 @@ func (s *MessengerInstallationSuite) TestReceiveInstallation() {
|
|||
actualContact := response.Contacts[0]
|
||||
s.Require().Equal(contact.ID, actualContact.ID)
|
||||
s.Require().True(actualContact.IsAdded())
|
||||
|
||||
chat := CreatePublicChat("status")
|
||||
err = s.m.SaveChat(&chat)
|
||||
s.Require().NoError(err)
|
||||
|
||||
// Wait for the message to reach its destination
|
||||
err = tt.RetryWithBackOff(func() error {
|
||||
var err error
|
||||
response, err = theirMessenger.RetrieveAll()
|
||||
if err == nil && len(response.Chats) == 0 {
|
||||
err = errors.New("sync chat not received")
|
||||
}
|
||||
return err
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
|
||||
actualChat := response.Chats[0]
|
||||
s.Require().Equal("status", actualChat.ID)
|
||||
s.Require().True(actualChat.Active)
|
||||
}
|
||||
|
||||
func (s *MessengerInstallationSuite) TestReceiveSyncInstallation() {
|
||||
func (s *MessengerInstallationSuite) TestSyncInstallation() {
|
||||
|
||||
// add contact
|
||||
contactKey, err := crypto.GenerateKey()
|
||||
s.Require().NoError(err)
|
||||
|
||||
contact, err := buildContact(&contactKey.PublicKey)
|
||||
s.Require().NoError(err)
|
||||
contact.SystemTags = append(contact.SystemTags, contactAdded)
|
||||
err = s.m.SaveContact(contact)
|
||||
s.Require().NoError(err)
|
||||
|
||||
// add chat
|
||||
chat := CreatePublicChat("status")
|
||||
err = s.m.SaveChat(&chat)
|
||||
s.Require().NoError(err)
|
||||
|
||||
// pair
|
||||
theirMessenger := s.newMessengerWithKey(s.shh, s.privateKey)
|
||||
|
||||
err = theirMessenger.SetInstallationMetadata(theirMessenger.installationID, &multidevice.InstallationMetadata{
|
||||
Name: "their-name",
|
||||
DeviceType: "their-device-type",
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
response, err := theirMessenger.SendPairInstallation(context.Background())
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(response)
|
||||
s.Require().Len(response.Chats, 1)
|
||||
s.Require().False(response.Chats[0].Active)
|
||||
|
||||
// Wait for the message to reach its destination
|
||||
err = tt.RetryWithBackOff(func() error {
|
||||
var err error
|
||||
response, err = s.m.RetrieveAll()
|
||||
if err == nil && len(response.Installations) == 0 {
|
||||
err = errors.New("installation not received")
|
||||
}
|
||||
return err
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
actualInstallation := response.Installations[0]
|
||||
s.Require().Equal(theirMessenger.installationID, actualInstallation.ID)
|
||||
s.Require().NotNil(actualInstallation.InstallationMetadata)
|
||||
s.Require().Equal("their-name", actualInstallation.InstallationMetadata.Name)
|
||||
s.Require().Equal("their-device-type", actualInstallation.InstallationMetadata.DeviceType)
|
||||
|
||||
err = s.m.EnableInstallation(theirMessenger.installationID)
|
||||
s.Require().NoError(err)
|
||||
|
||||
// sync
|
||||
err = s.m.SyncDevices(context.Background(), "ens-name", "profile-image")
|
||||
s.Require().NoError(err)
|
||||
|
||||
var allChats []*Chat
|
||||
var allContacts []*Contact
|
||||
// Wait for the message to reach its destination
|
||||
err = tt.RetryWithBackOff(func() error {
|
||||
var err error
|
||||
response, err = theirMessenger.RetrieveAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
allChats = append(allChats, response.Chats...)
|
||||
allContacts = append(allContacts, response.Contacts...)
|
||||
|
||||
if len(allChats) >= 2 && len(allContacts) >= 3 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("Not received all chats & contacts")
|
||||
|
||||
})
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
||||
var statusChat *Chat
|
||||
for _, c := range allChats {
|
||||
if c.ID == "status" {
|
||||
statusChat = c
|
||||
}
|
||||
}
|
||||
|
||||
s.Require().NotNil(statusChat)
|
||||
|
||||
var actualContact *Contact
|
||||
for _, c := range allContacts {
|
||||
if c.ID == contact.ID {
|
||||
actualContact = c
|
||||
}
|
||||
}
|
||||
|
||||
s.Require().True(actualContact.IsAdded())
|
||||
|
||||
var ourContact *Contact
|
||||
|
||||
myID := types.EncodeHex(crypto.FromECDSAPub(&s.m.identity.PublicKey))
|
||||
for _, c := range allContacts {
|
||||
if c.ID == myID {
|
||||
if ourContact == nil || ourContact.LastUpdated < c.LastUpdated {
|
||||
ourContact = c
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s.Require().NotNil(ourContact)
|
||||
s.Require().Equal("ens-name", ourContact.Name)
|
||||
s.Require().Equal("profile-image", ourContact.Photo)
|
||||
|
||||
}
|
||||
|
|
|
@ -1014,6 +1014,19 @@ func (s *MessengerSuite) TestChatPersistenceOneToOne() {
|
|||
}
|
||||
|
||||
func (s *MessengerSuite) TestChatPersistencePrivateGroupChat() {
|
||||
|
||||
member1Key, err := crypto.GenerateKey()
|
||||
s.Require().NoError(err)
|
||||
member1ID := types.EncodeHex(crypto.FromECDSAPub(&member1Key.PublicKey))
|
||||
|
||||
member2Key, err := crypto.GenerateKey()
|
||||
s.Require().NoError(err)
|
||||
member2ID := types.EncodeHex(crypto.FromECDSAPub(&member2Key.PublicKey))
|
||||
|
||||
member3Key, err := crypto.GenerateKey()
|
||||
s.Require().NoError(err)
|
||||
member3ID := types.EncodeHex(crypto.FromECDSAPub(&member3Key.PublicKey))
|
||||
|
||||
chat := Chat{
|
||||
ID: "chat-id",
|
||||
Name: "chat-id",
|
||||
|
@ -1023,17 +1036,17 @@ func (s *MessengerSuite) TestChatPersistencePrivateGroupChat() {
|
|||
Timestamp: 10,
|
||||
Members: []ChatMember{
|
||||
{
|
||||
ID: "1",
|
||||
ID: member1ID,
|
||||
Admin: false,
|
||||
Joined: true,
|
||||
},
|
||||
{
|
||||
ID: "2",
|
||||
ID: member2ID,
|
||||
Admin: true,
|
||||
Joined: false,
|
||||
},
|
||||
{
|
||||
ID: "3",
|
||||
ID: member3ID,
|
||||
Admin: true,
|
||||
Joined: true,
|
||||
},
|
||||
|
@ -1554,6 +1567,7 @@ func (s *MessengerSuite) TestDeclineRequestAddressForTransaction() {
|
|||
|
||||
func (s *MessengerSuite) TestSendEthTransaction() {
|
||||
value := "2000"
|
||||
contract := "some-contract"
|
||||
|
||||
theirMessenger := s.newMessenger(s.shh)
|
||||
theirPkString := types.EncodeHex(crypto.FromECDSAPub(&theirMessenger.identity.PublicKey))
|
||||
|
@ -1569,7 +1583,7 @@ func (s *MessengerSuite) TestSendEthTransaction() {
|
|||
signature, err := buildSignature(s.m.identity, &s.m.identity.PublicKey, transactionHash)
|
||||
s.Require().NoError(err)
|
||||
|
||||
response, err := s.m.SendTransaction(context.Background(), theirPkString, transactionHash, signature)
|
||||
response, err := s.m.SendTransaction(context.Background(), theirPkString, value, contract, transactionHash, signature)
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(response)
|
||||
s.Require().Len(response.Chats, 1)
|
||||
|
@ -1580,6 +1594,8 @@ func (s *MessengerSuite) TestSendEthTransaction() {
|
|||
s.Require().Equal("Transaction sent", senderMessage.Text)
|
||||
s.Require().NotNil(senderMessage.CommandParameters)
|
||||
s.Require().Equal(transactionHash, senderMessage.CommandParameters.TransactionHash)
|
||||
s.Require().Equal(contract, senderMessage.CommandParameters.Contract)
|
||||
s.Require().Equal(value, senderMessage.CommandParameters.Value)
|
||||
s.Require().Equal(signature, senderMessage.CommandParameters.Signature)
|
||||
s.Require().Equal(CommandStateTransactionSent, senderMessage.CommandParameters.CommandState)
|
||||
s.Require().NotEmpty(senderMessage.ID)
|
||||
|
@ -1667,7 +1683,7 @@ func (s *MessengerSuite) TestSendTokenTransaction() {
|
|||
signature, err := buildSignature(s.m.identity, &s.m.identity.PublicKey, transactionHash)
|
||||
s.Require().NoError(err)
|
||||
|
||||
response, err := s.m.SendTransaction(context.Background(), theirPkString, transactionHash, signature)
|
||||
response, err := s.m.SendTransaction(context.Background(), theirPkString, value, contract, transactionHash, signature)
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(response)
|
||||
s.Require().Len(response.Chats, 1)
|
||||
|
@ -1678,6 +1694,8 @@ func (s *MessengerSuite) TestSendTokenTransaction() {
|
|||
s.Require().Equal("Transaction sent", senderMessage.Text)
|
||||
s.Require().NotNil(senderMessage.CommandParameters)
|
||||
s.Require().Equal(transactionHash, senderMessage.CommandParameters.TransactionHash)
|
||||
s.Require().Equal(value, senderMessage.CommandParameters.Value)
|
||||
s.Require().Equal(contract, senderMessage.CommandParameters.Contract)
|
||||
s.Require().Equal(signature, senderMessage.CommandParameters.Signature)
|
||||
s.Require().Equal(CommandStateTransactionSent, senderMessage.CommandParameters.CommandState)
|
||||
s.Require().NotEmpty(senderMessage.ID)
|
||||
|
|
|
@ -342,10 +342,6 @@ func (g Group) LastClockValue() uint64 {
|
|||
return g.events[len(g.events)-1].ClockValue
|
||||
}
|
||||
|
||||
func (g Group) NextClockValue() uint64 {
|
||||
return g.LastClockValue() + 1
|
||||
}
|
||||
|
||||
func (g Group) creator() (string, error) {
|
||||
if len(g.events) == 0 {
|
||||
return "", errors.New("no events in the group")
|
||||
|
|
|
@ -721,8 +721,8 @@ func (api *PublicAPI) AcceptRequestAddressForTransaction(ctx context.Context, me
|
|||
return api.service.messenger.AcceptRequestAddressForTransaction(ctx, messageID, address)
|
||||
}
|
||||
|
||||
func (api *PublicAPI) SendTransaction(ctx context.Context, chatID, transactionHash string, signature types.HexBytes) (*protocol.MessengerResponse, error) {
|
||||
return api.service.messenger.SendTransaction(ctx, chatID, transactionHash, signature)
|
||||
func (api *PublicAPI) SendTransaction(ctx context.Context, chatID, value, contract, transactionHash string, signature types.HexBytes) (*protocol.MessengerResponse, error) {
|
||||
return api.service.messenger.SendTransaction(ctx, chatID, value, contract, transactionHash, signature)
|
||||
}
|
||||
|
||||
func (api *PublicAPI) AcceptRequestTransaction(ctx context.Context, transactionHash, messageID string, signature types.HexBytes) (*protocol.MessengerResponse, error) {
|
||||
|
@ -737,6 +737,14 @@ func (api *PublicAPI) SendContactUpdate(ctx context.Context, contactID, name, pi
|
|||
return api.service.messenger.SendContactUpdate(ctx, contactID, name, picture)
|
||||
}
|
||||
|
||||
func (api *PublicAPI) SendPairInstallation(ctx context.Context) (*protocol.MessengerResponse, error) {
|
||||
return api.service.messenger.SendPairInstallation(ctx)
|
||||
}
|
||||
|
||||
func (api *PublicAPI) SyncDevices(ctx context.Context, name, picture string) error {
|
||||
return api.service.messenger.SyncDevices(ctx, name, picture)
|
||||
}
|
||||
|
||||
// -----
|
||||
// HELPER
|
||||
// -----
|
||||
|
|
|
@ -75,6 +75,10 @@ func (c *Chat) PublicKey() (*ecdsa.PublicKey, error) {
|
|||
|
||||
}
|
||||
|
||||
func (c *Chat) Public() bool {
|
||||
return c.ChatType == ChatTypePublic
|
||||
}
|
||||
|
||||
func (c *Chat) MarshalJSON() ([]byte, error) {
|
||||
type ChatAlias Chat
|
||||
item := struct {
|
||||
|
@ -212,20 +216,22 @@ func OneToOneFromPublicKey(pk *ecdsa.PublicKey) *Chat {
|
|||
|
||||
func CreateOneToOneChat(name string, publicKey *ecdsa.PublicKey) Chat {
|
||||
return Chat{
|
||||
ID: oneToOneChatID(publicKey),
|
||||
Name: name,
|
||||
Active: true,
|
||||
ChatType: ChatTypeOneToOne,
|
||||
ID: oneToOneChatID(publicKey),
|
||||
Name: name,
|
||||
Timestamp: int64(timestampInMs()),
|
||||
Active: true,
|
||||
ChatType: ChatTypeOneToOne,
|
||||
}
|
||||
}
|
||||
|
||||
func CreatePublicChat(name string) Chat {
|
||||
return Chat{
|
||||
ID: name,
|
||||
Name: name,
|
||||
Active: true,
|
||||
Color: chatColors[rand.Intn(len(chatColors))],
|
||||
ChatType: ChatTypePublic,
|
||||
ID: name,
|
||||
Name: name,
|
||||
Active: true,
|
||||
Timestamp: int64(timestampInMs()),
|
||||
Color: chatColors[rand.Intn(len(chatColors))],
|
||||
ChatType: ChatTypePublic,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -106,3 +106,8 @@ func buildContact(publicKey *ecdsa.PublicKey) (*Contact, error) {
|
|||
|
||||
return contact, nil
|
||||
}
|
||||
|
||||
func contactIDFromPublicKey(key *ecdsa.PublicKey) string {
|
||||
return types.EncodeHex(crypto.FromECDSAPub(key))
|
||||
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ go 1.13
|
|||
|
||||
replace github.com/ethereum/go-ethereum v1.9.5 => github.com/status-im/go-ethereum v1.9.5-status.7
|
||||
|
||||
replace github.com/gomarkdown/markdown v0.0.0-20191104174740-4d42851d4d5a => github.com/status-im/markdown v0.0.0-20191209105822-e3ba6c6109ba
|
||||
replace github.com/gomarkdown/markdown v0.0.0-20191209105822-e3ba6c6109ba => github.com/status-im/markdown v0.0.0-20191209105822-e3ba6c6109ba
|
||||
|
||||
replace github.com/status-im/status-go/eth-node => ../eth-node
|
||||
|
||||
|
@ -14,7 +14,7 @@ require (
|
|||
github.com/cenkalti/backoff/v3 v3.0.0
|
||||
github.com/ethereum/go-ethereum v1.9.5
|
||||
github.com/golang/protobuf v1.3.2
|
||||
github.com/gomarkdown/markdown v0.0.0-20191104174740-4d42851d4d5a
|
||||
github.com/gomarkdown/markdown v0.0.0-20191209105822-e3ba6c6109ba
|
||||
github.com/google/uuid v1.1.1
|
||||
github.com/jinzhu/copier v0.0.0-20190625015134-976e0346caa8
|
||||
github.com/lucasb-eyer/go-colorful v1.0.2
|
||||
|
@ -23,9 +23,8 @@ require (
|
|||
github.com/russolsen/transit v0.0.0-20180705123435-0794b4c4505a
|
||||
github.com/status-im/doubleratchet v3.0.0+incompatible
|
||||
github.com/status-im/migrate/v4 v4.6.2-status.2
|
||||
github.com/status-im/status-go v0.38.5 // indirect
|
||||
github.com/status-im/status-go/eth-node v1.0.0
|
||||
github.com/status-im/status-go/whisper/v6 v6.0.1
|
||||
github.com/status-im/status-go/eth-node v1.1.0
|
||||
github.com/status-im/status-go/whisper/v6 v6.1.0
|
||||
github.com/stretchr/testify v1.4.0
|
||||
github.com/vacp2p/mvds v0.0.23
|
||||
go.uber.org/zap v1.13.0
|
||||
|
|
|
@ -156,9 +156,11 @@ github.com/gizak/termui v0.0.0-20170117222342-991cd3d38091/go.mod h1:PkJoWUt/zac
|
|||
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
|
||||
github.com/go-kit/kit v0.8.0 h1:Wz+5lgoB0kkuqLEc6NVmwRknTKP6dTGbSqvhZtBI/j0=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0 h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0 h1:8HUsc87TaSWLKwrnumgC8/YconD2fJQsRJAsWaPg2ic=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
|
||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||
|
@ -196,6 +198,7 @@ github.com/gomarkdown/markdown v0.0.0-20191104174740-4d42851d4d5a h1:DsPLKbIJTzH
|
|||
github.com/gomarkdown/markdown v0.0.0-20191104174740-4d42851d4d5a/go.mod h1:aii0r/K0ZnHv7G0KF7xy1v0A7s2Ljrb5byB7MO5p6TU=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
|
@ -289,6 +292,7 @@ github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6
|
|||
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/reedsolomon v1.9.2/go.mod h1:CwCi+NUr9pqSVktrkN+Ondf06rkhYZ/pcNv7fu+8Un4=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/koron/go-ssdp v0.0.0-20180514024734-4a0ed625a78b/go.mod h1:5Ky9EC2xfoUKUor0Hjgi2BJhCSXJfMOFlmyYrVKGQMk=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=
|
||||
|
@ -763,6 +767,7 @@ golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtn
|
|||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191109212701-97ad0ed33101 h1:LCmXVkvpQCDj724eX6irUTPCJP5GelFHxqGSWL2D1R0=
|
||||
golang.org/x/tools v0.0.0-20191109212701-97ad0ed33101/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191213032237-7093a17b0467 h1:Jybbe55FT+YYZIJGWmJIA4ZGcglFuZOduakIW3+gHXY=
|
||||
golang.org/x/tools v0.0.0-20191213032237-7093a17b0467/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||
|
|
|
@ -113,6 +113,10 @@ type RawMessage struct {
|
|||
|
||||
func (m *Message) MarshalJSON() ([]byte, error) {
|
||||
type MessageAlias Message
|
||||
type StickerAlias struct {
|
||||
Hash string `json:"hash"`
|
||||
Pack int32 `json:"pack"`
|
||||
}
|
||||
item := struct {
|
||||
ID string `json:"id"`
|
||||
WhisperTimestamp uint64 `json:"whisperTimestamp"`
|
||||
|
@ -132,7 +136,7 @@ func (m *Message) MarshalJSON() ([]byte, error) {
|
|||
Replace string `json:"replace,omitEmpty"`
|
||||
ResponseTo string `json:"responseTo"`
|
||||
EnsName string `json:"ensName"`
|
||||
Sticker *protobuf.StickerMessage `json:"sticker"`
|
||||
Sticker *StickerAlias `json:"sticker"`
|
||||
CommandParameters *CommandParameters `json:"commandParameters"`
|
||||
Timestamp uint64 `json:"timestamp"`
|
||||
ContentType protobuf.ChatMessage_ContentType `json:"contentType"`
|
||||
|
@ -159,10 +163,15 @@ func (m *Message) MarshalJSON() ([]byte, error) {
|
|||
Timestamp: m.Timestamp,
|
||||
ContentType: m.ContentType,
|
||||
MessageType: m.MessageType,
|
||||
Sticker: m.GetSticker(),
|
||||
CommandParameters: m.CommandParameters,
|
||||
}
|
||||
|
||||
if sticker := m.GetSticker(); sticker != nil {
|
||||
item.Sticker = &StickerAlias{
|
||||
Pack: sticker.Pack,
|
||||
Hash: sticker.Hash,
|
||||
}
|
||||
}
|
||||
return json.Marshal(item)
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@ import (
|
|||
"go.uber.org/zap"
|
||||
|
||||
"github.com/status-im/status-go/eth-node/crypto"
|
||||
"github.com/status-im/status-go/eth-node/types"
|
||||
"github.com/status-im/status-go/protocol/encryption/multidevice"
|
||||
"github.com/status-im/status-go/protocol/protobuf"
|
||||
v1protocol "github.com/status-im/status-go/protocol/v1"
|
||||
|
@ -49,7 +48,7 @@ func (m *MessageHandler) HandleMembershipUpdate(messageState *ReceivedMessageSta
|
|||
}
|
||||
|
||||
// A new chat must contain us
|
||||
if !group.IsMember(types.EncodeHex(crypto.FromECDSAPub(&m.identity.PublicKey))) {
|
||||
if !group.IsMember(contactIDFromPublicKey(&m.identity.PublicKey)) {
|
||||
return errors.New("can't create a new group chat without us being a member")
|
||||
}
|
||||
newChat := createGroupChat()
|
||||
|
@ -198,6 +197,21 @@ func (m *MessageHandler) HandleSyncInstallationContact(state *ReceivedMessageSta
|
|||
return nil
|
||||
}
|
||||
|
||||
func (m *MessageHandler) HandleSyncInstallationPublicChat(state *ReceivedMessageState, message protobuf.SyncInstallationPublicChat) error {
|
||||
chatID := message.Id
|
||||
_, ok := state.AllChats[chatID]
|
||||
if ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
chat := CreatePublicChat(chatID)
|
||||
|
||||
state.AllChats[chat.ID] = &chat
|
||||
state.ModifiedChats[chat.ID] = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MessageHandler) HandleContactUpdate(state *ReceivedMessageState, message protobuf.ContactUpdate) error {
|
||||
logger := m.logger.With(zap.String("site", "HandleContactUpdate"))
|
||||
contact := state.CurrentMessageState.Contact
|
||||
|
@ -212,7 +226,7 @@ func (m *MessageHandler) HandleContactUpdate(state *ReceivedMessageState, messag
|
|||
|
||||
if contact.LastUpdated < message.Clock {
|
||||
logger.Info("Updating contact")
|
||||
if !contact.HasBeenAdded() {
|
||||
if !contact.HasBeenAdded() && contact.ID != contactIDFromPublicKey(&m.identity.PublicKey) {
|
||||
contact.SystemTags = append(contact.SystemTags, contactRequestReceived)
|
||||
}
|
||||
if contact.Name != message.EnsName {
|
||||
|
@ -333,7 +347,7 @@ func (m *MessageHandler) HandleRequestAddressForTransaction(messageState *Receiv
|
|||
Clock: command.Clock,
|
||||
Timestamp: messageState.CurrentMessageState.WhisperTimestamp,
|
||||
Text: "Request address for transaction",
|
||||
ChatId: types.EncodeHex(crypto.FromECDSAPub(&m.identity.PublicKey)),
|
||||
ChatId: contactIDFromPublicKey(&m.identity.PublicKey),
|
||||
MessageType: protobuf.ChatMessage_ONE_TO_ONE,
|
||||
ContentType: protobuf.ChatMessage_TRANSACTION_COMMAND,
|
||||
},
|
||||
|
@ -357,7 +371,7 @@ func (m *MessageHandler) HandleRequestTransaction(messageState *ReceivedMessageS
|
|||
Clock: command.Clock,
|
||||
Timestamp: messageState.CurrentMessageState.WhisperTimestamp,
|
||||
Text: "Request transaction",
|
||||
ChatId: types.EncodeHex(crypto.FromECDSAPub(&m.identity.PublicKey)),
|
||||
ChatId: contactIDFromPublicKey(&m.identity.PublicKey),
|
||||
MessageType: protobuf.ChatMessage_ONE_TO_ONE,
|
||||
ContentType: protobuf.ChatMessage_TRANSACTION_COMMAND,
|
||||
},
|
||||
|
@ -563,7 +577,7 @@ func (m *MessageHandler) matchMessage(message *Message, chats map[string]*Chat)
|
|||
case message.MessageType == protobuf.ChatMessage_ONE_TO_ONE:
|
||||
// It's an incoming private message. ChatID is calculated from the signature.
|
||||
// If a chat does not exist, a new one is created and saved.
|
||||
chatID := types.EncodeHex(crypto.FromECDSAPub(message.SigPubKey))
|
||||
chatID := contactIDFromPublicKey(message.SigPubKey)
|
||||
chat := chats[chatID]
|
||||
if chat == nil {
|
||||
// TODO: this should be a three-word name used in the mobile client
|
||||
|
@ -580,8 +594,8 @@ func (m *MessageHandler) matchMessage(message *Message, chats map[string]*Chat)
|
|||
return nil, errors.New("received group chat message for non-existing chat")
|
||||
}
|
||||
|
||||
theirKeyHex := types.EncodeHex(crypto.FromECDSAPub(message.SigPubKey))
|
||||
myKeyHex := types.EncodeHex(crypto.FromECDSAPub(&m.identity.PublicKey))
|
||||
theirKeyHex := contactIDFromPublicKey(message.SigPubKey)
|
||||
myKeyHex := contactIDFromPublicKey(&m.identity.PublicKey)
|
||||
var theyJoined bool
|
||||
var iJoined bool
|
||||
for _, member := range chat.Members {
|
||||
|
|
|
@ -83,7 +83,7 @@ type MessengerResponse struct {
|
|||
}
|
||||
|
||||
func (m *MessengerResponse) IsEmpty() bool {
|
||||
return len(m.Chats) == 0 && len(m.Messages) == 0 && len(m.Contacts) == 0 && len(m.RawMessages) == 0
|
||||
return len(m.Chats) == 0 && len(m.Messages) == 0 && len(m.Contacts) == 0 && len(m.RawMessages) == 0 && len(m.Installations) == 0
|
||||
}
|
||||
|
||||
type featureFlags struct {
|
||||
|
@ -216,7 +216,7 @@ func NewMessenger(
|
|||
onNewInstallationsHandler := func(installations []*multidevice.Installation) {
|
||||
|
||||
for _, installation := range installations {
|
||||
if installation.Identity == types.EncodeHex(crypto.FromECDSAPub(&messenger.identity.PublicKey)) {
|
||||
if installation.Identity == contactIDFromPublicKey(&messenger.identity.PublicKey) {
|
||||
if _, ok := messenger.allInstallations[installation.ID]; !ok {
|
||||
messenger.allInstallations[installation.ID] = installation
|
||||
messenger.modifiedInstallations[installation.ID] = true
|
||||
|
@ -595,7 +595,7 @@ func (m *Messenger) Join(chat Chat) error {
|
|||
}
|
||||
return m.transport.JoinGroup(members)
|
||||
case ChatTypePublic:
|
||||
return m.transport.JoinPublic(chat.Name)
|
||||
return m.transport.JoinPublic(chat.ID)
|
||||
default:
|
||||
return errors.New("chat is neither public nor private")
|
||||
}
|
||||
|
@ -638,8 +638,9 @@ func (m *Messenger) CreateGroupChatWithMembers(ctx context.Context, name string,
|
|||
}
|
||||
chat.updateChatFromProtocolGroup(group)
|
||||
|
||||
clock, _ := chat.NextClockAndTimestamp()
|
||||
// Add members
|
||||
event := v1protocol.NewMembersAddedEvent(members, group.NextClockValue())
|
||||
event := v1protocol.NewMembersAddedEvent(members, clock)
|
||||
event.ChatID = chat.ID
|
||||
err = event.Sign(m.identity)
|
||||
if err != nil {
|
||||
|
@ -704,8 +705,9 @@ func (m *Messenger) RemoveMemberFromGroupChat(ctx context.Context, chatID string
|
|||
return nil, err
|
||||
}
|
||||
|
||||
clock, _ := chat.NextClockAndTimestamp()
|
||||
// Remove member
|
||||
event := v1protocol.NewMemberRemovedEvent(member, group.NextClockValue())
|
||||
event := v1protocol.NewMemberRemovedEvent(member, clock)
|
||||
event.ChatID = chat.ID
|
||||
err = event.Sign(m.identity)
|
||||
if err != nil {
|
||||
|
@ -755,8 +757,9 @@ func (m *Messenger) AddMembersToGroupChat(ctx context.Context, chatID string, me
|
|||
return nil, err
|
||||
}
|
||||
|
||||
clock, _ := chat.NextClockAndTimestamp()
|
||||
// Add members
|
||||
event := v1protocol.NewMembersAddedEvent(members, group.NextClockValue())
|
||||
event := v1protocol.NewMembersAddedEvent(members, clock)
|
||||
event.ChatID = chat.ID
|
||||
err = event.Sign(m.identity)
|
||||
if err != nil {
|
||||
|
@ -815,8 +818,9 @@ func (m *Messenger) AddAdminsToGroupChat(ctx context.Context, chatID string, mem
|
|||
return nil, err
|
||||
}
|
||||
|
||||
clock, _ := chat.NextClockAndTimestamp()
|
||||
// Add members
|
||||
event := v1protocol.NewAdminsAddedEvent(members, group.NextClockValue())
|
||||
event := v1protocol.NewAdminsAddedEvent(members, clock)
|
||||
event.ChatID = chat.ID
|
||||
err = event.Sign(m.identity)
|
||||
if err != nil {
|
||||
|
@ -877,8 +881,9 @@ func (m *Messenger) ConfirmJoiningGroup(ctx context.Context, chatID string) (*Me
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clock, _ := chat.NextClockAndTimestamp()
|
||||
event := v1protocol.NewMemberJoinedEvent(
|
||||
group.NextClockValue(),
|
||||
clock,
|
||||
)
|
||||
event.ChatID = chat.ID
|
||||
err = event.Sign(m.identity)
|
||||
|
@ -939,9 +944,10 @@ func (m *Messenger) LeaveGroupChat(ctx context.Context, chatID string) (*Messeng
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clock, _ := chat.NextClockAndTimestamp()
|
||||
event := v1protocol.NewMemberRemovedEvent(
|
||||
types.EncodeHex(crypto.FromECDSAPub(&m.identity.PublicKey)),
|
||||
group.NextClockValue(),
|
||||
contactIDFromPublicKey(&m.identity.PublicKey),
|
||||
clock,
|
||||
)
|
||||
event.ChatID = chat.ID
|
||||
err = event.Sign(m.identity)
|
||||
|
@ -983,6 +989,14 @@ func (m *Messenger) LeaveGroupChat(ctx context.Context, chatID string) (*Messeng
|
|||
}
|
||||
|
||||
func (m *Messenger) saveChat(chat *Chat) error {
|
||||
_, ok := m.allChats[chat.ID]
|
||||
// Sync chat if it's a new active public chat
|
||||
if !ok && chat.Active && chat.Public() {
|
||||
if err := m.syncPublicChat(context.Background(), chat); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err := m.persistence.SaveChat(*chat)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -1008,12 +1022,7 @@ func (m *Messenger) saveChats(chats []*Chat) error {
|
|||
func (m *Messenger) SaveChat(chat *Chat) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
err := m.saveChat(chat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return m.saveChat(chat)
|
||||
}
|
||||
|
||||
func (m *Messenger) Chats() []*Chat {
|
||||
|
@ -1345,6 +1354,13 @@ func (m *Messenger) SendContactUpdates(ctx context.Context, ensName, profileImag
|
|||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
myID := contactIDFromPublicKey(&m.identity.PublicKey)
|
||||
|
||||
if _, err := m.sendContactUpdate(ctx, myID, ensName, profileImage); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: This should not be sending paired messages, as we do it above
|
||||
for _, contact := range m.allContacts {
|
||||
if contact.IsAdded() {
|
||||
if _, err := m.sendContactUpdate(ctx, contact.ID, ensName, profileImage); err != nil {
|
||||
|
@ -1353,7 +1369,6 @@ func (m *Messenger) SendContactUpdates(ctx context.Context, ensName, profileImag
|
|||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// SendContactUpdate sends a contact update to a user and adds the user to contacts
|
||||
|
@ -1417,7 +1432,7 @@ func (m *Messenger) sendContactUpdate(ctx context.Context, chatID, ensName, prof
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if !contact.IsAdded() {
|
||||
if !contact.IsAdded() && contact.ID != contactIDFromPublicKey(&m.identity.PublicKey) {
|
||||
contact.SystemTags = append(contact.SystemTags, contactAdded)
|
||||
}
|
||||
|
||||
|
@ -1432,6 +1447,36 @@ func (m *Messenger) sendContactUpdate(ctx context.Context, chatID, ensName, prof
|
|||
return &response, m.saveContact(contact)
|
||||
}
|
||||
|
||||
// SyncDevices sends all public chats and contacts to paired devices
|
||||
func (m *Messenger) SyncDevices(ctx context.Context, ensName, photoPath string) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
myID := contactIDFromPublicKey(&m.identity.PublicKey)
|
||||
|
||||
if _, err := m.sendContactUpdate(ctx, myID, ensName, photoPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, chat := range m.allChats {
|
||||
if chat.Public() && chat.Active {
|
||||
if err := m.syncPublicChat(ctx, chat); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, contact := range m.allContacts {
|
||||
if contact.IsAdded() && contact.ID != myID {
|
||||
if err := m.syncContact(ctx, contact); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendPairInstallation sends a pair installation message
|
||||
func (m *Messenger) SendPairInstallation(ctx context.Context) (*MessengerResponse, error) {
|
||||
m.mutex.Lock()
|
||||
|
@ -1449,7 +1494,7 @@ func (m *Messenger) SendPairInstallation(ctx context.Context) (*MessengerRespons
|
|||
return nil, errors.New("no installation metadata")
|
||||
}
|
||||
|
||||
chatID := types.EncodeHex(crypto.FromECDSAPub(&m.identity.PublicKey))
|
||||
chatID := contactIDFromPublicKey(&m.identity.PublicKey)
|
||||
|
||||
chat, ok := m.allChats[chatID]
|
||||
if !ok {
|
||||
|
@ -1491,13 +1536,54 @@ func (m *Messenger) SendPairInstallation(ctx context.Context) (*MessengerRespons
|
|||
return &response, nil
|
||||
}
|
||||
|
||||
// syncPublicChat sync a public chat with paired devices
|
||||
func (m *Messenger) syncPublicChat(ctx context.Context, publicChat *Chat) error {
|
||||
var err error
|
||||
if !m.hasPairedDevices() {
|
||||
return nil
|
||||
}
|
||||
chatID := contactIDFromPublicKey(&m.identity.PublicKey)
|
||||
|
||||
chat, ok := m.allChats[chatID]
|
||||
if !ok {
|
||||
chat = OneToOneFromPublicKey(&m.identity.PublicKey)
|
||||
// We don't want to show the chat to the user
|
||||
chat.Active = false
|
||||
}
|
||||
|
||||
m.allChats[chat.ID] = chat
|
||||
clock, _ := chat.NextClockAndTimestamp()
|
||||
|
||||
syncMessage := &protobuf.SyncInstallationPublicChat{
|
||||
Clock: clock,
|
||||
Id: publicChat.ID,
|
||||
}
|
||||
encodedMessage, err := proto.Marshal(syncMessage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = m.dispatchMessage(ctx, &RawMessage{
|
||||
LocalChatID: chatID,
|
||||
Payload: encodedMessage,
|
||||
MessageType: protobuf.ApplicationMetadataMessage_SYNC_INSTALLATION_PUBLIC_CHAT,
|
||||
ResendAutomatically: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
chat.LastClockValue = clock
|
||||
return m.saveChat(chat)
|
||||
}
|
||||
|
||||
// syncContact sync as contact with paired devices
|
||||
func (m *Messenger) syncContact(ctx context.Context, contact *Contact) error {
|
||||
var err error
|
||||
if !m.hasPairedDevices() {
|
||||
return nil
|
||||
}
|
||||
chatID := types.EncodeHex(crypto.FromECDSAPub(&m.identity.PublicKey))
|
||||
chatID := contactIDFromPublicKey(&m.identity.PublicKey)
|
||||
|
||||
chat, ok := m.allChats[chatID]
|
||||
if !ok {
|
||||
|
@ -1624,7 +1710,7 @@ func (m *Messenger) handleRetrievedMessages(chatWithMessages map[transport.Filte
|
|||
publicKey := msg.SigPubKey()
|
||||
|
||||
// Check for messages from blocked users
|
||||
senderID := types.EncodeHex(crypto.FromECDSAPub(publicKey))
|
||||
senderID := contactIDFromPublicKey(publicKey)
|
||||
if _, ok := messageState.AllContacts[senderID]; ok && messageState.AllContacts[senderID].IsBlocked() {
|
||||
continue
|
||||
}
|
||||
|
@ -1706,6 +1792,19 @@ func (m *Messenger) handleRetrievedMessages(chatWithMessages map[transport.Filte
|
|||
logger.Warn("failed to handle SyncInstallationContact", zap.Error(err))
|
||||
continue
|
||||
}
|
||||
case protobuf.SyncInstallationPublicChat:
|
||||
if !isPubKeyEqual(messageState.CurrentMessageState.PublicKey, &m.identity.PublicKey) {
|
||||
logger.Warn("not coming from us, ignoring")
|
||||
continue
|
||||
}
|
||||
|
||||
p := msg.ParsedMessage.(protobuf.SyncInstallationPublicChat)
|
||||
logger.Debug("Handling SyncInstallationPublicChat", zap.Any("message", p))
|
||||
err = m.handler.HandleSyncInstallationPublicChat(messageState, p)
|
||||
if err != nil {
|
||||
logger.Warn("failed to handle SyncInstallationPublicChat", zap.Error(err))
|
||||
continue
|
||||
}
|
||||
case protobuf.RequestAddressForTransaction:
|
||||
command := msg.ParsedMessage.(protobuf.RequestAddressForTransaction)
|
||||
logger.Debug("Handling RequestAddressForTransaction", zap.Any("message", command))
|
||||
|
@ -2479,7 +2578,7 @@ func (m *Messenger) AcceptRequestTransaction(ctx context.Context, transactionHas
|
|||
return &response, m.saveChat(chat)
|
||||
}
|
||||
|
||||
func (m *Messenger) SendTransaction(ctx context.Context, chatID, transactionHash string, signature []byte) (*MessengerResponse, error) {
|
||||
func (m *Messenger) SendTransaction(ctx context.Context, chatID, value, contract, transactionHash string, signature []byte) (*MessengerResponse, error) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
|
@ -2534,6 +2633,8 @@ func (m *Messenger) SendTransaction(ctx context.Context, chatID, transactionHash
|
|||
message.ID = types.EncodeHex(newMessageID)
|
||||
message.CommandParameters = &CommandParameters{
|
||||
TransactionHash: transactionHash,
|
||||
Value: value,
|
||||
Contract: contract,
|
||||
Signature: signature,
|
||||
CommandState: CommandStateTransactionSent,
|
||||
}
|
||||
|
@ -2584,7 +2685,7 @@ func (m *Messenger) ValidateTransactions(ctx context.Context, addresses []types.
|
|||
}
|
||||
for _, validationResult := range responses {
|
||||
var message *Message
|
||||
chatID := types.EncodeHex(crypto.FromECDSAPub(validationResult.Transaction.From))
|
||||
chatID := contactIDFromPublicKey(validationResult.Transaction.From)
|
||||
chat, ok := m.allChats[chatID]
|
||||
if !ok {
|
||||
chat = OneToOneFromPublicKey(validationResult.Transaction.From)
|
||||
|
|
|
@ -342,10 +342,6 @@ func (g Group) LastClockValue() uint64 {
|
|||
return g.events[len(g.events)-1].ClockValue
|
||||
}
|
||||
|
||||
func (g Group) NextClockValue() uint64 {
|
||||
return g.LastClockValue() + 1
|
||||
}
|
||||
|
||||
func (g Group) creator() (string, error) {
|
||||
if len(g.events) == 0 {
|
||||
return "", errors.New("no events in the group")
|
||||
|
|
|
@ -53,6 +53,7 @@ type sockaddrInet6 struct {
|
|||
const (
|
||||
sizeofIovec = 0x10
|
||||
sizeofMsghdr = 0x30
|
||||
sizeofMmsghdr = 0x38
|
||||
sizeofCmsghdr = 0xc
|
||||
|
||||
sizeofSockaddrInet = 0x10
|
||||
|
|
|
@ -47,6 +47,7 @@ type sockaddrInet6 struct {
|
|||
const (
|
||||
sizeofIovec = 0x8
|
||||
sizeofMsghdr = 0x1c
|
||||
sizeofMmsghdr = 0x20
|
||||
sizeofCmsghdr = 0xc
|
||||
|
||||
sizeofSockaddrInet = 0x10
|
||||
|
|
|
@ -50,6 +50,7 @@ type sockaddrInet6 struct {
|
|||
const (
|
||||
sizeofIovec = 0x10
|
||||
sizeofMsghdr = 0x38
|
||||
sizeofMmsghdr = 0x40
|
||||
sizeofCmsghdr = 0x10
|
||||
|
||||
sizeofSockaddrInet = 0x10
|
||||
|
|
|
@ -45,9 +45,9 @@ type sockaddrInet6 struct {
|
|||
}
|
||||
|
||||
const (
|
||||
sizeofIovec = 0x8
|
||||
sizeofMsghdr = 0x1c
|
||||
|
||||
sizeofIovec = 0x8
|
||||
sizeofMsghdr = 0x1c
|
||||
sizeofMmsghdr = 0x20
|
||||
sizeofCmsghdr = 0xc
|
||||
|
||||
sizeofSockaddrInet = 0x10
|
||||
|
|
|
@ -48,9 +48,9 @@ type sockaddrInet6 struct {
|
|||
}
|
||||
|
||||
const (
|
||||
sizeofIovec = 0x10
|
||||
sizeofMsghdr = 0x38
|
||||
|
||||
sizeofIovec = 0x10
|
||||
sizeofMsghdr = 0x38
|
||||
sizeofMmsghdr = 0x40
|
||||
sizeofCmsghdr = 0x10
|
||||
|
||||
sizeofSockaddrInet = 0x10
|
||||
|
|
|
@ -45,9 +45,9 @@ type sockaddrInet6 struct {
|
|||
}
|
||||
|
||||
const (
|
||||
sizeofIovec = 0x8
|
||||
sizeofMsghdr = 0x1c
|
||||
|
||||
sizeofIovec = 0x8
|
||||
sizeofMsghdr = 0x1c
|
||||
sizeofMmsghdr = 0x20
|
||||
sizeofCmsghdr = 0xc
|
||||
|
||||
sizeofSockaddrInet = 0x10
|
||||
|
|
|
@ -48,9 +48,9 @@ type sockaddrInet6 struct {
|
|||
}
|
||||
|
||||
const (
|
||||
sizeofIovec = 0x10
|
||||
sizeofMsghdr = 0x38
|
||||
|
||||
sizeofIovec = 0x10
|
||||
sizeofMsghdr = 0x38
|
||||
sizeofMmsghdr = 0x40
|
||||
sizeofCmsghdr = 0x10
|
||||
|
||||
sizeofSockaddrInet = 0x10
|
||||
|
|
|
@ -48,9 +48,9 @@ type sockaddrInet6 struct {
|
|||
}
|
||||
|
||||
const (
|
||||
sizeofIovec = 0x10
|
||||
sizeofMsghdr = 0x38
|
||||
|
||||
sizeofIovec = 0x10
|
||||
sizeofMsghdr = 0x38
|
||||
sizeofMmsghdr = 0x40
|
||||
sizeofCmsghdr = 0x10
|
||||
|
||||
sizeofSockaddrInet = 0x10
|
||||
|
|
|
@ -45,9 +45,9 @@ type sockaddrInet6 struct {
|
|||
}
|
||||
|
||||
const (
|
||||
sizeofIovec = 0x8
|
||||
sizeofMsghdr = 0x1c
|
||||
|
||||
sizeofIovec = 0x8
|
||||
sizeofMsghdr = 0x1c
|
||||
sizeofMmsghdr = 0x20
|
||||
sizeofCmsghdr = 0xc
|
||||
|
||||
sizeofSockaddrInet = 0x10
|
||||
|
|
|
@ -48,9 +48,9 @@ type sockaddrInet6 struct {
|
|||
}
|
||||
|
||||
const (
|
||||
sizeofIovec = 0x10
|
||||
sizeofMsghdr = 0x38
|
||||
|
||||
sizeofIovec = 0x10
|
||||
sizeofMsghdr = 0x38
|
||||
sizeofMmsghdr = 0x40
|
||||
sizeofCmsghdr = 0x10
|
||||
|
||||
sizeofSockaddrInet = 0x10
|
||||
|
|
|
@ -48,9 +48,9 @@ type sockaddrInet6 struct {
|
|||
}
|
||||
|
||||
const (
|
||||
sizeofIovec = 0x10
|
||||
sizeofMsghdr = 0x38
|
||||
|
||||
sizeofIovec = 0x10
|
||||
sizeofMsghdr = 0x38
|
||||
sizeofMmsghdr = 0x40
|
||||
sizeofCmsghdr = 0x10
|
||||
|
||||
sizeofSockaddrInet = 0x10
|
||||
|
|
|
@ -49,9 +49,9 @@ type sockaddrInet6 struct {
|
|||
}
|
||||
|
||||
const (
|
||||
sizeofIovec = 0x10
|
||||
sizeofMsghdr = 0x38
|
||||
|
||||
sizeofIovec = 0x10
|
||||
sizeofMsghdr = 0x38
|
||||
sizeofMmsghdr = 0x40
|
||||
sizeofCmsghdr = 0x10
|
||||
|
||||
sizeofSockaddrInet = 0x10
|
||||
|
|
|
@ -48,9 +48,9 @@ type sockaddrInet6 struct {
|
|||
}
|
||||
|
||||
const (
|
||||
sizeofIovec = 0x10
|
||||
sizeofMsghdr = 0x38
|
||||
|
||||
sizeofIovec = 0x10
|
||||
sizeofMsghdr = 0x38
|
||||
sizeofMmsghdr = 0x40
|
||||
sizeofCmsghdr = 0x10
|
||||
|
||||
sizeofSockaddrInet = 0x10
|
||||
|
|
|
@ -47,9 +47,9 @@ type sockaddrInet6 struct {
|
|||
}
|
||||
|
||||
const (
|
||||
sizeofIovec = 0x8
|
||||
sizeofMsghdr = 0x1c
|
||||
|
||||
sizeofIovec = 0x8
|
||||
sizeofMsghdr = 0x1c
|
||||
sizeofMmsghdr = 0x20
|
||||
sizeofCmsghdr = 0xc
|
||||
|
||||
sizeofSockaddrInet = 0x10
|
||||
|
|
|
@ -50,9 +50,9 @@ type sockaddrInet6 struct {
|
|||
}
|
||||
|
||||
const (
|
||||
sizeofIovec = 0x10
|
||||
sizeofMsghdr = 0x30
|
||||
|
||||
sizeofIovec = 0x10
|
||||
sizeofMsghdr = 0x30
|
||||
sizeofMmsghdr = 0x40
|
||||
sizeofCmsghdr = 0xc
|
||||
|
||||
sizeofSockaddrInet = 0x10
|
||||
|
|
|
@ -47,9 +47,9 @@ type sockaddrInet6 struct {
|
|||
}
|
||||
|
||||
const (
|
||||
sizeofIovec = 0x8
|
||||
sizeofMsghdr = 0x1c
|
||||
|
||||
sizeofIovec = 0x8
|
||||
sizeofMsghdr = 0x1c
|
||||
sizeofMmsghdr = 0x20
|
||||
sizeofCmsghdr = 0xc
|
||||
|
||||
sizeofSockaddrInet = 0x10
|
||||
|
|
|
@ -52,6 +52,7 @@ type sockaddrInet6 struct {
|
|||
const (
|
||||
sizeofIovec = 0x10
|
||||
sizeofMsghdr = 0x30
|
||||
sizeofMmsghdr = 0x40
|
||||
sizeofCmsghdr = 0xc
|
||||
|
||||
sizeofSockaddrInet = 0x10
|
||||
|
|
|
@ -60,7 +60,8 @@ causes Load to run in LoadFiles mode, collecting minimal information.
|
|||
See the documentation for type Config for details.
|
||||
|
||||
As noted earlier, the Config.Mode controls the amount of detail
|
||||
reported about the loaded packages. See the documentation for type LoadMode
|
||||
reported about the loaded packages, with each mode returning all the data of the
|
||||
previous mode with some extra added. See the documentation for type LoadMode
|
||||
for details.
|
||||
|
||||
Most tools should pass their command-line arguments (after any flags)
|
||||
|
|
|
@ -84,14 +84,13 @@ func findExternalDriver(cfg *Config) driver {
|
|||
cmd.Stdin = bytes.NewReader(req)
|
||||
cmd.Stdout = buf
|
||||
cmd.Stderr = stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return nil, fmt.Errorf("%v: %v: %s", tool, err, cmd.Stderr)
|
||||
}
|
||||
if len(stderr.Bytes()) != 0 && os.Getenv("GOPACKAGESPRINTDRIVERERRORS") != "" {
|
||||
fmt.Fprintf(os.Stderr, "%s stderr: <<%s>>\n", cmdDebugStr(cmd, words...), stderr)
|
||||
}
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return nil, fmt.Errorf("%v: %v: %s", tool, err, cmd.Stderr)
|
||||
}
|
||||
var response driverResponse
|
||||
if err := json.Unmarshal(buf.Bytes(), &response); err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -26,6 +26,7 @@ import (
|
|||
"golang.org/x/tools/go/internal/packagesdriver"
|
||||
"golang.org/x/tools/internal/gopathwalk"
|
||||
"golang.org/x/tools/internal/semver"
|
||||
"golang.org/x/tools/internal/span"
|
||||
)
|
||||
|
||||
// debug controls verbose logging.
|
||||
|
@ -253,7 +254,12 @@ func addNeededOverlayPackages(cfg *Config, driver driver, response *responseDedu
|
|||
if len(pkgs) == 0 {
|
||||
return nil
|
||||
}
|
||||
dr, err := driver(cfg, pkgs...)
|
||||
drivercfg := *cfg
|
||||
if getGoInfo().env.modulesOn {
|
||||
drivercfg.BuildFlags = append(drivercfg.BuildFlags, "-mod=readonly")
|
||||
}
|
||||
dr, err := driver(&drivercfg, pkgs...)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -264,7 +270,10 @@ func addNeededOverlayPackages(cfg *Config, driver driver, response *responseDedu
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return addNeededOverlayPackages(cfg, driver, response, needPkgs, getGoInfo)
|
||||
if err := addNeededOverlayPackages(cfg, driver, response, needPkgs, getGoInfo); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runContainsQueries(cfg *Config, driver driver, response *responseDeduper, queries []string, goInfo func() *goInfo) error {
|
||||
|
@ -278,43 +287,42 @@ func runContainsQueries(cfg *Config, driver driver, response *responseDeduper, q
|
|||
return fmt.Errorf("could not determine absolute path of file= query path %q: %v", query, err)
|
||||
}
|
||||
dirResponse, err := driver(cfg, pattern)
|
||||
if err != nil || (len(dirResponse.Packages) == 1 && len(dirResponse.Packages[0].Errors) == 1) {
|
||||
// There was an error loading the package. Try to load the file as an ad-hoc package.
|
||||
// Usually the error will appear in a returned package, but may not if we're in modules mode
|
||||
// and the ad-hoc is located outside a module.
|
||||
if err != nil {
|
||||
var queryErr error
|
||||
dirResponse, queryErr = driver(cfg, query)
|
||||
if queryErr != nil {
|
||||
// Return the original error if the attempt to fall back failed.
|
||||
return err
|
||||
if dirResponse, queryErr = adHocPackage(cfg, driver, pattern, query); queryErr != nil {
|
||||
return err // return the original error
|
||||
}
|
||||
// If we get nothing back from `go list`, try to make this file into its own ad-hoc package.
|
||||
if len(dirResponse.Packages) == 0 && queryErr == nil {
|
||||
dirResponse.Packages = append(dirResponse.Packages, &Package{
|
||||
ID: "command-line-arguments",
|
||||
PkgPath: query,
|
||||
GoFiles: []string{query},
|
||||
CompiledGoFiles: []string{query},
|
||||
Imports: make(map[string]*Package),
|
||||
})
|
||||
dirResponse.Roots = append(dirResponse.Roots, "command-line-arguments")
|
||||
}
|
||||
// Special case to handle issue #33482:
|
||||
// If this is a file= query for ad-hoc packages where the file only exists on an overlay,
|
||||
// and exists outside of a module, add the file in for the package.
|
||||
if len(dirResponse.Packages) == 1 && (dirResponse.Packages[0].ID == "command-line-arguments" ||
|
||||
filepath.ToSlash(dirResponse.Packages[0].PkgPath) == filepath.ToSlash(query)) {
|
||||
if len(dirResponse.Packages[0].GoFiles) == 0 {
|
||||
filename := filepath.Join(pattern, filepath.Base(query)) // avoid recomputing abspath
|
||||
// TODO(matloob): check if the file is outside of a root dir?
|
||||
for path := range cfg.Overlay {
|
||||
if path == filename {
|
||||
dirResponse.Packages[0].Errors = nil
|
||||
dirResponse.Packages[0].GoFiles = []string{path}
|
||||
dirResponse.Packages[0].CompiledGoFiles = []string{path}
|
||||
}
|
||||
}
|
||||
}
|
||||
// `go list` can report errors for files that are not listed as part of a package's GoFiles.
|
||||
// In the case of an invalid Go file, we should assume that it is part of package if only
|
||||
// one package is in the response. The file may have valid contents in an overlay.
|
||||
if len(dirResponse.Packages) == 1 {
|
||||
pkg := dirResponse.Packages[0]
|
||||
for i, err := range pkg.Errors {
|
||||
s := errorSpan(err)
|
||||
if !s.IsValid() {
|
||||
break
|
||||
}
|
||||
if len(pkg.CompiledGoFiles) == 0 {
|
||||
break
|
||||
}
|
||||
dir := filepath.Dir(pkg.CompiledGoFiles[0])
|
||||
filename := filepath.Join(dir, filepath.Base(s.URI().Filename()))
|
||||
if info, err := os.Stat(filename); err != nil || info.IsDir() {
|
||||
break
|
||||
}
|
||||
if !contains(pkg.CompiledGoFiles, filename) {
|
||||
pkg.CompiledGoFiles = append(pkg.CompiledGoFiles, filename)
|
||||
pkg.GoFiles = append(pkg.GoFiles, filename)
|
||||
pkg.Errors = append(pkg.Errors[:i], pkg.Errors[i+1:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
// A final attempt to construct an ad-hoc package.
|
||||
if len(dirResponse.Packages) == 1 && len(dirResponse.Packages[0].Errors) == 1 {
|
||||
var queryErr error
|
||||
if dirResponse, queryErr = adHocPackage(cfg, driver, pattern, query); queryErr != nil {
|
||||
return err // return the original error
|
||||
}
|
||||
}
|
||||
isRoot := make(map[string]bool, len(dirResponse.Roots))
|
||||
|
@ -342,6 +350,74 @@ func runContainsQueries(cfg *Config, driver driver, response *responseDeduper, q
|
|||
return nil
|
||||
}
|
||||
|
||||
// adHocPackage attempts to construct an ad-hoc package given a query that failed.
|
||||
func adHocPackage(cfg *Config, driver driver, pattern, query string) (*driverResponse, error) {
|
||||
// There was an error loading the package. Try to load the file as an ad-hoc package.
|
||||
// Usually the error will appear in a returned package, but may not if we're in modules mode
|
||||
// and the ad-hoc is located outside a module.
|
||||
dirResponse, err := driver(cfg, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// If we get nothing back from `go list`, try to make this file into its own ad-hoc package.
|
||||
if len(dirResponse.Packages) == 0 && err == nil {
|
||||
dirResponse.Packages = append(dirResponse.Packages, &Package{
|
||||
ID: "command-line-arguments",
|
||||
PkgPath: query,
|
||||
GoFiles: []string{query},
|
||||
CompiledGoFiles: []string{query},
|
||||
Imports: make(map[string]*Package),
|
||||
})
|
||||
dirResponse.Roots = append(dirResponse.Roots, "command-line-arguments")
|
||||
}
|
||||
// Special case to handle issue #33482:
|
||||
// If this is a file= query for ad-hoc packages where the file only exists on an overlay,
|
||||
// and exists outside of a module, add the file in for the package.
|
||||
if len(dirResponse.Packages) == 1 && (dirResponse.Packages[0].ID == "command-line-arguments" || dirResponse.Packages[0].PkgPath == filepath.ToSlash(query)) {
|
||||
if len(dirResponse.Packages[0].GoFiles) == 0 {
|
||||
filename := filepath.Join(pattern, filepath.Base(query)) // avoid recomputing abspath
|
||||
// TODO(matloob): check if the file is outside of a root dir?
|
||||
for path := range cfg.Overlay {
|
||||
if path == filename {
|
||||
dirResponse.Packages[0].Errors = nil
|
||||
dirResponse.Packages[0].GoFiles = []string{path}
|
||||
dirResponse.Packages[0].CompiledGoFiles = []string{path}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return dirResponse, nil
|
||||
}
|
||||
|
||||
func contains(files []string, filename string) bool {
|
||||
for _, f := range files {
|
||||
if f == filename {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// errorSpan attempts to parse a standard `go list` error message
|
||||
// by stripping off the trailing error message.
|
||||
//
|
||||
// It works only on errors whose message is prefixed by colon,
|
||||
// followed by a space (": "). For example:
|
||||
//
|
||||
// attributes.go:13:1: expected 'package', found 'type'
|
||||
//
|
||||
func errorSpan(err Error) span.Span {
|
||||
if err.Pos == "" {
|
||||
input := strings.TrimSpace(err.Msg)
|
||||
msgIndex := strings.Index(input, ": ")
|
||||
if msgIndex < 0 {
|
||||
return span.Parse(input)
|
||||
}
|
||||
return span.Parse(input[:msgIndex])
|
||||
}
|
||||
return span.Parse(err.Pos)
|
||||
}
|
||||
|
||||
// modCacheRegexp splits a path in a module cache into module, module version, and package.
|
||||
var modCacheRegexp = regexp.MustCompile(`(.*)@([^/\\]*)(.*)`)
|
||||
|
||||
|
@ -805,14 +881,9 @@ func golistDriver(cfg *Config, rootsDirs func() *goInfo, words ...string) (*driv
|
|||
}
|
||||
|
||||
if p.Error != nil {
|
||||
msg := strings.TrimSpace(p.Error.Err) // Trim to work around golang.org/issue/32363.
|
||||
// Address golang.org/issue/35964 by appending import stack to error message.
|
||||
if msg == "import cycle not allowed" && len(p.Error.ImportStack) != 0 {
|
||||
msg += fmt.Sprintf(": import stack: %v", p.Error.ImportStack)
|
||||
}
|
||||
pkg.Errors = append(pkg.Errors, Error{
|
||||
Pos: p.Error.Pos,
|
||||
Msg: msg,
|
||||
Msg: strings.TrimSpace(p.Error.Err), // Trim to work around golang.org/issue/32363.
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package packages
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var allModes = []LoadMode{
|
||||
NeedName,
|
||||
NeedFiles,
|
||||
NeedCompiledGoFiles,
|
||||
NeedImports,
|
||||
NeedDeps,
|
||||
NeedExportsFile,
|
||||
NeedTypes,
|
||||
NeedSyntax,
|
||||
NeedTypesInfo,
|
||||
NeedTypesSizes,
|
||||
}
|
||||
|
||||
var modeStrings = []string{
|
||||
"NeedName",
|
||||
"NeedFiles",
|
||||
"NeedCompiledGoFiles",
|
||||
"NeedImports",
|
||||
"NeedDeps",
|
||||
"NeedExportsFile",
|
||||
"NeedTypes",
|
||||
"NeedSyntax",
|
||||
"NeedTypesInfo",
|
||||
"NeedTypesSizes",
|
||||
}
|
||||
|
||||
func (mod LoadMode) String() string {
|
||||
m := mod
|
||||
if m == 0 {
|
||||
return fmt.Sprintf("LoadMode(0)")
|
||||
}
|
||||
var out []string
|
||||
for i, x := range allModes {
|
||||
if x > m {
|
||||
break
|
||||
}
|
||||
if (m & x) != 0 {
|
||||
out = append(out, modeStrings[i])
|
||||
m = m ^ x
|
||||
}
|
||||
}
|
||||
if m != 0 {
|
||||
out = append(out, "Unknown")
|
||||
}
|
||||
return fmt.Sprintf("LoadMode(%s)", strings.Join(out, "|"))
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package span
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Parse returns the location represented by the input.
|
||||
// All inputs are valid locations, as they can always be a pure filename.
|
||||
// The returned span will be normalized, and thus if printed may produce a
|
||||
// different string.
|
||||
func Parse(input string) Span {
|
||||
// :0:0#0-0:0#0
|
||||
valid := input
|
||||
var hold, offset int
|
||||
hadCol := false
|
||||
suf := rstripSuffix(input)
|
||||
if suf.sep == "#" {
|
||||
offset = suf.num
|
||||
suf = rstripSuffix(suf.remains)
|
||||
}
|
||||
if suf.sep == ":" {
|
||||
valid = suf.remains
|
||||
hold = suf.num
|
||||
hadCol = true
|
||||
suf = rstripSuffix(suf.remains)
|
||||
}
|
||||
switch {
|
||||
case suf.sep == ":":
|
||||
return New(NewURI(suf.remains), NewPoint(suf.num, hold, offset), Point{})
|
||||
case suf.sep == "-":
|
||||
// we have a span, fall out of the case to continue
|
||||
default:
|
||||
// separator not valid, rewind to either the : or the start
|
||||
return New(NewURI(valid), NewPoint(hold, 0, offset), Point{})
|
||||
}
|
||||
// only the span form can get here
|
||||
// at this point we still don't know what the numbers we have mean
|
||||
// if have not yet seen a : then we might have either a line or a column depending
|
||||
// on whether start has a column or not
|
||||
// we build an end point and will fix it later if needed
|
||||
end := NewPoint(suf.num, hold, offset)
|
||||
hold, offset = 0, 0
|
||||
suf = rstripSuffix(suf.remains)
|
||||
if suf.sep == "#" {
|
||||
offset = suf.num
|
||||
suf = rstripSuffix(suf.remains)
|
||||
}
|
||||
if suf.sep != ":" {
|
||||
// turns out we don't have a span after all, rewind
|
||||
return New(NewURI(valid), end, Point{})
|
||||
}
|
||||
valid = suf.remains
|
||||
hold = suf.num
|
||||
suf = rstripSuffix(suf.remains)
|
||||
if suf.sep != ":" {
|
||||
// line#offset only
|
||||
return New(NewURI(valid), NewPoint(hold, 0, offset), end)
|
||||
}
|
||||
// we have a column, so if end only had one number, it is also the column
|
||||
if !hadCol {
|
||||
end = NewPoint(suf.num, end.v.Line, end.v.Offset)
|
||||
}
|
||||
return New(NewURI(suf.remains), NewPoint(suf.num, hold, offset), end)
|
||||
}
|
||||
|
||||
type suffix struct {
|
||||
remains string
|
||||
sep string
|
||||
num int
|
||||
}
|
||||
|
||||
func rstripSuffix(input string) suffix {
|
||||
if len(input) == 0 {
|
||||
return suffix{"", "", -1}
|
||||
}
|
||||
remains := input
|
||||
num := -1
|
||||
// first see if we have a number at the end
|
||||
last := strings.LastIndexFunc(remains, func(r rune) bool { return r < '0' || r > '9' })
|
||||
if last >= 0 && last < len(remains)-1 {
|
||||
number, err := strconv.ParseInt(remains[last+1:], 10, 64)
|
||||
if err == nil {
|
||||
num = int(number)
|
||||
remains = remains[:last+1]
|
||||
}
|
||||
}
|
||||
// now see if we have a trailing separator
|
||||
r, w := utf8.DecodeLastRuneInString(remains)
|
||||
if r != ':' && r != '#' && r == '#' {
|
||||
return suffix{input, "", -1}
|
||||
}
|
||||
remains = remains[:len(remains)-w]
|
||||
return suffix{remains, string(r), num}
|
||||
}
|
|
@ -0,0 +1,285 @@
|
|||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package span contains support for representing with positions and ranges in
|
||||
// text files.
|
||||
package span
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path"
|
||||
)
|
||||
|
||||
// Span represents a source code range in standardized form.
|
||||
type Span struct {
|
||||
v span
|
||||
}
|
||||
|
||||
// Point represents a single point within a file.
|
||||
// In general this should only be used as part of a Span, as on its own it
|
||||
// does not carry enough information.
|
||||
type Point struct {
|
||||
v point
|
||||
}
|
||||
|
||||
type span struct {
|
||||
URI URI `json:"uri"`
|
||||
Start point `json:"start"`
|
||||
End point `json:"end"`
|
||||
}
|
||||
|
||||
type point struct {
|
||||
Line int `json:"line"`
|
||||
Column int `json:"column"`
|
||||
Offset int `json:"offset"`
|
||||
}
|
||||
|
||||
// Invalid is a span that reports false from IsValid
|
||||
var Invalid = Span{v: span{Start: invalidPoint.v, End: invalidPoint.v}}
|
||||
|
||||
var invalidPoint = Point{v: point{Line: 0, Column: 0, Offset: -1}}
|
||||
|
||||
// Converter is the interface to an object that can convert between line:column
|
||||
// and offset forms for a single file.
|
||||
type Converter interface {
|
||||
//ToPosition converts from an offset to a line:column pair.
|
||||
ToPosition(offset int) (int, int, error)
|
||||
//ToOffset converts from a line:column pair to an offset.
|
||||
ToOffset(line, col int) (int, error)
|
||||
}
|
||||
|
||||
func New(uri URI, start Point, end Point) Span {
|
||||
s := Span{v: span{URI: uri, Start: start.v, End: end.v}}
|
||||
s.v.clean()
|
||||
return s
|
||||
}
|
||||
|
||||
func NewPoint(line, col, offset int) Point {
|
||||
p := Point{v: point{Line: line, Column: col, Offset: offset}}
|
||||
p.v.clean()
|
||||
return p
|
||||
}
|
||||
|
||||
func Compare(a, b Span) int {
|
||||
if r := CompareURI(a.URI(), b.URI()); r != 0 {
|
||||
return r
|
||||
}
|
||||
if r := comparePoint(a.v.Start, b.v.Start); r != 0 {
|
||||
return r
|
||||
}
|
||||
return comparePoint(a.v.End, b.v.End)
|
||||
}
|
||||
|
||||
func ComparePoint(a, b Point) int {
|
||||
return comparePoint(a.v, b.v)
|
||||
}
|
||||
|
||||
func comparePoint(a, b point) int {
|
||||
if !a.hasPosition() {
|
||||
if a.Offset < b.Offset {
|
||||
return -1
|
||||
}
|
||||
if a.Offset > b.Offset {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
if a.Line < b.Line {
|
||||
return -1
|
||||
}
|
||||
if a.Line > b.Line {
|
||||
return 1
|
||||
}
|
||||
if a.Column < b.Column {
|
||||
return -1
|
||||
}
|
||||
if a.Column > b.Column {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (s Span) HasPosition() bool { return s.v.Start.hasPosition() }
|
||||
func (s Span) HasOffset() bool { return s.v.Start.hasOffset() }
|
||||
func (s Span) IsValid() bool { return s.v.Start.isValid() }
|
||||
func (s Span) IsPoint() bool { return s.v.Start == s.v.End }
|
||||
func (s Span) URI() URI { return s.v.URI }
|
||||
func (s Span) Start() Point { return Point{s.v.Start} }
|
||||
func (s Span) End() Point { return Point{s.v.End} }
|
||||
func (s *Span) MarshalJSON() ([]byte, error) { return json.Marshal(&s.v) }
|
||||
func (s *Span) UnmarshalJSON(b []byte) error { return json.Unmarshal(b, &s.v) }
|
||||
|
||||
func (p Point) HasPosition() bool { return p.v.hasPosition() }
|
||||
func (p Point) HasOffset() bool { return p.v.hasOffset() }
|
||||
func (p Point) IsValid() bool { return p.v.isValid() }
|
||||
func (p *Point) MarshalJSON() ([]byte, error) { return json.Marshal(&p.v) }
|
||||
func (p *Point) UnmarshalJSON(b []byte) error { return json.Unmarshal(b, &p.v) }
|
||||
func (p Point) Line() int {
|
||||
if !p.v.hasPosition() {
|
||||
panic(fmt.Errorf("position not set in %v", p.v))
|
||||
}
|
||||
return p.v.Line
|
||||
}
|
||||
func (p Point) Column() int {
|
||||
if !p.v.hasPosition() {
|
||||
panic(fmt.Errorf("position not set in %v", p.v))
|
||||
}
|
||||
return p.v.Column
|
||||
}
|
||||
func (p Point) Offset() int {
|
||||
if !p.v.hasOffset() {
|
||||
panic(fmt.Errorf("offset not set in %v", p.v))
|
||||
}
|
||||
return p.v.Offset
|
||||
}
|
||||
|
||||
func (p point) hasPosition() bool { return p.Line > 0 }
|
||||
func (p point) hasOffset() bool { return p.Offset >= 0 }
|
||||
func (p point) isValid() bool { return p.hasPosition() || p.hasOffset() }
|
||||
func (p point) isZero() bool {
|
||||
return (p.Line == 1 && p.Column == 1) || (!p.hasPosition() && p.Offset == 0)
|
||||
}
|
||||
|
||||
func (s *span) clean() {
|
||||
//this presumes the points are already clean
|
||||
if !s.End.isValid() || (s.End == point{}) {
|
||||
s.End = s.Start
|
||||
}
|
||||
}
|
||||
|
||||
func (p *point) clean() {
|
||||
if p.Line < 0 {
|
||||
p.Line = 0
|
||||
}
|
||||
if p.Column <= 0 {
|
||||
if p.Line > 0 {
|
||||
p.Column = 1
|
||||
} else {
|
||||
p.Column = 0
|
||||
}
|
||||
}
|
||||
if p.Offset == 0 && (p.Line > 1 || p.Column > 1) {
|
||||
p.Offset = -1
|
||||
}
|
||||
}
|
||||
|
||||
// Format implements fmt.Formatter to print the Location in a standard form.
|
||||
// The format produced is one that can be read back in using Parse.
|
||||
func (s Span) Format(f fmt.State, c rune) {
|
||||
fullForm := f.Flag('+')
|
||||
preferOffset := f.Flag('#')
|
||||
// we should always have a uri, simplify if it is file format
|
||||
//TODO: make sure the end of the uri is unambiguous
|
||||
uri := string(s.v.URI)
|
||||
if c == 'f' {
|
||||
uri = path.Base(uri)
|
||||
} else if !fullForm {
|
||||
uri = s.v.URI.Filename()
|
||||
}
|
||||
fmt.Fprint(f, uri)
|
||||
if !s.IsValid() || (!fullForm && s.v.Start.isZero() && s.v.End.isZero()) {
|
||||
return
|
||||
}
|
||||
// see which bits of start to write
|
||||
printOffset := s.HasOffset() && (fullForm || preferOffset || !s.HasPosition())
|
||||
printLine := s.HasPosition() && (fullForm || !printOffset)
|
||||
printColumn := printLine && (fullForm || (s.v.Start.Column > 1 || s.v.End.Column > 1))
|
||||
fmt.Fprint(f, ":")
|
||||
if printLine {
|
||||
fmt.Fprintf(f, "%d", s.v.Start.Line)
|
||||
}
|
||||
if printColumn {
|
||||
fmt.Fprintf(f, ":%d", s.v.Start.Column)
|
||||
}
|
||||
if printOffset {
|
||||
fmt.Fprintf(f, "#%d", s.v.Start.Offset)
|
||||
}
|
||||
// start is written, do we need end?
|
||||
if s.IsPoint() {
|
||||
return
|
||||
}
|
||||
// we don't print the line if it did not change
|
||||
printLine = fullForm || (printLine && s.v.End.Line > s.v.Start.Line)
|
||||
fmt.Fprint(f, "-")
|
||||
if printLine {
|
||||
fmt.Fprintf(f, "%d", s.v.End.Line)
|
||||
}
|
||||
if printColumn {
|
||||
if printLine {
|
||||
fmt.Fprint(f, ":")
|
||||
}
|
||||
fmt.Fprintf(f, "%d", s.v.End.Column)
|
||||
}
|
||||
if printOffset {
|
||||
fmt.Fprintf(f, "#%d", s.v.End.Offset)
|
||||
}
|
||||
}
|
||||
|
||||
func (s Span) WithPosition(c Converter) (Span, error) {
|
||||
if err := s.update(c, true, false); err != nil {
|
||||
return Span{}, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s Span) WithOffset(c Converter) (Span, error) {
|
||||
if err := s.update(c, false, true); err != nil {
|
||||
return Span{}, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s Span) WithAll(c Converter) (Span, error) {
|
||||
if err := s.update(c, true, true); err != nil {
|
||||
return Span{}, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *Span) update(c Converter, withPos, withOffset bool) error {
|
||||
if !s.IsValid() {
|
||||
return fmt.Errorf("cannot add information to an invalid span")
|
||||
}
|
||||
if withPos && !s.HasPosition() {
|
||||
if err := s.v.Start.updatePosition(c); err != nil {
|
||||
return err
|
||||
}
|
||||
if s.v.End.Offset == s.v.Start.Offset {
|
||||
s.v.End = s.v.Start
|
||||
} else if err := s.v.End.updatePosition(c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if withOffset && (!s.HasOffset() || (s.v.End.hasPosition() && !s.v.End.hasOffset())) {
|
||||
if err := s.v.Start.updateOffset(c); err != nil {
|
||||
return err
|
||||
}
|
||||
if s.v.End.Line == s.v.Start.Line && s.v.End.Column == s.v.Start.Column {
|
||||
s.v.End.Offset = s.v.Start.Offset
|
||||
} else if err := s.v.End.updateOffset(c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *point) updatePosition(c Converter) error {
|
||||
line, col, err := c.ToPosition(p.Offset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Line = line
|
||||
p.Column = col
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *point) updateOffset(c Converter) error {
|
||||
offset, err := c.ToOffset(p.Line, p.Column)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Offset = offset
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package span
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/token"
|
||||
)
|
||||
|
||||
// Range represents a source code range in token.Pos form.
|
||||
// It also carries the FileSet that produced the positions, so that it is
|
||||
// self contained.
|
||||
type Range struct {
|
||||
FileSet *token.FileSet
|
||||
Start token.Pos
|
||||
End token.Pos
|
||||
}
|
||||
|
||||
// TokenConverter is a Converter backed by a token file set and file.
|
||||
// It uses the file set methods to work out the conversions, which
|
||||
// makes it fast and does not require the file contents.
|
||||
type TokenConverter struct {
|
||||
fset *token.FileSet
|
||||
file *token.File
|
||||
}
|
||||
|
||||
// NewRange creates a new Range from a FileSet and two positions.
|
||||
// To represent a point pass a 0 as the end pos.
|
||||
func NewRange(fset *token.FileSet, start, end token.Pos) Range {
|
||||
return Range{
|
||||
FileSet: fset,
|
||||
Start: start,
|
||||
End: end,
|
||||
}
|
||||
}
|
||||
|
||||
// NewTokenConverter returns an implementation of Converter backed by a
|
||||
// token.File.
|
||||
func NewTokenConverter(fset *token.FileSet, f *token.File) *TokenConverter {
|
||||
return &TokenConverter{fset: fset, file: f}
|
||||
}
|
||||
|
||||
// NewContentConverter returns an implementation of Converter for the
|
||||
// given file content.
|
||||
func NewContentConverter(filename string, content []byte) *TokenConverter {
|
||||
fset := token.NewFileSet()
|
||||
f := fset.AddFile(filename, -1, len(content))
|
||||
f.SetLinesForContent(content)
|
||||
return &TokenConverter{fset: fset, file: f}
|
||||
}
|
||||
|
||||
// IsPoint returns true if the range represents a single point.
|
||||
func (r Range) IsPoint() bool {
|
||||
return r.Start == r.End
|
||||
}
|
||||
|
||||
// Span converts a Range to a Span that represents the Range.
|
||||
// It will fill in all the members of the Span, calculating the line and column
|
||||
// information.
|
||||
func (r Range) Span() (Span, error) {
|
||||
f := r.FileSet.File(r.Start)
|
||||
if f == nil {
|
||||
return Span{}, fmt.Errorf("file not found in FileSet")
|
||||
}
|
||||
s := Span{v: span{URI: FileURI(f.Name())}}
|
||||
var err error
|
||||
s.v.Start.Offset, err = offset(f, r.Start)
|
||||
if err != nil {
|
||||
return Span{}, err
|
||||
}
|
||||
if r.End.IsValid() {
|
||||
s.v.End.Offset, err = offset(f, r.End)
|
||||
if err != nil {
|
||||
return Span{}, err
|
||||
}
|
||||
}
|
||||
s.v.Start.clean()
|
||||
s.v.End.clean()
|
||||
s.v.clean()
|
||||
converter := NewTokenConverter(r.FileSet, f)
|
||||
return s.WithPosition(converter)
|
||||
}
|
||||
|
||||
// offset is a copy of the Offset function in go/token, but with the adjustment
|
||||
// that it does not panic on invalid positions.
|
||||
func offset(f *token.File, pos token.Pos) (int, error) {
|
||||
if int(pos) < f.Base() || int(pos) > f.Base()+f.Size() {
|
||||
return 0, fmt.Errorf("invalid pos")
|
||||
}
|
||||
return int(pos) - f.Base(), nil
|
||||
}
|
||||
|
||||
// Range converts a Span to a Range that represents the Span for the supplied
|
||||
// File.
|
||||
func (s Span) Range(converter *TokenConverter) (Range, error) {
|
||||
s, err := s.WithOffset(converter)
|
||||
if err != nil {
|
||||
return Range{}, err
|
||||
}
|
||||
// go/token will panic if the offset is larger than the file's size,
|
||||
// so check here to avoid panicking.
|
||||
if s.Start().Offset() > converter.file.Size() {
|
||||
return Range{}, fmt.Errorf("start offset %v is past the end of the file %v", s.Start(), converter.file.Size())
|
||||
}
|
||||
if s.End().Offset() > converter.file.Size() {
|
||||
return Range{}, fmt.Errorf("end offset %v is past the end of the file %v", s.End(), converter.file.Size())
|
||||
}
|
||||
return Range{
|
||||
FileSet: converter.fset,
|
||||
Start: converter.file.Pos(s.Start().Offset()),
|
||||
End: converter.file.Pos(s.End().Offset()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *TokenConverter) ToPosition(offset int) (int, int, error) {
|
||||
if offset > l.file.Size() {
|
||||
return 0, 0, fmt.Errorf("offset %v is past the end of the file %v", offset, l.file.Size())
|
||||
}
|
||||
pos := l.file.Pos(offset)
|
||||
p := l.fset.Position(pos)
|
||||
if offset == l.file.Size() {
|
||||
return p.Line + 1, 1, nil
|
||||
}
|
||||
return p.Line, p.Column, nil
|
||||
}
|
||||
|
||||
func (l *TokenConverter) ToOffset(line, col int) (int, error) {
|
||||
if line < 0 {
|
||||
return -1, fmt.Errorf("line is not valid")
|
||||
}
|
||||
lineMax := l.file.LineCount() + 1
|
||||
if line > lineMax {
|
||||
return -1, fmt.Errorf("line is beyond end of file %v", lineMax)
|
||||
} else if line == lineMax {
|
||||
if col > 1 {
|
||||
return -1, fmt.Errorf("column is beyond end of file")
|
||||
}
|
||||
// at the end of the file, allowing for a trailing eol
|
||||
return l.file.Size(), nil
|
||||
}
|
||||
pos := lineStart(l.file, line)
|
||||
if !pos.IsValid() {
|
||||
return -1, fmt.Errorf("line is not in file")
|
||||
}
|
||||
// we assume that column is in bytes here, and that the first byte of a
|
||||
// line is at column 1
|
||||
pos += token.Pos(col - 1)
|
||||
return offset(l.file, pos)
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !go1.12
|
||||
|
||||
package span
|
||||
|
||||
import (
|
||||
"go/token"
|
||||
)
|
||||
|
||||
// lineStart is the pre-Go 1.12 version of (*token.File).LineStart. For Go
|
||||
// versions <= 1.11, we borrow logic from the analysisutil package.
|
||||
// TODO(rstambler): Delete this file when we no longer support Go 1.11.
|
||||
func lineStart(f *token.File, line int) token.Pos {
|
||||
// Use binary search to find the start offset of this line.
|
||||
|
||||
min := 0 // inclusive
|
||||
max := f.Size() // exclusive
|
||||
for {
|
||||
offset := (min + max) / 2
|
||||
pos := f.Pos(offset)
|
||||
posn := f.Position(pos)
|
||||
if posn.Line == line {
|
||||
return pos - (token.Pos(posn.Column) - 1)
|
||||
}
|
||||
|
||||
if min+1 >= max {
|
||||
return token.NoPos
|
||||
}
|
||||
|
||||
if posn.Line < line {
|
||||
min = offset
|
||||
} else {
|
||||
max = offset
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build go1.12
|
||||
|
||||
package span
|
||||
|
||||
import (
|
||||
"go/token"
|
||||
)
|
||||
|
||||
// TODO(rstambler): Delete this file when we no longer support Go 1.11.
|
||||
func lineStart(f *token.File, line int) token.Pos {
|
||||
return f.LineStart(line)
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package span
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
const fileScheme = "file"
|
||||
|
||||
// URI represents the full URI for a file.
|
||||
type URI string
|
||||
|
||||
// Filename returns the file path for the given URI.
|
||||
// It is an error to call this on a URI that is not a valid filename.
|
||||
func (uri URI) Filename() string {
|
||||
filename, err := filename(uri)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return filepath.FromSlash(filename)
|
||||
}
|
||||
|
||||
func filename(uri URI) (string, error) {
|
||||
if uri == "" {
|
||||
return "", nil
|
||||
}
|
||||
u, err := url.ParseRequestURI(string(uri))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if u.Scheme != fileScheme {
|
||||
return "", fmt.Errorf("only file URIs are supported, got %q from %q", u.Scheme, uri)
|
||||
}
|
||||
if isWindowsDriveURI(u.Path) {
|
||||
u.Path = u.Path[1:]
|
||||
}
|
||||
return u.Path, nil
|
||||
}
|
||||
|
||||
// NewURI returns a span URI for the string.
|
||||
// It will attempt to detect if the string is a file path or uri.
|
||||
func NewURI(s string) URI {
|
||||
if u, err := url.PathUnescape(s); err == nil {
|
||||
s = u
|
||||
}
|
||||
if strings.HasPrefix(s, fileScheme+"://") {
|
||||
return URI(s)
|
||||
}
|
||||
return FileURI(s)
|
||||
}
|
||||
|
||||
func CompareURI(a, b URI) int {
|
||||
if equalURI(a, b) {
|
||||
return 0
|
||||
}
|
||||
if a < b {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
func equalURI(a, b URI) bool {
|
||||
if a == b {
|
||||
return true
|
||||
}
|
||||
// If we have the same URI basename, we may still have the same file URIs.
|
||||
if !strings.EqualFold(path.Base(string(a)), path.Base(string(b))) {
|
||||
return false
|
||||
}
|
||||
fa, err := filename(a)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
fb, err := filename(b)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
// Stat the files to check if they are equal.
|
||||
infoa, err := os.Stat(filepath.FromSlash(fa))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
infob, err := os.Stat(filepath.FromSlash(fb))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return os.SameFile(infoa, infob)
|
||||
}
|
||||
|
||||
// FileURI returns a span URI for the supplied file path.
|
||||
// It will always have the file scheme.
|
||||
func FileURI(path string) URI {
|
||||
if path == "" {
|
||||
return ""
|
||||
}
|
||||
// Handle standard library paths that contain the literal "$GOROOT".
|
||||
// TODO(rstambler): The go/packages API should allow one to determine a user's $GOROOT.
|
||||
const prefix = "$GOROOT"
|
||||
if len(path) >= len(prefix) && strings.EqualFold(prefix, path[:len(prefix)]) {
|
||||
suffix := path[len(prefix):]
|
||||
path = runtime.GOROOT() + suffix
|
||||
}
|
||||
if !isWindowsDrivePath(path) {
|
||||
if abs, err := filepath.Abs(path); err == nil {
|
||||
path = abs
|
||||
}
|
||||
}
|
||||
// Check the file path again, in case it became absolute.
|
||||
if isWindowsDrivePath(path) {
|
||||
path = "/" + path
|
||||
}
|
||||
path = filepath.ToSlash(path)
|
||||
u := url.URL{
|
||||
Scheme: fileScheme,
|
||||
Path: path,
|
||||
}
|
||||
uri := u.String()
|
||||
if unescaped, err := url.PathUnescape(uri); err == nil {
|
||||
uri = unescaped
|
||||
}
|
||||
return URI(uri)
|
||||
}
|
||||
|
||||
// isWindowsDrivePath returns true if the file path is of the form used by
|
||||
// Windows. We check if the path begins with a drive letter, followed by a ":".
|
||||
func isWindowsDrivePath(path string) bool {
|
||||
if len(path) < 4 {
|
||||
return false
|
||||
}
|
||||
return unicode.IsLetter(rune(path[0])) && path[1] == ':'
|
||||
}
|
||||
|
||||
// isWindowsDriveURI returns true if the file URI is of the format used by
|
||||
// Windows URIs. The url.Parse package does not specially handle Windows paths
|
||||
// (see https://golang.org/issue/6027). We check if the URI path has
|
||||
// a drive prefix (e.g. "/C:"). If so, we trim the leading "/".
|
||||
func isWindowsDriveURI(uri string) bool {
|
||||
if len(uri) < 4 {
|
||||
return false
|
||||
}
|
||||
return uri[0] == '/' && unicode.IsLetter(rune(uri[1])) && uri[2] == ':'
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package span
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"unicode/utf16"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// ToUTF16Column calculates the utf16 column expressed by the point given the
|
||||
// supplied file contents.
|
||||
// This is used to convert from the native (always in bytes) column
|
||||
// representation and the utf16 counts used by some editors.
|
||||
func ToUTF16Column(p Point, content []byte) (int, error) {
|
||||
if content == nil {
|
||||
return -1, fmt.Errorf("ToUTF16Column: missing content")
|
||||
}
|
||||
if !p.HasPosition() {
|
||||
return -1, fmt.Errorf("ToUTF16Column: point is missing position")
|
||||
}
|
||||
if !p.HasOffset() {
|
||||
return -1, fmt.Errorf("ToUTF16Column: point is missing offset")
|
||||
}
|
||||
offset := p.Offset() // 0-based
|
||||
colZero := p.Column() - 1 // 0-based
|
||||
if colZero == 0 {
|
||||
// 0-based column 0, so it must be chr 1
|
||||
return 1, nil
|
||||
} else if colZero < 0 {
|
||||
return -1, fmt.Errorf("ToUTF16Column: column is invalid (%v)", colZero)
|
||||
}
|
||||
// work out the offset at the start of the line using the column
|
||||
lineOffset := offset - colZero
|
||||
if lineOffset < 0 || offset > len(content) {
|
||||
return -1, fmt.Errorf("ToUTF16Column: offsets %v-%v outside file contents (%v)", lineOffset, offset, len(content))
|
||||
}
|
||||
// Use the offset to pick out the line start.
|
||||
// This cannot panic: offset > len(content) and lineOffset < offset.
|
||||
start := content[lineOffset:]
|
||||
|
||||
// Now, truncate down to the supplied column.
|
||||
start = start[:colZero]
|
||||
|
||||
// and count the number of utf16 characters
|
||||
// in theory we could do this by hand more efficiently...
|
||||
return len(utf16.Encode([]rune(string(start)))) + 1, nil
|
||||
}
|
||||
|
||||
// FromUTF16Column advances the point by the utf16 character offset given the
|
||||
// supplied line contents.
|
||||
// This is used to convert from the utf16 counts used by some editors to the
|
||||
// native (always in bytes) column representation.
|
||||
func FromUTF16Column(p Point, chr int, content []byte) (Point, error) {
|
||||
if !p.HasOffset() {
|
||||
return Point{}, fmt.Errorf("FromUTF16Column: point is missing offset")
|
||||
}
|
||||
// if chr is 1 then no adjustment needed
|
||||
if chr <= 1 {
|
||||
return p, nil
|
||||
}
|
||||
if p.Offset() >= len(content) {
|
||||
return p, fmt.Errorf("FromUTF16Column: offset (%v) greater than length of content (%v)", p.Offset(), len(content))
|
||||
}
|
||||
remains := content[p.Offset():]
|
||||
// scan forward the specified number of characters
|
||||
for count := 1; count < chr; count++ {
|
||||
if len(remains) <= 0 {
|
||||
return Point{}, fmt.Errorf("FromUTF16Column: chr goes beyond the content")
|
||||
}
|
||||
r, w := utf8.DecodeRune(remains)
|
||||
if r == '\n' {
|
||||
// Per the LSP spec:
|
||||
//
|
||||
// > If the character value is greater than the line length it
|
||||
// > defaults back to the line length.
|
||||
break
|
||||
}
|
||||
remains = remains[w:]
|
||||
if r >= 0x10000 {
|
||||
// a two point rune
|
||||
count++
|
||||
// if we finished in a two point rune, do not advance past the first
|
||||
if count >= chr {
|
||||
break
|
||||
}
|
||||
}
|
||||
p.v.Column += w
|
||||
p.v.Offset += w
|
||||
}
|
||||
return p, nil
|
||||
}
|
|
@ -1,11 +1,13 @@
|
|||
GOCMD=go
|
||||
|
||||
linters-install:
|
||||
$(GOCMD) get -u github.com/alecthomas/gometalinter
|
||||
gometalinter --install
|
||||
@golangci-lint --version >/dev/null 2>&1 || { \
|
||||
echo "installing linting tools..."; \
|
||||
curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s v1.19.1; \
|
||||
}
|
||||
|
||||
lint: linters-install
|
||||
gometalinter --vendor --disable-all --enable=vet --enable=vetshadow --enable=golint --enable=maligned --enable=megacheck --enable=ineffassign --enable=misspell --enable=errcheck --enable=goconst ./...
|
||||
golangci-lint run
|
||||
|
||||
test:
|
||||
$(GOCMD) test -cover -race ./...
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
**NOTICE:** v9 has entered maintenance status as of 2019-12-24. Please make all new functionality PR's against master.
|
||||
|
||||
Package validator
|
||||
================
|
||||
<img align="right" src="https://raw.githubusercontent.com/go-playground/validator/v9/logo.png">[![Join the chat at https://gitter.im/go-playground/validator](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/go-playground/validator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
![Project status](https://img.shields.io/badge/version-9.29.1-green.svg)
|
||||
![Project status](https://img.shields.io/badge/version-9.31.0-green.svg)
|
||||
[![Build Status](https://semaphoreci.com/api/v1/joeybloggs/validator/branches/v9/badge.svg)](https://semaphoreci.com/joeybloggs/validator)
|
||||
[![Coverage Status](https://coveralls.io/repos/go-playground/validator/badge.svg?branch=v9&service=github)](https://coveralls.io/github/go-playground/validator?branch=v9)
|
||||
[![Go Report Card](https://goreportcard.com/badge/github.com/go-playground/validator)](https://goreportcard.com/report/github.com/go-playground/validator)
|
||||
|
|
|
@ -103,6 +103,7 @@ var (
|
|||
"rgba": isRGBA,
|
||||
"hsl": isHSL,
|
||||
"hsla": isHSLA,
|
||||
"e164": isE164,
|
||||
"email": isEmail,
|
||||
"url": isURL,
|
||||
"uri": isURI,
|
||||
|
@ -224,14 +225,28 @@ func isOneOf(fl FieldLevel) bool {
|
|||
func isUnique(fl FieldLevel) bool {
|
||||
|
||||
field := fl.Field()
|
||||
param := fl.Param()
|
||||
v := reflect.ValueOf(struct{}{})
|
||||
|
||||
switch field.Kind() {
|
||||
case reflect.Slice, reflect.Array:
|
||||
m := reflect.MakeMap(reflect.MapOf(field.Type().Elem(), v.Type()))
|
||||
if param == "" {
|
||||
m := reflect.MakeMap(reflect.MapOf(field.Type().Elem(), v.Type()))
|
||||
|
||||
for i := 0; i < field.Len(); i++ {
|
||||
m.SetMapIndex(field.Index(i), v)
|
||||
}
|
||||
return field.Len() == m.Len()
|
||||
}
|
||||
|
||||
sf, ok := field.Type().Elem().FieldByName(param)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("Bad field name %s", param))
|
||||
}
|
||||
|
||||
m := reflect.MakeMap(reflect.MapOf(sf.Type, v.Type()))
|
||||
for i := 0; i < field.Len(); i++ {
|
||||
m.SetMapIndex(field.Index(i), v)
|
||||
m.SetMapIndex(field.Index(i).FieldByName(param), v)
|
||||
}
|
||||
return field.Len() == m.Len()
|
||||
case reflect.Map:
|
||||
|
@ -1219,6 +1234,11 @@ func isFile(fl FieldLevel) bool {
|
|||
panic(fmt.Sprintf("Bad field type %T", field.Interface()))
|
||||
}
|
||||
|
||||
// IsE164 is the validation function for validating if the current field's value is a valid e.164 formatted phone number.
|
||||
func isE164(fl FieldLevel) bool {
|
||||
return e164Regex.MatchString(fl.Field().String())
|
||||
}
|
||||
|
||||
// IsEmail is the validation function for validating if the current field's value is a valid email address.
|
||||
func isEmail(fl FieldLevel) bool {
|
||||
return emailRegex.MatchString(fl.Field().String())
|
||||
|
@ -1301,19 +1321,7 @@ func isDefault(fl FieldLevel) bool {
|
|||
|
||||
// HasValue is the validation function for validating if the current field's value is not the default static value.
|
||||
func hasValue(fl FieldLevel) bool {
|
||||
return requireCheckFieldKind(fl, "")
|
||||
}
|
||||
|
||||
// requireCheckField is a func for check field kind
|
||||
func requireCheckFieldKind(fl FieldLevel, param string) bool {
|
||||
field := fl.Field()
|
||||
if len(param) > 0 {
|
||||
if fl.Parent().Kind() == reflect.Ptr {
|
||||
field = fl.Parent().Elem().FieldByName(param)
|
||||
} else {
|
||||
field = fl.Parent().FieldByName(param)
|
||||
}
|
||||
}
|
||||
switch field.Kind() {
|
||||
case reflect.Slice, reflect.Map, reflect.Ptr, reflect.Interface, reflect.Chan, reflect.Func:
|
||||
return !field.IsNil()
|
||||
|
@ -1325,79 +1333,73 @@ func requireCheckFieldKind(fl FieldLevel, param string) bool {
|
|||
}
|
||||
}
|
||||
|
||||
// requireCheckField is a func for check field kind
|
||||
func requireCheckFieldKind(fl FieldLevel, param string, defaultNotFoundValue bool) bool {
|
||||
field := fl.Field()
|
||||
kind := field.Kind()
|
||||
var nullable, found bool
|
||||
if len(param) > 0 {
|
||||
field, kind, nullable, found = fl.GetStructFieldOKAdvanced2(fl.Parent(), param)
|
||||
if !found {
|
||||
return defaultNotFoundValue
|
||||
}
|
||||
}
|
||||
switch kind {
|
||||
case reflect.Invalid:
|
||||
return defaultNotFoundValue
|
||||
case reflect.Slice, reflect.Map, reflect.Ptr, reflect.Interface, reflect.Chan, reflect.Func:
|
||||
return field.IsNil()
|
||||
default:
|
||||
if nullable && field.Interface() != nil {
|
||||
return false
|
||||
}
|
||||
return field.IsValid() && field.Interface() == reflect.Zero(field.Type()).Interface()
|
||||
}
|
||||
}
|
||||
|
||||
// RequiredWith is the validation function
|
||||
// The field under validation must be present and not empty only if any of the other specified fields are present.
|
||||
func requiredWith(fl FieldLevel) bool {
|
||||
|
||||
params := parseOneOfParam2(fl.Param())
|
||||
for _, param := range params {
|
||||
|
||||
if requireCheckFieldKind(fl, param) {
|
||||
return requireCheckFieldKind(fl, "")
|
||||
if !requireCheckFieldKind(fl, param, true) {
|
||||
return hasValue(fl)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// RequiredWithAll is the validation function
|
||||
// The field under validation must be present and not empty only if all of the other specified fields are present.
|
||||
func requiredWithAll(fl FieldLevel) bool {
|
||||
|
||||
isValidateCurrentField := true
|
||||
params := parseOneOfParam2(fl.Param())
|
||||
for _, param := range params {
|
||||
|
||||
if !requireCheckFieldKind(fl, param) {
|
||||
isValidateCurrentField = false
|
||||
if requireCheckFieldKind(fl, param, true) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if isValidateCurrentField {
|
||||
return requireCheckFieldKind(fl, "")
|
||||
}
|
||||
|
||||
return true
|
||||
return hasValue(fl)
|
||||
}
|
||||
|
||||
// RequiredWithout is the validation function
|
||||
// The field under validation must be present and not empty only when any of the other specified fields are not present.
|
||||
func requiredWithout(fl FieldLevel) bool {
|
||||
|
||||
isValidateCurrentField := false
|
||||
params := parseOneOfParam2(fl.Param())
|
||||
for _, param := range params {
|
||||
|
||||
if requireCheckFieldKind(fl, param) {
|
||||
isValidateCurrentField = true
|
||||
}
|
||||
if requireCheckFieldKind(fl, strings.TrimSpace(fl.Param()), true) {
|
||||
return hasValue(fl)
|
||||
}
|
||||
|
||||
if !isValidateCurrentField {
|
||||
return requireCheckFieldKind(fl, "")
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// RequiredWithoutAll is the validation function
|
||||
// The field under validation must be present and not empty only when all of the other specified fields are not present.
|
||||
func requiredWithoutAll(fl FieldLevel) bool {
|
||||
|
||||
isValidateCurrentField := true
|
||||
params := parseOneOfParam2(fl.Param())
|
||||
for _, param := range params {
|
||||
|
||||
if requireCheckFieldKind(fl, param) {
|
||||
isValidateCurrentField = false
|
||||
if !requireCheckFieldKind(fl, param, true) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if isValidateCurrentField {
|
||||
return requireCheckFieldKind(fl, "")
|
||||
}
|
||||
|
||||
return true
|
||||
return hasValue(fl)
|
||||
}
|
||||
|
||||
// IsGteField is the validation function for validating if the current field's value is greater than or equal to the field specified by the param's value.
|
||||
|
|
|
@ -39,9 +39,7 @@ func (sc *structCache) Get(key reflect.Type) (c *cStruct, found bool) {
|
|||
}
|
||||
|
||||
func (sc *structCache) Set(key reflect.Type, value *cStruct) {
|
||||
|
||||
m := sc.m.Load().(map[reflect.Type]*cStruct)
|
||||
|
||||
nm := make(map[reflect.Type]*cStruct, len(m)+1)
|
||||
for k, v := range m {
|
||||
nm[k] = v
|
||||
|
@ -61,9 +59,7 @@ func (tc *tagCache) Get(key string) (c *cTag, found bool) {
|
|||
}
|
||||
|
||||
func (tc *tagCache) Set(key string, value *cTag) {
|
||||
|
||||
m := tc.m.Load().(map[string]*cTag)
|
||||
|
||||
nm := make(map[string]*cTag, len(m)+1)
|
||||
for k, v := range m {
|
||||
nm[k] = v
|
||||
|
@ -87,22 +83,22 @@ type cField struct {
|
|||
}
|
||||
|
||||
type cTag struct {
|
||||
tag string
|
||||
aliasTag string
|
||||
actualAliasTag string
|
||||
param string
|
||||
keys *cTag // only populated when using tag's 'keys' and 'endkeys' for map key validation
|
||||
next *cTag
|
||||
fn FuncCtx
|
||||
typeof tagType
|
||||
hasTag bool
|
||||
hasAlias bool
|
||||
hasParam bool // true if parameter used eg. eq= where the equal sign has been set
|
||||
isBlockEnd bool // indicates the current tag represents the last validation in the block
|
||||
tag string
|
||||
aliasTag string
|
||||
actualAliasTag string
|
||||
param string
|
||||
keys *cTag // only populated when using tag's 'keys' and 'endkeys' for map key validation
|
||||
next *cTag
|
||||
fn FuncCtx
|
||||
typeof tagType
|
||||
hasTag bool
|
||||
hasAlias bool
|
||||
hasParam bool // true if parameter used eg. eq= where the equal sign has been set
|
||||
isBlockEnd bool // indicates the current tag represents the last validation in the block
|
||||
runValidationWhenNil bool
|
||||
}
|
||||
|
||||
func (v *Validate) extractStructCache(current reflect.Value, sName string) *cStruct {
|
||||
|
||||
v.structCache.lock.Lock()
|
||||
defer v.structCache.lock.Unlock() // leave as defer! because if inner panics, it will never get unlocked otherwise!
|
||||
|
||||
|
@ -141,9 +137,7 @@ func (v *Validate) extractStructCache(current reflect.Value, sName string) *cStr
|
|||
customName = fld.Name
|
||||
|
||||
if v.hasTagNameFunc {
|
||||
|
||||
name := v.tagNameFunc(fld)
|
||||
|
||||
if len(name) > 0 {
|
||||
customName = name
|
||||
}
|
||||
|
@ -168,23 +162,17 @@ func (v *Validate) extractStructCache(current reflect.Value, sName string) *cStr
|
|||
namesEqual: fld.Name == customName,
|
||||
})
|
||||
}
|
||||
|
||||
v.structCache.Set(typ, cs)
|
||||
|
||||
return cs
|
||||
}
|
||||
|
||||
func (v *Validate) parseFieldTagsRecursive(tag string, fieldName string, alias string, hasAlias bool) (firstCtag *cTag, current *cTag) {
|
||||
|
||||
var t string
|
||||
var ok bool
|
||||
noAlias := len(alias) == 0
|
||||
tags := strings.Split(tag, tagSeparator)
|
||||
|
||||
for i := 0; i < len(tags); i++ {
|
||||
|
||||
t = tags[i]
|
||||
|
||||
if noAlias {
|
||||
alias = t
|
||||
}
|
||||
|
@ -198,14 +186,13 @@ func (v *Validate) parseFieldTagsRecursive(tag string, fieldName string, alias s
|
|||
current.next, current = next, curr
|
||||
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
var prevTag tagType
|
||||
|
||||
if i == 0 {
|
||||
current = &cTag{aliasTag: alias, hasAlias: hasAlias, hasTag: true}
|
||||
current = &cTag{aliasTag: alias, hasAlias: hasAlias, hasTag: true, typeof: typeDefault}
|
||||
firstCtag = current
|
||||
} else {
|
||||
prevTag = current.typeof
|
||||
|
@ -214,7 +201,6 @@ func (v *Validate) parseFieldTagsRecursive(tag string, fieldName string, alias s
|
|||
}
|
||||
|
||||
switch t {
|
||||
|
||||
case diveTag:
|
||||
current.typeof = typeDive
|
||||
continue
|
||||
|
@ -270,18 +256,14 @@ func (v *Validate) parseFieldTagsRecursive(tag string, fieldName string, alias s
|
|||
continue
|
||||
|
||||
default:
|
||||
|
||||
if t == isdefault {
|
||||
current.typeof = typeIsDefault
|
||||
}
|
||||
|
||||
// if a pipe character is needed within the param you must use the utf8Pipe representation "0x7C"
|
||||
orVals := strings.Split(t, orSeparator)
|
||||
|
||||
for j := 0; j < len(orVals); j++ {
|
||||
|
||||
vals := strings.SplitN(orVals[j], tagKeySeparator, 2)
|
||||
|
||||
if noAlias {
|
||||
alias = vals[0]
|
||||
current.aliasTag = alias
|
||||
|
@ -300,7 +282,10 @@ func (v *Validate) parseFieldTagsRecursive(tag string, fieldName string, alias s
|
|||
panic(strings.TrimSpace(fmt.Sprintf(invalidValidation, fieldName)))
|
||||
}
|
||||
|
||||
if current.fn, ok = v.validations[current.tag]; !ok {
|
||||
if wrapper, ok := v.validations[current.tag]; ok {
|
||||
current.fn = wrapper.fn
|
||||
current.runValidationWhenNil = wrapper.runValidatinOnNil
|
||||
} else {
|
||||
panic(strings.TrimSpace(fmt.Sprintf(undefinedValidation, current.tag, fieldName)))
|
||||
}
|
||||
|
||||
|
|
|
@ -585,9 +585,15 @@ Unique
|
|||
|
||||
For arrays & slices, unique will ensure that there are no duplicates.
|
||||
For maps, unique will ensure that there are no duplicate values.
|
||||
For slices of struct, unique will ensure that there are no duplicate values
|
||||
in a field of the struct specified via a parameter.
|
||||
|
||||
// For arrays, slices, and maps:
|
||||
Usage: unique
|
||||
|
||||
// For slices of struct:
|
||||
Usage: unique=field
|
||||
|
||||
Alpha Only
|
||||
|
||||
This validates that a string value contains ASCII alpha characters only
|
||||
|
@ -1058,6 +1064,35 @@ Validator notes:
|
|||
And the best reason, you can submit a pull request and we can keep on
|
||||
adding to the validation library of this package!
|
||||
|
||||
Non standard validators
|
||||
|
||||
A collection of validation rules that are frequently needed but are more
|
||||
complex than the ones found in the baked in validators.
|
||||
A non standard validator must be registered manually like you would
|
||||
with your own custom validation functions.
|
||||
|
||||
Example of registration and use:
|
||||
|
||||
type Test struct {
|
||||
TestField string `validate:"yourtag"`
|
||||
}
|
||||
|
||||
t := &Test{
|
||||
TestField: "Test"
|
||||
}
|
||||
|
||||
validate := validator.New()
|
||||
validate.RegisterValidation("yourtag", validators.NotBlank)
|
||||
|
||||
Here is a list of the current non standard validators:
|
||||
|
||||
NotBlank
|
||||
This validates that the value is not blank or with length zero.
|
||||
For strings ensures they do not contain only spaces. For channels, maps, slices and arrays
|
||||
ensures they don't have zero length. For others, a non empty value is required.
|
||||
|
||||
Usage: notblank
|
||||
|
||||
Panics
|
||||
|
||||
This package panics when bad input is provided, this is by design, bad code like
|
||||
|
@ -1072,30 +1107,5 @@ that should not make it to production.
|
|||
}
|
||||
|
||||
validate.Struct(t) // this will panic
|
||||
|
||||
Non standard validators
|
||||
|
||||
A collection of validation rules that are frequently needed but are more
|
||||
complex than the ones found in the baked in validators.
|
||||
A non standard validator must be registered manually using any tag you like.
|
||||
See below examples of registration and use.
|
||||
|
||||
type Test struct {
|
||||
TestField string `validate:"yourtag"`
|
||||
}
|
||||
|
||||
t := &Test{
|
||||
TestField: "Test"
|
||||
}
|
||||
|
||||
validate := validator.New()
|
||||
validate.RegisterValidation("yourtag", validations.ValidatorName)
|
||||
|
||||
NotBlank
|
||||
This validates that the value is not blank or with length zero.
|
||||
For strings ensures they do not contain only spaces. For channels, maps, slices and arrays
|
||||
ensures they don't have zero length. For others, a non empty value is required.
|
||||
|
||||
Usage: notblank
|
||||
*/
|
||||
package validator
|
||||
|
|
|
@ -5,7 +5,6 @@ import "reflect"
|
|||
// FieldLevel contains all the information and helper functions
|
||||
// to validate a field
|
||||
type FieldLevel interface {
|
||||
|
||||
// returns the top level struct, if any
|
||||
Top() reflect.Value
|
||||
|
||||
|
@ -26,6 +25,9 @@ type FieldLevel interface {
|
|||
// returns param for validation against current field
|
||||
Param() string
|
||||
|
||||
// GetTag returns the current validations tag name
|
||||
GetTag() string
|
||||
|
||||
// ExtractType gets the actual underlying type of field value.
|
||||
// It will dive into pointers, customTypes and return you the
|
||||
// underlying value and it's kind.
|
||||
|
@ -37,7 +39,27 @@ type FieldLevel interface {
|
|||
//
|
||||
// NOTE: when not successful ok will be false, this can happen when a nested struct is nil and so the field
|
||||
// could not be retrieved because it didn't exist.
|
||||
//
|
||||
// Deprecated: Use GetStructFieldOK2() instead which also return if the value is nullable.
|
||||
GetStructFieldOK() (reflect.Value, reflect.Kind, bool)
|
||||
|
||||
// GetStructFieldOKAdvanced is the same as GetStructFieldOK except that it accepts the parent struct to start looking for
|
||||
// the field and namespace allowing more extensibility for validators.
|
||||
//
|
||||
// Deprecated: Use GetStructFieldOKAdvanced2() instead which also return if the value is nullable.
|
||||
GetStructFieldOKAdvanced(val reflect.Value, namespace string) (reflect.Value, reflect.Kind, bool)
|
||||
|
||||
// traverses the parent struct to retrieve a specific field denoted by the provided namespace
|
||||
// in the param and returns the field, field kind, if it's a nullable type and whether is was successful in retrieving
|
||||
// the field at all.
|
||||
//
|
||||
// NOTE: when not successful ok will be false, this can happen when a nested struct is nil and so the field
|
||||
// could not be retrieved because it didn't exist.
|
||||
GetStructFieldOK2() (reflect.Value, reflect.Kind, bool, bool)
|
||||
|
||||
// GetStructFieldOKAdvanced is the same as GetStructFieldOK except that it accepts the parent struct to start looking for
|
||||
// the field and namespace allowing more extensibility for validators.
|
||||
GetStructFieldOKAdvanced2(val reflect.Value, namespace string) (reflect.Value, reflect.Kind, bool, bool)
|
||||
}
|
||||
|
||||
var _ FieldLevel = new(validate)
|
||||
|
@ -48,11 +70,16 @@ func (v *validate) Field() reflect.Value {
|
|||
}
|
||||
|
||||
// FieldName returns the field's name with the tag
|
||||
// name takeing precedence over the fields actual name.
|
||||
// name taking precedence over the fields actual name.
|
||||
func (v *validate) FieldName() string {
|
||||
return v.cf.altName
|
||||
}
|
||||
|
||||
// GetTag returns the current validations tag name
|
||||
func (v *validate) GetTag() string {
|
||||
return v.ct.tag
|
||||
}
|
||||
|
||||
// StructFieldName returns the struct field's name
|
||||
func (v *validate) StructFieldName() string {
|
||||
return v.cf.name
|
||||
|
@ -64,6 +91,29 @@ func (v *validate) Param() string {
|
|||
}
|
||||
|
||||
// GetStructFieldOK returns Param returns param for validation against current field
|
||||
//
|
||||
// Deprecated: Use GetStructFieldOK2() instead which also return if the value is nullable.
|
||||
func (v *validate) GetStructFieldOK() (reflect.Value, reflect.Kind, bool) {
|
||||
current, kind, _, found := v.getStructFieldOKInternal(v.slflParent, v.ct.param)
|
||||
return current, kind, found
|
||||
}
|
||||
|
||||
// GetStructFieldOKAdvanced is the same as GetStructFieldOK except that it accepts the parent struct to start looking for
|
||||
// the field and namespace allowing more extensibility for validators.
|
||||
//
|
||||
// Deprecated: Use GetStructFieldOKAdvanced2() instead which also return if the value is nullable.
|
||||
func (v *validate) GetStructFieldOKAdvanced(val reflect.Value, namespace string) (reflect.Value, reflect.Kind, bool) {
|
||||
current, kind, _, found := v.GetStructFieldOKAdvanced2(val, namespace)
|
||||
return current, kind, found
|
||||
}
|
||||
|
||||
// GetStructFieldOK returns Param returns param for validation against current field
|
||||
func (v *validate) GetStructFieldOK2() (reflect.Value, reflect.Kind, bool, bool) {
|
||||
return v.getStructFieldOKInternal(v.slflParent, v.ct.param)
|
||||
}
|
||||
|
||||
// GetStructFieldOKAdvanced is the same as GetStructFieldOK except that it accepts the parent struct to start looking for
|
||||
// the field and namespace allowing more extensibility for validators.
|
||||
func (v *validate) GetStructFieldOKAdvanced2(val reflect.Value, namespace string) (reflect.Value, reflect.Kind, bool, bool) {
|
||||
return v.getStructFieldOKInternal(val, namespace)
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ const (
|
|||
hslRegexString = "^hsl\\(\\s*(?:0|[1-9]\\d?|[12]\\d\\d|3[0-5]\\d|360)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*\\)$"
|
||||
hslaRegexString = "^hsla\\(\\s*(?:0|[1-9]\\d?|[12]\\d\\d|3[0-5]\\d|360)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*,\\s*(?:(?:0.[1-9]*)|[01])\\s*\\)$"
|
||||
emailRegexString = "^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22))))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$"
|
||||
e164RegexString = "^\\+[1-9]?[0-9]{7,14}$"
|
||||
base64RegexString = "^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=|[A-Za-z0-9+\\/]{4})$"
|
||||
base64URLRegexString = "^(?:[A-Za-z0-9-_]{4})*(?:[A-Za-z0-9-_]{2}==|[A-Za-z0-9-_]{3}=|[A-Za-z0-9-_]{4})$"
|
||||
iSBN10RegexString = "^(?:[0-9]{9}X|[0-9]{10})$"
|
||||
|
@ -61,6 +62,7 @@ var (
|
|||
rgbaRegex = regexp.MustCompile(rgbaRegexString)
|
||||
hslRegex = regexp.MustCompile(hslRegexString)
|
||||
hslaRegex = regexp.MustCompile(hslaRegexString)
|
||||
e164Regex = regexp.MustCompile(e164RegexString)
|
||||
emailRegex = regexp.MustCompile(emailRegexString)
|
||||
base64Regex = regexp.MustCompile(base64RegexString)
|
||||
base64URLRegex = regexp.MustCompile(base64URLRegexString)
|
||||
|
|
|
@ -57,11 +57,10 @@ BEGIN:
|
|||
//
|
||||
// NOTE: when not successful ok will be false, this can happen when a nested struct is nil and so the field
|
||||
// could not be retrieved because it didn't exist.
|
||||
func (v *validate) getStructFieldOKInternal(val reflect.Value, namespace string) (current reflect.Value, kind reflect.Kind, found bool) {
|
||||
func (v *validate) getStructFieldOKInternal(val reflect.Value, namespace string) (current reflect.Value, kind reflect.Kind, nullable bool, found bool) {
|
||||
|
||||
BEGIN:
|
||||
current, kind, _ = v.ExtractType(val)
|
||||
|
||||
current, kind, nullable = v.ExtractType(val)
|
||||
if kind == reflect.Invalid {
|
||||
return
|
||||
}
|
||||
|
@ -112,7 +111,7 @@ BEGIN:
|
|||
arrIdx, _ := strconv.Atoi(namespace[idx+1 : idx2])
|
||||
|
||||
if arrIdx >= current.Len() {
|
||||
return current, kind, false
|
||||
return
|
||||
}
|
||||
|
||||
startIdx := idx2 + 1
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
"strconv"
|
||||
)
|
||||
|
||||
// per validate contruct
|
||||
// per validate construct
|
||||
type validate struct {
|
||||
v *Validate
|
||||
top reflect.Value
|
||||
|
@ -94,7 +94,6 @@ func (v *validate) validateStruct(ctx context.Context, parent reflect.Value, cur
|
|||
|
||||
// traverseField validates any field, be it a struct or single field, ensures it's validity and passes it along to be validated via it's tag options
|
||||
func (v *validate) traverseField(ctx context.Context, parent reflect.Value, current reflect.Value, ns []byte, structNs []byte, cf *cField, ct *cTag) {
|
||||
|
||||
var typ reflect.Type
|
||||
var kind reflect.Kind
|
||||
|
||||
|
@ -112,16 +111,13 @@ func (v *validate) traverseField(ctx context.Context, parent reflect.Value, curr
|
|||
}
|
||||
|
||||
if ct.hasTag {
|
||||
|
||||
v.str1 = string(append(ns, cf.altName...))
|
||||
|
||||
if v.v.hasTagNameFunc {
|
||||
v.str2 = string(append(structNs, cf.name...))
|
||||
} else {
|
||||
v.str2 = v.str1
|
||||
}
|
||||
|
||||
if kind == reflect.Invalid {
|
||||
v.str1 = string(append(ns, cf.altName...))
|
||||
if v.v.hasTagNameFunc {
|
||||
v.str2 = string(append(structNs, cf.name...))
|
||||
} else {
|
||||
v.str2 = v.str1
|
||||
}
|
||||
v.errs = append(v.errs,
|
||||
&fieldError{
|
||||
v: v.v,
|
||||
|
@ -135,27 +131,33 @@ func (v *validate) traverseField(ctx context.Context, parent reflect.Value, curr
|
|||
kind: kind,
|
||||
},
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
v.errs = append(v.errs,
|
||||
&fieldError{
|
||||
v: v.v,
|
||||
tag: ct.aliasTag,
|
||||
actualTag: ct.tag,
|
||||
ns: v.str1,
|
||||
structNs: v.str2,
|
||||
fieldLen: uint8(len(cf.altName)),
|
||||
structfieldLen: uint8(len(cf.name)),
|
||||
value: current.Interface(),
|
||||
param: ct.param,
|
||||
kind: kind,
|
||||
typ: current.Type(),
|
||||
},
|
||||
)
|
||||
|
||||
return
|
||||
v.str1 = string(append(ns, cf.altName...))
|
||||
if v.v.hasTagNameFunc {
|
||||
v.str2 = string(append(structNs, cf.name...))
|
||||
} else {
|
||||
v.str2 = v.str1
|
||||
}
|
||||
if !ct.runValidationWhenNil {
|
||||
v.errs = append(v.errs,
|
||||
&fieldError{
|
||||
v: v.v,
|
||||
tag: ct.aliasTag,
|
||||
actualTag: ct.tag,
|
||||
ns: v.str1,
|
||||
structNs: v.str2,
|
||||
fieldLen: uint8(len(cf.altName)),
|
||||
structfieldLen: uint8(len(cf.name)),
|
||||
value: current.Interface(),
|
||||
param: ct.param,
|
||||
kind: kind,
|
||||
typ: current.Type(),
|
||||
},
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
case reflect.Struct:
|
||||
|
|
|
@ -13,27 +13,31 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
defaultTagName = "validate"
|
||||
utf8HexComma = "0x2C"
|
||||
utf8Pipe = "0x7C"
|
||||
tagSeparator = ","
|
||||
orSeparator = "|"
|
||||
tagKeySeparator = "="
|
||||
structOnlyTag = "structonly"
|
||||
noStructLevelTag = "nostructlevel"
|
||||
omitempty = "omitempty"
|
||||
isdefault = "isdefault"
|
||||
skipValidationTag = "-"
|
||||
diveTag = "dive"
|
||||
keysTag = "keys"
|
||||
endKeysTag = "endkeys"
|
||||
requiredTag = "required"
|
||||
namespaceSeparator = "."
|
||||
leftBracket = "["
|
||||
rightBracket = "]"
|
||||
restrictedTagChars = ".[],|=+()`~!@#$%^&*\\\"/?<>{}"
|
||||
restrictedAliasErr = "Alias '%s' either contains restricted characters or is the same as a restricted tag needed for normal operation"
|
||||
restrictedTagErr = "Tag '%s' either contains restricted characters or is the same as a restricted tag needed for normal operation"
|
||||
defaultTagName = "validate"
|
||||
utf8HexComma = "0x2C"
|
||||
utf8Pipe = "0x7C"
|
||||
tagSeparator = ","
|
||||
orSeparator = "|"
|
||||
tagKeySeparator = "="
|
||||
structOnlyTag = "structonly"
|
||||
noStructLevelTag = "nostructlevel"
|
||||
omitempty = "omitempty"
|
||||
isdefault = "isdefault"
|
||||
requiredWithoutAllTag = "required_without_all"
|
||||
requiredWithoutTag = "required_without"
|
||||
requiredWithTag = "required_with"
|
||||
requiredWithAllTag = "required_with_all"
|
||||
skipValidationTag = "-"
|
||||
diveTag = "dive"
|
||||
keysTag = "keys"
|
||||
endKeysTag = "endkeys"
|
||||
requiredTag = "required"
|
||||
namespaceSeparator = "."
|
||||
leftBracket = "["
|
||||
rightBracket = "]"
|
||||
restrictedTagChars = ".[],|=+()`~!@#$%^&*\\\"/?<>{}"
|
||||
restrictedAliasErr = "Alias '%s' either contains restricted characters or is the same as a restricted tag needed for normal operation"
|
||||
restrictedTagErr = "Tag '%s' either contains restricted characters or is the same as a restricted tag needed for normal operation"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -55,6 +59,11 @@ type CustomTypeFunc func(field reflect.Value) interface{}
|
|||
// TagNameFunc allows for adding of a custom tag name parser
|
||||
type TagNameFunc func(field reflect.StructField) string
|
||||
|
||||
type internalValidationFuncWrapper struct {
|
||||
fn FuncCtx
|
||||
runValidatinOnNil bool
|
||||
}
|
||||
|
||||
// Validate contains the validator settings and cache
|
||||
type Validate struct {
|
||||
tagName string
|
||||
|
@ -65,7 +74,7 @@ type Validate struct {
|
|||
structLevelFuncs map[reflect.Type]StructLevelFuncCtx
|
||||
customFuncs map[reflect.Type]CustomTypeFunc
|
||||
aliases map[string]string
|
||||
validations map[string]FuncCtx
|
||||
validations map[string]internalValidationFuncWrapper
|
||||
transTagFunc map[ut.Translator]map[string]TranslationFunc // map[<locale>]map[<tag>]TranslationFunc
|
||||
tagCache *tagCache
|
||||
structCache *structCache
|
||||
|
@ -83,7 +92,7 @@ func New() *Validate {
|
|||
v := &Validate{
|
||||
tagName: defaultTagName,
|
||||
aliases: make(map[string]string, len(bakedInAliases)),
|
||||
validations: make(map[string]FuncCtx, len(bakedInValidators)),
|
||||
validations: make(map[string]internalValidationFuncWrapper, len(bakedInValidators)),
|
||||
tagCache: tc,
|
||||
structCache: sc,
|
||||
}
|
||||
|
@ -96,8 +105,14 @@ func New() *Validate {
|
|||
// must copy validators for separate validations to be used in each instance
|
||||
for k, val := range bakedInValidators {
|
||||
|
||||
// no need to error check here, baked in will always be valid
|
||||
_ = v.registerValidation(k, wrapFunc(val), true)
|
||||
switch k {
|
||||
// these require that even if the value is nil that the validation should run, omitempty still overrides this behaviour
|
||||
case requiredWithTag, requiredWithAllTag, requiredWithoutTag, requiredWithoutAllTag:
|
||||
_ = v.registerValidation(k, wrapFunc(val), true, true)
|
||||
default:
|
||||
// no need to error check here, baked in will always be valid
|
||||
_ = v.registerValidation(k, wrapFunc(val), true, false)
|
||||
}
|
||||
}
|
||||
|
||||
v.pool = &sync.Pool{
|
||||
|
@ -140,18 +155,21 @@ func (v *Validate) RegisterTagNameFunc(fn TagNameFunc) {
|
|||
// NOTES:
|
||||
// - if the key already exists, the previous validation function will be replaced.
|
||||
// - this method is not thread-safe it is intended that these all be registered prior to any validation
|
||||
func (v *Validate) RegisterValidation(tag string, fn Func) error {
|
||||
return v.RegisterValidationCtx(tag, wrapFunc(fn))
|
||||
func (v *Validate) RegisterValidation(tag string, fn Func, callValidationEvenIfNull ...bool) error {
|
||||
return v.RegisterValidationCtx(tag, wrapFunc(fn), callValidationEvenIfNull...)
|
||||
}
|
||||
|
||||
// RegisterValidationCtx does the same as RegisterValidation on accepts a FuncCtx validation
|
||||
// allowing context.Context validation support.
|
||||
func (v *Validate) RegisterValidationCtx(tag string, fn FuncCtx) error {
|
||||
return v.registerValidation(tag, fn, false)
|
||||
func (v *Validate) RegisterValidationCtx(tag string, fn FuncCtx, callValidationEvenIfNull ...bool) error {
|
||||
var nilCheckable bool
|
||||
if len(callValidationEvenIfNull) > 0 {
|
||||
nilCheckable = callValidationEvenIfNull[0]
|
||||
}
|
||||
return v.registerValidation(tag, fn, false, nilCheckable)
|
||||
}
|
||||
|
||||
func (v *Validate) registerValidation(tag string, fn FuncCtx, bakedIn bool) error {
|
||||
|
||||
func (v *Validate) registerValidation(tag string, fn FuncCtx, bakedIn bool, nilCheckable bool) error {
|
||||
if len(tag) == 0 {
|
||||
return errors.New("Function Key cannot be empty")
|
||||
}
|
||||
|
@ -161,13 +179,10 @@ func (v *Validate) registerValidation(tag string, fn FuncCtx, bakedIn bool) erro
|
|||
}
|
||||
|
||||
_, ok := restrictedTags[tag]
|
||||
|
||||
if !bakedIn && (ok || strings.ContainsAny(tag, restrictedTagChars)) {
|
||||
panic(fmt.Sprintf(restrictedTagErr, tag))
|
||||
}
|
||||
|
||||
v.validations[tag] = fn
|
||||
|
||||
v.validations[tag] = internalValidationFuncWrapper{fn: fn, runValidatinOnNil: nilCheckable}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -133,7 +133,7 @@ github.com/golang/protobuf/proto
|
|||
github.com/golang/protobuf/protoc-gen-go/descriptor
|
||||
# github.com/golang/snappy v0.0.1
|
||||
github.com/golang/snappy
|
||||
# github.com/gomarkdown/markdown v0.0.0-20191104174740-4d42851d4d5a => github.com/status-im/markdown v0.0.0-20191209105822-e3ba6c6109ba
|
||||
# github.com/gomarkdown/markdown v0.0.0-20191209105822-e3ba6c6109ba => github.com/status-im/markdown v0.0.0-20191209105822-e3ba6c6109ba
|
||||
github.com/gomarkdown/markdown
|
||||
github.com/gomarkdown/markdown/ast
|
||||
github.com/gomarkdown/markdown/parser
|
||||
|
@ -504,7 +504,7 @@ golang.org/x/crypto/ssh/terminal
|
|||
# golang.org/x/lint v0.0.0-20190930215403-16217165b5de
|
||||
golang.org/x/lint
|
||||
golang.org/x/lint/golint
|
||||
# golang.org/x/net v0.0.0-20190930134127-c5a3c61f89f3
|
||||
# golang.org/x/net v0.0.0-20190912160710-24e19bdeb0f2
|
||||
golang.org/x/net/bpf
|
||||
golang.org/x/net/context
|
||||
golang.org/x/net/html
|
||||
|
@ -541,7 +541,7 @@ golang.org/x/text/secure/bidirule
|
|||
golang.org/x/text/transform
|
||||
golang.org/x/text/unicode/bidi
|
||||
golang.org/x/text/unicode/norm
|
||||
# golang.org/x/tools v0.0.0-20191213032237-7093a17b0467
|
||||
# golang.org/x/tools v0.0.0-20191109212701-97ad0ed33101
|
||||
golang.org/x/tools/go/analysis
|
||||
golang.org/x/tools/go/analysis/passes/inspect
|
||||
golang.org/x/tools/go/ast/astutil
|
||||
|
@ -556,7 +556,8 @@ golang.org/x/tools/go/types/typeutil
|
|||
golang.org/x/tools/internal/fastwalk
|
||||
golang.org/x/tools/internal/gopathwalk
|
||||
golang.org/x/tools/internal/semver
|
||||
# gopkg.in/go-playground/validator.v9 v9.29.1
|
||||
golang.org/x/tools/internal/span
|
||||
# gopkg.in/go-playground/validator.v9 v9.31.0
|
||||
gopkg.in/go-playground/validator.v9
|
||||
# gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||
gopkg.in/natefinch/lumberjack.v2
|
||||
|
|
Loading…
Reference in New Issue