diff --git a/core/contact.go b/core/contact.go index 4abd19c..5e1cc3f 100644 --- a/core/contact.go +++ b/core/contact.go @@ -133,6 +133,12 @@ func (c *Contact) Data() *ricochet.Contact { return data } +func (c *Contact) IsRequest() bool { + c.mutex.Lock() + defer c.mutex.Unlock() + return c.data.Request.Pending +} + func (c *Contact) Conversation() *Conversation { c.mutex.Lock() defer c.mutex.Unlock() @@ -312,6 +318,7 @@ func (c *Contact) connectOutbound(ctx context.Context, connChannel chan *protoco // is fragile and dumb. // XXX BUG: This means no backoff for authentication failure handler := &ProtocolConnection{ + Core: c.core, Conn: oc, Contact: c, MyHostname: c.core.Identity.Address()[9:], diff --git a/core/contactlist.go b/core/contactlist.go index 1ab0012..ef379af 100644 --- a/core/contactlist.go +++ b/core/contactlist.go @@ -16,7 +16,8 @@ type ContactList struct { mutex sync.RWMutex events *utils.Publisher - contacts map[int]*Contact + contacts map[int]*Contact + inboundRequests map[string]*InboundContactRequest } func LoadContactList(core *Ricochet) (*ContactList, error) { @@ -80,30 +81,43 @@ func (this *ContactList) ContactByAddress(address string) *Contact { return nil } -func (this *ContactList) AddContactRequest(address, name, fromName, text string) (*Contact, error) { - if !IsAddressValid(address) { - return nil, errors.New("Invalid ricochet address") - } - if len(fromName) > 0 && !IsNicknameAcceptable(fromName) { - return nil, errors.New("Invalid nickname") - } - if len(text) > 0 && !IsMessageAcceptable(text) { - return nil, errors.New("Invalid message") +func (cl *ContactList) InboundRequestByAddress(address string) *InboundContactRequest { + cl.mutex.RLock() + defer cl.mutex.RUnlock() + for _, request := range cl.inboundRequests { + if request.Address == address { + return request + } } + return nil +} +// AddNewContact adds a new contact to the persistent contact list, broadcasts a +// contact add RPC event, and returns a newly constructed Contact. AddNewContact +// does not create contact requests or trigger any other protocol behavior. +// +// Generally, you will use AddContactRequest (for outbound requests) and +// AddOrUpdateInboundContactRequest plus InboundContactRequest.Accept() instead of +// using this function directly. +func (this *ContactList) AddNewContact(configContact ConfigContact) (*Contact, error) { this.mutex.Lock() defer this.mutex.Unlock() + address, ok := AddressFromOnion(configContact.Hostname) + if !ok { + return nil, errors.New("Invalid ricochet address") + } + for _, contact := range this.contacts { if contact.Address() == address { return nil, errors.New("Contact already exists with this address") } - if contact.Nickname() == name { + if contact.Nickname() == configContact.Nickname { return nil, errors.New("Contact already exists with this nickname") } } - // XXX check inbound requests + // XXX check inbound requests (but this can be called for an inbound req too) // Write new contact into config config := this.core.Config.OpenWrite() @@ -118,18 +132,6 @@ func (this *ContactList) AddContactRequest(address, name, fromName, text string) } contactId := maxContactId + 1 - onion, _ := OnionFromAddress(address) - configContact := ConfigContact{ - Hostname: onion, - Nickname: name, - WhenCreated: time.Now().Format(time.RFC3339), - Request: ConfigContactRequest{ - Pending: true, - MyNickname: fromName, - Message: text, - }, - } - config.Contacts[strconv.Itoa(contactId)] = configContact if err := config.Save(); err != nil { return nil, err @@ -150,10 +152,54 @@ func (this *ContactList) AddContactRequest(address, name, fromName, text string) } this.events.Publish(event) + // XXX Should this be here? Is it ok for inbound where we might pass conn over momentarily? contact.StartConnection() return contact, nil } +// AddContactRequest creates a new outbound contact request with the given parameters, +// adds it to the contact list, and returns the newly constructed Contact. +// +// If an inbound request already exists for this address, that request will be automatically +// accepted, and the returned contact will already be fully established. +func (cl *ContactList) AddContactRequest(address, name, fromName, text string) (*Contact, error) { + onion, valid := OnionFromAddress(address) + if !valid { + return nil, errors.New("Invalid ricochet address") + } + if !IsNicknameAcceptable(name) { + return nil, errors.New("Invalid nickname") + } + if len(fromName) > 0 && !IsNicknameAcceptable(fromName) { + return nil, errors.New("Invalid 'from' nickname") + } + if len(text) > 0 && !IsMessageAcceptable(text) { + return nil, errors.New("Invalid message") + } + + configContact := ConfigContact{ + Hostname: onion, + Nickname: name, + WhenCreated: time.Now().Format(time.RFC3339), + Request: ConfigContactRequest{ + Pending: true, + MyNickname: fromName, + Message: text, + }, + } + contact, err := cl.AddNewContact(configContact) + if err != nil { + return nil, err + } + + if inboundRequest := cl.InboundRequestByAddress(address); inboundRequest != nil { + contact.UpdateContactRequest("Accepted") + inboundRequest.AcceptWithContact(contact) + } + + return contact, nil +} + func (this *ContactList) RemoveContact(contact *Contact) error { this.mutex.Lock() defer this.mutex.Unlock() @@ -186,6 +232,99 @@ func (this *ContactList) RemoveContact(contact *Contact) error { return nil } +// AddOrUpdateInboundContactRequest creates or modifies an inbound contact request for +// the hostname, with an optional nickname suggestion and introduction message +// +// The nickname, message, and address must be validated before calling this function. +// +// This function may change the state of the request, and the caller is responsible for +// sending a reply message and closing the channel or connection as appropriate. If the +// request is still active when the state changes spontaneously later (e.g. it's accepted +// by the user), replies will be sent by InboundContactRequest. +// +// This function may return either an InboundContactRequest (which may be pending or already +// rejected), an existing contact (which should be treated as accepting the request), or +// neither, which is considered a rejection. +func (cl *ContactList) AddOrUpdateInboundContactRequest(address, nickname, message string) (*InboundContactRequest, *Contact) { + cl.mutex.Lock() + defer cl.mutex.Unlock() + + // Look up existing request + if request := cl.inboundRequests[address]; request != nil { + // Errors in Update will change the state of the request, which the caller sends as a reply + request.Update(nickname, message) + return request, nil + } + + // Check for existing contacts or outbound contact requests + for _, contact := range cl.contacts { + if contact.Address() == address { + if contact.IsRequest() { + contact.UpdateContactRequest("Accepted") + } + return nil, contact + } + } + + // Create new request + request := CreateInboundContactRequest(cl.core, address, nickname, message) + request.StatusChanged = cl.inboundRequestChanged + cl.inboundRequests[address] = request + // XXX update config + requestData := request.Data() + event := ricochet.ContactEvent{ + Type: ricochet.ContactEvent_ADD, + Subject: &ricochet.ContactEvent_Request{ + Request: &requestData, + }, + } + cl.events.Publish(event) + return request, nil +} + +// RemoveInboundContactRequest removes the record of an inbound contact request, +// without taking any other actions. Generally you will want to act on a request with +// Accept() or Reject(), rather than call this function directly, but it is valid to +// call on any inbound request regardless. +func (cl *ContactList) RemoveInboundContactRequest(request *InboundContactRequest) error { + cl.mutex.Lock() + defer cl.mutex.Unlock() + requestData := request.Data() + + if cl.inboundRequests[requestData.Address] != request { + return errors.New("Request is not in contact list") + } + + request.CloseConnection() + // XXX Remove from config + + delete(cl.inboundRequests, requestData.Address) + + event := ricochet.ContactEvent{ + Type: ricochet.ContactEvent_DELETE, + Subject: &ricochet.ContactEvent_Request{ + Request: &requestData, + }, + } + cl.events.Publish(event) + return nil +} + +// inboundRequestChanged is called by the StatusChanged callback of InboundContactRequest +// after the request has been modified, such as through Update() or Reject(). Accepted +// requests do not pass through this function, because they're immediately removed. +func (cl *ContactList) inboundRequestChanged(request *InboundContactRequest) { + // XXX update config + requestData := request.Data() + event := ricochet.ContactEvent{ + Type: ricochet.ContactEvent_UPDATE, + Subject: &ricochet.ContactEvent_Request{ + Request: &requestData, + }, + } + cl.events.Publish(event) +} + func (this *ContactList) StartConnections() { for _, contact := range this.Contacts() { contact.StartConnection() diff --git a/core/identity.go b/core/identity.go index 200e319..249b390 100644 --- a/core/identity.go +++ b/core/identity.go @@ -114,6 +114,7 @@ func (is *identityService) OnNewConnection(oc *protocol.OpenConnection) { // XXX Should have pre-auth handling, timeouts identity := is.Identity handler := &ProtocolConnection{ + Core: identity.core, Conn: oc, GetContactByHostname: func(hostname string) *Contact { address, ok := AddressFromPlainHost(hostname) diff --git a/core/inboundcontactrequest.go b/core/inboundcontactrequest.go new file mode 100644 index 0000000..14f522f --- /dev/null +++ b/core/inboundcontactrequest.go @@ -0,0 +1,167 @@ +package core + +import ( + "errors" + "github.com/ricochet-im/ricochet-go/rpc" + protocol "github.com/s-rah/go-ricochet" + "log" + "sync" + "time" +) + +type InboundContactRequest struct { + core *Ricochet + mutex sync.Mutex + data ricochet.ContactRequest + conn *protocol.OpenConnection + channelID int32 + Address string + + // Called when the request state is changed + StatusChanged func(request *InboundContactRequest) +} + +// CreateInboundContactRequest constructs a new InboundContactRequest, usually from a newly +// received request on an open connection. Requests are managed through the ContactList, so +// generally you should use ContactList.AddOrUpdateInboundContactRequest instead of calling +// this function directly. +func CreateInboundContactRequest(core *Ricochet, address, nickname, message string) *InboundContactRequest { + cr := &InboundContactRequest{ + core: core, + data: ricochet.ContactRequest{ + Direction: ricochet.ContactRequest_INBOUND, + Address: address, + Text: message, + FromNickname: nickname, + WhenCreated: time.Now().Format(time.RFC3339), + }, + Address: address, + } + + return cr +} + +// XXX There should be stricter management & a timeout for this connection +func (cr *InboundContactRequest) SetConnection(conn *protocol.OpenConnection, channelID int32) { + cr.mutex.Lock() + defer cr.mutex.Unlock() + + if cr.conn != nil && cr.conn != conn { + log.Printf("Replacing connection on an inbound contact request") + cr.conn.Close() + } + cr.conn = conn + cr.channelID = channelID +} + +func (cr *InboundContactRequest) CloseConnection() { + if cr.conn != nil { + cr.conn.Close() + cr.conn = nil + } +} + +func (cr *InboundContactRequest) Update(nickname, message string) { + cr.mutex.Lock() + defer cr.mutex.Unlock() + + if cr.data.Rejected { + return + } + + // These should already be validated, but just in case.. + if len(nickname) == 0 || IsNicknameAcceptable(nickname) { + cr.data.FromNickname = nickname + } + if len(message) == 0 || IsMessageAcceptable(message) { + cr.data.Text = message + } + + if cr.StatusChanged != nil { + cr.StatusChanged(cr) + } +} + +func (cr *InboundContactRequest) Accept() (*Contact, error) { + cr.mutex.Lock() + defer cr.mutex.Unlock() + + if cr.data.Rejected { + log.Printf("Accept called on an inbound contact request that was already rejected; request is %v", cr) + return nil, errors.New("Contact request has already been rejected") + } + + log.Printf("Accepting contact request from %s", cr.data.Address) + + onion, _ := OnionFromAddress(cr.data.Address) + configContact := ConfigContact{ + Hostname: onion, + Nickname: cr.data.FromNickname, + WhenCreated: cr.data.WhenCreated, + } + contact, err := cr.core.Identity.ContactList().AddNewContact(configContact) + if err != nil { + log.Printf("Error occurred in accepting contact request: %s", err) + return nil, err + } + + if err := cr.AcceptWithContact(contact); err != nil { + return contact, err + } + return contact, nil +} + +func (cr *InboundContactRequest) AcceptWithContact(contact *Contact) error { + if contact.Address() != cr.data.Address { + return errors.New("Contact address does not match request in accept") + } + + cr.core.Identity.ContactList().RemoveInboundContactRequest(cr) + + // Pass the open connection to the new contact + if cr.conn != nil && !cr.conn.Closed { + cr.conn.AckContactRequest(cr.channelID, "Accepted") + cr.conn.CloseChannel(cr.channelID) + contact.OnConnectionAuthenticated(cr.conn, true) + cr.conn = nil + } + + return nil +} + +func (cr *InboundContactRequest) Reject() { + cr.mutex.Lock() + defer cr.mutex.Unlock() + + if cr.data.Rejected { + return + } + + log.Printf("Rejecting contact request from %s", cr.data.Address) + cr.data.Rejected = true + + // Signal update to the callback (probably from ContactList) + if cr.StatusChanged != nil { + cr.StatusChanged(cr) + } + + if cr.conn != nil && !cr.conn.Closed { + cr.conn.AckContactRequest(cr.channelID, "Rejected") + cr.conn.CloseChannel(cr.channelID) + cr.conn.Close() + cr.conn = nil + + // The request can be removed once a protocol response is sent + cr.core.Identity.ContactList().RemoveInboundContactRequest(cr) + } +} + +func (cr *InboundContactRequest) Data() ricochet.ContactRequest { + return cr.data +} + +func (cr *InboundContactRequest) IsRejected() bool { + cr.mutex.Lock() + defer cr.mutex.Unlock() + return cr.data.Rejected +} diff --git a/core/protocol.go b/core/protocol.go index c1d90df..ca8bab7 100644 --- a/core/protocol.go +++ b/core/protocol.go @@ -9,6 +9,8 @@ import ( ) type ProtocolConnection struct { + Core *Ricochet + Conn *protocol.OpenConnection Contact *Contact @@ -95,6 +97,46 @@ func (pc *ProtocolConnection) OnAuthenticationResult(channelID int32, result boo // Contact Management func (pc *ProtocolConnection) OnContactRequest(channelID int32, nick string, message string) { + if pc.Conn.Client || !pc.Conn.IsAuthed || pc.Contact != nil { + pc.Conn.CloseChannel(channelID) + return + } + + address, ok := AddressFromPlainHost(pc.Conn.OtherHostname) + if !ok { + pc.Conn.CloseChannel(channelID) + return + } + if len(nick) > 0 && !IsNicknameAcceptable(nick) { + log.Printf("protocol: Stripping unacceptable nickname from inbound request; encoded: %x", []byte(nick)) + nick = "" + } + if len(message) > 0 && !IsMessageAcceptable(message) { + log.Printf("protocol: Stripping unacceptable message from inbound request; len: %d, encoded: %x", len(message), []byte(message)) + message = "" + } + + contactList := pc.Core.Identity.ContactList() + request, contact := contactList.AddOrUpdateInboundContactRequest(address, nick, message) + + if contact != nil { + // Accepted immediately + pc.Conn.AckContactRequestOnResponse(channelID, "Accepted") + pc.Conn.CloseChannel(channelID) + contact.OnConnectionAuthenticated(pc.Conn, true) + } else if request != nil && !request.IsRejected() { + // Pending + pc.Conn.AckContactRequestOnResponse(channelID, "Pending") + request.SetConnection(pc.Conn, channelID) + } else { + // Rejected + pc.Conn.AckContactRequestOnResponse(channelID, "Rejected") + pc.Conn.CloseChannel(channelID) + pc.Conn.Close() + if request != nil { + contactList.RemoveInboundContactRequest(request) + } + } } func (pc *ProtocolConnection) OnContactRequestAck(channelID int32, status string) { diff --git a/core/rpcserver.go b/core/rpcserver.go index 0af3a86..f7a8f58 100644 --- a/core/rpcserver.go +++ b/core/rpcserver.go @@ -151,11 +151,32 @@ func (s *RpcServer) DeleteContact(ctx context.Context, req *ricochet.DeleteConta } func (s *RpcServer) AcceptInboundRequest(ctx context.Context, req *ricochet.ContactRequest) (*ricochet.Contact, error) { - return nil, NotImplementedError + if req.Direction != ricochet.ContactRequest_INBOUND { + return nil, errors.New("Request must be inbound") + } + contactList := s.Core.Identity.ContactList() + request := contactList.InboundRequestByAddress(req.Address) + if request == nil { + return nil, errors.New("Request does not exist") + } + contact, err := request.Accept() + if err != nil { + return nil, err + } + return contact.Data(), nil } func (s *RpcServer) RejectInboundRequest(ctx context.Context, req *ricochet.ContactRequest) (*ricochet.RejectInboundRequestReply, error) { - return nil, NotImplementedError + if req.Direction != ricochet.ContactRequest_INBOUND { + return nil, errors.New("Request must be inbound") + } + contactList := s.Core.Identity.ContactList() + request := contactList.InboundRequestByAddress(req.Address) + if request == nil { + return nil, errors.New("Request does not exist") + } + request.Reject() + return &ricochet.RejectInboundRequestReply{}, nil } func (s *RpcServer) MonitorConversations(req *ricochet.MonitorConversationsRequest, stream ricochet.RicochetCore_MonitorConversationsServer) error { diff --git a/rpc/contact.pb.go b/rpc/contact.pb.go index aa51a84..76efe4b 100644 --- a/rpc/contact.pb.go +++ b/rpc/contact.pb.go @@ -207,6 +207,8 @@ type ContactRequest struct { Nickname string `protobuf:"bytes,3,opt,name=nickname" json:"nickname,omitempty"` Text string `protobuf:"bytes,4,opt,name=text" json:"text,omitempty"` FromNickname string `protobuf:"bytes,5,opt,name=fromNickname" json:"fromNickname,omitempty"` + WhenCreated string `protobuf:"bytes,6,opt,name=whenCreated" json:"whenCreated,omitempty"` + Rejected bool `protobuf:"varint,7,opt,name=rejected" json:"rejected,omitempty"` } func (m *ContactRequest) Reset() { *m = ContactRequest{} } @@ -249,6 +251,20 @@ func (m *ContactRequest) GetFromNickname() string { return "" } +func (m *ContactRequest) GetWhenCreated() string { + if m != nil { + return m.WhenCreated + } + return "" +} + +func (m *ContactRequest) GetRejected() bool { + if m != nil { + return m.Rejected + } + return false +} + type MonitorContactsRequest struct { } @@ -451,38 +467,39 @@ func init() { func init() { proto.RegisterFile("contact.proto", fileDescriptor0) } var fileDescriptor0 = []byte{ - // 520 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x9c, 0x53, 0x51, 0x6f, 0xd3, 0x30, - 0x10, 0x6e, 0xd2, 0xac, 0x69, 0xaf, 0x5d, 0xc9, 0xac, 0x09, 0x05, 0xf6, 0x52, 0x59, 0x08, 0xf5, - 0x85, 0x80, 0x0a, 0xef, 0xac, 0x6b, 0x32, 0x51, 0x08, 0x49, 0xf1, 0x12, 0xf1, 0x9c, 0x26, 0x46, - 0x0b, 0x74, 0x76, 0x49, 0x5c, 0xa0, 0xff, 0x90, 0x9f, 0xc2, 0x23, 0x3f, 0x01, 0xd9, 0x49, 0x3a, - 0xba, 0x01, 0x42, 0x7b, 0xf3, 0x7d, 0xdf, 0x77, 0xf6, 0x7d, 0xbe, 0x3b, 0x38, 0x4c, 0x39, 0x13, - 0x49, 0x2a, 0x9c, 0x75, 0xc1, 0x05, 0x47, 0xdd, 0x22, 0x4f, 0x79, 0x7a, 0x49, 0x05, 0xfe, 0xae, - 0x83, 0x39, 0xab, 0x38, 0x34, 0x04, 0x3d, 0xcf, 0x6c, 0x6d, 0xa4, 0x8d, 0x0f, 0x88, 0x9e, 0x67, - 0xc8, 0x06, 0x33, 0xc9, 0xb2, 0x82, 0x96, 0xa5, 0xad, 0x8f, 0xb4, 0x71, 0x8f, 0x34, 0x21, 0x7a, - 0x08, 0x5d, 0x96, 0xa7, 0x9f, 0x58, 0x72, 0x45, 0xed, 0xb6, 0xa2, 0x76, 0x31, 0x1a, 0x41, 0xff, - 0xeb, 0x25, 0x65, 0xb3, 0x82, 0x26, 0x82, 0x66, 0xb6, 0xa1, 0xe8, 0xdf, 0x21, 0xf4, 0x08, 0x0e, - 0x57, 0x49, 0x29, 0x66, 0x9c, 0x31, 0x9a, 0x4a, 0xcd, 0x81, 0xd2, 0xec, 0x83, 0x68, 0x02, 0x66, - 0x41, 0x3f, 0x6f, 0x68, 0x29, 0xec, 0xce, 0x48, 0x1b, 0xf7, 0x27, 0xb6, 0xd3, 0x54, 0xed, 0xd4, - 0x15, 0x93, 0x8a, 0x27, 0x8d, 0x10, 0x3d, 0x83, 0x4e, 0x29, 0x12, 0xb1, 0x29, 0x6d, 0x18, 0x69, - 0xe3, 0xe1, 0x1f, 0x52, 0x9c, 0x0b, 0xc5, 0x93, 0x5a, 0x87, 0xe7, 0xd0, 0xa9, 0x10, 0xd4, 0x07, - 0x33, 0x0e, 0xde, 0x04, 0xe1, 0xfb, 0xc0, 0x6a, 0xc9, 0x20, 0x3c, 0x3f, 0xf7, 0xe7, 0x81, 0x67, - 0x69, 0x08, 0xa0, 0x13, 0x06, 0xea, 0xac, 0x4b, 0x82, 0x78, 0xef, 0x62, 0xef, 0x22, 0xb2, 0xda, - 0x68, 0x00, 0x5d, 0xe2, 0xbd, 0xf6, 0x66, 0x91, 0xe7, 0x5a, 0x06, 0xfe, 0xa1, 0xc1, 0x70, 0xbf, - 0x30, 0x74, 0x0a, 0xbd, 0x2c, 0x2f, 0x68, 0x2a, 0x72, 0xce, 0xd4, 0xc7, 0x0e, 0x27, 0xf8, 0x6f, - 0x2e, 0x1c, 0xb7, 0x51, 0x92, 0xeb, 0xa4, 0x3b, 0xf6, 0x00, 0x81, 0x21, 0xe8, 0x37, 0x51, 0x7f, - 0xbe, 0x3a, 0x23, 0x0c, 0x83, 0x0f, 0x05, 0xbf, 0x0a, 0x9a, 0x9c, 0xea, 0xd3, 0xf7, 0x30, 0xfc, - 0x18, 0x7a, 0xbb, 0x2a, 0xa4, 0xd5, 0x79, 0x70, 0x16, 0xc6, 0x81, 0x6b, 0xb5, 0xa4, 0xd5, 0x30, - 0x8e, 0xaa, 0x48, 0xc3, 0x36, 0xdc, 0x7f, 0xcb, 0x59, 0x2e, 0x78, 0x51, 0x7b, 0x28, 0x6b, 0x13, - 0xf8, 0xa7, 0x06, 0x83, 0x1a, 0xf3, 0xbe, 0x50, 0x26, 0xd0, 0x53, 0x30, 0xc4, 0x76, 0x4d, 0x6b, - 0xf7, 0x27, 0xb7, 0xdc, 0x2b, 0x95, 0x13, 0x6d, 0xd7, 0x94, 0x28, 0x21, 0x7a, 0x02, 0x66, 0x3d, - 0xac, 0xca, 0x71, 0x7f, 0x72, 0x74, 0x2b, 0xe7, 0x55, 0x8b, 0x34, 0x1a, 0xf4, 0xe2, 0x7a, 0x4c, - 0xda, 0xff, 0x1e, 0x13, 0x99, 0x55, 0x4b, 0xf1, 0x4b, 0x30, 0xe4, 0x93, 0xa8, 0x0b, 0x46, 0x10, - 0xfb, 0x7e, 0x65, 0x70, 0x11, 0x2e, 0x62, 0x7f, 0x1a, 0xc9, 0x96, 0x9b, 0xd0, 0x9e, 0xba, 0xae, - 0xa5, 0xcb, 0xde, 0xc7, 0x0b, 0x57, 0x82, 0x6d, 0x79, 0x76, 0x3d, 0xdf, 0x8b, 0x3c, 0xcb, 0x38, - 0xeb, 0x81, 0x59, 0x6e, 0x96, 0x1f, 0x69, 0x2a, 0xf0, 0x11, 0xdc, 0x9b, 0x66, 0xd9, 0xee, 0xad, - 0xf5, 0x6a, 0x8b, 0x4f, 0xe1, 0xd8, 0xa5, 0x2b, 0x2a, 0xe8, 0x8d, 0x79, 0xf8, 0xef, 0x0d, 0xc3, - 0xc7, 0x80, 0x6e, 0xdc, 0x20, 0xef, 0x3d, 0x81, 0x07, 0x84, 0xca, 0x47, 0xe7, 0x6c, 0xc9, 0x37, - 0x2c, 0x6b, 0x16, 0x40, 0x92, 0xcb, 0x8e, 0xda, 0xed, 0xe7, 0xbf, 0x02, 0x00, 0x00, 0xff, 0xff, - 0x94, 0xcf, 0x1c, 0xce, 0xec, 0x03, 0x00, 0x00, + // 539 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x9c, 0x54, 0xd1, 0x72, 0x93, 0x40, + 0x14, 0x2d, 0x84, 0x02, 0xb9, 0x69, 0x23, 0xdd, 0xe9, 0x38, 0xd8, 0xbe, 0x30, 0x3b, 0x8e, 0x93, + 0x17, 0xd1, 0x89, 0xbe, 0xdb, 0x34, 0xd0, 0x31, 0x1a, 0x21, 0x6e, 0x61, 0x7c, 0x26, 0xb0, 0x4e, + 0xd1, 0x74, 0x89, 0xb0, 0x51, 0xf3, 0x43, 0x7e, 0x8b, 0x9f, 0xe3, 0x27, 0x38, 0xbb, 0x40, 0x6a, + 0x12, 0x75, 0x1c, 0xdf, 0xf6, 0x9e, 0x73, 0x2e, 0xbb, 0x7b, 0xee, 0x59, 0xe0, 0x38, 0x2d, 0x18, + 0x4f, 0x52, 0xee, 0x2e, 0xcb, 0x82, 0x17, 0xc8, 0x2c, 0xf3, 0xb4, 0x48, 0x6f, 0x28, 0xc7, 0xdf, + 0x55, 0x30, 0xc6, 0x35, 0x87, 0xfa, 0xa0, 0xe6, 0x99, 0xad, 0x38, 0xca, 0xe0, 0x90, 0xa8, 0x79, + 0x86, 0x6c, 0x30, 0x92, 0x2c, 0x2b, 0x69, 0x55, 0xd9, 0xaa, 0xa3, 0x0c, 0xba, 0xa4, 0x2d, 0xd1, + 0x19, 0x98, 0x2c, 0x4f, 0x3f, 0xb2, 0xe4, 0x96, 0xda, 0x1d, 0x49, 0x6d, 0x6a, 0xe4, 0x40, 0xef, + 0xcb, 0x0d, 0x65, 0xe3, 0x92, 0x26, 0x9c, 0x66, 0xb6, 0x26, 0xe9, 0x5f, 0x21, 0xf4, 0x10, 0x8e, + 0x17, 0x49, 0xc5, 0xc7, 0x05, 0x63, 0x34, 0x15, 0x9a, 0x43, 0xa9, 0xd9, 0x06, 0xd1, 0x10, 0x8c, + 0x92, 0x7e, 0x5a, 0xd1, 0x8a, 0xdb, 0xba, 0xa3, 0x0c, 0x7a, 0x43, 0xdb, 0x6d, 0x4f, 0xed, 0x36, + 0x27, 0x26, 0x35, 0x4f, 0x5a, 0x21, 0x7a, 0x0a, 0x7a, 0xc5, 0x13, 0xbe, 0xaa, 0x6c, 0x70, 0x94, + 0x41, 0xff, 0x37, 0x2d, 0xee, 0xb5, 0xe4, 0x49, 0xa3, 0xc3, 0x13, 0xd0, 0x6b, 0x04, 0xf5, 0xc0, + 0x88, 0x83, 0xd7, 0x41, 0xf8, 0x2e, 0xb0, 0x0e, 0x44, 0x11, 0x5e, 0x5d, 0x4d, 0x27, 0x81, 0x6f, + 0x29, 0x08, 0x40, 0x0f, 0x03, 0xb9, 0x56, 0x05, 0x41, 0xfc, 0xb7, 0xb1, 0x7f, 0x1d, 0x59, 0x1d, + 0x74, 0x04, 0x26, 0xf1, 0x5f, 0xf9, 0xe3, 0xc8, 0xf7, 0x2c, 0x0d, 0x7f, 0x53, 0xa1, 0xbf, 0x7d, + 0x30, 0x74, 0x01, 0xdd, 0x2c, 0x2f, 0x69, 0xca, 0xf3, 0x82, 0x49, 0x63, 0xfb, 0x43, 0xfc, 0xa7, + 0x5b, 0xb8, 0x5e, 0xab, 0x24, 0x77, 0x4d, 0xff, 0x39, 0x03, 0x04, 0x1a, 0xa7, 0x5f, 0x79, 0x63, + 0xbe, 0x5c, 0x23, 0x0c, 0x47, 0xef, 0xcb, 0xe2, 0x36, 0x68, 0x7b, 0x6a, 0xd3, 0xb7, 0xb0, 0xdd, + 0xd9, 0xe9, 0xfb, 0xb3, 0x3b, 0x03, 0xb3, 0xa4, 0x1f, 0xea, 0xb1, 0x19, 0x8e, 0x32, 0x30, 0xc9, + 0xa6, 0xc6, 0x8f, 0xa0, 0xbb, 0xb9, 0x83, 0x30, 0x6a, 0x12, 0x5c, 0x86, 0x71, 0xe0, 0x59, 0x07, + 0xc2, 0xa8, 0x30, 0x8e, 0xea, 0x4a, 0xc1, 0x36, 0xdc, 0x7f, 0x53, 0xb0, 0x9c, 0x17, 0x65, 0xe3, + 0x40, 0xd5, 0x58, 0x80, 0x7f, 0x28, 0x70, 0xd4, 0x60, 0xfe, 0x67, 0xca, 0x38, 0x7a, 0x02, 0x1a, + 0x5f, 0x2f, 0x69, 0xe3, 0xdd, 0xf9, 0x9e, 0x77, 0x52, 0xe5, 0x46, 0xeb, 0x25, 0x25, 0x52, 0x88, + 0x1e, 0x83, 0xd1, 0x44, 0x5d, 0xfa, 0xd5, 0x1b, 0x9e, 0xec, 0xf5, 0xbc, 0x3c, 0x20, 0xad, 0x06, + 0x3d, 0xbf, 0x0b, 0x59, 0xe7, 0xef, 0x21, 0x13, 0x5d, 0x8d, 0x14, 0xbf, 0x00, 0x4d, 0x6c, 0x89, + 0x4c, 0xd0, 0x82, 0x78, 0x3a, 0xad, 0x2f, 0x38, 0x0b, 0x67, 0xf1, 0x74, 0x14, 0x89, 0xc0, 0x18, + 0xd0, 0x19, 0x79, 0x9e, 0xa5, 0x8a, 0xe4, 0xc4, 0x33, 0x4f, 0x80, 0x1d, 0xb1, 0xf6, 0xfc, 0xa9, + 0x1f, 0xf9, 0x96, 0x76, 0xd9, 0x05, 0xa3, 0x5a, 0xcd, 0x85, 0x6d, 0xf8, 0x04, 0xee, 0x8d, 0xb2, + 0x6c, 0xb3, 0xd7, 0x72, 0xb1, 0xc6, 0x17, 0x70, 0xea, 0xd1, 0x05, 0xe5, 0x74, 0x27, 0x4d, 0xff, + 0xfc, 0x3e, 0xf1, 0x29, 0xa0, 0x9d, 0x2f, 0x88, 0xef, 0x9e, 0xc3, 0x03, 0x22, 0x67, 0x35, 0x61, + 0xf3, 0x62, 0xc5, 0xb2, 0xf6, 0xf9, 0x08, 0x72, 0xae, 0xcb, 0x3f, 0xc3, 0xb3, 0x9f, 0x01, 0x00, + 0x00, 0xff, 0xff, 0xe5, 0x62, 0x19, 0x3f, 0x2a, 0x04, 0x00, 0x00, } diff --git a/rpc/contact.proto b/rpc/contact.proto index 07fc6a7..950f545 100644 --- a/rpc/contact.proto +++ b/rpc/contact.proto @@ -32,6 +32,8 @@ message ContactRequest { string nickname = 3; string text = 4; string fromNickname = 5; + string whenCreated = 6; + bool rejected = 7; } message MonitorContactsRequest {