diff --git a/README.md b/README.md
index d310575..260dce3 100644
--- a/README.md
+++ b/README.md
@@ -75,8 +75,8 @@ cd $(go env GOPATH)/src/github.com/cryptag/leapchat/db
chmod a+rx ~/
createdb
sudo -u $USER bash init_sql.sh
-wget https://github.com/PostgREST/postgrest/releases/download/v7.0.0/postgrest-v7.0.0-osx.tar.xz
-tar xvf postgrest-v7.0.0-osx.tar.xz
+wget https://github.com/PostgREST/postgrest/releases/download/v10.1.1/postgrest-v10.1.1-macos-x64.tar.xz
+tar xvf postgrest-v10.1.1-macos-x64.tar.xz
./postgrest postgrest.conf
```
@@ -157,8 +157,8 @@ and have `postgrest` connect to Postgres:
cd $(go env GOPATH)/src/github.com/cryptag/leapchat/db
chmod a+rx ~/
sudo -u postgres bash init_sql.sh
-wget https://github.com/PostgREST/postgrest/releases/download/v7.0.0/postgrest-v7.0.0-ubuntu.tar.xz
-tar xvf postgrest-v7.0.0-ubuntu.tar.xz
+wget https://github.com/PostgREST/postgrest/releases/download/v10.1.1/postgrest-v10.1.1-linux-static-x64.tar.xz
+tar xvf postgrest-v10.1.1-linux-static-x64.tar.xz
./postgrest postgrest.conf
```
diff --git a/db/sql/table03_todo_lists.sql b/db/sql/table03_todo_lists.sql
new file mode 100644
index 0000000..d8b5364
--- /dev/null
+++ b/db/sql/table03_todo_lists.sql
@@ -0,0 +1,13 @@
+CREATE TABLE todo_lists (
+ id uuid NOT NULL UNIQUE PRIMARY KEY DEFAULT uuid_generate_v4(),
+ room_id text NOT NULL REFERENCES rooms ON DELETE CASCADE,
+
+ -- This is called 'title_enc' (encrypted title) but stores
+ -- miniLock ciphertext, which can also store metadata
+ title_enc text NOT NULL -- base64-encoded ciphertext
+
+ -- ASSUMPTION: todo lists don't expire and must be manually deleted by the user
+
+ -- ASSUMPTION: no timestamp needed; better for user metadata privacy to not store it
+);
+ALTER TABLE todo_lists OWNER TO superuser;
diff --git a/db/sql/table04_tasks.sql b/db/sql/table04_tasks.sql
new file mode 100644
index 0000000..e0fb92f
--- /dev/null
+++ b/db/sql/table04_tasks.sql
@@ -0,0 +1,16 @@
+CREATE TABLE tasks (
+ id uuid NOT NULL UNIQUE PRIMARY KEY DEFAULT uuid_generate_v4(),
+ room_id text NOT NULL REFERENCES rooms ON DELETE CASCADE,
+
+ -- This is called 'title_enc' (encrypted title) but stores
+ -- miniLock ciphertext, which can also store metadata
+ title_enc text NOT NULL, -- base64-encoded ciphertext
+
+ list_id uuid NOT NULL REFERENCES todo_lists ON DELETE CASCADE,
+ index double precision NOT NULL -- index in the todo list with list_id
+
+ -- ASSUMPTION: tasks don't expire and must be manually deleted by the user
+
+ -- ASSUMPTION: no timestamp needed; better for user metadata privacy to not store it
+);
+ALTER TABLE tasks OWNER TO superuser;
diff --git a/messages.go b/messages.go
index 2000860..4a4ca83 100644
--- a/messages.go
+++ b/messages.go
@@ -11,6 +11,10 @@ import (
type Message []byte
+type TodoList struct {
+ TitleEnc string `json:"title_enc"` // base64-encoded, encrypted title
+}
+
type OutgoingPayload struct {
Ephemeral []Message `json:"ephemeral"`
FromServer FromServer `json:"from_server,omitempty"`
@@ -26,8 +30,10 @@ type ToServer struct {
}
type IncomingPayload struct {
- Ephemeral []Message `json:"ephemeral"`
- ToServer ToServer `json:"to_server"`
+ Ephemeral []Message `json:"ephemeral"`
+ TodoLists []TodoList `json:"todo_lists"`
+ // Tasks []string `json:"tasks"`
+ ToServer ToServer `json:"to_server"`
}
func WSMessagesHandler(rooms *RoomManager) func(w http.ResponseWriter, r *http.Request) {
@@ -92,12 +98,31 @@ func messageReader(room *Room, client *Client) {
continue
}
- err = room.AddMessages(payload.Ephemeral, payload.ToServer.TTL)
- if err != nil {
- log.Debugf("Error from AddMessages: %v", err)
- continue
+ if len(payload.Ephemeral) > 0 {
+ err = room.AddMessages(payload.Ephemeral, payload.ToServer.TTL)
+ if err != nil {
+ log.Debugf("Error from AddMessages: %v", err)
+ continue
+ }
}
+ if len(payload.TodoLists) > 0 {
+ jsonToBroadcast, err := room.AddTodoLists(payload.TodoLists)
+ if err != nil {
+ log.Debugf("Error from AddTodoLists: %v", err)
+ continue
+ }
+ room.BroadcastJSON(client, jsonToBroadcast)
+ }
+
+ // if len(payload.Tasks) > 0 {
+ // err = room.AddTasks(payload.Tasks)
+ // if err != nil {
+ // log.Debugf("Error from AddTasks: %v", err)
+ // continue
+ // }
+ // }
+
room.BroadcastMessages(client, payload.Ephemeral...)
case websocket.BinaryMessage:
diff --git a/pg_types.go b/pg_types.go
index 83b9bd7..475f758 100644
--- a/pg_types.go
+++ b/pg_types.go
@@ -127,6 +127,12 @@ type PGMessage struct {
Created *time.Time `json:"created,omitempty"`
}
+type PGTodoList struct {
+ ID *string `json:"id,omitempty"`
+ RoomID string `json:"room_id"`
+ TitleEnc string `json:"title_enc"`
+}
+
type pgPostMessage PGMessage
func (msg *PGMessage) MarshalJSON() ([]byte, error) {
diff --git a/room.go b/room.go
index 754ebfd..5f1044f 100644
--- a/room.go
+++ b/room.go
@@ -1,11 +1,14 @@
package main
import (
+ "bytes"
"encoding/base64"
"encoding/hex"
"encoding/json"
+ "errors"
"fmt"
"io/ioutil"
+ "net/http"
"sync"
"time"
@@ -110,17 +113,57 @@ func (r *Room) GetMessages() ([]Message, error) {
}
func (r *Room) AddMessages(msgs []Message, ttlSecs *int) error {
- post := make(PGMessages, len(msgs))
+ toPost := make(PGMessages, len(msgs))
for i := 0; i < len(msgs); i++ {
- post[i] = &PGMessage{
+ toPost[i] = &PGMessage{
RoomID: r.ID,
MessageEnc: string(msgs[i]),
TTL: ttlSecs,
}
}
- return post.Create(r.pgClient)
+ return toPost.Create(r.pgClient)
+}
+
+func (r *Room) AddTodoLists(lists []TodoList) (toBroadcast []byte, err error) {
+ toPost := make([]PGTodoList, len(lists))
+
+ for i := 0; i < len(lists); i++ {
+ toPost[i].RoomID = r.ID
+ toPost[i].TitleEnc = lists[i].TitleEnc
+ }
+
+ return MarshalToPostgrestResp("/todo_lists", toPost, http.StatusCreated)
+}
+
+func MarshalToPostgrestResp(urlSuffix string, toPost interface{}, wantedCode int) (respBytes []byte, err error) {
+ toPostBytes, err := json.Marshal(toPost)
+ if err != nil {
+ return nil, err
+ }
+
+ r := bytes.NewReader(toPostBytes)
+ req, _ := http.NewRequest("POST", POSTGREST_BASE_URL+urlSuffix, r)
+ req.Header.Add("Prefer", "return=representation")
+ req.Header.Add("Content-Type", "application/json")
+
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ respBytes, err = ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return nil, err
+ }
+
+ if resp.StatusCode != wantedCode {
+ return nil, errors.New(string(respBytes))
+ }
+
+ return
}
func byteaToBytes(hexdata string) ([]byte, error) {
@@ -183,6 +226,20 @@ func (r *Room) BroadcastMessages(sender *Client, msgs ...Message) {
}
}
+func (r *Room) BroadcastJSON(sender *Client, jsonToBroadcast []byte) {
+ r.clientLock.RLock()
+ defer r.clientLock.RUnlock()
+
+ for _, client := range r.Clients {
+ go func(client *Client) {
+ err := client.SendJSON(jsonToBroadcast)
+ if err != nil {
+ log.Debugf("Error sending message. Err: %s", err)
+ }
+ }(client)
+ }
+}
+
func (r *Room) DeleteAllMessages() error {
resp, err := r.pgClient.Delete("/messages?room_id=eq." + r.ID)
if err != nil {
@@ -224,9 +281,6 @@ type Client struct {
}
func (c *Client) SendMessages(msgs ...Message) error {
- c.writeLock.Lock()
- defer c.writeLock.Unlock()
-
outgoing := OutgoingPayload{Ephemeral: msgs}
body, err := json.Marshal(outgoing)
@@ -234,9 +288,16 @@ func (c *Client) SendMessages(msgs ...Message) error {
return err
}
- err = c.wsConn.WriteMessage(websocket.TextMessage, body)
+ return c.SendJSON(body)
+}
+
+func (c *Client) SendJSON(body []byte) error {
+ c.writeLock.Lock()
+ defer c.writeLock.Unlock()
+
+ err := c.wsConn.WriteMessage(websocket.TextMessage, body)
if err != nil {
- log.Debugf("Error sending message to client. Removing client from room. Err: %s", err)
+ log.Debugf("Error sending JSON to client. Removing client from room. Err: %s", err)
c.room.RemoveClient(c)
return err
}
diff --git a/src/components/App.js b/src/components/App.js
index eab8253..55615b7 100644
--- a/src/components/App.js
+++ b/src/components/App.js
@@ -12,6 +12,7 @@ import {
} from '../actions/alertActions';
import Header from './layout/Header';
+import RightPanel from './layout/RightPanel';
import ChatContainer from './chat/ChatContainer';
@@ -170,6 +171,8 @@ class App extends Component {
isAudioEnabled={isAudioEnabled}
onSetIsAudioEnabled={this.onSetIsAudioEnabled} />
+