package storage import ( "cwtch.im/cwtch/protocol/groups" "database/sql" "encoding/base64" "fmt" "git.openprivacy.ca/openprivacy/log" _ "github.com/mattn/go-sqlite3" // sqlite3 driver ) // MessageStoreInterface defines an interface to interact with a store of cwtch messages. type MessageStoreInterface interface { AddMessage(groups.EncryptedGroupMessage) FetchMessages() []*groups.EncryptedGroupMessage FetchMessagesFrom(signature []byte) []*groups.EncryptedGroupMessage } // SqliteMessageStore is an sqlite3 backed message store type SqliteMessageStore struct { database *sql.DB // Some prepared queries... preparedInsertStatement *sql.Stmt // A Stmt is safe for concurrent use by multiple goroutines. preparedFetchFromQuery *sql.Stmt } // Close closes the underlying sqlite3 database to further changes func (s *SqliteMessageStore) Close() { s.preparedInsertStatement.Close() s.preparedFetchFromQuery.Close() s.database.Close() } // AddMessage implements the MessageStoreInterface AddMessage for sqlite message store func (s *SqliteMessageStore) AddMessage(message groups.EncryptedGroupMessage) { // ignore this clearly invalid message... if len(message.Signature) == 0 { return } stmt, err := s.preparedInsertStatement.Exec(base64.StdEncoding.EncodeToString(message.Signature), base64.StdEncoding.EncodeToString(message.Ciphertext)) if err != nil { log.Errorf("%v %q", stmt, err) return } } // FetchMessages implements the MessageStoreInterface FetchMessages for sqlite message store func (s SqliteMessageStore) FetchMessages() []*groups.EncryptedGroupMessage { rows, err := s.database.Query("SELECT id, signature,ciphertext from messages") if err != nil { log.Errorf("%v", err) return nil } defer rows.Close() return s.compileRows(rows) } // FetchMessagesFrom implements the MessageStoreInterface FetchMessagesFrom for sqlite message store func (s SqliteMessageStore) FetchMessagesFrom(signature []byte) []*groups.EncryptedGroupMessage { // If signature is empty then treat this as a complete sync request if signature == nil || len(signature) == 0 { return s.FetchMessages() } rows, err := s.preparedFetchFromQuery.Query(base64.StdEncoding.EncodeToString(signature)) if err != nil { log.Errorf("%v", err) return nil } defer rows.Close() messages := s.compileRows(rows) // if we don't have *any* messages then either the signature next existed // or the server purged it...either way treat this as a full sync... if len(messages) < 1 { return s.FetchMessages() } return messages } func (s *SqliteMessageStore) compileRows(rows *sql.Rows) []*groups.EncryptedGroupMessage { var messages []*groups.EncryptedGroupMessage for rows.Next() { var id int var signature string var ciphertext string err := rows.Scan(&id, &signature, &ciphertext) if err != nil { log.Errorf("Error fetching row %v", err) } rawSignature, _ := base64.StdEncoding.DecodeString(signature) rawCiphertext, _ := base64.StdEncoding.DecodeString(ciphertext) messages = append(messages, &groups.EncryptedGroupMessage{ Signature: rawSignature, Ciphertext: rawCiphertext, }) } return messages } // InitializeSqliteMessageStore creates a database `dbfile` with the necessary tables (if it doesn't already exist) // and returns an open database func InitializeSqliteMessageStore(dbfile string) (*SqliteMessageStore, error) { db, err := sql.Open("sqlite3", dbfile) if err != nil { log.Errorf("database %v cannot be created or opened %v", dbfile, err) return nil, fmt.Errorf("database %v cannot be created or opened: %v", dbfile, err) } sqlStmt := `CREATE TABLE IF NOT EXISTS messages (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, signature TEXT UNIQUE NOT NULL, ciphertext TEXT NOT NULL);` _, err = db.Exec(sqlStmt) if err != nil { db.Close() log.Errorf("%q: %s", err, sqlStmt) return nil, fmt.Errorf("%s: %q", sqlStmt, err) } log.Infof("Database Initialized") slms := new(SqliteMessageStore) slms.database = db sqlStmt = `INSERT INTO messages(signature, ciphertext) values (?,?);` stmt, err := slms.database.Prepare(sqlStmt) if err != nil { log.Errorf("%q: %s", err, sqlStmt) return nil, fmt.Errorf("%s: %q", sqlStmt, err) } slms.preparedInsertStatement = stmt query, err := slms.database.Prepare("SELECT id, signature,ciphertext FROM messages WHERE id>=(SELECT id FROM messages WHERE signature=(?));") if err != nil { log.Errorf("%v", err) return nil, fmt.Errorf("%s: %q", sqlStmt, err) } slms.preparedFetchFromQuery = query return slms, nil }