Rename Contact -> chats and remove histories (#116)

This commit renames contacts -> chats and remove histories.

I have also collapsed the migrations for now so it's not backward
compatible, but while in heavy development is probably best just to keep
a single migration.
This commit is contained in:
Andrea Maria Piana 2019-07-10 20:58:28 +02:00 committed by GitHub
parent af2f7d96f0
commit 695bdfb61c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 921 additions and 1134 deletions

20
chat.go
View File

@ -21,7 +21,7 @@ import (
type ChatViewController struct {
*ViewController
contact client.Contact
contact client.Chat
identity *ecdsa.PrivateKey
messenger *client.Messenger
@ -31,7 +31,7 @@ type ChatViewController struct {
cancel chan struct{} // cancel the current chat loop
done chan struct{} // wait for the current chat loop to finish
changeContact chan client.Contact
changeChat chan client.Chat
}
// NewChatViewController returns a new chat view controller.
@ -45,11 +45,11 @@ func NewChatViewController(vc *ViewController, id Identity, m *client.Messenger,
identity: id,
messenger: m,
onError: onError,
changeContact: make(chan client.Contact, 1),
changeChat: make(chan client.Chat, 1),
}
}
func (c *ChatViewController) readEventsLoop(contact client.Contact) {
func (c *ChatViewController) readEventsLoop(contact client.Chat) {
c.done = make(chan struct{})
defer close(c.done)
@ -100,9 +100,9 @@ func (c *ChatViewController) readEventsLoop(contact client.Contact) {
switch ev := event.Interface.(type) {
case client.EventWithError:
c.onError(ev.GetError())
case client.EventWithContact:
if !ev.GetContact().Equal(contact) {
log.Printf("[ChatViewController::readEventsLoop] selected and received message contact are not equal: %s, %s", contact, ev.GetContact())
case client.EventWithChat:
if !ev.GetChat().Equal(contact) {
log.Printf("[ChatViewController::readEventsLoop] selected and received message contact are not equal: %s, %s", contact, ev.GetChat())
continue
}
msgev, ok := ev.(client.EventWithMessage)
@ -126,7 +126,7 @@ func (c *ChatViewController) readEventsLoop(contact client.Contact) {
messages = append(messages, msg)
}
case contact = <-c.changeContact:
case contact = <-c.changeChat:
inorder = false
clock = 0
messages = []*protocol.Message{}
@ -138,14 +138,14 @@ func (c *ChatViewController) readEventsLoop(contact client.Contact) {
// Select informs the chat view controller about a selected contact.
// The chat view controller setup subscribers and request recent messages.
func (c *ChatViewController) Select(contact client.Contact) error {
func (c *ChatViewController) Select(contact client.Chat) error {
log.Printf("[ChatViewController::Select] contact %s", contact.Name)
if c.cancel == nil {
c.cancel = make(chan struct{})
go c.readEventsLoop(contact)
}
c.changeContact <- contact
c.changeChat <- contact
c.contact = contact
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)

View File

@ -39,18 +39,9 @@ func TestSendMessage(t *testing.T) {
Return(nil).
Times(1)
protoMock.EXPECT().
Request(
gomock.Any(),
gomock.Any(),
).
Return(nil).
Times(1)
err = vc.Select(client.Contact{
Name: chatName,
Type: client.ContactPublicRoom,
Topic: chatName,
err = vc.Select(client.Chat{
Name: chatName,
Type: client.PublicChat,
})
require.NoError(t, err)
// close reading loops
@ -72,13 +63,4 @@ func TestSendMessage(t *testing.T) {
err = vc.Send(payload)
require.NoError(t, err)
// TODO: move to another layer
// statusMessage, err := protocol.DecodeMessage(message.data)
// require.NoError(t, err)
// require.EqualValues(t, payload, statusMessage.Text)
// require.Equal(t, protocol.ContentTypeTextPlain, statusMessage.ContentT)
// require.Equal(t, protocol.MessageTypePublicGroup, statusMessage.MessageT)
// require.Equal(t,
// protocol.Content{ChatID: chatName, Text: string(payload)},
// statusMessage.Content)
}

144
chats.go Normal file
View File

@ -0,0 +1,144 @@
package main
import (
"context"
"fmt"
"log"
"sync"
"time"
"github.com/jroimartin/gocui"
"github.com/status-im/status-console-client/protocol/client"
)
const (
refreshInterval = time.Second
)
// chatToString returns a string representation.
func chatToString(c client.Chat) string {
switch c.Type {
case client.PublicChat:
return fmt.Sprintf("#%s", c.Name)
case client.OneToOneChat:
return fmt.Sprintf("@%s", c.Name)
default:
return c.Name
}
}
// ChatsViewController manages chats view.
type ChatsViewController struct {
*ViewController
messenger *client.Messenger
chats []client.Chat
quit chan struct{}
once sync.Once
}
// NewChatsViewController returns a new chat view controller.
func NewChatsViewController(vm *ViewController, m *client.Messenger) *ChatsViewController {
return &ChatsViewController{ViewController: vm, messenger: m, quit: make(chan struct{})}
}
// refresh repaints the current list of chats.
func (c *ChatsViewController) refresh() {
c.g.Update(func(*gocui.Gui) error {
if err := c.Clear(); err != nil {
return err
}
for _, chat := range c.chats {
if _, err := fmt.Fprintln(c.ViewController, chatToString(chat)); err != nil {
return err
}
}
return nil
})
}
// load loads chats from the storage.
func (c *ChatsViewController) load() error {
chats, err := c.messenger.Chats()
if err != nil {
return err
}
c.chats = chats
return nil
}
// LoadAndRefresh loads chats from the storage and refreshes the view.
func (c *ChatsViewController) LoadAndRefresh() error {
c.once.Do(func() {
go func() {
ticker := time.Tick(refreshInterval)
for {
select {
case <-ticker:
_ = c.refreshOnChanges()
case <-c.quit:
return
}
}
}()
})
if err := c.load(); err != nil {
return err
}
c.refresh()
return nil
}
func (c *ChatsViewController) refreshOnChanges() error {
chats, err := c.messenger.Chats()
if err != nil {
return err
}
if c.containsChanges(chats) {
log.Printf("[chatS] new chats %v", chats)
c.chats = chats
c.refresh()
}
return nil
}
func (c *ChatsViewController) containsChanges(chats []client.Chat) bool {
if len(chats) != len(c.chats) {
return true
}
// every time chats are sorted in a same way.
for i := range chats {
if !chats[i].Equal(c.chats[i]) {
return true
}
}
return false
}
// ChatByIdx allows to retrieve a chat for a given index.
func (c *ChatsViewController) ChatByIdx(idx int) (client.Chat, bool) {
if idx > -1 && idx < len(c.chats) {
return c.chats[idx], true
}
return client.Chat{}, false
}
// Add adds a new chat to the list.
func (c *ChatsViewController) Add(chat client.Chat) error {
if err := c.messenger.Join(context.TODO(), chat); err != nil {
return err
}
return c.LoadAndRefresh()
}
// Remove removes a chat from the list.
func (c *ChatsViewController) Remove(chat client.Chat) error {
if err := c.messenger.RemoveChat(chat); err != nil {
return err
}
return c.LoadAndRefresh()
}

View File

@ -1,143 +0,0 @@
package main
import (
"fmt"
"log"
"sync"
"time"
"github.com/jroimartin/gocui"
"github.com/status-im/status-console-client/protocol/client"
)
const (
refreshInterval = time.Second
)
// contactToString returns a string representation.
func contactToString(c client.Contact) string {
switch c.Type {
case client.ContactPublicRoom:
return fmt.Sprintf("#%s", c.Name)
case client.ContactPrivate:
return fmt.Sprintf("@%s", c.Name)
default:
return c.Name
}
}
// ContactsViewController manages contacts view.
type ContactsViewController struct {
*ViewController
messenger *client.Messenger
contacts []client.Contact
quit chan struct{}
once sync.Once
}
// NewContactsViewController returns a new contact view controller.
func NewContactsViewController(vm *ViewController, m *client.Messenger) *ContactsViewController {
return &ContactsViewController{ViewController: vm, messenger: m, quit: make(chan struct{})}
}
// refresh repaints the current list of contacts.
func (c *ContactsViewController) refresh() {
c.g.Update(func(*gocui.Gui) error {
if err := c.Clear(); err != nil {
return err
}
for _, contact := range c.contacts {
if _, err := fmt.Fprintln(c.ViewController, contactToString(contact)); err != nil {
return err
}
}
return nil
})
}
// load loads contacts from the storage.
func (c *ContactsViewController) load() error {
contacts, err := c.messenger.Contacts()
if err != nil {
return err
}
c.contacts = contacts
return nil
}
// LoadAndRefresh loads contacts from the storage and refreshes the view.
func (c *ContactsViewController) LoadAndRefresh() error {
c.once.Do(func() {
go func() {
ticker := time.Tick(refreshInterval)
for {
select {
case <-ticker:
_ = c.refreshOnChanges()
case <-c.quit:
return
}
}
}()
})
if err := c.load(); err != nil {
return err
}
c.refresh()
return nil
}
func (c *ContactsViewController) refreshOnChanges() error {
contacts, err := c.messenger.Contacts()
if err != nil {
return err
}
if c.containsChanges(contacts) {
log.Printf("[CONTACTS] new contacts %v", contacts)
c.contacts = contacts
c.refresh()
}
return nil
}
func (c *ContactsViewController) containsChanges(contacts []client.Contact) bool {
if len(contacts) != len(c.contacts) {
return true
}
// every time contacts are sorted in a same way.
for i := range contacts {
if !contacts[i].Equal(c.contacts[i]) {
return true
}
}
return false
}
// ContactByIdx allows to retrieve a contact for a given index.
func (c *ContactsViewController) ContactByIdx(idx int) (client.Contact, bool) {
if idx > -1 && idx < len(c.contacts) {
return c.contacts[idx], true
}
return client.Contact{}, false
}
// Add adds a new contact to the list.
func (c *ContactsViewController) Add(contact client.Contact) error {
if err := c.messenger.AddContact(contact); err != nil {
return err
}
return c.LoadAndRefresh()
}
// Remove removes a contact from the list.
func (c *ContactsViewController) Remove(contact client.Contact) error {
if err := c.messenger.RemoveContact(contact); err != nil {
return err
}
return c.LoadAndRefresh()
}

View File

@ -67,40 +67,38 @@ func bytesToArgs(b []byte) []string {
return argsStr
}
func contactAddCmdHandler(args []string) (c client.Contact, err error) {
func chatAddCmdHandler(args []string) (c client.Chat, err error) {
if len(args) == 1 {
name := args[0]
c = client.Contact{
Name: name,
Type: client.ContactPublicRoom,
Topic: name,
c = client.Chat{
Name: name,
Type: client.PublicChat,
}
} else if len(args) == 2 {
c, err = client.CreateContactPrivate(args[1], args[0], client.ContactAdded)
c, err = client.CreateOneToOneChat(args[1], args[0])
} else {
err = errors.New("/contact: incorect arguments to add subcommand")
err = errors.New("/chat: incorect arguments to add subcommand")
}
return
}
func ContactCmdFactory(c *ContactsViewController) CmdHandler {
func ChatCmdFactory(c *ChatsViewController) CmdHandler {
return func(b []byte) error {
args := bytesToArgs(b)[1:] // remove first item, i.e. "/contact"
args := bytesToArgs(b)[1:] // remove first item, i.e. "/chat"
log.Printf("handle /contact command: %s", b)
log.Printf("handle /chat command: %s", b)
switch args[0] {
case "add":
contact, err := contactAddCmdHandler(args[1:])
chat, err := chatAddCmdHandler(args[1:])
if err != nil {
return err
}
log.Printf("adding contact with topic %s\n", contact.Topic)
if err := c.Add(contact); err != nil {
if err := c.Add(chat); err != nil {
return err
}
// TODO: fix removing contacts
// TODO: fix removing chats
// case "remove":
// if len(args) == 2 {
// if err := c.Remove(args[1]); err != nil {
@ -109,7 +107,7 @@ func ContactCmdFactory(c *ContactsViewController) CmdHandler {
// c.Refresh()
// return nil
// }
// return errors.New("/contact: incorect arguments to remove subcommand")
// return errors.New("/chat: incorect arguments to remove subcommand")
}
return nil

71
main.go
View File

@ -55,7 +55,7 @@ var (
// flags acting like commands
createKeyPair = fs.Bool("create-key-pair", false, "creates and prints a key pair instead of running")
addContact = fs.String("add-contact", "", "add contact using format: type,name[,public-key] where type can be 'private' or 'public' and 'public-key' is required for 'private' type")
addChat = fs.String("add-chat", "", "add chat using format: type,name[,public-key] where type can be 'private' or 'public' and 'public-key' is required for 'private' type")
// flags for in-proc node
dataDir = fs.String("data-dir", filepath.Join(os.TempDir(), "status-term-client"), "data directory for Ethereum node")
@ -147,60 +147,59 @@ func main() {
}
defer db.Close()
// Log the current contact info in two places for easy retrieval.
fmt.Printf("Contact address: %#x\n", crypto.FromECDSAPub(&privateKey.PublicKey))
log.Printf("contact address: %#x", crypto.FromECDSAPub(&privateKey.PublicKey))
// Log the current chat info in two places for easy retrieval.
fmt.Printf("Chat address: %#x\n", crypto.FromECDSAPub(&privateKey.PublicKey))
log.Printf("chat address: %#x", crypto.FromECDSAPub(&privateKey.PublicKey))
// Manage initial contacts.
if contacts, err := db.Contacts(); len(contacts) == 0 || err != nil {
debugContacts := []client.Contact{
{Name: "status", Type: client.ContactPublicRoom, Topic: "status"},
{Name: "status-core", Type: client.ContactPublicRoom, Topic: "status-core"},
// Manage initial chats.
if chats, err := db.Chats(); len(chats) == 0 || err != nil {
debugChats := []client.Chat{
{Name: "status", Type: client.PublicChat},
{Name: "status-core", Type: client.PublicChat},
}
uniqueContacts := []client.Contact{}
for _, c := range debugContacts {
exist, err := db.ContactExist(c)
uniqueChats := []client.Chat{}
for _, c := range debugChats {
exist, err := db.ChatExist(c)
if err != nil {
exitErr(err)
}
if !exist {
uniqueContacts = append(uniqueContacts, c)
uniqueChats = append(uniqueChats, c)
}
}
if len(uniqueContacts) != 0 {
if err := db.SaveContacts(uniqueContacts); err != nil {
if len(uniqueChats) != 0 {
if err := db.SaveChats(uniqueChats); err != nil {
exitErr(err)
}
}
}
// Handle add contact.
if *addContact != "" {
options := strings.Split(*addContact, ",")
// Handle add chat.
if *addChat != "" {
options := strings.Split(*addChat, ",")
var c client.Contact
var c client.Chat
if len(options) == 2 && options[0] == "public" {
c = client.Contact{
Name: options[1],
Type: client.ContactPublicRoom,
Topic: options[1],
c = client.Chat{
Name: options[1],
Type: client.PublicChat,
}
} else if len(options) == 3 && options[0] == "private" {
c, err = client.CreateContactPrivate(options[1], options[2], client.ContactAdded)
c, err = client.CreateOneToOneChat(options[1], options[2])
if err != nil {
exitErr(err)
}
} else {
exitErr(errors.Errorf("invalid -add-contact value"))
exitErr(errors.Errorf("invalid -add-chat value"))
}
exists, err := db.ContactExist(c)
exists, err := db.ChatExist(c)
if err != nil {
exitErr(err)
}
if !exists {
if err := db.SaveContacts([]client.Contact{c}); err != nil {
if err := db.SaveChats([]client.Chat{c}); err != nil {
exitErr(err)
}
}
@ -399,13 +398,13 @@ func createMessengerInProc(pk *ecdsa.PrivateKey, db client.Database) (*client.Me
}
// Init and start Publisher
broadcastContactCode := true
broadcastChatCode := true
online := func() bool {
return statusNode.Server().PeerCount() > 0
}
publisherService.Init(persistence.DB, protocol, protocolAdapter.OnNewMessages)
if err := publisherService.Start(online, broadcastContactCode); err != nil {
if err := publisherService.Start(online, broadcastChatCode); err != nil {
return nil, errors.Wrap(err, "failed to start Publisher")
}
@ -439,8 +438,8 @@ func setupGUI(privateKey *ecdsa.PrivateKey, messenger *client.Messenger) error {
},
)
contacts := NewContactsViewController(&ViewController{vm, g, ViewContacts}, messenger)
if err := contacts.LoadAndRefresh(); err != nil {
chats := NewChatsViewController(&ViewController{vm, g, ViewChats}, messenger)
if err := chats.LoadAndRefresh(); err != nil {
return err
}
@ -449,12 +448,12 @@ func setupGUI(privateKey *ecdsa.PrivateKey, messenger *client.Messenger) error {
log.Printf("default multiplexer handler")
return chat.Send(b)
})
inputMultiplexer.AddHandler("/contact", ContactCmdFactory(contacts))
inputMultiplexer.AddHandler("/chat", ChatCmdFactory(chats))
inputMultiplexer.AddHandler("/request", RequestCmdFactory(chat))
views := []*View{
&View{
Name: ViewContacts,
Name: ViewChats,
Enabled: true,
Cursor: true,
Highlight: true,
@ -479,16 +478,16 @@ func setupGUI(privateKey *ecdsa.PrivateKey, messenger *client.Messenger) error {
Key: gocui.KeyEnter,
Mod: gocui.ModNone,
Handler: GetLineHandler(func(idx int, _ string) error {
contact, ok := contacts.ContactByIdx(idx)
selectedChat, ok := chats.ChatByIdx(idx)
if !ok {
return errors.New("contact could not be found")
return errors.New("chat could not be found")
}
// We need to call Select asynchronously,
// otherwise the main thread is blocked
// and nothing is rendered.
go func() {
if err := chat.Select(contact); err != nil {
if err := chat.Select(selectedChat); err != nil {
log.Printf("[GetLineHandler] error selecting a chat: %v", err)
}
}()

View File

@ -158,13 +158,6 @@ func (w *ProtocolWhisperAdapter) Send(ctx context.Context, data []byte, options
if options.Recipient != nil {
newMessage, err = w.publisher.CreateDirectMessage(privateKey, options.Recipient, false, data)
} else {
_, filterErr := w.publisher.LoadFilter(&msgfilter.Chat{
ChatID: options.ChatName,
})
if filterErr != nil {
return nil, errors.Wrap(filterErr, "failed to load filter")
}
// Public messages are not wrapped (i.e have not bundle),
// when sending in public chats as it would be a breaking change.
// When we send a contact code, we send a public message but wrapped,

154
protocol/client/chat.go Normal file
View File

@ -0,0 +1,154 @@
package client
import (
"crypto/ecdsa"
"encoding/json"
"fmt"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
)
//go:generate stringer -type=ChatType
// ChatType defines a type of a chat.
type ChatType int
func (c ChatType) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf(`"%s"`, c)), nil
}
func (c *ChatType) UnmarshalJSON(data []byte) error {
switch string(data) {
case fmt.Sprintf(`"%s"`, PublicChat):
*c = PublicChat
case fmt.Sprintf(`"%s"`, OneToOneChat):
*c = OneToOneChat
default:
return fmt.Errorf("invalid ChatType: %s", data)
}
return nil
}
// Types of chats.
const (
PublicChat ChatType = iota + 1
OneToOneChat
PrivateGroupChat
)
// ChatMember is a member of a group chat
type ChatMember struct {
Admin bool
Joined bool
PublicKey *ecdsa.PublicKey `json:"-"`
}
// Chat is a single chat
type Chat struct {
// The id of the chat
ID string `json:"id"`
Name string `json:"name"`
Type ChatType `json:"type"`
// Creation time, is that been used?
Timestamp int64 `json:"timestamp"`
UpdatedAt int64 `json:"updatedAt"`
// Soft delete flag
Active bool `json:"active"`
// The color of the chat, makes no sense outside a UI context
Color string `json:"color"`
// Clock value of the last message before chat has been deleted
DeletedAtClockValue int64 `json:"deletedAtClockValue"`
PublicKey *ecdsa.PublicKey `json:"-"`
// Denormalized fields
UnviewedMessageCount int `json:"unviewedMessageCount"`
LastClockValue int64 `json:"lastClockValue"`
LastMessageContentType string `json:"lastMessageContentType"`
LastMessageContent string `json:"lastMessageContent"`
}
// CreateOneToOneChat creates a new private chat.
func CreateOneToOneChat(name, pubKeyHex string) (c Chat, err error) {
pubKeyBytes, err := hexutil.Decode(pubKeyHex)
if err != nil {
return
}
c.Name = name
c.Type = OneToOneChat
c.PublicKey, err = crypto.UnmarshalPubkey(pubKeyBytes)
return
}
// CreatePublicChat creates a public room chat.
func CreatePublicChat(name string) Chat {
return Chat{
Name: name,
Type: PublicChat,
}
}
// String returns a string representation of Chat.
func (c Chat) String() string {
return c.Name
}
// Equal returns true if chats have same name and same type.
func (c Chat) Equal(other Chat) bool {
return c.Name == other.Name && c.Type == other.Type
}
func (c Chat) MarshalJSON() ([]byte, error) {
type ChatAlias Chat
item := struct {
ChatAlias
PublicKey string `json:"public_key,omitempty"`
}{
ChatAlias: ChatAlias(c),
}
if c.PublicKey != nil {
item.PublicKey = EncodePublicKeyAsString(c.PublicKey)
}
return json.Marshal(&item)
}
func (c *Chat) UnmarshalJSON(data []byte) error {
type ChatAlias Chat
var item struct {
*ChatAlias
PublicKey string `json:"public_key,omitempty"`
}
if err := json.Unmarshal(data, &item); err != nil {
return err
}
if len(item.PublicKey) > 2 {
pubKey, err := hexutil.Decode(item.PublicKey)
if err != nil {
return err
}
item.ChatAlias.PublicKey, err = crypto.UnmarshalPubkey(pubKey)
if err != nil {
return err
}
}
*c = *(*Chat)(item.ChatAlias)
return nil
}
// EncodePublicKeyAsString encodes a public key as a string.
// It starts with 0x to indicate it's hex encoding.
func EncodePublicKeyAsString(pubKey *ecdsa.PublicKey) string {
return hexutil.Encode(crypto.FromECDSAPub(pubKey))
}

View File

@ -0,0 +1,63 @@
package client
import (
"encoding/json"
"fmt"
"testing"
"github.com/ethereum/go-ethereum/crypto"
"github.com/stretchr/testify/require"
)
func TestChatTypeMarshal(t *testing.T) {
ct := PublicChat
data, err := json.Marshal(ct)
require.NoError(t, err)
require.Equal(t, `"PublicChat"`, string(data))
}
func TestChatMarshalUnmarshal(t *testing.T) {
privateKey, _ := crypto.GenerateKey()
publicKeyStr := fmt.Sprintf("%#x", crypto.FromECDSAPub(&privateKey.PublicKey))
testCases := []struct {
name string
c Chat
result string
}{
{
name: "PublicChat",
c: Chat{
ID: "status",
Name: "status",
Type: PublicChat,
Timestamp: 20,
UpdatedAt: 21,
},
result: `{"id":"status","name":"status","type":"PublicChat","timestamp":20,"updatedAt":21,"active":false,"color":"","deletedAtClockValue":0,"unviewedMessageCount":0,"lastClockValue":0,"lastMessageContentType":"","lastMessageContent":""}`,
},
{
name: "ChatPublicKey",
c: Chat{
Name: "user1",
Type: OneToOneChat,
PublicKey: &privateKey.PublicKey,
},
result: fmt.Sprintf(`{"id":"","name":"user1","type":"OneToOneChat","timestamp":0,"updatedAt":0,"active":false,"color":"","deletedAtClockValue":0,"unviewedMessageCount":0,"lastClockValue":0,"lastMessageContentType":"","lastMessageContent":"","public_key":"%s"}`, publicKeyStr),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
data, err := json.Marshal(tc.c)
require.NoError(t, err)
require.Equal(t, tc.result, string(data))
var c Chat
err = json.Unmarshal(data, &c)
require.NoError(t, err)
require.Equal(t, tc.c, c)
})
}
}

View File

@ -0,0 +1,17 @@
// Code generated by "stringer -type=ChatType"; DO NOT EDIT.
package client
import "strconv"
const _ChatType_name = "PublicChatOneToOneChatPrivateGroupChat"
var _ChatType_index = [...]uint8{0, 10, 22, 38}
func (i ChatType) String() string {
i -= 1
if i < 0 || i >= ChatType(len(_ChatType_index)-1) {
return "ChatType(" + strconv.FormatInt(int64(i+1), 10) + ")"
}
return _ChatType_name[_ChatType_index[i]:_ChatType_index[i+1]]
}

View File

@ -1,147 +0,0 @@
package client
import (
"crypto/ecdsa"
"encoding/json"
"fmt"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
)
//go:generate stringer -type=ContactType
// ContactType defines a type of a contact.
type ContactType int
func (c ContactType) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf(`"%s"`, c)), nil
}
func (c *ContactType) UnmarshalJSON(data []byte) error {
switch string(data) {
case fmt.Sprintf(`"%s"`, ContactPublicRoom):
*c = ContactPublicRoom
case fmt.Sprintf(`"%s"`, ContactPrivate):
*c = ContactPrivate
default:
return fmt.Errorf("invalid ContactType: %s", data)
}
return nil
}
// Types of contacts.
const (
ContactPublicRoom ContactType = iota + 1
ContactPrivate
)
// ContactState defines state of the contact.
type ContactState int
const (
// ContactAdded default level. Added or confirmed by user.
ContactAdded ContactState = iota + 1
// ContactNew contact got connected to us and waits for being added or blocked.
ContactNew
// ContactBlocked means that all incoming messages from it will be discarded.
ContactBlocked
)
// Contact is a single contact which has a type and name.
type Contact struct {
Name string `json:"name"`
Type ContactType `json:"type"`
State ContactState `json:"state"`
Topic string `json:"topic"`
PublicKey *ecdsa.PublicKey `json:"-"`
}
// CreateContactPrivate creates a new private contact.
func CreateContactPrivate(name, pubKeyHex string, state ContactState) (c Contact, err error) {
pubKeyBytes, err := hexutil.Decode(pubKeyHex)
if err != nil {
return
}
c.Name = name
c.Type = ContactPrivate
c.State = state
c.Topic = DefaultPrivateTopic()
c.PublicKey, err = crypto.UnmarshalPubkey(pubKeyBytes)
return
}
// CreateContactPublicRoom creates a public room contact.
func CreateContactPublicRoom(name string, state ContactState) Contact {
return Contact{
Name: name,
Type: ContactPublicRoom,
State: state,
Topic: name,
}
}
// String returns a string representation of Contact.
func (c Contact) String() string {
return c.Name
}
// Equal returns true if contacts have same name and same type.
func (c Contact) Equal(other Contact) bool {
return c.Name == other.Name && c.Type == other.Type
}
func (c Contact) MarshalJSON() ([]byte, error) {
type ContactAlias Contact
item := struct {
ContactAlias
PublicKey string `json:"public_key,omitempty"`
}{
ContactAlias: ContactAlias(c),
}
if c.PublicKey != nil {
item.PublicKey = EncodePublicKeyAsString(c.PublicKey)
}
return json.Marshal(&item)
}
func (c *Contact) UnmarshalJSON(data []byte) error {
type ContactAlias Contact
var item struct {
*ContactAlias
PublicKey string `json:"public_key,omitempty"`
}
if err := json.Unmarshal(data, &item); err != nil {
return err
}
if len(item.PublicKey) > 2 {
pubKey, err := hexutil.Decode(item.PublicKey)
if err != nil {
return err
}
item.ContactAlias.PublicKey, err = crypto.UnmarshalPubkey(pubKey)
if err != nil {
return err
}
}
*c = *(*Contact)(item.ContactAlias)
return nil
}
// EncodePublicKeyAsString encodes a public key as a string.
// It starts with 0x to indicate it's hex encoding.
func EncodePublicKeyAsString(pubKey *ecdsa.PublicKey) string {
return hexutil.Encode(crypto.FromECDSAPub(pubKey))
}

View File

@ -1,60 +0,0 @@
package client
import (
"encoding/json"
"fmt"
"testing"
"github.com/ethereum/go-ethereum/crypto"
"github.com/stretchr/testify/require"
)
func TestContactTypeMarshal(t *testing.T) {
ct := ContactPublicRoom
data, err := json.Marshal(ct)
require.NoError(t, err)
require.Equal(t, `"ContactPublicRoom"`, string(data))
}
func TestContactMarshalUnmarshal(t *testing.T) {
privateKey, _ := crypto.GenerateKey()
publicKeyStr := fmt.Sprintf("%#x", crypto.FromECDSAPub(&privateKey.PublicKey))
testCases := []struct {
name string
c Contact
result string
}{
{
name: "ContactPublicRoom",
c: Contact{
Name: "status",
Type: ContactPublicRoom,
},
result: `{"name":"status","type":"ContactPublicRoom","state":0,"topic":""}`,
},
{
name: "ContactPublicKey",
c: Contact{
Name: "user1",
Type: ContactPrivate,
PublicKey: &privateKey.PublicKey,
},
result: fmt.Sprintf(`{"name":"user1","type":"ContactPrivate","state":0,"topic":"","public_key":"%s"}`, publicKeyStr),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
data, err := json.Marshal(tc.c)
require.NoError(t, err)
require.Equal(t, tc.result, string(data))
var c Contact
err = json.Unmarshal(data, &c)
require.NoError(t, err)
require.Equal(t, tc.c, c)
})
}
}

View File

@ -1,25 +0,0 @@
// Code generated by "stringer -type=ContactType"; DO NOT EDIT.
package client
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[ContactPublicRoom-1]
_ = x[ContactPrivate-2]
}
const _ContactType_name = "ContactPublicRoomContactPrivate"
var _ContactType_index = [...]uint8{0, 17, 31}
func (i ContactType) String() string {
i -= 1
if i < 0 || i >= ContactType(len(_ContactType_index)-1) {
return "ContactType(" + strconv.FormatInt(int64(i+1), 10) + ")"
}
return _ContactType_name[_ContactType_index[i]:_ContactType_index[i+1]]
}

View File

@ -65,19 +65,17 @@ var (
// Database is an interface for all db operations.
type Database interface {
Close() error
Messages(c Contact, from, to time.Time) ([]*protocol.Message, error)
NewMessages(c Contact, rowid int64) ([]*protocol.Message, error)
UnreadMessages(c Contact) ([]*protocol.Message, error)
SaveMessages(c Contact, messages []*protocol.Message) (int64, error)
LastMessageClock(Contact) (int64, error)
Contacts() ([]Contact, error)
SaveContacts(contacts []Contact) error
DeleteContact(Contact) error
ContactExist(Contact) (bool, error)
Histories() ([]History, error)
UpdateHistories([]History) error
GetPublicChat(name string) (*Contact, error)
GetOneToOneChat(*ecdsa.PublicKey) (*Contact, error)
Messages(c Chat, from, to time.Time) ([]*protocol.Message, error)
NewMessages(c Chat, rowid int64) ([]*protocol.Message, error)
UnreadMessages(c Chat) ([]*protocol.Message, error)
SaveMessages(c Chat, messages []*protocol.Message) (int64, error)
LastMessageClock(Chat) (int64, error)
Chats() ([]Chat, error)
SaveChats(chats []Chat) error
DeleteChat(Chat) error
ChatExist(Chat) (bool, error)
GetPublicChat(name string) (*Chat, error)
GetOneToOneChat(*ecdsa.PublicKey) (*Chat, error)
}
// Migrate applies migrations.
@ -173,9 +171,9 @@ func (db SQLLiteDatabase) Close() error {
return db.db.Close()
}
// SaveContacts inserts or replaces provided contacts.
// TODO should it delete all previous contacts?
func (db SQLLiteDatabase) SaveContacts(contacts []Contact) (err error) {
// SaveChats inserts or replaces provided chats.
// TODO should it delete all previous chats?
func (db SQLLiteDatabase) SaveChats(chats []Chat) (err error) {
var (
tx *sql.Tx
stmt *sql.Stmt
@ -184,36 +182,57 @@ func (db SQLLiteDatabase) SaveContacts(contacts []Contact) (err error) {
if err != nil {
return err
}
stmt, err = tx.Prepare("INSERT INTO user_contacts(id, name, type, state, topic, public_key) VALUES (?, ?, ?, ?, ?, ?)")
if err != nil {
return err
}
history, err := tx.Prepare("INSERT INTO history_user_contact_topic(contact_id) VALUES (?)")
if err != nil {
return err
}
defer func() {
if err == nil {
err = tx.Commit()
return
} else {
// don't shadow original error
_ = tx.Rollback()
return
}
}()
for i := range contacts {
stmt, err = tx.Prepare(`INSERT INTO chats(
id,
name,
color,
type,
active,
updated_at,
deleted_at_clock_value,
public_key,
unviewed_message_count,
last_clock_value,
last_message_content_type,
last_message_content
)
VALUES
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
if err != nil {
return err
}
for i := range chats {
pkey := []byte{}
if contacts[i].PublicKey != nil {
pkey, err = marshalEcdsaPub(contacts[i].PublicKey)
if chats[i].PublicKey != nil {
pkey, err = marshalEcdsaPub(chats[i].PublicKey)
if err != nil {
return err
}
}
id := contactID(contacts[i])
_, err = stmt.Exec(id, contacts[i].Name, contacts[i].Type, contacts[i].State, contacts[i].Topic, pkey)
if err != nil {
return err
}
// to avoid unmarshalling null into sql.NullInt64
_, err = history.Exec(id)
id := chatID(chats[i])
_, err = stmt.Exec(id,
chats[i].Name,
chats[i].Color,
chats[i].Type,
chats[i].Active,
chats[i].UpdatedAt,
chats[i].DeletedAtClockValue,
pkey,
chats[i].UnviewedMessageCount,
chats[i].LastClockValue,
chats[i].LastMessageContentType,
chats[i].LastMessageContent,
)
if err != nil {
return err
}
@ -221,105 +240,50 @@ func (db SQLLiteDatabase) SaveContacts(contacts []Contact) (err error) {
return err
}
// Contacts returns all available contacts.
func (db SQLLiteDatabase) Contacts() ([]Contact, error) {
rows, err := db.db.Query("SELECT name, type, state, topic, public_key FROM user_contacts")
// Chats returns all available chats.
func (db SQLLiteDatabase) Chats() ([]Chat, error) {
rows, err := db.db.Query("SELECT name, type, public_key FROM chats")
if err != nil {
return nil, err
}
var (
rst = []Contact{}
rst = []Chat{}
)
for rows.Next() {
// do not reuse same gob instance. same instance marshalls two same objects differently
// if used repetitively.
contact := Contact{}
chat := Chat{}
pkey := []byte{}
err = rows.Scan(&contact.Name, &contact.Type, &contact.State, &contact.Topic, &pkey)
err = rows.Scan(&chat.Name, &chat.Type, &pkey)
if err != nil {
return nil, err
}
if len(pkey) != 0 {
contact.PublicKey, err = unmarshalEcdsaPub(pkey)
chat.PublicKey, err = unmarshalEcdsaPub(pkey)
if err != nil {
return nil, err
}
}
rst = append(rst, contact)
rst = append(rst, chat)
}
return rst, nil
}
func (db SQLLiteDatabase) Histories() ([]History, error) {
rows, err := db.db.Query("SELECT synced, u.name, u.type, u.state, u.topic, u.public_key FROM history_user_contact_topic JOIN user_contacts u ON contact_id = u.id")
func (db SQLLiteDatabase) DeleteChat(c Chat) error {
_, err := db.db.Exec("DELETE FROM chats WHERE id = ?", fmt.Sprintf("%s:%d", c.Name, c.Type))
if err != nil {
return nil, err
}
rst := []History{}
for rows.Next() {
h := History{
Contact: Contact{},
}
pkey := []byte{}
err = rows.Scan(&h.Synced, &h.Contact.Name, &h.Contact.Type, &h.Contact.State, &h.Contact.Topic, &pkey)
if err != nil {
return nil, err
}
if len(pkey) != 0 {
h.Contact.PublicKey, err = unmarshalEcdsaPub(pkey)
if err != nil {
return nil, err
}
}
rst = append(rst, h)
}
return rst, nil
}
func (db SQLLiteDatabase) UpdateHistories(histories []History) (err error) {
var (
tx *sql.Tx
stmt *sql.Stmt
)
tx, err = db.db.BeginTx(context.Background(), &sql.TxOptions{})
if err != nil {
return err
}
stmt, err = tx.Prepare("UPDATE history_user_contact_topic SET synced = ? WHERE contact_Id = ?")
if err != nil {
return err
}
defer func() {
if err == nil {
err = tx.Commit()
} else {
_ = tx.Rollback()
}
}()
for i := range histories {
_, err = stmt.Exec(histories[i].Synced, contactID(histories[i].Contact))
if err != nil {
return err
}
return errors.Wrap(err, "error deleting chat from db")
}
return nil
}
func (db SQLLiteDatabase) DeleteContact(c Contact) error {
_, err := db.db.Exec("DELETE FROM user_contacts WHERE id = ?", fmt.Sprintf("%s:%d", c.Name, c.Type))
if err != nil {
return errors.Wrap(err, "error deleting contact from db")
}
return nil
}
func (db SQLLiteDatabase) ContactExist(c Contact) (exists bool, err error) {
err = db.db.QueryRow("SELECT EXISTS(SELECT id FROM user_contacts WHERE id = ?)", contactID(c)).Scan(&exists)
func (db SQLLiteDatabase) ChatExist(c Chat) (exists bool, err error) {
err = db.db.QueryRow("SELECT EXISTS(SELECT id FROM chats WHERE id = ?)", chatID(c)).Scan(&exists)
return
}
func (db SQLLiteDatabase) GetOneToOneChat(publicKey *ecdsa.PublicKey) (*Contact, error) {
func (db SQLLiteDatabase) GetOneToOneChat(publicKey *ecdsa.PublicKey) (*Chat, error) {
if publicKey == nil {
return nil, errors.New("No public key provided")
}
@ -329,9 +293,9 @@ func (db SQLLiteDatabase) GetOneToOneChat(publicKey *ecdsa.PublicKey) (*Contact,
return nil, err
}
c := &Contact{}
err = db.db.QueryRow("SELECT name, state, topic FROM user_contacts WHERE public_key = ?", pkey).Scan(&c.Name, &c.State, &c.Topic)
c.Type = ContactPrivate
c := &Chat{}
err = db.db.QueryRow("SELECT name FROM chats WHERE public_key = ?", pkey).Scan(&c.Name)
c.Type = OneToOneChat
c.PublicKey = publicKey
if err == sql.ErrNoRows {
return nil, nil
@ -342,10 +306,10 @@ func (db SQLLiteDatabase) GetOneToOneChat(publicKey *ecdsa.PublicKey) (*Contact,
return c, nil
}
func (db SQLLiteDatabase) GetPublicChat(name string) (*Contact, error) {
c := &Contact{}
err := db.db.QueryRow("SELECT name, state, topic FROM user_contacts WHERE id = ?", formatID(name, ContactPublicRoom)).Scan(&c.Name, &c.State, &c.Topic)
c.Type = ContactPublicRoom
func (db SQLLiteDatabase) GetPublicChat(name string) (*Chat, error) {
c := &Chat{}
err := db.db.QueryRow("SELECT name FROM chats WHERE id = ?", formatID(name, PublicChat)).Scan(&c.Name)
c.Type = PublicChat
if err == sql.ErrNoRows {
return nil, nil
} else if err != nil {
@ -355,16 +319,16 @@ func (db SQLLiteDatabase) GetPublicChat(name string) (*Contact, error) {
return c, nil
}
func (db SQLLiteDatabase) LastMessageClock(c Contact) (int64, error) {
func (db SQLLiteDatabase) LastMessageClock(c Chat) (int64, error) {
var last sql.NullInt64
err := db.db.QueryRow("SELECT max(clock) FROM user_messages WHERE contact_id = ?", contactID(c)).Scan(&last)
err := db.db.QueryRow("SELECT max(clock) FROM user_messages WHERE chat_id = ?", chatID(c)).Scan(&last)
if err != nil {
return 0, err
}
return last.Int64, nil
}
func (db SQLLiteDatabase) SaveMessages(c Contact, messages []*protocol.Message) (last int64, err error) {
func (db SQLLiteDatabase) SaveMessages(c Chat, messages []*protocol.Message) (last int64, err error) {
var (
tx *sql.Tx
stmt *sql.Stmt
@ -374,7 +338,7 @@ func (db SQLLiteDatabase) SaveMessages(c Contact, messages []*protocol.Message)
return
}
stmt, err = tx.Prepare(`INSERT INTO user_messages(
id, contact_id, content_type, message_type, text, clock, timestamp, content_chat_id, content_text, public_key, flags)
id, chat_id, content_type, message_type, text, clock, timestamp, content_chat_id, content_text, public_key, flags)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
if err != nil {
return
@ -391,8 +355,8 @@ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
}()
var (
contactID = fmt.Sprintf("%s:%d", c.Name, c.Type)
rst sql.Result
chatID = fmt.Sprintf("%s:%d", c.Name, c.Type)
rst sql.Result
)
for _, msg := range messages {
pkey := []byte{}
@ -400,7 +364,7 @@ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
pkey, err = marshalEcdsaPub(msg.SigPubKey)
}
rst, err = stmt.Exec(
msg.ID, contactID, msg.ContentT, msg.MessageT, msg.Text,
msg.ID, chatID, msg.ContentT, msg.MessageT, msg.Text,
msg.Clock, msg.Timestamp, msg.Content.ChatID, msg.Content.Text,
pkey, msg.Flags)
if err != nil {
@ -417,13 +381,13 @@ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
return
}
// Messages returns messages for a given contact, in a given period. Ordered by a timestamp.
func (db SQLLiteDatabase) Messages(c Contact, from, to time.Time) (result []*protocol.Message, err error) {
contactID := fmt.Sprintf("%s:%d", c.Name, c.Type)
// Messages returns messages for a given chat, in a given period. Ordered by a timestamp.
func (db SQLLiteDatabase) Messages(c Chat, from, to time.Time) (result []*protocol.Message, err error) {
chatID := fmt.Sprintf("%s:%d", c.Name, c.Type)
rows, err := db.db.Query(`SELECT
id, content_type, message_type, text, clock, timestamp, content_chat_id, content_text, public_key, flags
FROM user_messages WHERE contact_id = ? AND timestamp >= ? AND timestamp <= ? ORDER BY timestamp`,
contactID, protocol.TimestampInMsFromTime(from), protocol.TimestampInMsFromTime(to))
FROM user_messages WHERE chat_id = ? AND timestamp >= ? AND timestamp <= ? ORDER BY timestamp`,
chatID, protocol.TimestampInMsFromTime(from), protocol.TimestampInMsFromTime(to))
if err != nil {
return nil, err
}
@ -452,12 +416,12 @@ FROM user_messages WHERE contact_id = ? AND timestamp >= ? AND timestamp <= ? OR
return rst, nil
}
func (db SQLLiteDatabase) NewMessages(c Contact, rowid int64) ([]*protocol.Message, error) {
contactID := contactID(c)
func (db SQLLiteDatabase) NewMessages(c Chat, rowid int64) ([]*protocol.Message, error) {
chatID := chatID(c)
rows, err := db.db.Query(`SELECT
id, content_type, message_type, text, clock, timestamp, content_chat_id, content_text, public_key, flags
FROM user_messages WHERE contact_id = ? AND rowid >= ? ORDER BY clock`,
contactID, rowid)
FROM user_messages WHERE chat_id = ? AND rowid >= ? ORDER BY clock`,
chatID, rowid)
if err != nil {
return nil, err
}
@ -488,8 +452,8 @@ FROM user_messages WHERE contact_id = ? AND rowid >= ? ORDER BY clock`,
// TODO(adam): refactor all message getters in order not to
// repeat the select fields over and over.
func (db SQLLiteDatabase) UnreadMessages(c Contact) ([]*protocol.Message, error) {
contactID := contactID(c)
func (db SQLLiteDatabase) UnreadMessages(c Chat) ([]*protocol.Message, error) {
chatID := chatID(c)
rows, err := db.db.Query(`
SELECT
id,
@ -505,10 +469,10 @@ func (db SQLLiteDatabase) UnreadMessages(c Contact) ([]*protocol.Message, error)
FROM
user_messages
WHERE
contact_id = ? AND
chat_id = ? AND
flags & ? == 0
ORDER BY clock`,
contactID, protocol.MessageRead,
chatID, protocol.MessageRead,
)
if err != nil {
return nil, err
@ -539,10 +503,10 @@ func (db SQLLiteDatabase) UnreadMessages(c Contact) ([]*protocol.Message, error)
return result, nil
}
func contactID(c Contact) string {
func chatID(c Chat) string {
return formatID(c.Name, c.Type)
}
func formatID(name string, t ContactType) string {
func formatID(name string, t ChatType) string {
return fmt.Sprintf("%s:%d", name, t)
}

View File

@ -10,27 +10,26 @@ import (
"github.com/stretchr/testify/require"
)
func TestContactUniqueConstraint(t *testing.T) {
func TestChatUniqueConstraint(t *testing.T) {
db, err := InitializeTmpDB()
require.NoError(t, err)
defer db.Close()
pk, err := crypto.GenerateKey()
require.NoError(t, err)
contact := Contact{
chat := Chat{
Name: "first",
Type: ContactPublicRoom,
Type: PublicChat,
PublicKey: &pk.PublicKey,
Topic: "first",
}
require.NoError(t, db.SaveContacts([]Contact{contact}))
require.EqualError(t, db.SaveContacts([]Contact{contact}), "UNIQUE constraint failed: user_contacts.id")
rst, err := db.Contacts()
require.NoError(t, db.SaveChats([]Chat{chat}))
require.EqualError(t, db.SaveChats([]Chat{chat}), "UNIQUE constraint failed: chats.id")
rst, err := db.Chats()
require.NoError(t, err)
require.Len(t, rst, 1)
require.Equal(t, contact.Name, rst[0].Name)
require.Equal(t, contact.Type, rst[0].Type)
require.Equal(t, contact.PublicKey.X, rst[0].PublicKey.X)
require.Equal(t, contact.PublicKey.Y, rst[0].PublicKey.Y)
require.Equal(t, chat.Name, rst[0].Name)
require.Equal(t, chat.Type, rst[0].Type)
require.Equal(t, chat.PublicKey.X, rst[0].PublicKey.X)
require.Equal(t, chat.PublicKey.Y, rst[0].PublicKey.Y)
}
func TestMessagesFilteredAndOrderedByTimestamp(t *testing.T) {
@ -39,16 +38,15 @@ func TestMessagesFilteredAndOrderedByTimestamp(t *testing.T) {
defer db.Close()
pk, err := crypto.GenerateKey()
require.NoError(t, err)
contact := Contact{
chat := Chat{
Name: "test",
Type: ContactPublicRoom,
Type: PublicChat,
PublicKey: &pk.PublicKey,
Topic: "first",
}
require.NoError(t, db.SaveContacts([]Contact{contact}))
contacts, err := db.Contacts()
require.NoError(t, db.SaveChats([]Chat{chat}))
chats, err := db.Chats()
require.NoError(t, err)
require.Len(t, contacts, 1)
require.Len(t, chats, 1)
msg1 := protocol.Message{
ID: []byte("hello1"),
SigPubKey: &pk.PublicKey,
@ -65,10 +63,10 @@ func TestMessagesFilteredAndOrderedByTimestamp(t *testing.T) {
Timestamp: 2000,
}
last, err := db.SaveMessages(contact, []*protocol.Message{&msg3, &msg1, &msg2})
last, err := db.SaveMessages(chat, []*protocol.Message{&msg3, &msg1, &msg2})
require.NoError(t, err)
require.Equal(t, int64(3), last)
msgs, err := db.Messages(contact, time.Unix(3, 0), time.Unix(11, 0))
msgs, err := db.Messages(chat, time.Unix(3, 0), time.Unix(11, 0))
require.NoError(t, err)
require.Len(t, msgs, 2)
require.Equal(t, msg2.Timestamp, msgs[0].Timestamp)
@ -79,10 +77,9 @@ func TestUnreadMessages(t *testing.T) {
db, err := InitializeTmpDB()
require.NoError(t, err)
defer db.Close()
contact := Contact{
Name: "test",
Type: ContactPublicRoom,
Topic: "first",
chat := Chat{
Name: "test",
Type: PublicChat,
}
// insert some messages
var messages []*protocol.Message
@ -100,11 +97,11 @@ func TestUnreadMessages(t *testing.T) {
}
messages = append(messages, &m)
}
_, err = db.SaveMessages(contact, messages)
_, err = db.SaveMessages(chat, messages)
require.NoError(t, err)
// verify that we get only unread messages
unread, err := db.UnreadMessages(contact)
unread, err := db.UnreadMessages(chat)
require.NoError(t, err)
require.Len(t, unread, 2)
for _, m := range unread {
@ -113,10 +110,9 @@ func TestUnreadMessages(t *testing.T) {
}
func TestSaveMessagesUniqueConstraint(t *testing.T) {
contact := Contact{
Name: "test",
Type: ContactPublicRoom,
Topic: "first",
chat := Chat{
Name: "test",
Type: PublicChat,
}
sameid := []byte("1")
msg1 := protocol.Message{
@ -129,7 +125,7 @@ func TestSaveMessagesUniqueConstraint(t *testing.T) {
require.NoError(t, err)
defer db.Close()
_, err = db.SaveMessages(contact, []*protocol.Message{&msg1, &msg2})
_, err = db.SaveMessages(chat, []*protocol.Message{&msg1, &msg2})
require.EqualError(t, err, ErrMsgAlreadyExist.Error())
}
@ -146,14 +142,13 @@ func TestGetLastMessageClock(t *testing.T) {
Clock: int64(count - i),
}
}
contact := Contact{
Name: "test",
Type: ContactPublicRoom,
Topic: "first",
chat := Chat{
Name: "test",
Type: PublicChat,
}
_, err = db.SaveMessages(contact, messages)
_, err = db.SaveMessages(chat, messages)
require.NoError(t, err)
last, err := db.LastMessageClock(contact)
last, err := db.LastMessageClock(chat)
require.NoError(t, err)
require.Equal(t, int64(count), last)
}
@ -164,54 +159,15 @@ func TestGetOneToOneChat(t *testing.T) {
defer db.Close()
pk, err := crypto.GenerateKey()
require.NoError(t, err)
expectedContact := Contact{
expectedChat := Chat{
Name: "first",
Type: ContactPrivate,
Type: OneToOneChat,
PublicKey: &pk.PublicKey,
Topic: "first",
}
require.NoError(t, db.SaveContacts([]Contact{expectedContact}))
contact, err := db.GetOneToOneChat(&pk.PublicKey)
require.NoError(t, db.SaveChats([]Chat{expectedChat}))
chat, err := db.GetOneToOneChat(&pk.PublicKey)
require.NoError(t, err)
require.Equal(t, &expectedContact, contact, "contact expected to exist in database")
}
func TestLoadHistories(t *testing.T) {
db, err := InitializeTmpDB()
require.NoError(t, err)
defer db.Close()
c1 := Contact{
Name: "first",
Type: ContactPublicRoom,
}
c2 := Contact{
Name: "second",
Type: ContactPublicRoom,
}
require.NoError(t, db.SaveContacts([]Contact{c1, c2}))
histories, err := db.Histories()
require.NoError(t, err)
require.Len(t, histories, 2)
}
func TestUpdateHistories(t *testing.T) {
db, err := InitializeTmpDB()
require.NoError(t, err)
defer db.Close()
c1 := Contact{
Name: "first",
Type: ContactPublicRoom,
}
require.NoError(t, db.SaveContacts([]Contact{c1}))
h := History{
Synced: 100,
Contact: c1,
}
require.NoError(t, db.UpdateHistories([]History{h}))
histories, err := db.Histories()
require.NoError(t, err)
require.Len(t, histories, 1)
require.Equal(t, h.Synced, histories[0].Synced)
require.Equal(t, &expectedChat, chat, "chat expected to exist in database")
}
func BenchmarkLoadMessages(b *testing.B) {
@ -220,29 +176,26 @@ func BenchmarkLoadMessages(b *testing.B) {
defer db.Close()
pk, err := crypto.GenerateKey()
require.NoError(b, err)
contacts := []Contact{
chats := []Chat{
{
Name: "first",
Type: ContactPrivate,
Type: OneToOneChat,
PublicKey: &pk.PublicKey,
Topic: "test",
},
{
Name: "second",
Type: ContactPrivate,
Type: OneToOneChat,
PublicKey: &pk.PublicKey,
Topic: "test",
},
{
Name: "third",
Type: ContactPrivate,
Type: OneToOneChat,
PublicKey: &pk.PublicKey,
Topic: "test",
},
}
count := 10000
require.NoError(b, db.SaveContacts(contacts))
for j, c := range contacts {
require.NoError(b, db.SaveChats(chats))
for j, c := range chats {
messages := make([]*protocol.Message, count)
for i := range messages {
id := [8]byte{}
@ -259,7 +212,7 @@ func BenchmarkLoadMessages(b *testing.B) {
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
rst, err := db.NewMessages(contacts[0], 0)
rst, err := db.NewMessages(chats[0], 0)
require.NoError(b, err)
require.Len(b, rst, count)
}

View File

@ -1,6 +1,6 @@
package client
// Client is a higher level protocol interface.
// It allows to interact with the messages and contacts
// It allows to interact with the messages and chats
// in a very abstract way where a lot of low-level operations
// are hidden.

View File

@ -26,8 +26,8 @@ type Event struct {
Interface interface{}
}
type EventWithContact interface {
GetContact() Contact
type EventWithChat interface {
GetChat() Chat
}
type EventWithType interface {
@ -43,12 +43,12 @@ type EventWithMessage interface {
}
type baseEvent struct {
Contact Contact `json:"contact"`
Type EventType `json:"type"`
Chat Chat `json:"chat"`
Type EventType `json:"type"`
}
func (e baseEvent) GetContact() Contact { return e.Contact }
func (e baseEvent) GetType() EventType { return e.Type }
func (e baseEvent) GetChat() Chat { return e.Chat }
func (e baseEvent) GetType() EventType { return e.Type }
type messageEvent struct {
baseEvent

View File

@ -14,7 +14,7 @@ func NewDatabaseWithEvents(db Database, feed *event.Feed) DatabaseWithEvents {
return DatabaseWithEvents{Database: db, feed: feed}
}
func (db DatabaseWithEvents) SaveMessages(c Contact, msgs []*protocol.Message) (int64, error) {
func (db DatabaseWithEvents) SaveMessages(c Chat, msgs []*protocol.Message) (int64, error) {
rowid, err := db.Database.SaveMessages(c, msgs)
if err != nil {
return rowid, err
@ -22,8 +22,8 @@ func (db DatabaseWithEvents) SaveMessages(c Contact, msgs []*protocol.Message) (
for _, m := range msgs {
ev := messageEvent{
baseEvent: baseEvent{
Contact: c,
Type: EventTypeMessage,
Chat: c,
Type: EventTypeMessage,
},
Message: m,
}

View File

@ -1,53 +0,0 @@
package client
import (
"time"
"github.com/status-im/status-console-client/protocol/v1"
)
// History is used to track when contact was synced last time.
// Contact extension. Deleted on cascade when parent contact is deleted.
type History struct {
// Synced is a timestamp in seconds.
Synced int64
Contact Contact
}
func splitIntoSyncedNotSynced(histories []History) (sync []History, nosync []History) {
for i := range histories {
if histories[i].Synced != 0 {
sync = append(sync, histories[i])
} else {
nosync = append(nosync, histories[i])
}
}
return
}
func syncedToOpts(histories []History, now time.Time) protocol.RequestOptions {
opts := protocol.RequestOptions{
To: now.Unix(),
Limit: 1000,
}
for i := range histories {
if opts.From == 0 || opts.From > histories[i].Synced {
opts.From = histories[i].Synced
}
// TODO(dshulyak) remove contact type validation in that function
// simply always add topic and (if set) public key
_ = enhanceRequestOptions(histories[i].Contact, &opts)
}
return opts
}
func notsyncedToOpts(histories []History, now time.Time) protocol.RequestOptions {
opts := protocol.DefaultRequestOptions()
opts.To = now.Unix()
for i := range histories {
// TODO(dshulyak) remove contact type validation in that function
// simply always add topic and (if set) public key
_ = enhanceRequestOptions(histories[i].Contact, &opts)
}
return opts
}

View File

@ -37,7 +37,7 @@ func NewMessenger(identity *ecdsa.PrivateKey, proto protocol.Protocol, db Databa
}
}
func contactToChatOptions(c Contact) protocol.ChatOptions {
func chatToChatOptions(c Chat) protocol.ChatOptions {
return protocol.ChatOptions{
ChatName: c.Name,
Recipient: c.PublicKey,
@ -51,13 +51,13 @@ func (m *Messenger) Start() error {
m.mu.Lock()
defer m.mu.Unlock()
contacts, err := m.db.Contacts()
chats, err := m.db.Chats()
if err != nil {
return errors.Wrap(err, "unable to read contacts from database")
return errors.Wrap(err, "unable to read chats from database")
}
for i := range contacts {
chatOptions = append(chatOptions, contactToChatOptions(contacts[i]))
for i := range chats {
chatOptions = append(chatOptions, chatToChatOptions(chats[i]))
}
if err := m.proto.LoadChats(context.Background(), chatOptions); err != nil {
@ -78,26 +78,24 @@ func (m *Messenger) Stop() {
}
func (m *Messenger) handleDirectMessage(chatType protocol.ChatOptions, message protocol.Message) error {
contact, err := m.db.GetOneToOneChat(message.SigPubKey)
chat, err := m.db.GetOneToOneChat(message.SigPubKey)
if err != nil {
return errors.Wrap(err, "could not fetch chat from database")
}
if contact == nil {
contact = &Contact{
Type: ContactPrivate,
State: ContactNew,
if chat == nil {
chat = &Chat{
Type: OneToOneChat,
Name: pubkeyToHex(message.SigPubKey), // TODO(dshulyak) replace with 3-word funny name
PublicKey: message.SigPubKey,
Topic: DefaultPrivateTopic(),
}
err := m.db.SaveContacts([]Contact{*contact})
err := m.db.SaveChats([]Chat{*chat})
if err != nil {
return errors.Wrap(err, "can't save a new contact")
return errors.Wrap(err, "can't save a new chat")
}
}
_, err = m.db.SaveMessages(*contact, []*protocol.Message{&message})
_, err = m.db.SaveMessages(*chat, []*protocol.Message{&message})
if err == ErrMsgAlreadyExist {
log.Printf("Message already exists")
return nil
@ -109,13 +107,13 @@ func (m *Messenger) handleDirectMessage(chatType protocol.ChatOptions, message p
}
func (m *Messenger) handlePublicMessage(chatType protocol.ChatOptions, message protocol.Message) error {
contact, err := m.db.GetPublicChat(chatType.ChatName)
chat, err := m.db.GetPublicChat(chatType.ChatName)
if err != nil {
return errors.Wrap(err, "error getting public chat")
} else if contact == nil {
} else if chat == nil {
return errors.Wrap(err, "no chat for this message, is that a deleted chat?")
}
_, err = m.db.SaveMessages(*contact, []*protocol.Message{&message})
_, err = m.db.SaveMessages(*chat, []*protocol.Message{&message})
if err == ErrMsgAlreadyExist {
log.Printf("Message already exists")
return nil
@ -186,30 +184,24 @@ func (m *Messenger) ProcessMessages() {
}
}
func (m *Messenger) Join(ctx context.Context, c Contact) error {
func (m *Messenger) Join(ctx context.Context, c Chat) error {
log.Printf("[Messenger::Join] Joining a chat with contact %#v", c)
m.mu.Lock()
defer m.mu.Unlock()
if err := m.proto.LoadChats(context.Background(), []protocol.ChatOptions{contactToChatOptions(c)}); err != nil {
if err := m.AddChat(c); err != nil {
return err
}
opts := protocol.DefaultRequestOptions()
// NOTE(dshulyak) join ctx shouldn't have an impact on history timeout.
if err := m.Request(context.Background(), c, opts); err != nil {
return err
}
return m.db.UpdateHistories([]History{{Contact: c, Synced: opts.To}})
return m.proto.LoadChats(context.Background(), []protocol.ChatOptions{chatToChatOptions(c)})
}
// Messages reads all messages from database.
func (m *Messenger) Messages(c Contact, offset int64) ([]*protocol.Message, error) {
func (m *Messenger) Messages(c Chat, offset int64) ([]*protocol.Message, error) {
return m.db.NewMessages(c, offset)
}
func (m *Messenger) Request(ctx context.Context, c Contact, options protocol.RequestOptions) error {
func (m *Messenger) Request(ctx context.Context, c Chat, options protocol.RequestOptions) error {
err := enhanceRequestOptions(c, &options)
if err != nil {
return err
@ -217,75 +209,22 @@ func (m *Messenger) Request(ctx context.Context, c Contact, options protocol.Req
return m.proto.Request(ctx, options)
}
func (m *Messenger) requestHistories(ctx context.Context, histories []History, opts protocol.RequestOptions) error {
log.Printf("[Messenger::requestHistories] requesting messages for chats %+v: from %d to %d\n", opts.Chats, opts.From, opts.To)
start := time.Now()
err := m.proto.Request(ctx, opts)
if err != nil {
return err
}
log.Printf("[Messenger::requestHistories] requesting message for chats %+v finished in %s\n", opts.Chats, time.Since(start))
for i := range histories {
histories[i].Synced = opts.To
}
return m.db.UpdateHistories(histories)
}
func (m *Messenger) RequestAll(ctx context.Context, newest bool) error {
// FIXME(dshulyak) if newest is false request 24 hour of messages older then the
// earliest envelope for each contact.
histories, err := m.db.Histories()
if err != nil {
return errors.Wrap(err, "error fetching contacts")
}
var (
now = time.Now()
synced, notsynced = splitIntoSyncedNotSynced(histories)
errors = make(chan error, 2)
wg sync.WaitGroup
)
if len(synced) != 0 {
wg.Add(1)
go func() {
errors <- m.requestHistories(ctx, synced, syncedToOpts(synced, now))
wg.Done()
}()
}
if len(notsynced) != 0 {
wg.Add(1)
go func() {
errors <- m.requestHistories(ctx, notsynced, notsyncedToOpts(notsynced, now))
wg.Done()
}()
}
wg.Wait()
log.Printf("[Messenger::RequestAll] finished requesting histories")
close(errors)
for err := range errors {
if err != nil {
return err
}
}
return nil
}
func (m *Messenger) Send(c Contact, data []byte) ([]byte, error) {
// FIXME(dshulyak) sending must be locked by contact to prevent sending second msg with same clock
func (m *Messenger) Send(c Chat, data []byte) ([]byte, error) {
// FIXME(dshulyak) sending must be locked by chat to prevent sending second msg with same clock
clock, err := m.db.LastMessageClock(c)
if err != nil {
return nil, errors.Wrap(err, "failed to read last message clock for contact")
return nil, errors.Wrap(err, "failed to read last message clock for chat")
}
var message protocol.Message
switch c.Type {
case ContactPublicRoom:
case PublicChat:
message = protocol.CreatePublicTextMessage(data, clock, c.Name)
case ContactPrivate:
case OneToOneChat:
message = protocol.CreatePrivateTextMessage(data, clock, c.Name)
default:
return nil, fmt.Errorf("failed to send message: unsupported contact type")
@ -326,24 +265,24 @@ func (m *Messenger) Send(c Contact, data []byte) ([]byte, error) {
}
}
func (m *Messenger) RemoveContact(c Contact) error {
return m.db.DeleteContact(c)
func (m *Messenger) RemoveChat(c Chat) error {
return m.db.DeleteChat(c)
}
func (m *Messenger) AddContact(c Contact) error {
return m.db.SaveContacts([]Contact{c})
func (m *Messenger) AddChat(c Chat) error {
return m.db.SaveChats([]Chat{c})
}
func (m *Messenger) Contacts() ([]Contact, error) {
return m.db.Contacts()
func (m *Messenger) Chats() ([]Chat, error) {
return m.db.Chats()
}
func (m *Messenger) Leave(c Contact) error {
func (m *Messenger) Leave(c Chat) error {
m.mu.Lock()
defer m.mu.Unlock()
if err := m.proto.RemoveChats(context.Background(), []protocol.ChatOptions{contactToChatOptions(c)}); err != nil {
if err := m.proto.RemoveChats(context.Background(), []protocol.ChatOptions{chatToChatOptions(c)}); err != nil {
return err
}

View File

@ -1,98 +0,0 @@
package client
import (
"context"
"sort"
"testing"
"time"
"github.com/status-im/status-console-client/protocol/subscription"
"github.com/status-im/status-console-client/protocol/v1"
"github.com/status-im/status-go/messaging/filter"
"github.com/status-im/status-go/messaging/multidevice"
"github.com/stretchr/testify/require"
)
type requestsMock struct {
requests []protocol.RequestOptions
}
func (proto *requestsMock) Send(ctx context.Context, data []byte, options protocol.SendOptions) ([]byte, error) {
return nil, nil
}
func (proto *requestsMock) Subscribe(ctx context.Context, messages chan *protocol.StatusMessage, options protocol.SubscribeOptions) (*subscription.Subscription, error) {
return nil, nil
}
func (proto *requestsMock) Request(ctx context.Context, params protocol.RequestOptions) error {
proto.requests = append(proto.requests, params)
return nil
}
func (proto *requestsMock) GetMessagesChan() chan *protocol.ReceivedMessages {
return nil
}
func (proto *requestsMock) LoadChats(ctx context.Context, chats []protocol.ChatOptions) error {
return nil
}
func (proto *requestsMock) RemoveChats(ctx context.Context, chats []protocol.ChatOptions) error {
return nil
}
func (proto *requestsMock) SetInstallationMetadata(ctx context.Context, installationID string, data *multidevice.InstallationMetadata) error {
return nil
}
func (proto *requestsMock) OnNewMessages(messages []*filter.Messages) {
}
func TestRequestHistoryOneRequest(t *testing.T) {
db, err := InitializeTmpDB()
require.NoError(t, err)
defer db.Close()
proto := &requestsMock{}
m := NewMessenger(nil, proto, db)
require.NoError(t, db.SaveContacts([]Contact{
{Name: "first", Type: ContactPublicRoom},
{Name: "second", Type: ContactPublicRoom}}))
require.NoError(t, m.RequestAll(context.TODO(), true))
require.Len(t, proto.requests, 1)
histories, err := db.Histories()
require.NoError(t, err)
require.Len(t, histories, 2)
require.Equal(t, histories[0].Synced, proto.requests[0].To)
require.Equal(t, histories[1].Synced, proto.requests[0].To)
}
func TestRequestHistoryTwoRequest(t *testing.T) {
db, err := InitializeTmpDB()
require.NoError(t, err)
defer db.Close()
proto := &requestsMock{}
m := NewMessenger(nil, proto, db)
contacts := []Contact{
{Name: "first", Type: ContactPublicRoom, Topic: "first"},
{Name: "second", Type: ContactPublicRoom, Topic: "second"},
{Name: "third", Type: ContactPublicRoom, Topic: "third"},
}
require.NoError(t, db.SaveContacts(contacts))
histories := []History{{Synced: time.Now().Add(-time.Hour).Unix(), Contact: contacts[0]}}
require.NoError(t, db.UpdateHistories(histories))
require.NoError(t, m.RequestAll(context.TODO(), true))
require.Len(t, proto.requests, 2)
sort.Slice(proto.requests, func(i, j int) bool {
return proto.requests[i].From < proto.requests[j].From
})
require.Len(t, proto.requests[0].Chats, 2)
require.Len(t, proto.requests[1].Chats, 1)
require.Equal(t, histories[0].Contact.Name, proto.requests[1].Chats[0].ChatName)
require.Equal(t, histories[0].Synced, proto.requests[1].From)
}

View File

@ -1,3 +1,10 @@
// Code generated by go-bindata.
// sources:
// 000001_add_messages_contacts.down.db.sql
// 000001_add_messages_contacts.up.db.sql
// doc.go
// DO NOT EDIT!
package migrations
import (
@ -5,10 +12,14 @@ import (
"compress/gzip"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
)
func bindata_read(data []byte, name string) ([]byte, error) {
func bindataRead(data []byte, name string) ([]byte, error) {
gz, err := gzip.NewReader(bytes.NewBuffer(data))
if err != nil {
return nil, fmt.Errorf("Read %q: %v", name, err)
@ -16,71 +27,150 @@ func bindata_read(data []byte, name string) ([]byte, error) {
var buf bytes.Buffer
_, err = io.Copy(&buf, gz)
gz.Close()
clErr := gz.Close()
if err != nil {
return nil, fmt.Errorf("Read %q: %v", name, err)
}
if clErr != nil {
return nil, err
}
return buf.Bytes(), nil
}
var __000001_add_messages_contacts_down_db_sql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x72\x09\xf2\x0f\x50\x08\x71\x74\xf2\x71\x55\x28\x2d\x4e\x2d\x8a\xcf\x4d\x2d\x2e\x4e\x4c\x4f\x2d\xb6\xe6\x42\x97\x49\xce\xcf\x2b\x49\x4c\x2e\x41\x95\xc9\xc8\x2c\x2e\xc9\x2f\xaa\x8c\x47\x56\x11\x5f\x92\x5f\x90\x99\x6c\xcd\x05\x08\x00\x00\xff\xff\x1b\x57\x14\x62\x5b\x00\x00\x00")
type asset struct {
bytes []byte
info os.FileInfo
}
func _000001_add_messages_contacts_down_db_sql() ([]byte, error) {
return bindata_read(
__000001_add_messages_contacts_down_db_sql,
type bindataFileInfo struct {
name string
size int64
mode os.FileMode
modTime time.Time
}
func (fi bindataFileInfo) Name() string {
return fi.name
}
func (fi bindataFileInfo) Size() int64 {
return fi.size
}
func (fi bindataFileInfo) Mode() os.FileMode {
return fi.mode
}
func (fi bindataFileInfo) ModTime() time.Time {
return fi.modTime
}
func (fi bindataFileInfo) IsDir() bool {
return false
}
func (fi bindataFileInfo) Sys() interface{} {
return nil
}
var __000001_add_messages_contactsDownDbSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x72\x09\xf2\x0f\x50\x08\x71\x74\xf2\x71\x55\x48\xce\x48\x2c\x29\xb6\xe6\x42\x12\x29\x2d\x4e\x2d\x8a\xcf\x4d\x2d\x2e\x4e\x4c\x4f\x45\x95\xc9\x4d\xcd\x4d\x4a\x2d\x2a\xce\xc8\x2c\x88\x2f\x2d\x48\x49\x2c\x41\x93\x06\x19\x15\x0f\x55\x63\xcd\xc5\x05\x08\x00\x00\xff\xff\x49\xbe\x78\x25\x65\x00\x00\x00")
func _000001_add_messages_contactsDownDbSqlBytes() ([]byte, error) {
return bindataRead(
__000001_add_messages_contactsDownDbSql,
"000001_add_messages_contacts.down.db.sql",
)
}
var __000001_add_messages_contacts_up_db_sql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x7c\x92\xcf\x6e\xb3\x30\x10\xc4\xef\x7e\x8a\x3d\xf2\x49\x1c\xbe\x7b\x4f\x06\x96\xc4\x2a\xb5\x5b\xc7\x34\xc9\x09\x51\xc7\x6a\x50\xc2\x1f\xc5\x8e\x54\xde\xbe\x82\x42\xe3\xa6\x69\xae\x33\xf6\x7a\xe6\xb7\x8e\x25\x52\x85\xa0\x68\x94\x21\xb0\x14\xb8\x50\x80\x1b\xb6\x52\x2b\x38\x5b\x73\x2a\x6a\x63\x6d\xf9\x6e\x2c\x04\xa4\xda\x41\x94\x89\x08\x72\xce\x5e\x72\x1c\x4f\xf2\x3c\xcb\x42\xa2\xdb\xc6\x95\xda\x15\xd5\x0e\x5e\xa9\x8c\x97\x54\x5e\x99\xa6\x71\x85\xeb\x3b\x33\xdb\x21\x99\xc6\x5e\xa9\xce\x7c\x38\x50\xb8\x51\x21\xd1\xc7\x56\x1f\x20\x62\x0b\xc6\x55\x48\x5c\x55\x1b\xeb\xca\xba\xfb\x56\xe6\xb1\x7a\x5f\x8e\x0f\x4f\xb7\xe6\xc7\x2e\x83\xba\xf3\xdb\xb1\xd2\xc5\xc1\xf4\x63\x7a\xf2\xef\x81\x90\xa9\x34\xe3\x09\x6e\xe0\x92\xde\x82\xe0\x3f\x5b\x07\x17\xd3\xbb\xf7\x27\xac\xe9\xf4\x04\x6b\x66\xf1\x2c\xd9\x13\x95\x5b\x78\xc4\xad\xc7\xa5\x29\x6b\x73\x03\x97\x6b\xbb\x4a\x8f\xd1\x7d\x71\xa0\xc4\xb8\x2f\x59\x57\xba\x51\xbb\xd1\x10\xd6\x4c\x2d\x45\xae\x40\x8a\x35\x4b\xee\xe7\xde\x57\xd6\xb5\xa7\xbe\xf0\xf3\x17\x5f\x21\x02\x62\xfb\x46\x9b\xdd\xc4\x1c\x12\x4c\x69\x9e\x29\xf8\x7f\x7f\xf5\xbf\xbe\x47\x2a\x24\xb2\x05\x1f\xfa\xfb\x3c\x41\x62\x8a\x12\x79\x8c\x57\xf4\x82\xc1\x14\x1c\x12\xcc\x50\x21\xc4\x74\x15\xd3\x04\x87\xc5\x7d\x06\x00\x00\xff\xff\x28\x6d\x42\x61\xad\x02\x00\x00")
func _000001_add_messages_contactsDownDbSql() (*asset, error) {
bytes, err := _000001_add_messages_contactsDownDbSqlBytes()
if err != nil {
return nil, err
}
func _000001_add_messages_contacts_up_db_sql() ([]byte, error) {
return bindata_read(
__000001_add_messages_contacts_up_db_sql,
info := bindataFileInfo{name: "000001_add_messages_contacts.down.db.sql", size: 101, mode: os.FileMode(420), modTime: time.Unix(1562751645, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
var __000001_add_messages_contactsUpDbSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x94\x54\xcb\x6e\xdb\x30\x10\xbc\xf3\x2b\x16\xe8\x21\x11\xa0\x43\x7b\x28\x5a\x20\x27\xda\x5e\x27\x44\x65\x2a\xa5\xa9\xc6\x39\x11\x8c\xc8\xc6\x6a\xf4\x82\x45\xb9\xcd\xdf\x17\xd6\xc3\x96\xab\xd8\x71\xaf\xcb\x99\xe5\xec\x70\x96\x53\x81\x54\x22\x48\x3a\x09\x10\xd8\x1c\x78\x28\x01\x57\x6c\x29\x97\x10\xaf\xb5\xab\xe0\x9a\x24\x06\x7e\x50\x31\xbd\xa3\x02\xee\x05\x5b\x50\xf1\x08\xdf\xf0\xb1\x41\xf2\x28\x08\x7c\x92\xeb\xcc\xee\x21\x87\x72\x5c\xa4\xc5\x66\x54\x87\x19\xce\x69\x14\x48\xb8\xfa\xa0\x3f\x7d\xfd\x62\x3e\x5f\xf9\xc4\xbd\x96\x16\x18\x97\x03\xb2\x8e\x5d\xb2\xb5\x30\x09\xc3\x00\x29\x1f\xb3\xa5\x88\xd0\x27\x2e\xc9\x6c\xe5\x74\x56\xc2\x8c\x4a\x94\x6c\x81\x63\xe4\x34\x12\x02\xb9\x54\xbb\xd3\xa5\xa4\x8b\x7b\x9f\xd4\xa5\xd1\xce\x1a\xa5\xdd\xff\xf1\x8c\x4d\x6d\xcb\x53\x71\x5a\xc4\x2f\x6a\xab\xd3\xfa\x58\xf9\x9e\xfe\xd1\x27\x65\xfd\x94\x26\xb1\x7a\xb1\xaf\x30\x09\xc2\x89\x4f\xea\x7c\x9b\xd8\xdf\xd6\xa8\xcc\x56\x95\x7e\xb6\x2a\x2e\xea\xdc\x9d\xe4\xa7\xba\xba\xec\xa2\x06\x78\xe8\x99\x3b\x9b\x3b\xd5\x98\xda\xb9\xff\x36\xa4\x3f\x25\x1e\x3c\x30\x79\x17\x46\x12\x44\xf8\xc0\x66\x37\x84\x9c\x89\x45\x5d\xd9\x4d\xdf\xa9\x8b\xc7\x6e\x3a\x88\x38\xfb\x1e\xe1\xf0\xfd\xd7\xda\xa9\x41\x78\x86\xc9\x78\x4b\x62\xaf\xee\xb8\xea\xec\x1f\x07\x12\x57\xd2\x27\x8d\x15\x30\x61\xb7\x8c\xcb\xe1\xdb\xf7\x95\xbe\x6d\x7f\x71\xc7\xea\x2f\x3b\x34\x1a\x3d\xcc\xcf\x54\x3f\x57\x27\xec\x25\xde\xc1\x0f\xc6\x67\xb8\x82\xae\x7f\x05\x21\x3f\x76\xe3\xba\x3b\xf1\xce\x3b\x98\xd9\xec\xc9\x6e\xaa\x75\x52\xaa\x36\x89\x3b\x1b\x01\xde\xdd\x33\x00\xa3\x9d\x6e\xdd\x1e\x56\x4f\x1b\x0d\x30\x0f\x05\xb2\x5b\xde\x74\xda\xcb\x03\x81\x73\x14\xc8\xa7\xd8\x6d\xf9\x75\x62\x3c\x02\x30\xce\x01\xc0\x3b\x1f\x84\xea\x86\x69\x26\x00\xf8\xc7\xd9\x23\x29\xe7\x85\x02\x68\x93\x25\xf9\xe9\x8d\x9f\xd3\x60\x89\x2d\xf2\x57\x91\xe4\xd6\x5c\x04\xbd\x7c\xfe\x16\xdf\xa6\xb8\x47\xfa\x83\x81\x3c\xef\x86\xfc\x0d\x00\x00\xff\xff\xb2\xa0\x14\x27\x2e\x05\x00\x00")
func _000001_add_messages_contactsUpDbSqlBytes() ([]byte, error) {
return bindataRead(
__000001_add_messages_contactsUpDbSql,
"000001_add_messages_contacts.up.db.sql",
)
}
var __000002_add_unread_to_user_messages_down_sql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x44\x8e\x31\x4e\xc5\x30\x10\x44\x7b\x4e\x31\x25\x14\xfe\x07\xa0\xa6\xa1\x4a\x03\x07\xd8\xd8\x43\x62\xc9\xde\xb5\xec\xb5\x42\x6e\x8f\x2c\x21\x68\xdf\xee\xbc\x99\x10\xf0\xb1\xbd\x6d\xcf\x92\xa4\xbe\xbc\x22\x8a\xaa\x39\x52\xb7\x86\x68\x65\x56\xc5\x95\xfd\xb4\xe9\x88\xd6\xee\xac\x07\x04\x2e\x7b\xe1\xe3\x29\x04\xbc\xd7\x56\x58\xa9\xff\xd7\x3c\x40\x19\x37\xf6\xe9\xb8\x08\x25\x13\xdc\x90\x66\x2b\x39\x8a\x13\x23\x9e\xac\xb2\xc2\x6e\xe8\x8c\x9d\x8b\x66\x7f\xe0\x53\x0b\xc7\x80\x9f\xec\x5c\x1e\xc1\xa8\xd2\x9d\x1d\x97\xdc\xeb\xfb\xa0\x2f\xf8\x27\xb0\x2f\x88\x82\xdf\x79\xf8\xaa\xfe\x9d\xf5\x13\x00\x00\xff\xff\x1c\xd9\x39\x28\xd2\x00\x00\x00")
func _000001_add_messages_contactsUpDbSql() (*asset, error) {
bytes, err := _000001_add_messages_contactsUpDbSqlBytes()
if err != nil {
return nil, err
}
func _000002_add_unread_to_user_messages_down_sql() ([]byte, error) {
return bindata_read(
__000002_add_unread_to_user_messages_down_sql,
"000002_add_unread_to_user_messages.down.sql",
)
info := bindataFileInfo{name: "000001_add_messages_contacts.up.db.sql", size: 1326, mode: os.FileMode(420), modTime: time.Unix(1562751940, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
var __000002_add_unread_to_user_messages_up_sql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x2c\xcc\x41\xca\xc2\x30\x10\x40\xe1\x7d\x4f\xf1\x2e\x50\xfe\x7f\xef\x2a\x9a\x08\xc2\x98\x82\x4c\xd6\x12\x70\x2c\xc5\xaa\x90\x21\x9e\x5f\x84\xae\xde\xea\x7d\x41\x34\x5d\xd0\xb0\x97\x44\x77\x6b\xd7\xa7\xb9\xd7\xd9\x7c\x00\x08\x31\x72\x98\xa4\x9c\x33\xf7\xb5\xce\xce\x29\x2b\x79\x52\x72\x11\x21\xa6\x63\x28\xa2\xfc\xef\x18\x47\x3e\xb5\x2d\xef\xee\x6c\xc0\x36\xac\xcb\xc3\x68\x56\x6f\x7f\xfd\xf5\xcb\xf0\x0d\x00\x00\xff\xff\xbc\x55\xda\x47\x71\x00\x00\x00")
var _docGo = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x2c\xc9\xb1\x0d\xc4\x20\x0c\x05\xd0\x9e\x29\xfe\x02\xd8\xfd\x6d\xe3\x4b\xac\x2f\x44\x82\x09\x78\x7f\xa5\x49\xfd\xa6\x1d\xdd\xe8\xd8\xcf\x55\x8a\x2a\xe3\x47\x1f\xbe\x2c\x1d\x8c\xfa\x6f\xe3\xb4\x34\xd4\xd9\x89\xbb\x71\x59\xb6\x18\x1b\x35\x20\xa2\x9f\x0a\x03\xa2\xe5\x0d\x00\x00\xff\xff\x60\xcd\x06\xbe\x4a\x00\x00\x00")
func _000002_add_unread_to_user_messages_up_sql() ([]byte, error) {
return bindata_read(
__000002_add_unread_to_user_messages_up_sql,
"000002_add_unread_to_user_messages.up.sql",
)
}
var _doc_go = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x2c\xc9\xb1\x0d\xc4\x20\x0c\x05\xd0\x9e\x29\xfe\x02\xd8\xfd\x6d\xe3\x4b\xac\x2f\x44\x82\x09\x78\x7f\xa5\x49\xfd\xa6\x1d\xdd\xe8\xd8\xcf\x55\x8a\x2a\xe3\x47\x1f\xbe\x2c\x1d\x8c\xfa\x6f\xe3\xb4\x34\xd4\xd9\x89\xbb\x71\x59\xb6\x18\x1b\x35\x20\xa2\x9f\x0a\x03\xa2\xe5\x0d\x00\x00\xff\xff\x60\xcd\x06\xbe\x4a\x00\x00\x00")
func doc_go() ([]byte, error) {
return bindata_read(
_doc_go,
func docGoBytes() ([]byte, error) {
return bindataRead(
_docGo,
"doc.go",
)
}
func docGo() (*asset, error) {
bytes, err := docGoBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{name: "doc.go", size: 74, mode: os.FileMode(420), modTime: time.Unix(1562307822, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
// Asset loads and returns the asset for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
func Asset(name string) ([]byte, error) {
cannonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[cannonicalName]; ok {
return f()
a, err := f()
if err != nil {
return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
}
return a.bytes, nil
}
return nil, fmt.Errorf("Asset %s not found", name)
}
// MustAsset is like Asset but panics when Asset would return an error.
// It simplifies safe initialization of global variables.
func MustAsset(name string) []byte {
a, err := Asset(name)
if err != nil {
panic("asset: Asset(" + name + "): " + err.Error())
}
return a
}
// AssetInfo loads and returns the asset info for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
func AssetInfo(name string) (os.FileInfo, error) {
cannonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[cannonicalName]; ok {
a, err := f()
if err != nil {
return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
}
return a.info, nil
}
return nil, fmt.Errorf("AssetInfo %s not found", name)
}
// AssetNames returns the names of the assets.
func AssetNames() []string {
names := make([]string, 0, len(_bindata))
@ -91,13 +181,12 @@ func AssetNames() []string {
}
// _bindata is a table, holding each asset generator, mapped to its name.
var _bindata = map[string]func() ([]byte, error){
"000001_add_messages_contacts.down.db.sql": _000001_add_messages_contacts_down_db_sql,
"000001_add_messages_contacts.up.db.sql": _000001_add_messages_contacts_up_db_sql,
"000002_add_unread_to_user_messages.down.sql": _000002_add_unread_to_user_messages_down_sql,
"000002_add_unread_to_user_messages.up.sql": _000002_add_unread_to_user_messages_up_sql,
"doc.go": doc_go,
var _bindata = map[string]func() (*asset, error){
"000001_add_messages_contacts.down.db.sql": _000001_add_messages_contactsDownDbSql,
"000001_add_messages_contacts.up.db.sql": _000001_add_messages_contactsUpDbSql,
"doc.go": docGo,
}
// AssetDir returns the file names below a certain
// directory embedded in the file by go-bindata.
// For example if you run go-bindata on data/... and data contains the
@ -127,25 +216,66 @@ func AssetDir(name string) ([]string, error) {
return nil, fmt.Errorf("Asset %s not found", name)
}
rv := make([]string, 0, len(node.Children))
for name := range node.Children {
rv = append(rv, name)
for childName := range node.Children {
rv = append(rv, childName)
}
return rv, nil
}
type _bintree_t struct {
Func func() ([]byte, error)
Children map[string]*_bintree_t
type bintree struct {
Func func() (*asset, error)
Children map[string]*bintree
}
var _bintree = &_bintree_t{nil, map[string]*_bintree_t{
"000001_add_messages_contacts.down.db.sql": &_bintree_t{_000001_add_messages_contacts_down_db_sql, map[string]*_bintree_t{
}},
"000001_add_messages_contacts.up.db.sql": &_bintree_t{_000001_add_messages_contacts_up_db_sql, map[string]*_bintree_t{
}},
"000002_add_unread_to_user_messages.down.sql": &_bintree_t{_000002_add_unread_to_user_messages_down_sql, map[string]*_bintree_t{
}},
"000002_add_unread_to_user_messages.up.sql": &_bintree_t{_000002_add_unread_to_user_messages_up_sql, map[string]*_bintree_t{
}},
"doc.go": &_bintree_t{doc_go, map[string]*_bintree_t{
}},
var _bintree = &bintree{nil, map[string]*bintree{
"000001_add_messages_contacts.down.db.sql": &bintree{_000001_add_messages_contactsDownDbSql, map[string]*bintree{}},
"000001_add_messages_contacts.up.db.sql": &bintree{_000001_add_messages_contactsUpDbSql, map[string]*bintree{}},
"doc.go": &bintree{docGo, map[string]*bintree{}},
}}
// RestoreAsset restores an asset under the given directory
func RestoreAsset(dir, name string) error {
data, err := Asset(name)
if err != nil {
return err
}
info, err := AssetInfo(name)
if err != nil {
return err
}
err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))
if err != nil {
return err
}
err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode())
if err != nil {
return err
}
err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
if err != nil {
return err
}
return nil
}
// RestoreAssets restores an asset under the given directory recursively
func RestoreAssets(dir, name string) error {
children, err := AssetDir(name)
// File
if err != nil {
return RestoreAsset(dir, name)
}
// Dir
for _, child := range children {
err = RestoreAssets(dir, filepath.Join(name, child))
if err != nil {
return err
}
}
return nil
}
func _filePath(dir, name string) string {
cannonicalName := strings.Replace(name, "\\", "/", -1)
return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...)
}

View File

@ -1,3 +1,5 @@
DROP TABLE chats;
DROP TABLE user_messages;
DROP TABLE user_contacts;
DROP TABLE history_user_contact_topic;
DROP TABLE membership_updates;
DROP TABLE chat_members;

View File

@ -1,6 +1,22 @@
CREATE TABLE IF NOT EXISTS chats (
id VARCHAR PRIMARY KEY NOT NULL,
name VARCHAR NOT NULL,
color VARCHAR NOT NULL DEFAULT '#a187d5',
type INT NOT NULL,
active BOOLEAN NOT NULL DEFAULT TRUE,
timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at_clock_value INT NOT NULL DEFAULT 0,
public_key BLOB,
unviewed_message_count INT NOT NULL DEFAULT 0,
last_clock_value INT NOT NULL DEFAULT 0,
last_message_content_type VARCHAR,
last_message_content VARCHAR
) WITHOUT ROWID;
CREATE TABLE IF NOT EXISTS user_messages (
id BLOB UNIQUE NOT NULL,
contact_id VARCHAR NOT NULL,
chat_id VARCHAR NOT NULL,
content_type VARCHAR,
message_type VARCHAR,
text TEXT,
@ -8,22 +24,23 @@ clock BIGINT,
timestamp BIGINT,
content_chat_id TEXT,
content_text TEXT,
public_key BLOB
public_key BLOB,
flags INT NOT NULL DEFAULT 0
);
CREATE INDEX contact_ids ON user_messages(contact_id);
CREATE INDEX chat_ids ON user_messages(chat_id);
CREATE TABLE IF NOT EXISTS user_contacts (
id VARCHAR PRIMARY KEY NOT NULL,
name VARCHAR NOT NULL,
topic TEXT NOT NULL,
type INT NOT NULL,
state INT,
public_key BLOB
) WITHOUT ROWID;
CREATE TABLE IF NOT EXISTS membership_updates (
id VARCHAR PRIMARY KEY NOT NULL,
data BLOB NOT NULL,
chat_id VARCHAR NOT NULL,
FOREIGN KEY (chat_id) REFERENCES chats(id)
) WITHOUT ROWID;
CREATE TABLE IF NOT EXISTS history_user_contact_topic (
synced BIGINT DEFAULT 0 NOT NULL,
contact_id VARCHAR UNIQUE NOT NULL,
FOREIGN KEY(contact_id) REFERENCES user_contacts(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS chat_members (
public_key BLOB NOT NULL,
chat_id VARCHAR NOT NULL,
admin BOOLEAN NOT NULL DEFAULT FALSE,
joined BOOLEAN NOT NULL DEFAULT FALSE,
FOREIGN KEY (chat_id) REFERENCES chats(id),
UNIQUE(chat_id, public_key));

View File

@ -1,4 +0,0 @@
-- TODO(adam): cannot drop column without copying a table.
-- Implement copying is easy but we need to duplicate schema
-- to recreate it. Unless there is a smarter way to get a schema
-- of an existing table.

View File

@ -1,2 +0,0 @@
ALTER TABLE user_messages
ADD COLUMN flags INT NOT NULL DEFAULT 0; -- various message flags like read/unread

View File

@ -50,55 +50,55 @@ func (mr *MockDatabaseMockRecorder) Close() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockDatabase)(nil).Close))
}
// ContactExist mocks base method
func (m *MockDatabase) ContactExist(arg0 client.Contact) (bool, error) {
// ChatExist mocks base method
func (m *MockDatabase) ChatExist(arg0 client.Chat) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ContactExist", arg0)
ret := m.ctrl.Call(m, "ChatExist", arg0)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ContactExist indicates an expected call of ContactExist
func (mr *MockDatabaseMockRecorder) ContactExist(arg0 interface{}) *gomock.Call {
// ChatExist indicates an expected call of ChatExist
func (mr *MockDatabaseMockRecorder) ChatExist(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContactExist", reflect.TypeOf((*MockDatabase)(nil).ContactExist), arg0)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ChatExist", reflect.TypeOf((*MockDatabase)(nil).ChatExist), arg0)
}
// Contacts mocks base method
func (m *MockDatabase) Contacts() ([]client.Contact, error) {
// Chats mocks base method
func (m *MockDatabase) Chats() ([]client.Chat, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Contacts")
ret0, _ := ret[0].([]client.Contact)
ret := m.ctrl.Call(m, "Chats")
ret0, _ := ret[0].([]client.Chat)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Contacts indicates an expected call of Contacts
func (mr *MockDatabaseMockRecorder) Contacts() *gomock.Call {
// Chats indicates an expected call of Chats
func (mr *MockDatabaseMockRecorder) Chats() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Contacts", reflect.TypeOf((*MockDatabase)(nil).Contacts))
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Chats", reflect.TypeOf((*MockDatabase)(nil).Chats))
}
// DeleteContact mocks base method
func (m *MockDatabase) DeleteContact(arg0 client.Contact) error {
// DeleteChat mocks base method
func (m *MockDatabase) DeleteChat(arg0 client.Chat) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteContact", arg0)
ret := m.ctrl.Call(m, "DeleteChat", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteContact indicates an expected call of DeleteContact
func (mr *MockDatabaseMockRecorder) DeleteContact(arg0 interface{}) *gomock.Call {
// DeleteChat indicates an expected call of DeleteChat
func (mr *MockDatabaseMockRecorder) DeleteChat(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteContact", reflect.TypeOf((*MockDatabase)(nil).DeleteContact), arg0)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteChat", reflect.TypeOf((*MockDatabase)(nil).DeleteChat), arg0)
}
// GetOneToOneChat mocks base method
func (m *MockDatabase) GetOneToOneChat(arg0 *ecdsa.PublicKey) (*client.Contact, error) {
func (m *MockDatabase) GetOneToOneChat(arg0 *ecdsa.PublicKey) (*client.Chat, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetOneToOneChat", arg0)
ret0, _ := ret[0].(*client.Contact)
ret0, _ := ret[0].(*client.Chat)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@ -110,10 +110,10 @@ func (mr *MockDatabaseMockRecorder) GetOneToOneChat(arg0 interface{}) *gomock.Ca
}
// GetPublicChat mocks base method
func (m *MockDatabase) GetPublicChat(arg0 string) (*client.Contact, error) {
func (m *MockDatabase) GetPublicChat(arg0 string) (*client.Chat, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetPublicChat", arg0)
ret0, _ := ret[0].(*client.Contact)
ret0, _ := ret[0].(*client.Chat)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@ -124,23 +124,8 @@ func (mr *MockDatabaseMockRecorder) GetPublicChat(arg0 interface{}) *gomock.Call
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPublicChat", reflect.TypeOf((*MockDatabase)(nil).GetPublicChat), arg0)
}
// Histories mocks base method
func (m *MockDatabase) Histories() ([]client.History, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Histories")
ret0, _ := ret[0].([]client.History)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Histories indicates an expected call of Histories
func (mr *MockDatabaseMockRecorder) Histories() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Histories", reflect.TypeOf((*MockDatabase)(nil).Histories))
}
// LastMessageClock mocks base method
func (m *MockDatabase) LastMessageClock(arg0 client.Contact) (int64, error) {
func (m *MockDatabase) LastMessageClock(arg0 client.Chat) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LastMessageClock", arg0)
ret0, _ := ret[0].(int64)
@ -155,7 +140,7 @@ func (mr *MockDatabaseMockRecorder) LastMessageClock(arg0 interface{}) *gomock.C
}
// Messages mocks base method
func (m *MockDatabase) Messages(arg0 client.Contact, arg1, arg2 time.Time) ([]*v1.Message, error) {
func (m *MockDatabase) Messages(arg0 client.Chat, arg1, arg2 time.Time) ([]*v1.Message, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Messages", arg0, arg1, arg2)
ret0, _ := ret[0].([]*v1.Message)
@ -170,7 +155,7 @@ func (mr *MockDatabaseMockRecorder) Messages(arg0, arg1, arg2 interface{}) *gomo
}
// NewMessages mocks base method
func (m *MockDatabase) NewMessages(arg0 client.Contact, arg1 int64) ([]*v1.Message, error) {
func (m *MockDatabase) NewMessages(arg0 client.Chat, arg1 int64) ([]*v1.Message, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NewMessages", arg0, arg1)
ret0, _ := ret[0].([]*v1.Message)
@ -184,22 +169,22 @@ func (mr *MockDatabaseMockRecorder) NewMessages(arg0, arg1 interface{}) *gomock.
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewMessages", reflect.TypeOf((*MockDatabase)(nil).NewMessages), arg0, arg1)
}
// SaveContacts mocks base method
func (m *MockDatabase) SaveContacts(arg0 []client.Contact) error {
// SaveChats mocks base method
func (m *MockDatabase) SaveChats(arg0 []client.Chat) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SaveContacts", arg0)
ret := m.ctrl.Call(m, "SaveChats", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// SaveContacts indicates an expected call of SaveContacts
func (mr *MockDatabaseMockRecorder) SaveContacts(arg0 interface{}) *gomock.Call {
// SaveChats indicates an expected call of SaveChats
func (mr *MockDatabaseMockRecorder) SaveChats(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveContacts", reflect.TypeOf((*MockDatabase)(nil).SaveContacts), arg0)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveChats", reflect.TypeOf((*MockDatabase)(nil).SaveChats), arg0)
}
// SaveMessages mocks base method
func (m *MockDatabase) SaveMessages(arg0 client.Contact, arg1 []*v1.Message) (int64, error) {
func (m *MockDatabase) SaveMessages(arg0 client.Chat, arg1 []*v1.Message) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SaveMessages", arg0, arg1)
ret0, _ := ret[0].(int64)
@ -214,7 +199,7 @@ func (mr *MockDatabaseMockRecorder) SaveMessages(arg0, arg1 interface{}) *gomock
}
// UnreadMessages mocks base method
func (m *MockDatabase) UnreadMessages(arg0 client.Contact) ([]*v1.Message, error) {
func (m *MockDatabase) UnreadMessages(arg0 client.Chat) ([]*v1.Message, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UnreadMessages", arg0)
ret0, _ := ret[0].([]*v1.Message)
@ -227,17 +212,3 @@ func (mr *MockDatabaseMockRecorder) UnreadMessages(arg0 interface{}) *gomock.Cal
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnreadMessages", reflect.TypeOf((*MockDatabase)(nil).UnreadMessages), arg0)
}
// UpdateHistories mocks base method
func (m *MockDatabase) UpdateHistories(arg0 []client.History) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateHistories", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateHistories indicates an expected call of UpdateHistories
func (mr *MockDatabaseMockRecorder) UpdateHistories(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateHistories", reflect.TypeOf((*MockDatabase)(nil).UpdateHistories), arg0)
}

View File

@ -7,30 +7,30 @@ import (
)
var (
errUnsupportedContactType = fmt.Errorf("unsupported contact type")
errUnsupportedChatType = fmt.Errorf("unsupported chat type")
)
func createSendOptions(c Contact) (opts protocol.SendOptions, err error) {
opts.ChatName = c.Topic
func createSendOptions(c Chat) (opts protocol.SendOptions, err error) {
opts.ChatName = c.Name
switch c.Type {
case ContactPublicRoom:
case ContactPrivate:
case PublicChat:
case OneToOneChat:
opts.Recipient = c.PublicKey
default:
err = errUnsupportedContactType
err = errUnsupportedChatType
}
return
}
func enhanceRequestOptions(c Contact, opts *protocol.RequestOptions) error {
func enhanceRequestOptions(c Chat, opts *protocol.RequestOptions) error {
var chatOptions protocol.ChatOptions
chatOptions.ChatName = c.Topic
chatOptions.ChatName = c.Name
switch c.Type {
case ContactPublicRoom:
case ContactPrivate:
case PublicChat:
case OneToOneChat:
chatOptions.Recipient = c.PublicKey
default:
return errUnsupportedContactType
return errUnsupportedChatType
}
opts.Chats = append(opts.Chats, chatOptions)

View File

@ -21,36 +21,36 @@ var (
// MessagesParams is an object with JSON-serializable parameters
// for Messages method.
type MessagesParams struct {
Contact
Chat
}
// SendParams is an object with JSON-serializable parameters for Send method.
type SendParams struct {
Contact
Chat
}
// RequestParams is an object with JSON-serializable parameters for Request method.
type RequestParams struct {
Contact
Chat
Limit int `json:"limit"`
From int64 `json:"from"`
To int64 `json:"to"`
}
// Contact
type Contact struct {
// Chat
type Chat struct {
Name string `json:"name"`
PublicKey hexutil.Bytes `json:"key"`
}
func parseContact(c Contact) (client.Contact, error) {
func parseChat(c Chat) (client.Chat, error) {
if len(c.PublicKey) != 0 {
c, err := client.CreateContactPrivate(c.Name, c.PublicKey.String(), client.ContactAdded)
c, err := client.CreateOneToOneChat(c.Name, c.PublicKey.String())
if err != nil {
return c, err
}
}
return client.CreateContactPublicRoom(c.Name, client.ContactAdded), nil
return client.CreatePublicChat(c.Name), nil
}
// PublicAPI provides an JSON-RPC API to interact with
@ -66,15 +66,15 @@ func NewPublicAPI(s *Service) *PublicAPI {
}
}
// Send sends payload to specified contact.
// Contact should be added before sending message,
// Send sends payload to specified chat.
// Chat should be added before sending message,
// otherwise error will be received.
func (api *PublicAPI) Send(ctx context.Context, contact Contact, payload string) (hexutil.Bytes, error) {
func (api *PublicAPI) Send(ctx context.Context, chat Chat, payload string) (hexutil.Bytes, error) {
if api.service.messenger == nil {
return nil, ErrMessengerNotSet
}
c, err := parseContact(contact)
c, err := parseChat(chat)
if err != nil {
return nil, err
}
@ -86,8 +86,7 @@ func (api *PublicAPI) Request(ctx context.Context, params RequestParams) (err er
if api.service.messenger == nil {
return ErrMessengerNotSet
}
c, err := parseContact(params.Contact)
c, err := parseChat(params.Chat)
if err != nil {
return err
}
@ -100,9 +99,9 @@ func (api *PublicAPI) Request(ctx context.Context, params RequestParams) (err er
}
// Messages is a high-level subscription-based RPC method.
// It joins a chat for selected contact and streams
// It joins a chat for selected chat and streams
// events for that chat.
func (api *PublicAPI) Messages(ctx context.Context, contact client.Contact) (*rpc.Subscription, error) {
func (api *PublicAPI) Messages(ctx context.Context, chat client.Chat) (*rpc.Subscription, error) {
notifier, supported := rpc.NotifierFromContext(ctx)
if !supported {
return nil, rpc.ErrNotificationsUnsupported
@ -120,9 +119,9 @@ func (api *PublicAPI) Messages(ctx context.Context, contact client.Contact) (*rp
// Subscription needs to be created
// before any events are delivered.
sub := api.broadcaster.Subscribe(contact)
sub := api.broadcaster.Subscribe(chat)
err := api.service.messenger.Join(ctx, contact)
err := api.service.messenger.Join(ctx, chat)
if err != nil {
api.broadcaster.Unsubscribe(sub)
return nil, err
@ -132,9 +131,9 @@ func (api *PublicAPI) Messages(ctx context.Context, contact client.Contact) (*rp
go func() {
defer func() {
err := api.service.messenger.Leave(contact)
err := api.service.messenger.Leave(chat)
if err != nil {
log.Printf("failed to leave chat for '%s' contact", contact)
log.Printf("failed to leave chat for '%s' chat", chat)
}
}()
defer api.broadcaster.Unsubscribe(sub)
@ -170,16 +169,16 @@ func (api *PublicAPI) RequestAll(ctx context.Context, newest bool) error {
return api.service.messenger.RequestAll(ctx, newest)
}
// AddContact will ensure that contact is added to messenger database and new stream spawned for a contact if needed.
func (api *PublicAPI) AddContact(ctx context.Context, contact Contact) (err error) {
// AddChat will ensure that chat is added to messenger database and new stream spawned for a chat if needed.
func (api *PublicAPI) AddChat(ctx context.Context, chat Chat) (err error) {
if api.service.messenger == nil {
return ErrMessengerNotSet
}
c, err := parseContact(contact)
c, err := parseChat(chat)
if err != nil {
return err
}
err = api.service.messenger.AddContact(c)
err = api.service.messenger.AddChat(c)
if err != nil {
return err
}
@ -188,11 +187,11 @@ func (api *PublicAPI) AddContact(ctx context.Context, contact Contact) (err erro
// ReadContactMessages read contact messages starting from offset.
// To read all offset should be zero. To read only new set offset to total number of previously read messages.
func (api *PublicAPI) ReadContactMessages(ctx context.Context, contact Contact, offset int64) (rst []*protocol.Message, err error) {
func (api *PublicAPI) ReadChatMessages(ctx context.Context, chat Chat, offset int64) (rst []*protocol.Message, err error) {
if api.service.messenger == nil {
return nil, ErrMessengerNotSet
}
c, err := parseContact(contact)
c, err := parseChat(chat)
if err != nil {
return nil, err
}

View File

@ -38,7 +38,7 @@ func TestPublicAPISend(t *testing.T) {
defer func() { go discardStop(aNode) }() // Stop() is slow so do it in a goroutine
data := []byte("some payload")
contact := Contact{
contact := Chat{
Name: "test-chat",
}
result := hexutil.Bytes("abc")
@ -84,7 +84,7 @@ func TestPublicAPIRequest(t *testing.T) {
now := time.Now().Unix()
params := RequestParams{
Contact: Contact{
Chat: Chat{
Name: "test-chat",
},
Limit: 100,
@ -126,6 +126,8 @@ func TestPublicAPIMessages(t *testing.T) {
require.NoError(t, err)
defer func() { go discardStop(aNode) }() // Stop() is slow so do it in a goroutine
database.EXPECT().SaveChats(gomock.Any()).Return(nil)
proto.EXPECT().
LoadChats(
gomock.Any(),
@ -135,17 +137,9 @@ func TestPublicAPIMessages(t *testing.T) {
).
Return(nil)
proto.EXPECT().
Request(gomock.Any(), gomock.Any()).
Return(nil)
database.EXPECT().
UpdateHistories(gomock.Any()).
Return(nil)
messages := make(chan protocol.Message)
// The first argument is a name of the method to use for subscription.
_, err = rpcClient.Subscribe(context.Background(), StatusSecureMessagingProtocolAPIName, messages, "messages", client.Contact{Type: 1, Name: "test-chat"})
_, err = rpcClient.Subscribe(context.Background(), StatusSecureMessagingProtocolAPIName, messages, "messages", client.Chat{Type: 1, Name: "test-chat"})
require.NoError(t, err)
}

View File

@ -11,14 +11,14 @@ type publisher interface {
type broadcaster struct {
source publisher
subs map[client.Contact][]chan interface{}
subs map[client.Chat][]chan interface{}
cancel chan struct{}
}
func newBroadcaster(source publisher) *broadcaster {
b := broadcaster{
source: source,
subs: make(map[client.Contact][]chan interface{}),
subs: make(map[client.Chat][]chan interface{}),
cancel: make(chan struct{}),
}
@ -36,8 +36,8 @@ func (b *broadcaster) start(cancel chan struct{}) {
var subs []chan interface{}
switch v := item.Interface.(type) {
case client.EventWithContact:
subs = b.subs[v.GetContact()]
case client.EventWithChat:
subs = b.subs[v.GetChat()]
}
// TODO: figure out if we need anything here
@ -54,7 +54,7 @@ func (b *broadcaster) start(cancel chan struct{}) {
}
}
func (b *broadcaster) Subscribe(c client.Contact) <-chan interface{} {
func (b *broadcaster) Subscribe(c client.Chat) <-chan interface{} {
// TODO: think about whether this can be a buffered channel.
sub := make(chan interface{})
b.subs[c] = append(b.subs[c], sub)

View File

@ -11,7 +11,7 @@ import (
// Type of views.
const (
ViewContacts = "contacts"
ViewChats = "chats"
ViewChat = "chat"
ViewInput = "input"
ViewNotification = "notification"