Refactor to a move event-driven library - incomplete

This commit is contained in:
Sarah Jamie Lewis 2016-02-28 16:18:25 -08:00
parent 5b013a76c3
commit 93754f2916
6 changed files with 346 additions and 250 deletions

View File

@ -1,32 +1,31 @@
package main
import (
"fmt"
"github.com/s-rah/go-ricochet"
"time"
)
type EchoBotService struct {
goricochet.StandardRicochetService
}
func (ebs * EchoBotService) OnAuthenticationResult(channelID int32, serverHostname string, result bool) {
if true {
ebs.Ricochet().OpenChatChannel(5)
ebs.Ricochet().SendMessage(5, "Hi I'm an echo bot, I echo what you say!")
}
}
func (ebs * EchoBotService) OnChatMessage(channelID int32, serverHostname string, messageId int32, message string) {
ebs.Ricochet().AckChatMessage(channelID, messageId)
ebs.Ricochet().SendMessage(5, message)
}
func main() {
ricochet := new(goricochet.Ricochet)
// You will want to replace these values with your own test credentials
ricochet.Init("./private_key", true)
ricochet.Connect("kwke2hntvyfqm7dr", "127.0.0.1:55555|jlq67qzo6s4yp3sp")
// Not needed past the initial run
// TODO need to wait for contact response before sending OpenChannel
// ricochet.SendContactRequest("EchoBot", "I'm an EchoBot")
go ricochet.ListenAndWait()
ricochet.OpenChatChannel(5)
time.Sleep(time.Second * 1)
ricochet.SendMessage(5, "Hi I'm an echo bot, I echo what you say!")
for true {
message,channel,_ := ricochet.Listen()
fmt.Print(channel, message)
if message != "" {
ricochet.SendMessage(5, message)
}
ricochetService := new(EchoBotService)
ricochetService.Init("./private_key", "kwke2hntvyfqm7dr")
err := ricochetService.Ricochet().Connect("kwke2hntvyfqm7dr", "127.0.0.1:55555|jlq67qzo6s4yp3sp")
if err == nil {
ricochetService.OnConnect("jlq67qzo6s4yp3sp")
ricochetService.Ricochet().ListenAndWait("jlq67qzo6s4yp3sp", ricochetService)
}
}

View File

@ -26,6 +26,18 @@ func (mb *MessageBuilder) OpenChatChannel(channelID int32) ([]byte, error) {
return proto.Marshal(pc)
}
// AckOpenChannel constructs a message to acknowledge a previous open channel operation.
func (mb *MessageBuilder) AckOpenChannel(channelID int32, opened bool) ([]byte, error) {
cr := &Protocol_Data_Control.ChannelResult{
ChannelIdentifier: proto.Int32(channelID),
Opened: proto.Bool(opened),
}
pc := &Protocol_Data_Control.Packet{
ChannelResult: cr,
}
return proto.Marshal(pc)
}
// OpenContactRequestChannel contructs a message which will reuqest to open a channel for
// a contact request on the given channelID, with the given nick and message.
func (mb *MessageBuilder) OpenContactRequestChannel(channelID int32, nick string, message string) ([]byte, error) {
@ -79,3 +91,15 @@ func (mb *MessageBuilder) ChatMessage(message string) ([]byte, error) {
}
return proto.Marshal(chatPacket)
}
// AckChatMessage constructs a chat message acknowledgement.
func (mb *MessageBuilder) AckChatMessage(messageID int32) ([]byte, error) {
cr := &Protocol_Data_Chat.ChatAcknowledge{
MessageId: proto.Uint32(uint32(messageID)),
Accepted: proto.Bool(true),
}
pc := &Protocol_Data_Chat.Packet{
ChatAcknowledge: cr,
}
return proto.Marshal(pc)
}

110
messagedecoder.go Normal file
View File

@ -0,0 +1,110 @@
package goricochet
import (
"errors"
"github.com/golang/protobuf/proto"
"github.com/s-rah/go-ricochet/auth"
"github.com/s-rah/go-ricochet/chat"
"github.com/s-rah/go-ricochet/control"
)
type MessageDecoder struct {
}
// Conceptual Chat Message - we construct this to avoid polluting the
// the main ricochet code with protobuf cruft - and enable us to minimise the
// code that may break in the future.
type RicochetChatMessage struct {
Ack bool
MessageID int32
Message string
Accepted bool
}
// Conceptual Control Message - we construct this to avoid polluting the
// the main ricochet code with protobuf cruft - and enable us to minimise the
// code that may break in the future.
type RicochetControlMessage struct {
Ack bool
Type string
ChannelID int32
Accepted bool
ClientCookie [16]byte
ServerCookie [16]byte
}
// DecodeAuthMessage
func (md *MessageDecoder) DecodeAuthMessage(data []byte) (bool, error) {
res := new(Protocol_Data_AuthHiddenService.Packet)
err := proto.Unmarshal(data[:], res)
if err != nil {
return false, errors.New("error unmarshalling control message type")
}
return res.GetResult().GetAccepted(), nil
}
// DecodeControlMessage
func (md *MessageDecoder) DecodeControlMessage(data []byte) (*RicochetControlMessage, error) {
res := new(Protocol_Data_Control.Packet)
err := proto.Unmarshal(data[:], res)
if err != nil {
return nil, errors.New("error unmarshalling control message type")
}
if res.GetOpenChannel() != nil {
ricochetControlMessage := new(RicochetControlMessage)
ricochetControlMessage.Ack = false
if res.GetOpenChannel().GetChannelType() == "im.ricochet.auth.hidden-service" {
ricochetControlMessage.Type = "openauthchannel"
}
ricochetControlMessage.Type = "openchannel"
ricochetControlMessage.ChannelID = int32(res.GetOpenChannel().GetChannelIdentifier())
return ricochetControlMessage, nil
} else if res.GetChannelResult() != nil {
ricochetControlMessage := new(RicochetControlMessage)
ricochetControlMessage.Ack = true
ricochetControlMessage.ChannelID = int32(res.GetOpenChannel().GetChannelIdentifier())
serverCookie, err := proto.GetExtension(res.GetChannelResult(), Protocol_Data_AuthHiddenService.E_ServerCookie)
if err == nil {
ricochetControlMessage.Type = "openauthchannel"
copy(ricochetControlMessage.ServerCookie[:], serverCookie.([]byte))
} else {
ricochetControlMessage.Type = "openchannel"
}
return ricochetControlMessage, nil
}
return nil, errors.New("unknown control message type")
}
// DecodeChatMessage takes a byte representing a data packet and returns a
// constructed RicochetControlMessage
func (md *MessageDecoder) DecodeChatMessage(data []byte) (*RicochetChatMessage, error) {
res := new(Protocol_Data_Chat.Packet)
err := proto.Unmarshal(data[:], res)
if err != nil {
return nil, err
}
if res.GetChatMessage() != nil {
ricochetChatMessage := new(RicochetChatMessage)
ricochetChatMessage.Ack = false
ricochetChatMessage.MessageID = int32(res.GetChatMessage().GetMessageId())
ricochetChatMessage.Message = res.GetChatMessage().GetMessageText()
return ricochetChatMessage, nil
} else if res.GetChatAcknowledge != nil {
ricochetChatMessage := new(RicochetChatMessage)
ricochetChatMessage.Ack = true
ricochetChatMessage.MessageID = int32(res.GetChatAcknowledge().GetMessageId())
ricochetChatMessage.Accepted = res.GetChatAcknowledge().GetAccepted()
return ricochetChatMessage, nil
}
return nil, errors.New("chat message type not supported")
}

View File

@ -1,44 +1,21 @@
package goricochet
import (
"crypto"
"crypto/rsa"
"crypto/x509"
"encoding/asn1"
"encoding/binary"
"encoding/pem"
"errors"
"fmt"
"github.com/golang/protobuf/proto"
"github.com/s-rah/go-ricochet/auth"
"github.com/s-rah/go-ricochet/chat"
"github.com/s-rah/go-ricochet/control"
"io/ioutil"
"log"
"net"
"os"
)
// MessageType details the different kinds of messages used by Ricochet
type MessageType int
const (
// CONTROL messages are those sent on channel 0
CONTROL MessageType = iota
// AUTH messages are those that deal with authentication
AUTH = iota
// DATA covers both chat and (later) file handling and other non-control messages.
DATA = iota
)
// Ricochet is a protocol to conducting anonymous IM.
type Ricochet struct {
conn net.Conn
privateKey *rsa.PrivateKey
logger *log.Logger
channelState map[int]int
channel chan RicochetMessage
known bool
conn net.Conn
logger *log.Logger
}
// RicochetData is a structure containing the raw data and the channel it the
@ -48,58 +25,14 @@ type RicochetData struct {
Data []byte
}
// RicochetMessage is a Wrapper Around Common Ricochet Protocol Strucutres
type RicochetMessage struct {
Channel int32
ControlPacket *Protocol_Data_Control.Packet
DataPacket *Protocol_Data_Chat.Packet
AuthPacket *Protocol_Data_AuthHiddenService.Packet
}
func (r *Ricochet) IsKnownContact() bool {
return r.known
}
// Init sets up the Ricochet object. It takes in a filename of a hidden service
// private_key file so it can successfully authenticate itself with other
// clients.
func (r *Ricochet) Init(filename string, debugLog bool) {
// Init sets up the Ricochet object.
func (r *Ricochet) Init(debugLog bool) {
if debugLog {
r.logger = log.New(os.Stdout, "[Ricochet]: ", log.Ltime|log.Lmicroseconds)
} else {
r.logger = log.New(ioutil.Discard, "[Ricochet]: ", log.Ltime|log.Lmicroseconds)
}
pemData, err := ioutil.ReadFile(filename)
if err != nil {
r.logger.Print("Error Reading Private Key: ", err)
}
block, _ := pem.Decode(pemData)
if block == nil || block.Type != "RSA PRIVATE KEY" {
r.logger.Print("No valid PEM data found")
}
r.privateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
r.handleFatal(err, "Private key can't be decoded")
r.channelState = make(map[int]int)
r.channel = make(chan RicochetMessage)
}
func (r *Ricochet) StartService(server RicochetService, port string) {
// Listen
ln, _ := net.Listen("tcp", port)
conn, _ := ln.Accept()
go r.runService(conn, server)
}
func (r *Ricochet) runService(conn net.Conn, server RicochetService) {
// Negotiate Version
// Loop For Messages
}
// Connect sets up a ricochet connection between from and to which are
@ -108,7 +41,6 @@ func (r *Ricochet) runService(conn net.Conn, server RicochetService) {
// be open and authenticated.
// To specify a local port using the format "127.0.0.1:[port]|ricochet-id".
func (r *Ricochet) Connect(from string, to string) error {
var err error
networkResolver := new(NetworkResolver)
r.conn, to, err = networkResolver.Resolve(to)
@ -117,46 +49,27 @@ func (r *Ricochet) Connect(from string, to string) error {
return err
}
r.negotiateVersion()
authHandler := new(AuthenticationHandler)
clientCookie := authHandler.GenClientCookie()
return r.negotiateVersion()
}
// Authenticate opens an Authentication Channel and send a client cookie
func (r *Ricochet) Authenticate(channelID int32, clientCookie [16]byte) error {
messageBuilder := new(MessageBuilder)
data, err := messageBuilder.OpenAuthenticationChannel(1, clientCookie)
data, err := messageBuilder.OpenAuthenticationChannel(channelID, clientCookie)
if err != nil {
return errors.New("Cannot Marshal Open Channel Message")
}
r.logger.Printf("Sending Open Channel with Auth Request (channel:%d)", channelID)
r.sendPacket(data, 0)
return nil
}
response, _ := r.getMessages()
openChannelResponse, _ := r.decodePacket(response[0], CONTROL)
r.logger.Print("Received Response: ", openChannelResponse)
channelResult := openChannelResponse.ControlPacket.GetChannelResult()
if channelResult.GetOpened() == true {
r.logger.Print("Channel Opened Successfully: ", channelResult.GetChannelIdentifier())
}
sCookie, _ := proto.GetExtension(channelResult, Protocol_Data_AuthHiddenService.E_ServerCookie)
authHandler.AddServerCookie(sCookie.([]byte))
// DER Encode the Public Key
publickeybytes, err := asn1.Marshal(rsa.PublicKey{
N: r.privateKey.PublicKey.N,
E: r.privateKey.PublicKey.E,
})
signature, _ := rsa.SignPKCS1v15(nil, r.privateKey, crypto.SHA256, authHandler.GenChallenge(from, to))
signatureBytes := make([]byte, 128)
copy(signatureBytes[:], signature[:])
// SendProof sends an authentication proof in response to a challenge.
func (r *Ricochet) SendProof(channelID int32, publickeyBytes []byte, signatureBytes []byte) error {
// Construct a Proof Message
proof := &Protocol_Data_AuthHiddenService.Proof{
PublicKey: publickeybytes,
PublicKey: publickeyBytes,
Signature: signatureBytes,
}
@ -165,23 +78,14 @@ func (r *Ricochet) Connect(from string, to string) error {
Result: nil,
}
data, err = proto.Marshal(ahsPacket)
r.sendPacket(data, 1)
response, err = r.getMessages()
data, err := proto.Marshal(ahsPacket)
if err != nil {
return err
}
resultResponse, _ := r.decodePacket(response[0], AUTH)
r.logger.Print("Received Result: ", resultResponse)
if resultResponse.AuthPacket.GetResult().GetAccepted() != true {
return errors.New("authorization failed")
}
r.known = resultResponse.AuthPacket.GetResult().GetIsKnownContact()
r.logger.Printf("Sending Proof Auth Request (channel:%d)", channelID)
r.sendPacket(data, channelID)
return nil
}
@ -217,6 +121,32 @@ func (r *Ricochet) SendContactRequest(channel int32, nick string, message string
return nil
}
// AckOpenChannel acknowledges a previously received open channel message
// Prerequisites:
// * Must have Previously issued a successful Connect()
func (r *Ricochet) AckOpenChannel(channel int32, result bool) error {
messageBuilder := new(MessageBuilder)
data, err := messageBuilder.AckOpenChannel(channel, result)
if err != nil {
return errors.New("Failed to serialize open channel ack")
}
r.sendPacket(data, 0)
return nil
}
// AckChatMessage acknowledges a previously received chat message.
// Prerequisites:
// * Must have Previously issued a successful Connect()
func (r *Ricochet) AckChatMessage(channel int32, messageID int32) error {
messageBuilder := new(MessageBuilder)
data, err := messageBuilder.AckChatMessage(messageID)
if err != nil {
return errors.New("Failed to serialize chat message ack")
}
r.sendPacket(data, channel)
return nil
}
// SendMessage sends a Chat Message (message) to a give Channel (channel).
// Prerequisites:
// * Must have previously issued a successful Connect()
@ -267,141 +197,75 @@ func (r *Ricochet) sendPacket(data []byte, channel int32) {
header[2] = 0x00
header[3] = byte(channel)
copy(header[4:], data[:])
fmt.Fprintf(r.conn, "%s", header)
}
// Listen blocks and waits for a new message to arrive from the connected user
// once a message has arrived, it returns the message and the channel it occured
// on, else it returns an error.
// Prerequisites:
// * Must have previously issued a successful Connect()
// * Must have previously ran "go ricochet.ListenAndWait()"
func (r *Ricochet) Listen() (string, int32, error) {
var message RicochetMessage
message = <-r.channel
r.logger.Printf("Received Chat Message on Channel %d", message.Channel)
if message.DataPacket.GetChatMessage() == nil {
return "", 0, errors.New("Did not receive a chat message")
}
messageID := message.DataPacket.GetChatMessage().GetMessageId()
cr := &Protocol_Data_Chat.ChatAcknowledge{
MessageId: proto.Uint32(messageID),
Accepted: proto.Bool(true),
}
pc := &Protocol_Data_Chat.Packet{
ChatAcknowledge: cr,
}
data, err := proto.Marshal(pc)
if err != nil {
return "", 0, errors.New("Failed to serialize chat message")
}
r.sendPacket(data, message.Channel)
return message.DataPacket.GetChatMessage().GetMessageText(), message.Channel, nil
}
// ListenAndWait is intended to be a background thread listening for all messages
// a client will send, automaticall responding to some, and making the others available to
// Listen()
// Prerequisites:
// * Must have previously issued a successful Connect()
func (r *Ricochet) ListenAndWait() error {
func (r *Ricochet) ListenAndWait(serverHostname string, service RicochetService) error {
for true {
packets, err := r.getMessages()
if err != nil {
return errors.New("Error attempted to get new messages")
}
r.handleFatal(err, "Error attempted to get new messages")
messageDecoder := new(MessageDecoder)
for _, packet := range packets {
if len(packet.Data) == 0 {
r.logger.Printf("Closing Channel %d", packet.Channel)
service.OnChannelClose(packet.Channel, serverHostname)
break
}
if packet.Channel == 0 {
// This is a Control Channel Message
message, err := r.decodePacket(packet, CONTROL)
if err != nil {
r.logger.Printf("Failed to decode control packet, discarding")
break
}
message, err := messageDecoder.DecodeControlMessage(packet.Data)
// Automatically accept new channels
if message.ControlPacket.GetOpenChannel() != nil {
// TODO Reject if already in use.
cr := &Protocol_Data_Control.ChannelResult{
ChannelIdentifier: proto.Int32(message.ControlPacket.GetOpenChannel().GetChannelIdentifier()),
Opened: proto.Bool(true),
}
pc := &Protocol_Data_Control.Packet{
ChannelResult: cr,
}
data, err := proto.Marshal(pc)
// TODO we should set up some kind of error channel.
r.handleFatal(err, "error marshalling control protocol")
r.logger.Printf("Client Opening Channel: %d\n", message.ControlPacket.GetOpenChannel().GetChannelIdentifier())
r.sendPacket(data, 0)
r.channelState[int(message.ControlPacket.GetOpenChannel().GetChannelIdentifier())] = 1
break
}
if message.ControlPacket.GetChannelResult() != nil {
channelResult := message.ControlPacket.GetChannelResult()
if channelResult.GetOpened() == true {
r.logger.Print("Channel Opened Successfully: ", channelResult.GetChannelIdentifier())
r.channelState[int(message.ControlPacket.GetChannelResult().GetChannelIdentifier())] = 1
}
break
}
r.logger.Printf("Received Unknown Control Message\n")
} else if packet.Channel == 3 {
// Contact Request
r.logger.Printf("Received Unknown Message on Channel 3\n")
} else {
// At this point the only other expected type of message
// is a Chat Message
message, err := r.decodePacket(packet, DATA)
if err != nil {
r.logger.Printf("Failed to decode data packet, discarding")
break
}
r.channel <- message
if message.Type == "openchannel" && message.Ack == false {
r.logger.Printf("new open channel request %d %s", message.ChannelID, serverHostname)
service.OnOpenChannelRequest(message.ChannelID, serverHostname)
} else if message.Type == "openchannel" && message.Ack == true {
r.logger.Printf("new open channel request ack %d %s", message.ChannelID, serverHostname)
service.OnOpenChannelRequestAck(message.ChannelID, serverHostname, message.Accepted)
} else if message.Type == "openauthchannel" && message.Ack == true {
r.logger.Printf("new authentication challenge %d %s", message.ChannelID, serverHostname)
service.OnAuthenticationChallenge(message.ChannelID, serverHostname, message.ServerCookie)
} else {
r.logger.Printf("Received Unknown Control Message\n", message)
}
} else if packet.Channel == 1 {
result, _ := messageDecoder.DecodeAuthMessage(packet.Data)
r.logger.Printf("newreceived auth result %d", packet.Channel)
service.OnAuthenticationResult(1, serverHostname, result)
} else {
// At this point the only other expected type of message is a Chat Message
messageDecoder := new(MessageDecoder)
message, err := messageDecoder.DecodeChatMessage(packet.Data)
if err != nil {
r.logger.Printf("Failed to decode data packet, discarding on channel %d", packet.Channel)
break
}
if message.Ack == true {
service.OnChatMessageAck(packet.Channel, serverHostname, message.MessageID)
} else {
service.OnChatMessage(packet.Channel, serverHostname, message.MessageID, message.Message)
}
}
}
}
return nil
}
// decodePacket take a raw RicochetData message and decodes it based on a given MessageType
func (r *Ricochet) decodePacket(packet RicochetData, t MessageType) (rm RicochetMessage, err error) {
rm.Channel = packet.Channel
if t == CONTROL {
res := new(Protocol_Data_Control.Packet)
err = proto.Unmarshal(packet.Data[:], res)
rm.ControlPacket = res
} else if t == AUTH {
res := new(Protocol_Data_AuthHiddenService.Packet)
err = proto.Unmarshal(packet.Data[:], res)
rm.AuthPacket = res
} else if t == DATA {
res := new(Protocol_Data_Chat.Packet)
err = proto.Unmarshal(packet.Data[:], res)
rm.DataPacket = res
}
if err != nil {
return rm, errors.New("Error Unmarshalling Response")
}
return rm, err
}
// getMessages returns an array of new messages received from the ricochet client
func (r *Ricochet) getMessages() ([]RicochetData, error) {
buf, err := r.recv()
@ -432,6 +296,7 @@ func (r *Ricochet) getMessages() ([]RicochetData, error) {
finished = true
}
}
r.logger.Printf("Got %d Packets", len(datas))
return datas, nil
}

View File

@ -1,7 +1,16 @@
package goricochet
type RicochetService interface {
OnConnect(id string) error
OnContactRequest(id string) error
OnMessage(id string, message string, channel int) error
OnConnect(serverHostname string)
OnAuthenticationChallenge(channelID int32, serverHostname string, serverCookie [16]byte)
OnAuthenticationResult(channelID int32, serverHostname string, result bool)
OnOpenChannelRequest(channelID int32, serverHostname string)
OnOpenChannelRequestAck(channelID int32, serverHostname string, result bool)
OnChannelClose(channelID int32, serverHostname string)
OnContactRequest(channelID string, serverHostname string, nick string, message string)
OnChatMessage(channelID int32, serverHostname string, messageID int32, message string)
OnChatMessageAck(channelID int32, serverHostname string, messageID int32)
}

View File

@ -0,0 +1,89 @@
package goricochet
import (
"crypto"
"crypto/rsa"
"crypto/x509"
"encoding/asn1"
//"encoding/binary"
"encoding/pem"
"io/ioutil"
)
type StandardRicochetService struct {
ricochet *Ricochet
authHandler map[string]*AuthenticationHandler
privateKey *rsa.PrivateKey
hostname string
}
func (srs *StandardRicochetService) Init(filename string, hostname string) {
srs.ricochet = new(Ricochet)
srs.ricochet.Init(true)
srs.authHandler = make(map[string]*AuthenticationHandler)
srs.hostname = hostname
pemData, err := ioutil.ReadFile(filename)
if err != nil {
// r.logger.Print("Error Reading Private Key: ", err)
}
block, _ := pem.Decode(pemData)
if block == nil || block.Type != "RSA PRIVATE KEY" {
// r.logger.Print("No valid PEM data found")
}
srs.privateKey, _ = x509.ParsePKCS1PrivateKey(block.Bytes)
}
func (srs *StandardRicochetService) OnConnect(serverHostname string) {
srs.authHandler[serverHostname] = new(AuthenticationHandler)
clientCookie := srs.authHandler[serverHostname].GenClientCookie()
srs.ricochet.Authenticate(1, clientCookie)
}
// OnAuthenticationChallenge constructs a valid authentication challenge to the serverCookie
func (srs *StandardRicochetService) OnAuthenticationChallenge(channelID int32, serverHostname string, serverCookie [16]byte) {
srs.authHandler[serverHostname].AddServerCookie(serverCookie[:])
// DER Encode the Public Key
publickeyBytes, _ := asn1.Marshal(rsa.PublicKey{
N: srs.privateKey.PublicKey.N,
E: srs.privateKey.PublicKey.E,
})
signature, _ := rsa.SignPKCS1v15(nil, srs.privateKey, crypto.SHA256, srs.authHandler[serverHostname].GenChallenge(srs.hostname, serverHostname))
// TODO Handle Errors
signatureBytes := make([]byte, 128)
copy(signatureBytes[:], signature[:])
srs.ricochet.SendProof(1, publickeyBytes, signatureBytes)
}
func (srs *StandardRicochetService) Ricochet() *Ricochet {
return srs.ricochet
}
func (srs *StandardRicochetService) OnAuthenticationResult(channelID int32, serverHostname string, result bool) {
}
func (srs *StandardRicochetService) OnOpenChannelRequest(channelID int32, serverHostname string) {
srs.ricochet.AckOpenChannel(channelID, true)
}
func (srs *StandardRicochetService) OnOpenChannelRequestAck(channelID int32, serverHostname string, result bool) {
}
func (srs *StandardRicochetService) OnChannelClose(channelID int32, serverHostname string) {
}
func (srs *StandardRicochetService) OnContactRequest(channelID string, serverHostname string, nick string, message string) {
}
func (srs *StandardRicochetService) OnChatMessage(channelID int32, serverHostname string, messageId int32, message string) {
srs.ricochet.AckChatMessage(channelID, messageId)
}
func (srs *StandardRicochetService) OnChatMessageAck(channelID int32, serverHostname string, messageId int32) {
}