[#ISSUE-3] New protocol messages support

This commit is contained in:
Adrià Cidre 2018-04-30 19:07:36 +02:00
parent 9b46298eca
commit 473a135243
No known key found for this signature in database
GPG Key ID: D246A27D58A92CAB
12 changed files with 257 additions and 86 deletions

35
.gometalinter.json Normal file
View File

@ -0,0 +1,35 @@
{
"Exclude": [
".*_mock.go",
"geth/jail/doc.go",
".*Errors unhandled.*fmt.Fprint.*gasv2.*"
],
"Skip": ["helpers", "static"],
"Vendor": true,
"Test": true,
"Linters": {
"gasv2": {
"Command": "gas -fmt=csv",
"Pattern": "^(?P<path>.*?\\.go),(?P<line>\\d+),(?P<message>[^,]+,[^,]+,[^,]+,\".*\")"
}
},
"Enable": [
"deadcode",
"errcheck",
"gasv2",
"goconst",
"gocyclo",
"gofmt",
"golint",
"ineffassign",
"interfacer",
"megacheck",
"misspell",
"structcheck",
"unconvert",
"varcheck",
"vet"
],
"Cyclo": 16,
"Deadline": "200s"
}

15
.travis.yml Normal file
View File

@ -0,0 +1,15 @@
notifications:
email: false
language: go
go:
- 1.9.x
sudo: false
dist: trusty
install:
- make lint-install
jobs:
include:
- stage: Lint
sudo: required
script:
- make lint

7
Makefile Normal file
View File

@ -0,0 +1,7 @@
lint-install:
go get -u github.com/alecthomas/gometalinter
gometalinter --install
lint:
@echo "lint"
@gometalinter ./...

79
chan.go
View File

@ -1,13 +1,14 @@
package sdk
import (
"encoding/json"
"fmt"
"log"
)
// Channel : ...
type Channel struct {
conn *Conn
conn *SDK
channelName string
filterID string
channelKey string
@ -17,18 +18,17 @@ type Channel struct {
// Publish : Publishes a message with the given body on the current channel
func (c *Channel) Publish(body string) error {
message := NewMsg(c.conn.userName, body, c.channelName)
msg := NewMsg(c.conn.userName, body, c.channelName)
cmd := fmt.Sprintf(standardMessageFormat,
c.conn.address,
c.channelKey,
message.ToPayload(),
msg.ToPayload(),
c.topic,
c.conn.minimumPoW,
)
_, err := c.conn.call(cmd)
c.conn.rpc.Call(cmd)
return nil
return err
}
// Subscribe : ...
@ -48,6 +48,73 @@ func (c *Channel) Close() {
}
}
// NewContactKeyRequest : First message that is sent to a future contact. At that
// point the only topic we know that the contact is filtering is the
// discovery-topic with his public key so that is what NewContactKey will
// be sent to.
// It contains the sym-key and topic that will be used for future communications
// as well as the actual message that we want to send.
// The sym-key and topic are generated randomly because we dont want to have
// any correlation between a topic and its participants to avoid leaking
// metadata.
// When one of the contacts recovers his account, a NewContactKey message is
// sent as well to change the symmetric key and topic.
func (c *Channel) NewContactKeyRequest(username string) {
contactRequest := fmt.Sprintf(contactRequestMsg, username, "", "", "")
msg := fmt.Sprintf(newContactKeyMsg, c.conn.address, c.topic, contactRequest)
c.callStandardMsg(msg)
}
// ContactRequest : Wrapped in a NewContactKey message when initiating a contact request.
func (c *Channel) ContactRequest(username, image string) {
msg := fmt.Sprintf(contactRequestMsg, username, image, c.conn.address, "")
c.callStandardMsg(msg)
}
// ConfirmedContactRequest : This is the message that will be sent when the
// contact accepts the contact request. It will be sent on the topic that
// was provided in the NewContactKey message and use the sym-key.
// Both users will therefore have the same filter.
func (c *Channel) ConfirmedContactRequest(username, image string) {
msg := fmt.Sprintf(confirmedContactRequestMsg, username, image, c.conn.address, "")
c.callStandardMsg(msg)
}
// ContactUpdateRequest : Sent when the user changes his name or profile-image.
func (c *Channel) ContactUpdateRequest(username, image string) {
msg := fmt.Sprintf(contactUpdateMsg, username, image)
c.callStandardMsg(msg)
}
// SeenRequest : Sent when a user sees a message (opens the chat and loads the
// message). Can acknowledge multiple messages at the same time
func (c *Channel) SeenRequest(ids []string) error {
body, err := json.Marshal(ids)
if err != nil {
return err
}
msg := fmt.Sprintf(seenMsg, body)
c.callStandardMsg(msg)
return nil
}
func (c *Channel) callStandardMsg(body string) {
msg := rawrChatMessage(body)
cmd := fmt.Sprintf(standardMessageFormat,
c.conn.address,
c.channelKey,
msg,
c.topic,
c.conn.minimumPoW)
// TODO (adriacidre) manage this error
_, _ = c.conn.call(cmd)
}
func (c *Channel) removeSubscription(sub *Subscription) {
var subs []*Subscription
for _, s := range c.subscriptions {

View File

@ -8,7 +8,11 @@ var (
web3ShaFormat = `{"jsonrpc":"2.0","method":"web3_sha3","params":["%s"],"id":%d}`
statusLoginFormat = `{"jsonrpc":"2.0","method":"status_login","params":["%s","%s"]}`
statusSignupFormat = `{"jsonrpc":"2.0","method":"status_signup","params":["%s","%s"]}`
statusJoinPublicChannel = `{"jsonrpc":"2.0","method":"status_joinpublicchannel","params":["%s"]}`
messagePayloadFormat = `["~#c4",["%s","text/plain","~:public-group-user-message",%d,%d]]`
newContactKeyMsg = `["~#c1",["%s","%s",%s]`
contactRequestMsg = `["~#c2",["%s","%s","%s","%s”]]]`
confirmedContactRequestMsg = `["~#c3",["%s","%s","%s","%s"]]`
messagePayloadMsg = `["~#c4",["%s","text/plain","~:public-group-user-message",%d,%d]]`
seenMsg = `["~#c5",["%s","%s"]]`
contactUpdateMsg = `["~#c6",["%s","%s"]]`
)

View File

@ -8,18 +8,18 @@ import (
)
func main() {
conn := sdk.NewConn("localhost:30303")
if err := conn.Signup("111222333"); err != nil {
sdk := sdk.New("localhost:30303")
if err := sdk.Signup("111222333"); err != nil {
panic("Couldn't create an account")
}
ch, err := conn.Join("supu")
ch, err := sdk.Join("supu")
if err != nil {
panic("Couldn't connect to status")
}
for range time.Tick(10 * time.Second) {
message := fmt.Sprintf("PING : %d", time.Now().Unix())
ch.Publish(message)
_ = ch.Publish(message)
}
}

View File

@ -11,24 +11,24 @@ import (
)
func main() {
conn := sdk.NewConn()
client := sdk.New("localhost:30303")
if err := conn.SignupOrLogin("supu", "password"); err != nil {
if err := client.SignupOrLogin("supu", "password"); err != nil {
panic(err)
}
ch, err := conn.Join("supu")
ch, err := client.Join("supu")
if err != nil {
panic("Couldn't connect to status")
}
ch.Subscribe(func(m *sdk.Msg) {
_, _ = ch.Subscribe(func(m *sdk.Msg) {
log.Println("Message from ", m.From, " with body: ", m.Text)
if strings.Contains(m.Text, "PING :") {
time.Sleep(5 * time.Second)
message := fmt.Sprintf("PONG : %d", time.Now().Unix())
ch.Publish(message)
_ = ch.Publish(message)
}
})

67
msg.go
View File

@ -1,51 +1,101 @@
package sdk
import (
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/ethereum/go-ethereum/crypto/sha3"
)
var (
// NewContactKeyType message type for newContactKeyFormat
NewContactKeyType = "~#c1"
// ContactRequestType message type for contactRequestFormat
ContactRequestType = "~#c2"
// ConfirmedContactRequestType message type for confirmedContactRequestFormat
ConfirmedContactRequestType = "~#c3"
// StandardMessageType message type for StandardMessageFormat
StandardMessageType = "~#c4"
// SeenType message type for SeentType
SeenType = "~#c5"
// ContactUpdateType message type for contactUpdateMsg
ContactUpdateType = "~#c6"
)
// supportedMessage check if the message type is supported
func supportedMessage(msgType string) bool {
_, ok := map[string]bool{
NewContactKeyType: true,
ContactRequestType: true,
ConfirmedContactRequestType: true,
StandardMessageType: true,
SeenType: true,
ContactUpdateType: true,
}[msgType]
return ok
}
// Msg is a structure used by Subscribers and Publish().
type Msg struct {
ID string `json:"id"`
From string `json:"from"`
Text string `json:"text"`
ChannelName string `json:"channel"`
Timestamp int64 `json:"ts"`
Raw string `json:"-"`
Type string `json:"-"`
}
// NewMsg : Creates a new Msg with a generated UUID
// NewMsg creates a new Msg with a generated UUID
func NewMsg(from, text, channel string) *Msg {
return &Msg{
ID: newUUID(),
From: from,
Text: text,
ChannelName: channel,
Timestamp: time.Now().Unix() * 1000,
Timestamp: time.Now().Unix(),
}
}
// ToPayload converts current struct to a valid payload
// ID gets the message id
func (m *Msg) ID() string {
return fmt.Sprintf("%X", sha3.Sum256([]byte(m.Raw)))
}
// ToPayload converts current struct to a valid payload
func (m *Msg) ToPayload() string {
message := fmt.Sprintf(messagePayloadFormat,
message := fmt.Sprintf(messagePayloadMsg,
m.Text,
m.Timestamp*100,
m.Timestamp)
println(message)
return rawrChatMessage(message)
}
// MessageFromPayload : TODO ...
func rawrChatMessage(raw string) string {
bytes := []byte(raw)
return fmt.Sprintf("0x%s", hex.EncodeToString(bytes))
}
func unrawrChatMessage(message string) ([]byte, error) {
return hex.DecodeString(message[2:])
}
// MessageFromPayload creates a message from a payload
func MessageFromPayload(payload string) (*Msg, error) {
message, err := unrawrChatMessage(payload)
if err != nil {
return nil, err
}
var x []interface{}
json.Unmarshal([]byte(message), &x)
if err := json.Unmarshal(message, &x); err != nil {
return nil, err
}
if len(x) < 1 {
return nil, errors.New("unsupported message type")
}
@ -55,6 +105,7 @@ func MessageFromPayload(payload string) (*Msg, error) {
properties := x[1].([]interface{})
return &Msg{
// TODO (adriacidre) add from username
From: "TODO : someone",
Text: properties[0].(string),
Timestamp: int64(properties[3].(float64)),

17
rpcclient.go Normal file
View File

@ -0,0 +1,17 @@
package sdk
import "github.com/valyala/gorpc"
// RPCClient is a client to manage all rpc calls
type RPCClient interface {
Call(request interface{}) (response interface{}, err error)
}
func newRPC(address string) RPCClient {
rpc := &gorpc.Client{
Addr: address, // "rpc.server.addr:12345",
}
rpc.Start()
return rpc
}

View File

@ -3,41 +3,36 @@ package sdk
import (
"encoding/hex"
"fmt"
"github.com/valyala/gorpc"
)
// Conn : TODO ...
type Conn struct {
rpc *gorpc.Client
// SDK is a set of tools to interact with status node
type SDK struct {
RPCClient RPCClient
address string
userName string
channels []*Channel
minimumPoW string
}
func NewConn(address string) *Conn {
rpc := &gorpc.Client{
Addr: address, // "rpc.server.addr:12345",
}
rpc.Start()
return &Conn{
rpc: rpc,
// New creates a default SDK object
func New(address string) *SDK {
return &SDK{
RPCClient: newRPC(address),
minimumPoW: "0.01",
}
}
func (c *Conn) Close() {
// Close all channels you're subscribed to
func (c *SDK) Close() {
for _, channel := range c.channels {
channel.Close()
}
}
// Login logs in to the network with the given credentials
func (c *Conn) Login(addr, pwd string) error {
// Login to status with the given credentials
func (c *SDK) Login(addr, pwd string) error {
cmd := fmt.Sprintf(statusLoginFormat, addr, pwd)
res, err := c.rpc.Call(cmd)
res, err := c.call(cmd)
if err != nil {
return err
}
@ -48,13 +43,9 @@ func (c *Conn) Login(addr, pwd string) error {
}
// Signup creates a new account with the given credentials
func (c *Conn) Signup(pwd string) error {
func (c *SDK) Signup(pwd string) error {
cmd := fmt.Sprintf(statusSignupFormat, pwd)
res, err := c.rpc.Call(cmd)
println("------")
println(res)
println(err.Error())
println("------")
res, err := c.call(cmd)
if err != nil {
return err
}
@ -66,9 +57,10 @@ func (c *Conn) Signup(pwd string) error {
// SignupOrLogin will attempt to login with given credentials, in first instance
// or will sign up in case login does not work
func (c *Conn) SignupOrLogin(user, password string) error {
func (c *SDK) SignupOrLogin(user, password string) error {
if err := c.Login(user, password); err != nil {
c.Signup(password)
// TODO (adriacidre) handle this error
_ = c.Signup(password)
return c.Login(user, password)
}
@ -76,7 +68,7 @@ func (c *Conn) SignupOrLogin(user, password string) error {
}
// Join a specific channel by name
func (c *Conn) Join(channelName string) (*Channel, error) {
func (c *SDK) Join(channelName string) (*Channel, error) {
ch, err := c.joinPublicChannel(channelName)
if err != nil {
c.channels = append(c.channels, ch)
@ -85,9 +77,9 @@ func (c *Conn) Join(channelName string) (*Channel, error) {
return ch, err
}
func (c *Conn) joinPublicChannel(channelName string) (*Channel, error) {
func (c *SDK) joinPublicChannel(channelName string) (*Channel, error) {
cmd := fmt.Sprintf(generateSymKeyFromPasswordFormat, channelName)
res, _ := c.rpc.Call(cmd)
res, _ := c.call(cmd)
f := unmarshalJSON(res.(string))
key := f.(map[string]interface{})["result"].(string)
@ -97,12 +89,12 @@ func (c *Conn) joinPublicChannel(channelName string) (*Channel, error) {
p := "0x" + hex.EncodeToString(src)
cmd = fmt.Sprintf(web3ShaFormat, p, id)
res, _ = c.rpc.Call(cmd)
res, _ = c.call(cmd)
topic := res.(map[string]interface{})["result"].(string)
topic = topic[0:10]
cmd = fmt.Sprintf(newMessageFilterFormat, topic, key)
res, _ = c.rpc.Call(cmd)
res, _ = c.call(cmd)
f3 := unmarshalJSON(res.(string))
filterID := f3.(map[string]interface{})["result"].(string)
@ -114,3 +106,7 @@ func (c *Conn) joinPublicChannel(channelName string) (*Channel, error) {
channelKey: key,
}, nil
}
func (c *SDK) call(cmd string) (interface{}, error) {
return c.RPCClient.Call(cmd)
}

View File

@ -10,11 +10,14 @@ import (
// asynchronous subscribers.
type MsgHandler func(msg *Msg)
// Subscription is a polling helper for a specific channel
type Subscription struct {
unsubscribe chan bool
channel *Channel
}
// Subscribe polls on specific channel topic and executes given function if
// any message is received
func (s *Subscription) Subscribe(channel *Channel, fn MsgHandler) {
s.channel = channel
s.unsubscribe = make(chan bool)
@ -24,7 +27,7 @@ func (s *Subscription) Subscribe(channel *Channel, fn MsgHandler) {
return
default:
cmd := fmt.Sprintf(getFilterMessagesFormat, channel.filterID)
response, err := channel.conn.rpc.Call(cmd)
response, err := channel.conn.call(cmd)
if err != nil {
log.Fatalf("Error when sending request to server: %s", err)
}
@ -39,7 +42,9 @@ func (s *Subscription) Subscribe(channel *Channel, fn MsgHandler) {
if err != nil {
log.Println(err)
} else {
fn(message)
if supportedMessage(message.Type) {
fn(message)
}
}
}
default:
@ -50,6 +55,7 @@ func (s *Subscription) Subscribe(channel *Channel, fn MsgHandler) {
}
}
// Unsubscribe stops polling on the current subscription channel
func (s *Subscription) Unsubscribe() {
s.unsubscribe <- true
s.channel.removeSubscription(s)

View File

@ -1,39 +1,12 @@
package sdk
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"io"
)
func unmarshalJSON(j string) interface{} {
var v interface{}
json.Unmarshal([]byte(j), &v)
// TODO(adriacidre) Handle this error
_ = json.Unmarshal([]byte(j), &v)
return v
}
// newUUID generates a random UUID according to RFC 4122
func newUUID() string {
uuid := make([]byte, 16)
n, err := io.ReadFull(rand.Reader, uuid)
if n != len(uuid) || err != nil {
panic(err)
}
// variant bits; see section 4.1.1
uuid[8] = uuid[8]&^0xc0 | 0x80
// version 4 (pseudo-random); see section 4.1.3
uuid[6] = uuid[6]&^0xf0 | 0x40
return fmt.Sprintf("%x-%x-%x-%x-%x", uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:])
}
func rawrChatMessage(raw string) string {
bytes := []byte(raw)
return fmt.Sprintf("0x%s", hex.EncodeToString(bytes))
}
func unrawrChatMessage(message string) ([]byte, error) {
return hex.DecodeString(message[2:])
}