| @ -1 +1 @@ | |||||
| /mobile/nsconfig.json | |||||
| /mobile/.env | |||||
| @ -0,0 +1,83 @@ | |||||
| package Messages | |||||
| import ( | |||||
| "encoding/json" | |||||
| "net/http" | |||||
| "net/url" | |||||
| "strings" | |||||
| "git.tovijaeschke.xyz/tovi/Envelope/Backend/Api/Auth" | |||||
| "git.tovijaeschke.xyz/tovi/Envelope/Backend/Database" | |||||
| "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" | |||||
| ) | |||||
| func EncryptedConversationList(w http.ResponseWriter, r *http.Request) { | |||||
| var ( | |||||
| userConversations []Models.UserConversation | |||||
| userSession Models.Session | |||||
| returnJson []byte | |||||
| err error | |||||
| ) | |||||
| userSession, err = Auth.CheckCookie(r) | |||||
| if err != nil { | |||||
| http.Error(w, "Forbidden", http.StatusUnauthorized) | |||||
| return | |||||
| } | |||||
| userConversations, err = Database.GetUserConversationsByUserId( | |||||
| userSession.UserID.String(), | |||||
| ) | |||||
| if err != nil { | |||||
| http.Error(w, "Error", http.StatusInternalServerError) | |||||
| return | |||||
| } | |||||
| returnJson, err = json.MarshalIndent(userConversations, "", " ") | |||||
| if err != nil { | |||||
| http.Error(w, "Error", http.StatusInternalServerError) | |||||
| return | |||||
| } | |||||
| w.WriteHeader(http.StatusOK) | |||||
| w.Write(returnJson) | |||||
| } | |||||
| func EncryptedConversationDetailsList(w http.ResponseWriter, r *http.Request) { | |||||
| var ( | |||||
| userConversations []Models.ConversationDetail | |||||
| query url.Values | |||||
| conversationIds []string | |||||
| returnJson []byte | |||||
| ok bool | |||||
| err error | |||||
| ) | |||||
| query = r.URL.Query() | |||||
| conversationIds, ok = query["conversation_detail_ids"] | |||||
| if !ok { | |||||
| http.Error(w, "Invalid Data", http.StatusBadGateway) | |||||
| return | |||||
| } | |||||
| // TODO: Fix error handling here | |||||
| conversationIds = strings.Split(conversationIds[0], ",") | |||||
| userConversations, err = Database.GetConversationDetailsByIds( | |||||
| conversationIds, | |||||
| ) | |||||
| if err != nil { | |||||
| http.Error(w, "Error", http.StatusInternalServerError) | |||||
| return | |||||
| } | |||||
| returnJson, err = json.MarshalIndent(userConversations, "", " ") | |||||
| if err != nil { | |||||
| http.Error(w, "Error", http.StatusInternalServerError) | |||||
| return | |||||
| } | |||||
| w.WriteHeader(http.StatusOK) | |||||
| w.Write(returnJson) | |||||
| } | |||||
| @ -0,0 +1,55 @@ | |||||
| package Database | |||||
| import ( | |||||
| "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" | |||||
| "gorm.io/gorm" | |||||
| "gorm.io/gorm/clause" | |||||
| ) | |||||
| func GetConversationDetailById(id string) (Models.ConversationDetail, error) { | |||||
| var ( | |||||
| messageThread Models.ConversationDetail | |||||
| err error | |||||
| ) | |||||
| err = DB.Preload(clause.Associations). | |||||
| Where("id = ?", id). | |||||
| First(&messageThread). | |||||
| Error | |||||
| return messageThread, err | |||||
| } | |||||
| func GetConversationDetailsByIds(id []string) ([]Models.ConversationDetail, error) { | |||||
| var ( | |||||
| messageThread []Models.ConversationDetail | |||||
| err error | |||||
| ) | |||||
| err = DB.Preload(clause.Associations). | |||||
| Where("id = ?", id). | |||||
| First(&messageThread). | |||||
| Error | |||||
| return messageThread, err | |||||
| } | |||||
| func CreateConversationDetail(messageThread *Models.ConversationDetail) error { | |||||
| return DB.Session(&gorm.Session{FullSaveAssociations: true}). | |||||
| Create(messageThread). | |||||
| Error | |||||
| } | |||||
| func UpdateConversationDetail(messageThread *Models.ConversationDetail) error { | |||||
| return DB.Session(&gorm.Session{FullSaveAssociations: true}). | |||||
| Where("id = ?", messageThread.ID). | |||||
| Updates(messageThread). | |||||
| Error | |||||
| } | |||||
| func DeleteConversationDetail(messageThread *Models.ConversationDetail) error { | |||||
| return DB.Session(&gorm.Session{FullSaveAssociations: true}). | |||||
| Delete(messageThread). | |||||
| Error | |||||
| } | |||||
| @ -0,0 +1,47 @@ | |||||
| package Database | |||||
| import ( | |||||
| "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" | |||||
| "gorm.io/gorm" | |||||
| "gorm.io/gorm/clause" | |||||
| ) | |||||
| func GetFriendRequestById(id string) (Models.FriendRequest, error) { | |||||
| var ( | |||||
| friendRequest Models.FriendRequest | |||||
| err error | |||||
| ) | |||||
| err = DB.Preload(clause.Associations). | |||||
| First(&friendRequest, "id = ?", id). | |||||
| Error | |||||
| return friendRequest, err | |||||
| } | |||||
| func GetFriendRequestsByUserId(userID string) ([]Models.FriendRequest, error) { | |||||
| var ( | |||||
| friends []Models.FriendRequest | |||||
| err error | |||||
| ) | |||||
| err = DB.Model(Models.FriendRequest{}). | |||||
| Where("user_id = ?", userID). | |||||
| Find(&friends). | |||||
| Error | |||||
| return friends, err | |||||
| } | |||||
| func CreateFriendRequest(FriendRequest *Models.FriendRequest) error { | |||||
| return DB.Session(&gorm.Session{FullSaveAssociations: true}). | |||||
| Create(FriendRequest). | |||||
| Error | |||||
| } | |||||
| func DeleteFriendRequest(FriendRequest *Models.FriendRequest) error { | |||||
| return DB.Session(&gorm.Session{FullSaveAssociations: true}). | |||||
| Delete(FriendRequest). | |||||
| Error | |||||
| } | |||||
| @ -1,39 +0,0 @@ | |||||
| package Database | |||||
| import ( | |||||
| "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" | |||||
| "gorm.io/gorm" | |||||
| "gorm.io/gorm/clause" | |||||
| ) | |||||
| func GetMessageThreadUserById(id string) (Models.MessageThreadUser, error) { | |||||
| var ( | |||||
| message Models.MessageThreadUser | |||||
| err error | |||||
| ) | |||||
| err = DB.Preload(clause.Associations). | |||||
| First(&message, "id = ?", id). | |||||
| Error | |||||
| return message, err | |||||
| } | |||||
| func CreateMessageThreadUser(messageThreadUser *Models.MessageThreadUser) error { | |||||
| var ( | |||||
| err error | |||||
| ) | |||||
| err = DB.Session(&gorm.Session{FullSaveAssociations: true}). | |||||
| Create(messageThreadUser). | |||||
| Error | |||||
| return err | |||||
| } | |||||
| func DeleteMessageThreadUser(messageThreadUser *Models.MessageThreadUser) error { | |||||
| return DB.Session(&gorm.Session{FullSaveAssociations: true}). | |||||
| Delete(messageThreadUser). | |||||
| Error | |||||
| } | |||||
| @ -1,42 +0,0 @@ | |||||
| package Database | |||||
| import ( | |||||
| "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" | |||||
| "gorm.io/gorm" | |||||
| "gorm.io/gorm/clause" | |||||
| ) | |||||
| func GetMessageThreadById(id string, user Models.User) (Models.MessageThread, error) { | |||||
| var ( | |||||
| messageThread Models.MessageThread | |||||
| err error | |||||
| ) | |||||
| err = DB.Preload(clause.Associations). | |||||
| Where("id = ?", id). | |||||
| Where("user_id = ?", user.ID). | |||||
| First(&messageThread). | |||||
| Error | |||||
| return messageThread, err | |||||
| } | |||||
| func CreateMessageThread(messageThread *Models.MessageThread) error { | |||||
| return DB.Session(&gorm.Session{FullSaveAssociations: true}). | |||||
| Create(messageThread). | |||||
| Error | |||||
| } | |||||
| func UpdateMessageThread(messageThread *Models.MessageThread) error { | |||||
| return DB.Session(&gorm.Session{FullSaveAssociations: true}). | |||||
| Where("id = ?", messageThread.ID). | |||||
| Updates(messageThread). | |||||
| Error | |||||
| } | |||||
| func DeleteMessageThread(messageThread *Models.MessageThread) error { | |||||
| return DB.Session(&gorm.Session{FullSaveAssociations: true}). | |||||
| Delete(messageThread). | |||||
| Error | |||||
| } | |||||
| @ -0,0 +1,188 @@ | |||||
| package Seeder | |||||
| // THIS FILE IS ONLY USED FOR SEEDING DATA DURING DEVELOPMENT | |||||
| import ( | |||||
| "bytes" | |||||
| "crypto/aes" | |||||
| "crypto/cipher" | |||||
| "crypto/hmac" | |||||
| "crypto/rand" | |||||
| "crypto/rsa" | |||||
| "crypto/sha256" | |||||
| "encoding/base64" | |||||
| "fmt" | |||||
| "hash" | |||||
| "golang.org/x/crypto/pbkdf2" | |||||
| ) | |||||
| type aesKey struct { | |||||
| Key []byte | |||||
| Iv []byte | |||||
| } | |||||
| func (key aesKey) encode() string { | |||||
| return base64.StdEncoding.EncodeToString(key.Key) | |||||
| } | |||||
| // Appends padding. | |||||
| func pkcs7Padding(data []byte, blocklen int) ([]byte, error) { | |||||
| var ( | |||||
| padlen int = 1 | |||||
| pad []byte | |||||
| ) | |||||
| if blocklen <= 0 { | |||||
| return nil, fmt.Errorf("invalid blocklen %d", blocklen) | |||||
| } | |||||
| for ((len(data) + padlen) % blocklen) != 0 { | |||||
| padlen = padlen + 1 | |||||
| } | |||||
| pad = bytes.Repeat([]byte{byte(padlen)}, padlen) | |||||
| return append(data, pad...), nil | |||||
| } | |||||
| // pkcs7strip remove pkcs7 padding | |||||
| func pkcs7strip(data []byte, blockSize int) ([]byte, error) { | |||||
| var ( | |||||
| length int | |||||
| padLen int | |||||
| ref []byte | |||||
| ) | |||||
| length = len(data) | |||||
| if length == 0 { | |||||
| return nil, fmt.Errorf("pkcs7: Data is empty") | |||||
| } | |||||
| if (length % blockSize) != 0 { | |||||
| return nil, fmt.Errorf("pkcs7: Data is not block-aligned") | |||||
| } | |||||
| padLen = int(data[length-1]) | |||||
| ref = bytes.Repeat([]byte{byte(padLen)}, padLen) | |||||
| if padLen > blockSize || padLen == 0 || !bytes.HasSuffix(data, ref) { | |||||
| return nil, fmt.Errorf("pkcs7: Invalid padding") | |||||
| } | |||||
| return data[:length-padLen], nil | |||||
| } | |||||
| func generateAesKey() (aesKey, error) { | |||||
| var ( | |||||
| saltBytes []byte = []byte{} | |||||
| password []byte | |||||
| seed []byte | |||||
| iv []byte | |||||
| err error | |||||
| ) | |||||
| password = make([]byte, 64) | |||||
| _, err = rand.Read(password) | |||||
| if err != nil { | |||||
| return aesKey{}, err | |||||
| } | |||||
| seed = make([]byte, 64) | |||||
| _, err = rand.Read(seed) | |||||
| if err != nil { | |||||
| return aesKey{}, err | |||||
| } | |||||
| iv = make([]byte, 16) | |||||
| _, err = rand.Read(iv) | |||||
| if err != nil { | |||||
| return aesKey{}, err | |||||
| } | |||||
| return aesKey{ | |||||
| Key: pbkdf2.Key( | |||||
| password, | |||||
| saltBytes, | |||||
| 1000, | |||||
| 32, | |||||
| func() hash.Hash { return hmac.New(sha256.New, seed) }, | |||||
| ), | |||||
| Iv: iv, | |||||
| }, nil | |||||
| } | |||||
| func (key aesKey) aesEncrypt(plaintext []byte) ([]byte, error) { | |||||
| var ( | |||||
| bPlaintext []byte | |||||
| ciphertext []byte | |||||
| block cipher.Block | |||||
| err error | |||||
| ) | |||||
| bPlaintext, err = pkcs7Padding(plaintext, 16) | |||||
| block, err = aes.NewCipher(key.Key) | |||||
| if err != nil { | |||||
| return []byte{}, err | |||||
| } | |||||
| ciphertext = make([]byte, len(bPlaintext)) | |||||
| mode := cipher.NewCBCEncrypter(block, key.Iv) | |||||
| mode.CryptBlocks(ciphertext, bPlaintext) | |||||
| ciphertext = append(key.Iv, ciphertext...) | |||||
| return ciphertext, nil | |||||
| } | |||||
| func (key aesKey) aesDecrypt(ciphertext []byte) ([]byte, error) { | |||||
| var ( | |||||
| plaintext []byte | |||||
| iv []byte | |||||
| block cipher.Block | |||||
| err error | |||||
| ) | |||||
| iv = ciphertext[:aes.BlockSize] | |||||
| plaintext = ciphertext[aes.BlockSize:] | |||||
| block, err = aes.NewCipher(key.Key) | |||||
| if err != nil { | |||||
| return []byte{}, err | |||||
| } | |||||
| decMode := cipher.NewCBCDecrypter(block, iv) | |||||
| decMode.CryptBlocks(plaintext, plaintext) | |||||
| return plaintext, nil | |||||
| } | |||||
| // EncryptWithPublicKey encrypts data with public key | |||||
| func encryptWithPublicKey(msg []byte, pub *rsa.PublicKey) []byte { | |||||
| var ( | |||||
| hash hash.Hash | |||||
| ) | |||||
| hash = sha256.New() | |||||
| ciphertext, err := rsa.EncryptOAEP(hash, rand.Reader, pub, msg, nil) | |||||
| if err != nil { | |||||
| panic(err) | |||||
| } | |||||
| return ciphertext | |||||
| } | |||||
| // DecryptWithPrivateKey decrypts data with private key | |||||
| func decryptWithPrivateKey(ciphertext []byte, priv *rsa.PrivateKey) ([]byte, error) { | |||||
| var ( | |||||
| hash hash.Hash | |||||
| plaintext []byte | |||||
| err error | |||||
| ) | |||||
| hash = sha256.New() | |||||
| plaintext, err = rsa.DecryptOAEP(hash, rand.Reader, priv, ciphertext, nil) | |||||
| if err != nil { | |||||
| return plaintext, err | |||||
| } | |||||
| return plaintext, nil | |||||
| } | |||||
| @ -0,0 +1,38 @@ | |||||
| package Database | |||||
| import ( | |||||
| "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" | |||||
| "gorm.io/gorm/clause" | |||||
| ) | |||||
| func GetSessionById(id string) (Models.Session, error) { | |||||
| var ( | |||||
| session Models.Session | |||||
| err error | |||||
| ) | |||||
| err = DB.Preload(clause.Associations). | |||||
| First(&session, "id = ?", id). | |||||
| Error | |||||
| return session, err | |||||
| } | |||||
| func CreateSession(session *Models.Session) error { | |||||
| var ( | |||||
| err error | |||||
| ) | |||||
| err = DB.Create(session).Error | |||||
| return err | |||||
| } | |||||
| func DeleteSession(session *Models.Session) error { | |||||
| return DB.Delete(session).Error | |||||
| } | |||||
| func DeleteSessionById(id string) error { | |||||
| return DB.Delete(&Models.Session{}, id).Error | |||||
| } | |||||
| @ -0,0 +1,49 @@ | |||||
| package Database | |||||
| import ( | |||||
| "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" | |||||
| "gorm.io/gorm" | |||||
| ) | |||||
| func GetUserConversationById(id string) (Models.UserConversation, error) { | |||||
| var ( | |||||
| message Models.UserConversation | |||||
| err error | |||||
| ) | |||||
| err = DB.First(&message, "id = ?", id). | |||||
| Error | |||||
| return message, err | |||||
| } | |||||
| func GetUserConversationsByUserId(id string) ([]Models.UserConversation, error) { | |||||
| var ( | |||||
| conversations []Models.UserConversation | |||||
| err error | |||||
| ) | |||||
| err = DB.Find(&conversations, "user_id = ?", id). | |||||
| Error | |||||
| return conversations, err | |||||
| } | |||||
| func CreateUserConversation(messageThreadUser *Models.UserConversation) error { | |||||
| var ( | |||||
| err error | |||||
| ) | |||||
| err = DB.Session(&gorm.Session{FullSaveAssociations: true}). | |||||
| Create(messageThreadUser). | |||||
| Error | |||||
| return err | |||||
| } | |||||
| func DeleteUserConversation(messageThreadUser *Models.UserConversation) error { | |||||
| return DB.Session(&gorm.Session{FullSaveAssociations: true}). | |||||
| Delete(messageThreadUser). | |||||
| Error | |||||
| } | |||||
| @ -0,0 +1,18 @@ | |||||
| package Models | |||||
| import ( | |||||
| "time" | |||||
| "github.com/gofrs/uuid" | |||||
| ) | |||||
| func (s Session) IsExpired() bool { | |||||
| return s.Expiry.Before(time.Now()) | |||||
| } | |||||
| type Session struct { | |||||
| Base | |||||
| UserID uuid.UUID `gorm:"type:uuid;column:user_id;not null;"` | |||||
| User User | |||||
| Expiry time.Time | |||||
| } | |||||
| @ -0,0 +1,39 @@ | |||||
| import 'package:flutter/material.dart'; | |||||
| class CustomCircleAvatar extends StatefulWidget { | |||||
| final String initials; | |||||
| final String? imagePath; | |||||
| const CustomCircleAvatar({ | |||||
| Key? key, | |||||
| required this.initials, | |||||
| this.imagePath, | |||||
| }) : super(key: key); | |||||
| @override | |||||
| _CustomCircleAvatarState createState() => _CustomCircleAvatarState(); | |||||
| } | |||||
| class _CustomCircleAvatarState extends State<CustomCircleAvatar>{ | |||||
| bool _checkLoading = true; | |||||
| @override | |||||
| void initState() { | |||||
| super.initState(); | |||||
| if (widget.imagePath != null) { | |||||
| _checkLoading = false; | |||||
| } | |||||
| } | |||||
| @override | |||||
| Widget build(BuildContext context) { | |||||
| return _checkLoading == true ? | |||||
| CircleAvatar( | |||||
| backgroundColor: Colors.grey[300], | |||||
| child: Text(widget.initials) | |||||
| ) : CircleAvatar( | |||||
| backgroundImage: AssetImage(widget.imagePath!) | |||||
| ); | |||||
| } | |||||
| } | |||||
| @ -1,30 +1,116 @@ | |||||
| const messageTypeSender = 'sender'; | |||||
| const messageTypeReceiver = 'receiver'; | |||||
| import 'dart:convert'; | |||||
| import 'package:pointycastle/export.dart'; | |||||
| import '/utils/encryption/crypto_utils.dart'; | |||||
| import '/utils/encryption/aes_helper.dart'; | |||||
| import '/utils/storage/database.dart'; | |||||
| class Message { | |||||
| Conversation findConversationByDetailId(List<Conversation> conversations, String id) { | |||||
| for (var conversation in conversations) { | |||||
| if (conversation.conversationDetailId == id) { | |||||
| return conversation; | |||||
| } | |||||
| } | |||||
| // Or return `null`. | |||||
| throw ArgumentError.value(id, "id", "No element with that id"); | |||||
| } | |||||
| class Conversation { | |||||
| String id; | String id; | ||||
| String conversationId; | |||||
| String userId; | |||||
| String conversationDetailId; | |||||
| String messageThreadKey; | |||||
| String symmetricKey; | String symmetricKey; | ||||
| String data; | |||||
| String messageType; | |||||
| String? decryptedData; | |||||
| Message({ | |||||
| bool admin; | |||||
| String name; | |||||
| String? users; | |||||
| Conversation({ | |||||
| required this.id, | required this.id, | ||||
| required this.conversationId, | |||||
| required this.userId, | |||||
| required this.conversationDetailId, | |||||
| required this.messageThreadKey, | |||||
| required this.symmetricKey, | required this.symmetricKey, | ||||
| required this.data, | |||||
| required this.messageType, | |||||
| this.decryptedData, | |||||
| required this.admin, | |||||
| required this.name, | |||||
| this.users, | |||||
| }); | }); | ||||
| factory Conversation.fromJson(Map<String, dynamic> json, RSAPrivateKey privKey) { | |||||
| var symmetricKeyDecrypted = CryptoUtils.rsaDecrypt( | |||||
| base64.decode(json['symmetric_key']), | |||||
| privKey, | |||||
| ); | |||||
| var detailId = AesHelper.aesDecrypt( | |||||
| symmetricKeyDecrypted, | |||||
| base64.decode(json['conversation_detail_id']), | |||||
| ); | |||||
| var threadKey = AesHelper.aesDecrypt( | |||||
| symmetricKeyDecrypted, | |||||
| base64.decode(json['message_thread_key']), | |||||
| ); | |||||
| var admin = AesHelper.aesDecrypt( | |||||
| symmetricKeyDecrypted, | |||||
| base64.decode(json['admin']), | |||||
| ); | |||||
| return Conversation( | |||||
| id: json['id'], | |||||
| userId: json['user_id'], | |||||
| conversationDetailId: detailId, | |||||
| messageThreadKey: threadKey, | |||||
| symmetricKey: base64.encode(symmetricKeyDecrypted), | |||||
| admin: admin == 'true', | |||||
| name: 'Unknown', | |||||
| ); | |||||
| } | |||||
| @override | |||||
| String toString() { | |||||
| return ''' | |||||
| id: $id | |||||
| userId: $userId | |||||
| name: $name | |||||
| admin: $admin'''; | |||||
| } | |||||
| Map<String, dynamic> toMap() { | |||||
| return { | |||||
| 'id': id, | |||||
| 'user_id': userId, | |||||
| 'conversation_detail_id': conversationDetailId, | |||||
| 'message_thread_key': messageThreadKey, | |||||
| 'symmetric_key': symmetricKey, | |||||
| 'admin': admin ? 1 : 0, | |||||
| 'name': name, | |||||
| 'users': users, | |||||
| }; | |||||
| } | |||||
| } | } | ||||
| class Conversation { | |||||
| String id; | |||||
| String friendId; | |||||
| String recentMessageId; | |||||
| Conversation({ | |||||
| required this.id, | |||||
| required this.friendId, | |||||
| required this.recentMessageId, | |||||
| // A method that retrieves all the dogs from the dogs table. | |||||
| Future<List<Conversation>> getConversations() async { | |||||
| final db = await getDatabaseConnection(); | |||||
| final List<Map<String, dynamic>> maps = await db.query('conversations'); | |||||
| return List.generate(maps.length, (i) { | |||||
| return Conversation( | |||||
| id: maps[i]['id'], | |||||
| userId: maps[i]['user_id'], | |||||
| conversationDetailId: maps[i]['conversation_detail_id'], | |||||
| messageThreadKey: maps[i]['message_thread_key'], | |||||
| symmetricKey: maps[i]['symmetric_key'], | |||||
| admin: maps[i]['admin'] == 1, | |||||
| name: maps[i]['name'], | |||||
| users: maps[i]['users'], | |||||
| ); | |||||
| }); | }); | ||||
| } | } | ||||
| @ -0,0 +1,20 @@ | |||||
| const messageTypeSender = 'sender'; | |||||
| const messageTypeReceiver = 'receiver'; | |||||
| class Message { | |||||
| String id; | |||||
| String symmetricKey; | |||||
| String messageThreadKey; | |||||
| String data; | |||||
| String senderId; | |||||
| String senderUsername; | |||||
| Message({ | |||||
| required this.id, | |||||
| required this.symmetricKey, | |||||
| required this.messageThreadKey, | |||||
| required this.data, | |||||
| required this.senderId, | |||||
| required this.senderUsername, | |||||
| }); | |||||
| } | |||||
| @ -0,0 +1,79 @@ | |||||
| import 'dart:convert'; | |||||
| import 'package:http/http.dart' as http; | |||||
| import 'package:flutter_dotenv/flutter_dotenv.dart'; | |||||
| import 'package:pointycastle/export.dart'; | |||||
| import 'package:sqflite/sqflite.dart'; | |||||
| import '/models/conversations.dart'; | |||||
| import '/utils/storage/database.dart'; | |||||
| import '/utils/storage/session_cookie.dart'; | |||||
| import '/utils/storage/encryption_keys.dart'; | |||||
| import '/utils/encryption/aes_helper.dart'; | |||||
| Future<void> updateConversations() async { | |||||
| RSAPrivateKey privKey = await getPrivateKey(); | |||||
| var resp = await http.get( | |||||
| Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/conversations'), | |||||
| headers: { | |||||
| 'cookie': await getSessionCookie(), | |||||
| } | |||||
| ); | |||||
| if (resp.statusCode != 200) { | |||||
| throw Exception(resp.body); | |||||
| } | |||||
| List<Conversation> conversations = []; | |||||
| List<String> conversationsDetailIds = []; | |||||
| List<dynamic> conversationsJson = jsonDecode(resp.body); | |||||
| for (var i = 0; i < conversationsJson.length; i++) { | |||||
| Conversation conversation = Conversation.fromJson( | |||||
| conversationsJson[i] as Map<String, dynamic>, | |||||
| privKey, | |||||
| ); | |||||
| conversations.add(conversation); | |||||
| conversationsDetailIds.add(conversation.conversationDetailId); | |||||
| } | |||||
| Map<String, String> params = {}; | |||||
| params['conversation_detail_ids'] = conversationsDetailIds.join(','); | |||||
| var uri = Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/conversation_details'); | |||||
| uri = uri.replace(queryParameters: params); | |||||
| resp = await http.get( | |||||
| uri, | |||||
| headers: { | |||||
| 'cookie': await getSessionCookie(), | |||||
| } | |||||
| ); | |||||
| if (resp.statusCode != 200) { | |||||
| throw Exception(resp.body); | |||||
| } | |||||
| final db = await getDatabaseConnection(); | |||||
| List<dynamic> conversationsDetailsJson = jsonDecode(resp.body); | |||||
| for (var i = 0; i < conversationsDetailsJson.length; i++) { | |||||
| var conversationDetailJson = conversationsDetailsJson[i] as Map<String, dynamic>; | |||||
| var conversation = findConversationByDetailId(conversations, conversationDetailJson['id']); | |||||
| conversation.name = AesHelper.aesDecrypt( | |||||
| base64.decode(conversation.symmetricKey), | |||||
| base64.decode(conversationDetailJson['name']), | |||||
| ); | |||||
| conversation.users = AesHelper.aesDecrypt( | |||||
| base64.decode(conversation.symmetricKey), | |||||
| base64.decode(conversationDetailJson['users']), | |||||
| ); | |||||
| await db.insert( | |||||
| 'conversations', | |||||
| conversation.toMap(), | |||||
| conflictAlgorithm: ConflictAlgorithm.replace, | |||||
| ); | |||||
| } | |||||
| } | |||||
| @ -1,60 +1,66 @@ | |||||
| import 'package:Envelope/components/custom_circle_avatar.dart'; | |||||
| import 'package:Envelope/models/conversations.dart'; | |||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||
| import '/views/main/conversation_detail.dart'; | import '/views/main/conversation_detail.dart'; | ||||
| class ConversationListItem extends StatefulWidget{ | class ConversationListItem extends StatefulWidget{ | ||||
| final String id; | |||||
| final String username; | |||||
| const ConversationListItem({ | |||||
| Key? key, | |||||
| required this.id, | |||||
| required this.username, | |||||
| }) : super(key: key); | |||||
| final Conversation conversation; | |||||
| const ConversationListItem({ | |||||
| Key? key, | |||||
| required this.conversation, | |||||
| }) : super(key: key); | |||||
| @override | |||||
| _ConversationListItemState createState() => _ConversationListItemState(); | |||||
| @override | |||||
| _ConversationListItemState createState() => _ConversationListItemState(); | |||||
| } | } | ||||
| class _ConversationListItemState extends State<ConversationListItem> { | class _ConversationListItemState extends State<ConversationListItem> { | ||||
| @override | |||||
| Widget build(BuildContext context) { | |||||
| return GestureDetector( | |||||
| onTap: () { | |||||
| Navigator.push(context, MaterialPageRoute(builder: (context){ | |||||
| return ConversationDetail(); | |||||
| })); | |||||
| }, | |||||
| child: Container( | |||||
| padding: const EdgeInsets.only(left: 16,right: 16,top: 10,bottom: 10), | |||||
| child: Row( | |||||
| children: <Widget>[ | |||||
| Expanded( | |||||
| child: Row( | |||||
| children: <Widget>[ | |||||
| // CircleAvatar( | |||||
| // backgroundImage: NetworkImage(widget.imageUrl), | |||||
| // maxRadius: 30, | |||||
| // ), | |||||
| //const SizedBox(width: 16), | |||||
| Expanded( | |||||
| child: Container( | |||||
| color: Colors.transparent, | |||||
| child: Column( | |||||
| crossAxisAlignment: CrossAxisAlignment.start, | |||||
| children: <Widget>[ | |||||
| Text(widget.username, style: const TextStyle(fontSize: 16)), | |||||
| const SizedBox(height: 6), | |||||
| //Text(widget.messageText,style: TextStyle(fontSize: 13,color: Colors.grey.shade600, fontWeight: widget.isMessageRead?FontWeight.bold:FontWeight.normal),), | |||||
| const Divider(), | |||||
| ], | |||||
| ), | |||||
| ), | |||||
| ), | |||||
| ], | |||||
| ), | |||||
| ), | |||||
| ], | |||||
| ), | |||||
| ), | |||||
| @override | |||||
| Widget build(BuildContext context) { | |||||
| return GestureDetector( | |||||
| onTap: () { | |||||
| Navigator.push(context, MaterialPageRoute(builder: (context){ | |||||
| return ConversationDetail( | |||||
| conversation: widget.conversation, | |||||
| ); | |||||
| })); | |||||
| }, | |||||
| child: Container( | |||||
| padding: const EdgeInsets.only(left: 16,right: 16,top: 10,bottom: 10), | |||||
| child: Row( | |||||
| children: <Widget>[ | |||||
| Expanded( | |||||
| child: Row( | |||||
| children: <Widget>[ | |||||
| CustomCircleAvatar( | |||||
| initials: widget.conversation.name[0].toUpperCase(), | |||||
| imagePath: null, | |||||
| ), | |||||
| const SizedBox(width: 16), | |||||
| Expanded( | |||||
| child: Align( | |||||
| alignment: Alignment.centerLeft, | |||||
| child: Container( | |||||
| color: Colors.transparent, | |||||
| child: Column( | |||||
| crossAxisAlignment: CrossAxisAlignment.start, | |||||
| children: <Widget>[ | |||||
| Text( | |||||
| widget.conversation.name, | |||||
| style: const TextStyle(fontSize: 16) | |||||
| ), | |||||
| //Text(widget.messageText,style: TextStyle(fontSize: 13,color: Colors.grey.shade600, fontWeight: widget.isMessageRead?FontWeight.bold:FontWeight.normal),), | |||||
| ], | |||||
| ), | |||||
| ), | |||||
| ), | |||||
| ), | |||||
| ], | |||||
| ), | |||||
| ), | |||||
| ], | |||||
| ), | |||||
| ), | |||||
| ); | ); | ||||
| } | |||||
| } | |||||
| } | } | ||||
| @ -1,56 +1,67 @@ | |||||
| import 'package:Envelope/components/custom_circle_avatar.dart'; | |||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||
| class FriendListItem extends StatefulWidget{ | class FriendListItem extends StatefulWidget{ | ||||
| final String id; | |||||
| final String username; | |||||
| const FriendListItem({ | |||||
| Key? key, | |||||
| required this.id, | |||||
| required this.username, | |||||
| }) : super(key: key); | |||||
| final String id; | |||||
| final String username; | |||||
| final String? imagePath; | |||||
| const FriendListItem({ | |||||
| Key? key, | |||||
| required this.id, | |||||
| required this.username, | |||||
| this.imagePath, | |||||
| }) : super(key: key); | |||||
| @override | |||||
| _FriendListItemState createState() => _FriendListItemState(); | |||||
| @override | |||||
| _FriendListItemState createState() => _FriendListItemState(); | |||||
| } | } | ||||
| class _FriendListItemState extends State<FriendListItem> { | class _FriendListItemState extends State<FriendListItem> { | ||||
| @override | |||||
| Widget build(BuildContext context) { | |||||
| return GestureDetector( | |||||
| onTap: (){ | |||||
| }, | |||||
| child: Container( | |||||
| padding: const EdgeInsets.only(left: 16,right: 16,top: 10,bottom: 10), | |||||
| child: Row( | |||||
| children: <Widget>[ | |||||
| Expanded( | |||||
| child: Row( | |||||
| children: <Widget>[ | |||||
| // CircleAvatar( | |||||
| // backgroundImage: NetworkImage(widget.imageUrl), | |||||
| // maxRadius: 30, | |||||
| // ), | |||||
| //const SizedBox(width: 16), | |||||
| Expanded( | |||||
| child: Container( | |||||
| color: Colors.transparent, | |||||
| child: Column( | |||||
| crossAxisAlignment: CrossAxisAlignment.start, | |||||
| children: <Widget>[ | |||||
| Text(widget.username, style: const TextStyle(fontSize: 16)), | |||||
| const SizedBox(height: 6), | |||||
| //Text(widget.messageText,style: TextStyle(fontSize: 13,color: Colors.grey.shade600, fontWeight: widget.isMessageRead?FontWeight.bold:FontWeight.normal),), | |||||
| const Divider(), | |||||
| ], | |||||
| ), | |||||
| ), | |||||
| ), | |||||
| ], | |||||
| ), | |||||
| ), | |||||
| ], | |||||
| ), | |||||
| ), | |||||
| @override | |||||
| Widget build(BuildContext context) { | |||||
| return GestureDetector( | |||||
| onTap: (){ | |||||
| }, | |||||
| child: Container( | |||||
| padding: const EdgeInsets.only(left: 16,right: 16,top: 10,bottom: 10), | |||||
| child: Row( | |||||
| children: <Widget>[ | |||||
| Expanded( | |||||
| child: Row( | |||||
| children: <Widget>[ | |||||
| CustomCircleAvatar( | |||||
| initials: widget.username[0].toUpperCase(), | |||||
| imagePath: widget.imagePath, | |||||
| ), | |||||
| const SizedBox(width: 16), | |||||
| Expanded( | |||||
| child: Align( | |||||
| alignment: Alignment.centerLeft, | |||||
| child: Container( | |||||
| color: Colors.transparent, | |||||
| child: Column( | |||||
| crossAxisAlignment: CrossAxisAlignment.start, | |||||
| children: <Widget>[ | |||||
| Text(widget.username, style: const TextStyle(fontSize: 16)), | |||||
| // Text( | |||||
| // widget.messageText, | |||||
| // style: TextStyle(fontSize: 13, | |||||
| // color: Colors.grey.shade600, | |||||
| // fontWeight: widget.isMessageRead?FontWeight.bold:FontWeight.normal | |||||
| // ), | |||||
| // ), | |||||
| ], | |||||
| ), | |||||
| ), | |||||
| ), | |||||
| ), | |||||
| ], | |||||
| ), | |||||
| ), | |||||
| ], | |||||
| ), | |||||
| ), | |||||
| ); | ); | ||||
| } | |||||
| } | |||||
| } | } | ||||