package client import ( "database/sql" "math" "os" "path/filepath" "forge.redroom.link/yves/meowlib" "github.com/google/uuid" _ "github.com/mattn/go-sqlite3" "google.golang.org/protobuf/proto" ) func StoreMessage(peer *Peer, usermessage *meowlib.UserMessage, filenames []string, password string) error { var dbid string cfg := GetConfig() identity := cfg.GetIdentity() // If no db/no ID create DB + Tablz // TODO : if file size > X new db if len(peer.DbIds) == 0 { dbid = uuid.NewString() peer.DbIds = []string{dbid} identity.Save() identity.CreateFolder() file, err := os.Create(filepath.Join(cfg.StoragePath, identity.Uuid, dbid+GetConfig().DbSuffix)) if err != nil { return err } file.Close() peer.DbIds = append(peer.DbIds, dbid) sqliteDatabase, _ := sql.Open("sqlite3", filepath.Join(cfg.StoragePath, identity.Uuid, dbid+GetConfig().DbSuffix)) // Open the created SQLite File err = createMessageTable(sqliteDatabase) if err != nil { return err } sqliteDatabase.Close() } else { dbid = peer.DbIds[len(peer.DbIds)-1] } // Open Db db, _ := sql.Open("sqlite3", filepath.Join(cfg.StoragePath, identity.Uuid, dbid+GetConfig().DbSuffix)) // Open the created SQLite File defer db.Close() // Detach Files hiddenFilenames := []string{} if len(usermessage.Files) > 0 { for _, f := range usermessage.Files { hiddenFilename := uuid.NewString() // Cypher file encData, err := meowlib.SymEncrypt(password, f.Data) if err != nil { return err } if _, err := os.Stat(filepath.Join(cfg.StoragePath, identity.Uuid, "securefiles")); os.IsNotExist(err) { err = os.MkdirAll(filepath.Join(cfg.StoragePath, identity.Uuid, "securefiles"), 0755) if err != nil { return err } } os.WriteFile(filepath.Join(cfg.StoragePath, identity.Uuid, "securefiles", hiddenFilename), encData, 0600) hiddenFilenames = append(hiddenFilenames, filepath.Join(cfg.StoragePath, identity.Uuid, "securefiles", hiddenFilename)) // replace f.Data by uuid filename f.Data = []byte(filepath.Join(cfg.StoragePath, identity.Uuid, "securefiles", hiddenFilename)) } } outbound := true if usermessage.From == peer.ContactPublicKey { outbound = false } // Convert UserMessage to DbMessage dbm := UserMessageToDbMessage(outbound, usermessage, hiddenFilenames) // Encrypt message out, err := proto.Marshal(dbm) if err != nil { return err } encData, err := meowlib.SymEncrypt(password, out) if err != nil { return err } // Insert message insertMessageSQL := `INSERT INTO message(m) VALUES (?) RETURNING ID` statement, err := db.Prepare(insertMessageSQL) // Prepare statement. if err != nil { return err } result, err := statement.Exec(encData) if err != nil { return err } id, err := result.LastInsertId() if err != nil { return err } ium := DbMessageToInternalUserMessage(id, dbid, dbm) peer.LastMessage = ium identity.Save() return nil } // Get new messages from a peer func GetNewMessages(peer *Peer, lastDbId int, password string) ([]*InternalUserMessage, error) { var messages []*InternalUserMessage cfg := GetConfig() identity := cfg.GetIdentity() // handle no db yet if len(peer.DbIds) == 0 { return messages, nil } fileidx := len(peer.DbIds) - 1 // There fileidx should provide the db that we need (unless wantMore overlaps the next DB) db, _ := sql.Open("sqlite3", filepath.Join(cfg.StoragePath, identity.Uuid, peer.DbIds[fileidx]+GetConfig().DbSuffix)) // Open the created SQLite File defer db.Close() // if it's first app query, it won't hold a lastIndex, so let's start from end if lastDbId == 0 { lastDbId = math.MaxInt64 } stm, err := db.Prepare("SELECT id, m FROM message WHERE id > ? ORDER BY id DESC") if err != nil { return nil, err } defer stm.Close() rows, err := stm.Query(lastDbId) if err != nil { return nil, err } defer rows.Close() for rows.Next() { var ium *InternalUserMessage var dbm meowlib.DbMessage var id int64 var m []byte err = rows.Scan(&id, &m) if err != nil { return nil, err } decdata, err := meowlib.SymDecrypt(password, m) if err != nil { return nil, err } err = proto.Unmarshal(decdata, &dbm) if err != nil { return nil, err } ium = DbMessageToInternalUserMessage(id, peer.DbIds[fileidx], &dbm) ium.Dbid = id ium.Dbfile = peer.DbIds[fileidx] messages = append(messages, ium) } // TODO DB overlap return messages, nil } // Get old messages from a peer func GetMessagesHistory(peer *Peer, inAppMsgCount int, lastDbId int, wantMore int, password string) ([]InternalUserMessage, error) { var messages []InternalUserMessage // handle no db yet if len(peer.DbIds) == 0 { return messages, nil } fileidx := len(peer.DbIds) - 1 // initialize count with last db message count countStack, err := getMessageCount(peer.DbIds[fileidx]) if err != nil { return nil, err } // while the db message count < what we already have in app, step to next db file for inAppMsgCount > countStack { fileidx-- if fileidx < 0 { return nil, nil } newCount, err := getMessageCount(peer.DbIds[fileidx]) if err != nil { return nil, err } countStack += newCount } // There fileidx should provide the db that we need (unless wantMore overlaps the next DB) db, _ := sql.Open("sqlite3", filepath.Join(GetConfig().StoragePath, GetConfig().GetIdentity().Uuid, peer.DbIds[fileidx]+GetConfig().DbSuffix)) // Open the created SQLite File defer db.Close() // if it's first app query, it won't hold a lastIndex, so let's start from end if lastDbId == 0 { lastDbId = math.MaxInt64 } stm, err := db.Prepare("SELECT id, m FROM message WHERE id < ? ORDER BY id DESC LIMIT ?") if err != nil { return nil, err } defer stm.Close() rows, err := stm.Query(lastDbId, wantMore) if err != nil { return nil, err } defer rows.Close() for rows.Next() { var ium *InternalUserMessage var dbm meowlib.DbMessage var id int64 var m []byte err = rows.Scan(&id, &m) if err != nil { return nil, err } decdata, err := meowlib.SymDecrypt(password, m) if err != nil { return nil, err } err = proto.Unmarshal(decdata, &dbm) if err != nil { return nil, err } ium = DbMessageToInternalUserMessage(id, peer.DbIds[fileidx], &dbm) ium.Dbid = id ium.Dbfile = peer.DbIds[fileidx] messages = append(messages, *ium) } // TODO DB overlap return messages, nil } // Get old messages from a peer func GetMessagePreview(peer *Peer, dbFile string, dbId int64, password string) ([]byte, error) { // There fileidx should provide the db that we need (unless wantMore overlaps the next DB) db, _ := sql.Open("sqlite3", filepath.Join(GetConfig().StoragePath, GetConfig().GetIdentity().Uuid, dbFile+GetConfig().DbSuffix)) // Open the created SQLite File defer db.Close() stm, err := db.Prepare("SELECT id, m FROM message WHERE id=?") if err != nil { return nil, err } defer stm.Close() rows, err := stm.Query(dbId) if err != nil { return nil, err } defer rows.Close() var dbm meowlib.DbMessage for rows.Next() { var id int64 var m []byte err = rows.Scan(&id, &m) if err != nil { return nil, err } decdata, err := meowlib.SymDecrypt(password, m) if err != nil { return nil, err } err = proto.Unmarshal(decdata, &dbm) if err != nil { return nil, err } } return FilePreview(dbm.FilePaths[0], password) } func FilePreview(filename string, password string) ([]byte, error) { // get the hidden file encData, err := os.ReadFile(filename) if err != nil { return nil, err } // decrypt the file data, err := meowlib.SymDecrypt(password, encData) if err != nil { return nil, err } return data, nil } // make an image from the files content (loads the first image, or build a more complex view) func InternalUserMessagePreview(msg *InternalUserMessage, password string) ([]byte, error) { // get the hidden file name if len(msg.FilePaths) == 0 { return nil, nil } return FilePreview(msg.FilePaths[0], password) } func getMessageCount(dbid string) (int, error) { db, _ := sql.Open("sqlite3", filepath.Join(GetConfig().StoragePath, GetConfig().GetIdentity().Uuid, dbid+GetConfig().DbSuffix)) // Open the created SQLite File defer db.Close() var count int query := "SELECT COUNT(*) FROM message" err := db.QueryRow(query).Scan(&count) if err != nil { return 0, err } return count, nil } func createMessageTable(db *sql.DB) error { createMessageTableSQL := `CREATE TABLE message ( "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "m" BLOB);` // SQL Statement for Create Table statement, err := db.Prepare(createMessageTableSQL) // Prepare SQL Statement if err != nil { return err } statement.Exec() // Execute SQL Statements return nil } func createServerTable(db *sql.DB) error { createServerTableSQL := `CREATE TABLE servers ( "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "country" varchar(2), "public" bool, "uptime" int, "bandwith" float, "load" float, "url" varchar(2000) "name" varchar(255); "description" varchar(5000) "publickey" varchar(10000) )` // SQL Statement for Create Table statement, err := db.Prepare(createServerTableSQL) // Prepare SQL Statement if err != nil { return err } statement.Exec() // Execute SQL Statements return nil }