Browse Source

Add conversation image support

pull/3/head
Tovi Jaeschke-Rogers 2 years ago
parent
commit
70f6d6546f
14 changed files with 268 additions and 48 deletions
  1. +1
    -1
      Backend/Api/Friends/AcceptFriendRequest.go
  2. +64
    -0
      Backend/Api/Messages/AddConversationImage.go
  3. +28
    -14
      Backend/Api/Messages/Conversations.go
  4. +4
    -3
      Backend/Api/Messages/UpdateConversation.go
  5. +1
    -0
      Backend/Api/Routes.go
  6. +42
    -0
      Backend/Database/Attachments.go
  7. +6
    -1
      Backend/Database/ConversationDetails.go
  8. +5
    -3
      Backend/Models/Conversations.go
  9. +13
    -0
      mobile/lib/exceptions/update_data_exception.dart
  10. +12
    -0
      mobile/lib/models/conversations.dart
  11. +5
    -21
      mobile/lib/models/image_message.dart
  12. +40
    -4
      mobile/lib/utils/storage/conversations.dart
  13. +32
    -0
      mobile/lib/utils/storage/get_file.dart
  14. +15
    -1
      mobile/lib/views/main/conversation/settings.dart

+ 1
- 1
Backend/Api/Friends/AcceptFriendRequest.go View File

@ -32,7 +32,7 @@ func AcceptFriendRequest(w http.ResponseWriter, r *http.Request) {
oldFriendRequest, err = Database.GetFriendRequestByID(friendRequestID) oldFriendRequest, err = Database.GetFriendRequestByID(friendRequestID)
if err != nil { if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
http.Error(w, "Not Found", http.StatusNotFound)
return return
} }


+ 64
- 0
Backend/Api/Messages/AddConversationImage.go View File

@ -0,0 +1,64 @@
package Messages
import (
"encoding/base64"
"encoding/json"
"net/http"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Models"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Util"
"github.com/gorilla/mux"
)
// AddConversationImage adds an image for a conversation icon
func AddConversationImage(w http.ResponseWriter, r *http.Request) {
var (
attachment Models.Attachment
conversationDetail Models.ConversationDetail
urlVars map[string]string
detailID string
decodedFile []byte
fileName string
ok bool
err error
)
urlVars = mux.Vars(r)
detailID, ok = urlVars["detailID"]
if !ok {
http.Error(w, "Not Found", http.StatusNotFound)
return
}
conversationDetail, err = Database.GetConversationDetailByID(detailID)
if err != nil {
http.Error(w, "Not Found", http.StatusNotFound)
return
}
err = json.NewDecoder(r.Body).Decode(&attachment)
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
if attachment.Data == "" {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
decodedFile, err = base64.StdEncoding.DecodeString(attachment.Data)
fileName, err = Util.WriteFile(decodedFile)
attachment.FilePath = fileName
conversationDetail.Attachment = attachment
err = Database.UpdateConversationDetail(&conversationDetail)
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}

+ 28
- 14
Backend/Api/Messages/Conversations.go View File

@ -2,6 +2,7 @@ package Messages
import ( import (
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
@ -14,10 +15,10 @@ import (
// EncryptedConversationList returns an encrypted list of all Conversations // EncryptedConversationList returns an encrypted list of all Conversations
func EncryptedConversationList(w http.ResponseWriter, r *http.Request) { func EncryptedConversationList(w http.ResponseWriter, r *http.Request) {
var ( var (
userConversations []Models.UserConversation
userSession Models.Session
returnJSON []byte
err error
conversationDetails []Models.UserConversation
userSession Models.Session
returnJSON []byte
err error
) )
userSession, err = Auth.CheckCookie(r) userSession, err = Auth.CheckCookie(r)
@ -26,7 +27,7 @@ func EncryptedConversationList(w http.ResponseWriter, r *http.Request) {
return return
} }
userConversations, err = Database.GetUserConversationsByUserId(
conversationDetails, err = Database.GetUserConversationsByUserId(
userSession.UserID.String(), userSession.UserID.String(),
) )
if err != nil { if err != nil {
@ -34,7 +35,7 @@ func EncryptedConversationList(w http.ResponseWriter, r *http.Request) {
return return
} }
returnJSON, err = json.MarshalIndent(userConversations, "", " ")
returnJSON, err = json.MarshalIndent(conversationDetails, "", " ")
if err != nil { if err != nil {
http.Error(w, "Error", http.StatusInternalServerError) http.Error(w, "Error", http.StatusInternalServerError)
return return
@ -47,12 +48,14 @@ func EncryptedConversationList(w http.ResponseWriter, r *http.Request) {
// EncryptedConversationDetailsList returns an encrypted list of all ConversationDetails // EncryptedConversationDetailsList returns an encrypted list of all ConversationDetails
func EncryptedConversationDetailsList(w http.ResponseWriter, r *http.Request) { func EncryptedConversationDetailsList(w http.ResponseWriter, r *http.Request) {
var ( var (
userConversations []Models.ConversationDetail
query url.Values
conversationIds []string
returnJSON []byte
ok bool
err error
conversationDetails []Models.ConversationDetail
detail Models.ConversationDetail
query url.Values
conversationIds []string
returnJSON []byte
i int
ok bool
err error
) )
query = r.URL.Query() query = r.URL.Query()
@ -65,7 +68,7 @@ func EncryptedConversationDetailsList(w http.ResponseWriter, r *http.Request) {
// TODO: Fix error handling here // TODO: Fix error handling here
conversationIds = strings.Split(conversationIds[0], ",") conversationIds = strings.Split(conversationIds[0], ",")
userConversations, err = Database.GetConversationDetailsByIds(
conversationDetails, err = Database.GetConversationDetailsByIds(
conversationIds, conversationIds,
) )
if err != nil { if err != nil {
@ -73,7 +76,18 @@ func EncryptedConversationDetailsList(w http.ResponseWriter, r *http.Request) {
return return
} }
returnJSON, err = json.MarshalIndent(userConversations, "", " ")
for i, detail = range conversationDetails {
if detail.AttachmentID == nil {
continue
}
conversationDetails[i].Attachment.ImageLink = fmt.Sprintf(
"http://192.168.1.5:8080/files/%s",
detail.Attachment.FilePath,
)
}
returnJSON, err = json.MarshalIndent(conversationDetails, "", " ")
if err != nil { if err != nil {
http.Error(w, "Error", http.StatusInternalServerError) http.Error(w, "Error", http.StatusInternalServerError)
return return


+ 4
- 3
Backend/Api/Messages/UpdateConversation.go View File

@ -10,16 +10,17 @@ import (
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models"
) )
type RawUpdateConversationData struct {
type rawUpdateConversationData struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Users []Models.ConversationDetailUser `json:"users"` Users []Models.ConversationDetailUser `json:"users"`
UserConversations []Models.UserConversation `json:"user_conversations"` UserConversations []Models.UserConversation `json:"user_conversations"`
} }
// UpdateConversation updates the conversation data, such as title, users, etc
func UpdateConversation(w http.ResponseWriter, r *http.Request) { func UpdateConversation(w http.ResponseWriter, r *http.Request) {
var ( var (
rawConversationData RawCreateConversationData
rawConversationData rawUpdateConversationData
messageThread Models.ConversationDetail messageThread Models.ConversationDetail
err error err error
) )
@ -52,5 +53,5 @@ func UpdateConversation(w http.ResponseWriter, r *http.Request) {
} }
} }
w.WriteHeader(http.StatusOK)
w.WriteHeader(http.StatusNoContent)
} }

+ 1
- 0
Backend/Api/Routes.go View File

@ -77,6 +77,7 @@ func InitAPIEndpoints(router *mux.Router) {
authAPI.HandleFunc("/conversation_details", Messages.EncryptedConversationDetailsList).Methods("GET") authAPI.HandleFunc("/conversation_details", Messages.EncryptedConversationDetailsList).Methods("GET")
authAPI.HandleFunc("/conversations", Messages.CreateConversation).Methods("POST") authAPI.HandleFunc("/conversations", Messages.CreateConversation).Methods("POST")
authAPI.HandleFunc("/conversations", Messages.UpdateConversation).Methods("PUT") authAPI.HandleFunc("/conversations", Messages.UpdateConversation).Methods("PUT")
authAPI.HandleFunc("/conversations/{detailID}/image", Messages.AddConversationImage).Methods("POST")
authAPI.HandleFunc("/message", Messages.CreateMessage).Methods("POST") authAPI.HandleFunc("/message", Messages.CreateMessage).Methods("POST")
authAPI.HandleFunc("/messages/{associationKey}", Messages.Messages).Methods("GET") authAPI.HandleFunc("/messages/{associationKey}", Messages.Messages).Methods("GET")


+ 42
- 0
Backend/Database/Attachments.go View File

@ -0,0 +1,42 @@
package Database
import (
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Models"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// GetAttachmentByID gets the attachment record by the id
func GetAttachmentByID(id string) (Models.MessageData, error) {
var (
messageData Models.MessageData
err error
)
err = DB.Preload(clause.Associations).
First(&messageData, "id = ?", id).
Error
return messageData, err
}
// CreateAttachment creates the attachment record
func CreateAttachment(messageData *Models.MessageData) error {
var (
err error
)
err = DB.Session(&gorm.Session{FullSaveAssociations: true}).
Create(messageData).
Error
return err
}
// DeleteAttachment deletes the attachment record
func DeleteAttachment(messageData *Models.MessageData) error {
return DB.Session(&gorm.Session{FullSaveAssociations: true}).
Delete(messageData).
Error
}

+ 6
- 1
Backend/Database/ConversationDetails.go View File

@ -7,7 +7,8 @@ import (
"gorm.io/gorm/clause" "gorm.io/gorm/clause"
) )
func GetConversationDetailById(id string) (Models.ConversationDetail, error) {
// GetConversationDetailByID gets by id
func GetConversationDetailByID(id string) (Models.ConversationDetail, error) {
var ( var (
messageThread Models.ConversationDetail messageThread Models.ConversationDetail
err error err error
@ -21,6 +22,7 @@ func GetConversationDetailById(id string) (Models.ConversationDetail, error) {
return messageThread, err return messageThread, err
} }
// GetConversationDetailsByIds gets by multiple ids
func GetConversationDetailsByIds(id []string) ([]Models.ConversationDetail, error) { func GetConversationDetailsByIds(id []string) ([]Models.ConversationDetail, error) {
var ( var (
messageThread []Models.ConversationDetail messageThread []Models.ConversationDetail
@ -35,12 +37,14 @@ func GetConversationDetailsByIds(id []string) ([]Models.ConversationDetail, erro
return messageThread, err return messageThread, err
} }
// CreateConversationDetail creates a ConversationDetail record
func CreateConversationDetail(messageThread *Models.ConversationDetail) error { func CreateConversationDetail(messageThread *Models.ConversationDetail) error {
return DB.Session(&gorm.Session{FullSaveAssociations: true}). return DB.Session(&gorm.Session{FullSaveAssociations: true}).
Create(messageThread). Create(messageThread).
Error Error
} }
// UpdateConversationDetail updates a ConversationDetail record
func UpdateConversationDetail(messageThread *Models.ConversationDetail) error { func UpdateConversationDetail(messageThread *Models.ConversationDetail) error {
return DB.Session(&gorm.Session{FullSaveAssociations: true}). return DB.Session(&gorm.Session{FullSaveAssociations: true}).
Where("id = ?", messageThread.ID). Where("id = ?", messageThread.ID).
@ -48,6 +52,7 @@ func UpdateConversationDetail(messageThread *Models.ConversationDetail) error {
Error Error
} }
// DeleteConversationDetail deletes a ConversationDetail record
func DeleteConversationDetail(messageThread *Models.ConversationDetail) error { func DeleteConversationDetail(messageThread *Models.ConversationDetail) error {
return DB.Session(&gorm.Session{FullSaveAssociations: true}). return DB.Session(&gorm.Session{FullSaveAssociations: true}).
Delete(messageThread). Delete(messageThread).


+ 5
- 3
Backend/Models/Conversations.go View File

@ -7,9 +7,11 @@ import (
// ConversationDetail stores the name for the conversation // ConversationDetail stores the name for the conversation
type ConversationDetail struct { type ConversationDetail struct {
Base Base
Name string `gorm:"not null" json:"name"` // Stored encrypted
Users []ConversationDetailUser ` json:"users"`
TwoUser string `gorm:"not null" json:"two_user"`
Name string `gorm:"not null" json:"name"` // Stored encrypted
Users []ConversationDetailUser ` json:"users"`
TwoUser string `gorm:"not null" json:"two_user"`
AttachmentID *uuid.UUID ` json:"attachment_id"`
Attachment Attachment ` json:"attachment"`
} }
// ConversationDetailUser all users associated with a customer // ConversationDetailUser all users associated with a customer


+ 13
- 0
mobile/lib/exceptions/update_data_exception.dart View File

@ -0,0 +1,13 @@
class UpdateDataException implements Exception {
final String _message;
UpdateDataException([
this._message = 'An error occured while updating data.',
]);
@override
String toString() {
return _message;
}
}

+ 12
- 0
mobile/lib/models/conversations.dart View File

@ -335,6 +335,18 @@ class Conversation {
return returnData; return returnData;
} }
Map<String, dynamic> payloadImageJson() {
if (icon == null) {
return {};
}
return {
'data': AesHelper.aesEncrypt(symmetricKey, Uint8List.fromList(icon!.readAsBytesSync())),
'mimetype': lookupMimeType(icon!.path),
'extension': getExtension(icon!.path),
};
}
Map<String, dynamic> toMap() { Map<String, dynamic> toMap() {
return { return {
'id': id, 'id': id,


+ 5
- 21
mobile/lib/models/image_message.dart View File

@ -2,9 +2,8 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:Envelope/utils/storage/session_cookie.dart';
import 'package:Envelope/utils/storage/get_file.dart';
import 'package:Envelope/utils/storage/write_file.dart'; import 'package:Envelope/utils/storage/write_file.dart';
import 'package:http/http.dart' as http;
import 'package:mime/mime.dart'; import 'package:mime/mime.dart';
import 'package:pointycastle/pointycastle.dart'; import 'package:pointycastle/pointycastle.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
@ -55,25 +54,10 @@ class ImageMessage extends Message {
base64.decode(json['message_data']['sender_id']), base64.decode(json['message_data']['sender_id']),
); );
var resp = await http.get(
Uri.parse(json['message_data']['attachment']['image_link']),
headers: {
'cookie': await getSessionCookie(),
}
);
if (resp.statusCode != 200) {
throw Exception('Could not get attachment file');
}
var data = AesHelper.aesDecryptBytes(
base64.decode(symmetricKey),
resp.bodyBytes,
);
File file = await writeImage(
File file = await getFile(
json['message_data']['attachment']['image_link'],
'${json['id']}', '${json['id']}',
data,
symmetricKey,
); );
return ImageMessage( return ImageMessage(
@ -127,7 +111,7 @@ class ImageMessage extends Message {
Uint8List.fromList(base64.encode(symmetricKey).codeUnits), Uint8List.fromList(base64.encode(symmetricKey).codeUnits),
), ),
'attachment': { 'attachment': {
'data': AesHelper.aesEncrypt(symmetricKey, Uint8List.fromList(file.readAsBytesSync())),
'data': AesHelper.aesEncrypt(base64.encode(symmetricKey), Uint8List.fromList(file.readAsBytesSync())),
'mimetype': lookupMimeType(file.path), 'mimetype': lookupMimeType(file.path),
'extension': getExtension(file.path), 'extension': getExtension(file.path),
} }


+ 40
- 4
mobile/lib/utils/storage/conversations.dart View File

@ -1,5 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:Envelope/exceptions/update_data_exception.dart';
import 'package:Envelope/utils/storage/get_file.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:pointycastle/export.dart'; import 'package:pointycastle/export.dart';
@ -13,12 +15,17 @@ import '/utils/encryption/aes_helper.dart';
import '/utils/storage/database.dart'; import '/utils/storage/database.dart';
import '/utils/storage/session_cookie.dart'; import '/utils/storage/session_cookie.dart';
Future<void> updateConversation(Conversation conversation, { includeUsers = true } ) async {
Future<void> updateConversation(
Conversation conversation,
{
includeUsers = false,
updatedImage = false,
} ) async {
String sessionCookie = await getSessionCookie(); String sessionCookie = await getSessionCookie();
Map<String, dynamic> conversationJson = await conversation.payloadJson(includeUsers: includeUsers); Map<String, dynamic> conversationJson = await conversation.payloadJson(includeUsers: includeUsers);
var x = await http.put(
var resp = await http.put(
await MyProfile.getServerUrl('api/v1/auth/conversations'), await MyProfile.getServerUrl('api/v1/auth/conversations'),
headers: <String, String>{ headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8', 'Content-Type': 'application/json; charset=UTF-8',
@ -27,8 +34,28 @@ Future<void> updateConversation(Conversation conversation, { includeUsers = true
body: jsonEncode(conversationJson), body: jsonEncode(conversationJson),
); );
// TODO: Handle errors here
print(x.statusCode);
if (resp.statusCode != 204) {
throw UpdateDataException('Unable to update conversation, please try again later.');
}
if (!updatedImage) {
return;
}
Map<String, dynamic> attachmentJson = conversation.payloadImageJson();
resp = await http.post(
await MyProfile.getServerUrl('api/v1/auth/conversations/${conversation.id}/image'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
'cookie': sessionCookie,
},
body: jsonEncode(attachmentJson),
);
if (resp.statusCode != 204) {
throw UpdateDataException('Unable to update conversation image, please try again later.');
}
} }
// TODO: Refactor this function // TODO: Refactor this function
@ -116,6 +143,15 @@ Future<void> updateConversations() async {
); );
} }
// TODO: Handle exception here
if (conversationDetailJson['attachment_id'] != null) {
conversation.icon = await getFile(
conversationDetailJson['attachment']['image_link'],
conversation.id,
conversation.symmetricKey,
);
}
await db.insert( await db.insert(
'conversations', 'conversations',
conversation.toMap(), conversation.toMap(),


+ 32
- 0
mobile/lib/utils/storage/get_file.dart View File

@ -0,0 +1,32 @@
import 'dart:io';
import 'package:http/http.dart' as http;
import '/utils/encryption/aes_helper.dart';
import '/utils/storage/session_cookie.dart';
import '/utils/storage/write_file.dart';
Future<File> getFile(String link, String imageName, dynamic symmetricKey) async {
var resp = await http.get(
Uri.parse(link),
headers: {
'cookie': await getSessionCookie(),
}
);
if (resp.statusCode != 200) {
throw Exception('Could not get attachment file');
}
var data = AesHelper.aesDecryptBytes(
symmetricKey,
resp.bodyBytes,
);
File file = await writeImage(
imageName,
data,
);
return file;
}

+ 15
- 1
mobile/lib/views/main/conversation/settings.dart View File

@ -1,6 +1,8 @@
import 'dart:io'; import 'dart:io';
import 'package:Envelope/components/custom_title_bar.dart'; import 'package:Envelope/components/custom_title_bar.dart';
import 'package:Envelope/components/flash_message.dart';
import 'package:Envelope/exceptions/update_data_exception.dart';
import 'package:Envelope/models/friends.dart'; import 'package:Envelope/models/friends.dart';
import 'package:Envelope/utils/encryption/crypto_utils.dart'; import 'package:Envelope/utils/encryption/crypto_utils.dart';
import 'package:Envelope/utils/storage/write_file.dart'; import 'package:Envelope/utils/storage/write_file.dart';
@ -103,10 +105,14 @@ class _ConversationSettingsState extends State<ConversationSettings> {
onPressed: () { onPressed: () {
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute(builder: (context) => ConversationEditDetails( MaterialPageRoute(builder: (context) => ConversationEditDetails(
// TODO: Move saveCallback to somewhere else
saveCallback: (String conversationName, File? file) async { saveCallback: (String conversationName, File? file) async {
bool updatedImage = false;
File? writtenFile; File? writtenFile;
if (file != null) { if (file != null) {
updatedImage = file.hashCode != widget.conversation.icon.hashCode;
writtenFile = await writeImage( writtenFile = await writeImage(
widget.conversation.id, widget.conversation.id,
file.readAsBytesSync(), file.readAsBytesSync(),
@ -124,7 +130,15 @@ class _ConversationSettingsState extends State<ConversationSettings> {
whereArgs: [widget.conversation.id], whereArgs: [widget.conversation.id],
); );
await updateConversation(widget.conversation, includeUsers: true);
await updateConversation(widget.conversation, updatedImage: updatedImage)
.catchError((error) {
String message = error.toString();
if (error.runtimeType != UpdateDataException) {
message = 'An error occured, please try again later';
}
showMessage(message, context);
});
setState(() {}); setState(() {});
Navigator.pop(context); Navigator.pop(context);
}, },


Loading…
Cancel
Save