Wesh App: Share Contact and Send Message
We continue to use the Wesh API to build more capable apps. In the previous blog post, we made an example app which creates an account using persistent on-disk storage, and we discussed the types of information in Wesh. This includes a Contact group where two user accounts can communicate. In this blog post we will make an example app to share contact and send a message in the Contact group. (This is a longer blog post because we’re building a running app.)
As before, we write an app similar to the Go tutorial. In a terminal enter:
cd mkdir contact cd contact
go mod init example/contact
The main function
In your text editor, create a file contact.go
in which to write your code. Paste the following code into your contact.go
file.
package main
import (
"context"
"fmt"
"io"
"os"
"time"
"berty.tech/weshnet/v2"
"berty.tech/weshnet/v2/pkg/protocoltypes"
"github.com/mr-tron/base58"
"google.golang.org/protobuf/proto"
)
(See the first example app blog post for explanation.) To continue the example, paste the following main function.
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
client1, err := weshnet.NewPersistentServiceClient("data1")
if err != nil {
panic(err)
}
defer client1.Close()
// client1 shares contact with client2.
binaryContact, err := client1.ShareContact(ctx,
&protocoltypes.ShareContact_Request{})
if err != nil {
panic(err)
}
fmt.Println(base58.Encode(binaryContact.EncodedContact))
// client1 receives the contact request from client2.
request, err := receiveContactRequest(ctx, client1)
if err != nil {
panic(err)
}
if request == nil {
fmt.Println("Error: Did not receive the contact request")
return
}
// client1 accepts the contact request from client2.
_, err = client1.ContactRequestAccept(ctx,
&protocoltypes.ContactRequestAccept_Request{
ContactPk: request.ContactPk,
})
if err != nil {
panic(err)
}
// Activate the contact group.
groupInfo, err := client1.GroupInfo(ctx, &protocoltypes.GroupInfo_Request{
ContactPk: request.ContactPk,
})
if err != nil {
panic(err)
}
_, err = client1.ActivateGroup(ctx, &protocoltypes.ActivateGroup_Request{
GroupPk: groupInfo.Group.PublicKey,
})
if err != nil {
panic(err)
}
// Receive a message from the group.
message, err := receiveMessage(ctx, client1, groupInfo)
if err != nil {
panic(err)
}
if message == nil {
fmt.Print("End of stream without receiving message")
return
}
fmt.Println("client2:", string(message.Message))
}
This uses some helper functions which we will define below. As in the previous example, we call NewPersistentServiceClient("data1")
to create a client with persistent storage on-disk. We name the folder “data1” because this is client1. It will communicate with client2 which we create below.
Next we call the Wesh API function ShareContact
which is documented here. It returns an encoded byte array with the information that client2 needs to make a contact request. We use base58.Encode
to make it a shareable string and print it to the console. (The Wesh API leaves it up to the application developer to decide how to encode the byte array. You may use base58, or make a QR code, or a URI, etc.) This string is used by client2, as we will see.
Let’s imagine that client2 has sent the contact request, so we call receiveContactRequest
which we will define below. It returns request
which has the account public key of client2, so we call ContactRequestAccept
(docs) to accept the contact request and create the Contact group.
Now we need to activate this group. We use the same account public key of client2 to identify the Contact group and call GroupInfo
(docs) to get the group’s public key, and then call ActivateGroup
(docs) to activate it.
Finally, let’s imagine that client2 has sent a message to the group, so we call receiveMessage
which we will define below. This returns the message (or nil if end of stream) which we print to the console.
Helper functions
That’s it for main! Now we need to define the helper functions receiveContactRequest
and receiveMessage
. These both follow the pattern of subscribing to an event stream and waiting for the desired event type. Paste the following function to the file contact.go
.
func receiveContactRequest(ctx context.Context, client weshnet.ServiceClient) (*protocoltypes.AccountContactRequestIncomingReceived, error) {
// Get the client's AccountGroupPk from the configuration.
config, err := client.ServiceGetConfiguration(ctx, &protocoltypes.ServiceGetConfiguration_Request{})
if err != nil {
return nil, err
}
// Subscribe to metadata events. ("sub" means "subscription".)
subCtx, subCancel := context.WithCancel(ctx)
defer subCancel()
subMetadata, err := client.GroupMetadataList(subCtx, &protocoltypes.GroupMetadataList_Request{
GroupPk: config.AccountGroupPk,
})
if err != nil {
return nil, err
}
for {
metadata, err := subMetadata.Recv()
if err == io.EOF || subMetadata.Context().Err() != nil {
// Not received.
return nil, nil
}
if err != nil {
return nil, err
}
if metadata == nil || metadata.Metadata.EventType !=
protocoltypes.EventType_EventTypeAccountContactRequestIncomingReceived {
continue
}
request := &protocoltypes.AccountContactRequestIncomingReceived{}
if err = proto.Unmarshal(metadata.Event, request); err != nil {
return nil, err
}
return request, nil
}
}
This function takes the client1 which we created in main
and calls ServiceGetConfiguration
(docs). Instead of getting the configuration’s peer ID as in previous examples, we get the public key of client1’s Account group. This public key is also in the shared contact, and is used by client2 to send a contact request to client1. To receive it, we call GroupMetadataList
(docs) with the Account group public key.
Most API functions return a data structure, but a few like GroupMetadataList
return a subscription stream like subMetadata
. We use a for
loop and call subMetadata.Recv()
which blocks until it receives an event (or end of stream). As you build more complex apps, an event loop like this may handle more event types and operations. For now, we just check that the event type is the one we’re waiting for, EventType_EventTypeAccountContactRequestIncomingReceived
.
Now we can use Unmarshal
to convert the metadata
event to the specific AccountContactRequestIncomingReceived
event. (Unmarshal
is part of the Protobuf interface. For more details, see the docs). This is the contact request
that we return from the function.
Now we need to define receiveMessage
which waits for the message. Paste the following function to the file contact.go
. (This one is shorter!)
func receiveMessage(ctx context.Context, client weshnet.ServiceClient, groupInfo *protocoltypes.GroupInfo_Reply) (*protocoltypes.GroupMessageEvent, error) {
// Subscribe to message events.
subCtx, subCancel := context.WithCancel(ctx)
defer subCancel()
subMessages, err := client.GroupMessageList(subCtx, &protocoltypes.GroupMessageList_Request{
GroupPk: groupInfo.Group.PublicKey,
})
if err != nil {
panic(err)
}
// client waits to receive the message.
for {
message, err := subMessages.Recv()
if err == io.EOF {
// Not received.
return nil, nil
}
if err != nil {
return nil, err
}
return message, nil
}
}
Similar to the previous function, this function takes the client1 which we created in main
. It also takes the groupInfo
object of the Contact group. (The main
function already used this to activate the group.)
As in the previous function, we want to subscribe to events. In the previous blog post, we briefly discussed the difference between the Metadata log and the Message log. A contact request is an event in the Metadata log, so receiveContactRequest
called GroupMetadataList
. But now we want to receive a “normal” message when it is added to the Message log.
We call the API method GroupMessageList
(docs) which returns the subscription stream subMessages
. We use a for
loop and call subMetadata.Recv()
which blocks until it receives an event (or end of stream). Finally, the function returns message
which is a GroupMessageEvent
so that the main function can print the message from client2.
Client 2
Wait a moment (you may be thinking). We don’t have the code where client2 sends the message. We will add it to this same file and you will run the app in two terminals. Remember that the code for client1 prints the contact string to the terminal. When we run the app for client2, we’ll add this string as a command-line parameter. At the very beginning of the main function, insert this code:
if len(os.Args) == 2 {
doClient2(os.Args[1])
return
}
Now we can define the function doClient2
which is called if we run using the contact string. Paste the following function to the file contact.go
and save it.
func doClient2(encodedContact string) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
client2, err := weshnet.NewPersistentServiceClient("data2")
if err != nil {
panic(err)
}
defer client2.Close()
contactBinary, err := base58.Decode(encodedContact)
if err != nil {
panic(err)
}
contact, err := client2.DecodeContact(ctx,
&protocoltypes.DecodeContact_Request{
EncodedContact: contactBinary,
})
if err != nil {
panic(err)
}
// Send the contact request.
_, err = client2.ContactRequestSend(ctx,
&protocoltypes.ContactRequestSend_Request{
Contact: contact.Contact,
})
if err != nil {
panic(err)
}
// Activate the contact group.
groupInfo, err := client2.GroupInfo(ctx, &protocoltypes.GroupInfo_Request{
ContactPk: contact.Contact.Pk,
})
if err != nil {
panic(err)
}
_, err = client2.ActivateGroup(ctx, &protocoltypes.ActivateGroup_Request{
GroupPk: groupInfo.Group.PublicKey,
})
if err != nil {
panic(err)
}
// Send a message to the contact group.
_, err = client2.AppMessageSend(ctx, &protocoltypes.AppMessageSend_Request{
GroupPk: groupInfo.Group.PublicKey,
Payload: []byte("Hello"),
})
if err != nil {
panic(err)
}
fmt.Println("Sending message...")
time.Sleep(time.Second * 5)
}
This function takes the encodedContact
from the command line. It runs as a separate process, so we need to use NewPersistentServiceClient
to create a separate client2 with persistent data stored in a separate folder, “data2”.
Next we use base58.Decode
to recover the encoded byte array with client1’s contact info, and use DecodeContact
(docs) to extract the contact
info. Now client2 can call ContactRequestSend
(docs) to send this to client1 as a contact request. You may be thinking, “client1 just sent this info to client2, so why does client2 need to send it back?” This is part of the secure handshake. client2 needs to make sure that the shared contact really came from client1, and needs to decide if creating a contact is actually desired.
Similar to the code above, client2 needs to activate the Contact group using GroupInfo
and ActivateGroup
. (In this case, client2 has client1’s account public key from the shared contact in contact.Contact.Pk
.)
We’re almost done! Client2 calls AppMessageSend
(docs) to use the Contact group public key to send a “Hello” message to the Contact group. (For generality, a message is any byte array. In this case we simply store the message string in it.) For efficiency, the AppMessageSend
function queues the message to be sent and returns immediately. If we exit the application too soon, then the Wesh services won’t have time to actually send the message. Therefore, we sleep this function for 5 seconds so that the service threads can complete.
Run the app
That’s all the code! It’s time to run the app. In a terminal enter:
go mod tidy
go run .
(You only need to do go mod tidy
the first time.) It should print the contact string from client1, something like 2KqzJQpZ2Y7EDaep6CnceT6ozqy1Ss6qJV8tsN59QSBejfa4TiYjMr8Z9PjHr1D2bYa4EozWudwaWMwB5jXqb5gRLj2bX
. Copy this to the clipboard.
Leave this app running. Now, in a separate terminal we run as client2. cd to the same directory and enter:
go run . <contact-string>
where <contact-string>
is the contact string from client1. It should print Sending message...
. Now look at the terminal for client1. It should print client2: Hello
.
You have established Wesh communication! You can use this example app as a basis for more sophisticated Wesh apps. Overall, this simply creates a Contact group and calls AppMessageSend
. But you may understand that Wesh communicates differently than using a traditional network connection. How does the message actually get from client2 to client1? In the next blog post, we’ll do a theory dive into how Wesh’s asynchronous communication works.
Posted by Jeff on 04/07/2023