diff --git a/.gitignore b/.gitignore index a752eac..f684ead 100644 --- a/.gitignore +++ b/.gitignore @@ -182,6 +182,7 @@ rcf/ AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml +Package.xml _pkginfo.txt # Visual Studio cache files diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3c9d1de --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,208 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.3.1] - 2021-03-12 + +## Added + +- Support for Signal UUIDs. This is an underlying change in libsignal-service-dotnet. +- Option to toggle sending messages with enter. If disabled on desktop press Shift-Enter to send messages. Thanks to @ShelbyBoss for contributing this feature in https://github.com/signal-csharp/Signal-Windows/pull/224! +- Support for saving and loading drafts including attachment drafts. Thanks to @ShelbyBoss for contributing this feature in https://github.com/signal-csharp/Signal-Windows/pull/225! + +## Fixed + +- Attachment uploads and downloads are now fixed. +- Adding an unknown number will first check if that number has registered with Signal. +- Contact color will not be reset back to its original color if you change it. Thanks to @ShelbyBoss for contributing this feature in https://github.com/signal-csharp/Signal-Windows/pull/225! + +## [0.3.0] - 2021-02-18 + +## Added + +- Support for disappearing messages +- Support for CAPTCHAs when registering + +## Changed + +- Use [Signal Contact Discovery Service](https://signal.org/blog/private-contact-discovery/) for discovering contacts. + +## Fixed + +- 429 error when trying to add new contacts https://github.com/signal-csharp/Signal-Windows/issues/212 +- Incoming messages failing to be received https://github.com/signal-csharp/Signal-Windows/issues/220 + +## [0.2.26] - 2019-12-05 + +## [0.2.25] - 2019-04-03 + +## [0.2.24] - 2019-03-16 + +## Added +- Fullscreen imageview + +## Fixes +- use new cert for pinning + +## [0.2.23] - 2018-12-04 + +## Fixes +- fix "database is locked errors" +- fix crash in privacy settings menu + +## [0.2.22] - 2018-10-02 + +## [0.2.21] - 2018-07-09 + +## Fixes +- fix invalid urls causing a message to be displayed incorrectly + +## [0.2.20] - 2018-07-06 + +## Features +- clickable hyperlinks +- send attachments +- block contacts +## Fixes +- use composed timestamps for the conversation list ordering +- mark bottommost message read when window gets focus +- stop creating multiple identity key change messages +- limit message maximum size to match Signal-Android + +## [0.2.19] - 2018-05-28 + +## Fixes +- Properly suspend on suspend +- Improve textbox reselection speed +- Disable debug log export on crash, the app did not always cleanly shutdown +- Request group/contact sync after linking + +## [0.2.18] - 2018-05-23 + +## Fixes +- rebuild on a different machine + +## [0.2.17] - 2018-05-23 + +## [0.2.16] - 2018-05-21 + +## Features +- offer ui debug log export on crash +- add fancy layout of our settings pages + +## Fixes +- fix conversation re-selection issues + +## [0.2.15] - 2018-05-18 + +## Fixes +- fix messages failing to send if the selected conversation was moved in the conversation list + +## [0.2.14] - 2018-05-17 + +## Features +- import contacts and groups from master device + +## Fixes +- fix not sending the correct disappearing timer +- remove notification when receiving synced sent message +- honor synced read messages from sibling devices + +## [0.2.13] - 2018-05-09 + +## Fixes +- fix app window not being properly activated in all cases + +## [0.2.12] - 2018-05-07 + +## Fixes +- fix crashes on mobile when scrolling + +## [0.2.11] - 2018-05-07 + +## [0.2.10] - 2018-05-04 + +## [0.2.9] - 2018-05-04 + +## Features +- remove notification if the frontend considers a message read + +## Fixes +- mitigate thread-pool blocking by the scroll handler + +## [0.2.8] - 2018-05-03 + +## Features +- support exporting debug logs + +## Fixes +- fix title bar colors for secondary windows +- properly switch to formerly suspended windows + +## [0.2.7] - 2018-04-28 + +## Features +- hide notification when the corresponding message is read + +## Fixes +- fix multiple notification click handling issues + +## [0.2.6] - 2018-04-27 + +## Features +- save read messages as read + +## [0.2.5] - 2018-04-26 + +## Bugfixes +- fix handle acquisition and release when registering/linking + +## [0.2.4] - 2018-04-26 + +## Bugfixes +- fix outgoing newlines + +## Features +- add a background task to poll messages +- initial support for incoming attachments +- support multiple windows on different virtual desktops + +## [0.2.3] - 2017-12-05 + +## Bugfixes +- fix crash on disconnect + +## [0.2.2] - 2017-11-02 + +## Bugfixes +- fix messages being lost on shutdown + +## General +- timestamps in the conversation list +- added logfiles + +## [0.2.1] - 2017-10-22 + +## Bugfixes: +- unrecoverable disconnect after being in the background on W10M + +## General: +- changed the package name +- use shorter timestamps for recent messages +- proper input scopes for mobile keyboards +- signed by a new key + +## Remarks +This release will be installed in a different folder. Backup your databases if you want to keep your old data! + +## [0.2.0] - 2017-10-21 + +Initial alpha release + +## [0.1.9] - 2017-08-29 + +## [0.1.8] - 2017-08-22 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 93910b9..d55a7d8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,7 @@ # Contributing +See more documentation in the `docs` folder. + ## Building ### Requirements @@ -29,7 +31,7 @@ You can link Signal-Windows as a slave to any signal device capable of linking s #### Using a Signal-Android or Signal-iOS master Same procedure as with Signal-Desktop: Scan the qr-code. -#### Using a signal-cli master +#### Using a [signal-cli](https://github.com/AsamK/signal-cli) master Signal-Windows kindly also displays the tsdevice string below the qrcode. Use `signal-cli addDevice --uri` like you would with a Signal-Desktop slave. ## Backing Up the Database @@ -38,3 +40,15 @@ Signal-Windows kindly also displays the tsdevice string below the qrcode. Use `s If you want to backup your database files, `Libsignal.db` and `Signal.db`, you can find them in `C:\Users\\AppData\Local\Packages\2383BenediktRadtke.SignalPrivateMessenger_teak1p7hcx9ga\LocalCache\` On mobile these are found at `LocalAppData\2383BenediktRadtke.SignalPrivateMessenger__arm__teak1p7hcx9ga\LocalCache\` + +## Adding a New View + +1. Add a new Blank Page in `Signal-Windows/Views`. For example NewPage.xaml. +2. Add a new class in `Signal-Windows/ViewModels`. The name of the class should be the `ViewModel` appended to the page name. For example NewPageViewModel.cs. +3. Inherit from `ViewModelBase` (namespace `GalaSoft.MvvmLight`) in the new view model class. For example `public class NewPageViewModel : ViewModelBase` +4. Add a property for the new page in the new view model class. For example `public NewView View { get; set; }` +5. In the new view code behind add a property for the new view model class. For example `public NewPageViewModel Vm { get { return (NewPageViewModel)DataContext; } }` +6. In the new view code behind assign the view model `View` to the code behind class in the constructor. For example in the NewPage constructor add `Vm.View = this;` after `this.InitializeComponent();` +7. In `Signal-Windows/ViewModels/ViewModelLocator.cs` register the new page view model in `SimpleIoc`. For example `SimpleIoc.Default.Register();` +8. Add a property that returns an instance of the new page view model. For example `public NewPageViewModel NewPageInstance { get { return ServiceLocator.Current.GetInstance(Key.ToString()); } }` +9. In the new page XAML set the DataContext of the page to the instance of the new page view model. This must be set in the Page opening tag. For example `DataContext="{Binding NewPageInstance, Source={StaticResource Locator}}"` diff --git a/README.md b/README.md index 0ab02f0..2b8441b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -[![Build Status](https://build.mobile.azure.com/v0.1/apps/cbdae38f-5ebb-4b11-ac7d-3d4b2648c50b/branches/master/badge)](https://build.mobile.azure.com/v0.1/apps/cbdae38f-5ebb-4b11-ac7d-3d4b2648c50b/branches/master/badge) +[![Build status](https://build.appcenter.ms/v0.1/apps/cbdae38f-5ebb-4b11-ac7d-3d4b2648c50b/branches/master/badge)](https://appcenter.ms) +![](https://tokei.rs/b1/github/signal-csharp/Signal-Windows) # Signal-Windows @@ -13,11 +14,16 @@ This is an unofficial Signal client for the Universal Windows Platform. It is (c - add/edit contacts - send and receive text messages -## What can i expect next? +## What can I expect next? - proper attachment handling - syncing groups and contacts on linking - a prettier interface +## I want to try it out but I'm not a developer +You can get Signal-Windows from the Windows store with [this invite link](http://go.microsoft.com/fwlink/?LinkId=532540&mstoken=CQ4QC-KPTJV-XP2VT-67J39-7R9QZ). + +Support for completing CAPTCHAs is provided by [CAPTCHA for Signal Private Messenger](https://github.com/signal-csharp/SignalCaptcha) and its store link is here: https://www.microsoft.com/store/apps/9NXCBTL0SBPJ + ## Bug reports Found a bug? Great! Open a pull request or an issue. @@ -26,12 +32,6 @@ Found a bug? Great! Open a pull request or an issue. [See the contributing docs](CONTRIBUTING.md). # Legal things -## Cryptography Notice - -This distribution includes cryptographic software. The country in which you currently reside may have restrictions on the import, possession, use, and/or re-export to another country, of encryption software. BEFORE using any encryption software, please check your country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. See http://www.wassenaar.org/ for more information. - -The U.S. Government Department of Commerce, Bureau of Industry and Security (BIS), has classified this software as Export Commodity Control Number (ECCN) 5D002.C.1, which includes information security software using or performing cryptographic functions with asymmetric algorithms. The form and manner of this distribution makes it eligible for export under the License Exception ENC Technology Software Unrestricted (TSU) exception (see the BIS Export Administration Regulations, Section 740.13) for both object code and source code. - ## License Copyright 2017 diff --git a/Signal-Windows.Lib/DisappearingMessagesManager.cs b/Signal-Windows.Lib/DisappearingMessagesManager.cs new file mode 100644 index 0000000..05035ca --- /dev/null +++ b/Signal-Windows.Lib/DisappearingMessagesManager.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using libsignalservice.util; +using Signal_Windows.Models; +using Signal_Windows.Storage; +using Windows.UI.Core; + +namespace Signal_Windows.Lib +{ + public static class DisappearingMessagesManager + { + private static Dictionary frames; + + static DisappearingMessagesManager() + { + frames = new Dictionary(); + } + + public static void AddFrontend(CoreDispatcher coreDispatcher, ISignalFrontend frontend) + { + if (!frames.ContainsKey(coreDispatcher)) + { + frames.Add(coreDispatcher, frontend); + } + } + + public static void RemoveFrontend(CoreDispatcher coreDispatcher) + { + if (frames.ContainsKey(coreDispatcher)) + { + frames.Remove(coreDispatcher); + } + } + + /// + /// Queues a message for deletion. + /// + /// The message to queue for deletion + /// If the message expire time is 0 then the message will not be deleted. + public static void QueueForDeletion(SignalMessage message) + { + if (message.ExpiresAt <= 0) + { + return; + } + Action deleteTask = async () => + { + DateTimeOffset expireTime = DateTimeOffset.FromUnixTimeMilliseconds(message.ExpiresAt); + DateTimeOffset receivedTime = DateTimeOffset.FromUnixTimeMilliseconds(message.ReceivedTimestamp); + TimeSpan deleteTimeSpan = expireTime - receivedTime; + if (deleteTimeSpan < TimeSpan.Zero) + { + await DeleteMessage(message); + return; + } + await Task.Delay(deleteTimeSpan); + await DeleteMessage(message); + }; + Task.Run(deleteTask); + } + + /// + /// Deletes expired messages from the database. + /// + public static void DeleteExpiredMessages() + { + long currentTimeMillis = Util.CurrentTimeMillis(); + List expiredMessages = SignalDBContext.GetExpiredMessages(currentTimeMillis); + foreach (var expiredMessage in expiredMessages) + { + DeleteFromDb(expiredMessage); + } + } + + private static async Task DeleteMessage(SignalMessage message) + { + List operations = new List(); + foreach (var dispatcher in frames.Keys) + { + var taskCompletionSource = new TaskCompletionSource(); + await dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => + { + try + { + frames[dispatcher].HandleMessageDelete(message); + } + catch (Exception e) + { + } + finally + { + taskCompletionSource.SetResult(false); + } + }); + operations.Add(taskCompletionSource.Task); + } + + foreach (var t in operations) + { + await t; + } + + DeleteFromDb(message); + } + + private static void DeleteFromDb(SignalMessage message) + { + foreach (var attachment in message.Attachments) + { + SignalDBContext.DeleteAttachment(attachment); + } + SignalDBContext.DeleteMessage(message); + } + } +} diff --git a/Signal-Windows.Lib/Events/SignalMessageEventArgs.cs b/Signal-Windows.Lib/Events/SignalMessageEventArgs.cs new file mode 100644 index 0000000..3d2f142 --- /dev/null +++ b/Signal-Windows.Lib/Events/SignalMessageEventArgs.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Signal_Windows.Models; + +namespace Signal_Windows.Lib.Events +{ + public enum SignalPipeMessageType + { + NormalMessage, + PipeEmptyMessage + } + public class SignalMessageEventArgs : EventArgs + { + public SignalMessage Message { get; private set; } + public SignalPipeMessageType MessageType { get; private set; } + + public SignalMessageEventArgs(SignalMessage message, SignalPipeMessageType type) + { + Message = message; + MessageType = type; + } + } +} diff --git a/Signal-Windows.Lib/GlobalSettingsManager.cs b/Signal-Windows.Lib/GlobalSettingsManager.cs new file mode 100644 index 0000000..776697b --- /dev/null +++ b/Signal-Windows.Lib/GlobalSettingsManager.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Windows.Storage; + +namespace Signal_Windows.Lib +{ + public static class GlobalSettingsManager + { + private const string NotificationsContainer = "notifications"; + private const string PrivacyContainer = "privacy"; + private const string AppearanceContainer = "appearance"; + private const string ChatsAndMediaContainer = "chatsandmedia"; + private const string AdvancedContainer = "advanced"; + + private const string ShowNotificationText = "ShowNotificationText"; + public enum ShowNotificationTextSettings + { + NameAndMessage, + NameOnly, + NoNameOrMessage + } + private const string BlockScreenshots = "BlockScreenshots"; + private const string EnableReadReceipts = "EnableReadReceipts"; + private const string SpellCheck = "SpellCheck"; + private const string SendMessageWithEnter = "SendMessageWithEnter"; + + private static ApplicationDataContainer localSettings; + private static IReadOnlyDictionary Containers + { + get { return localSettings.Containers; } + } + + static GlobalSettingsManager() + { + localSettings = ApplicationData.Current.LocalSettings; + var containers = localSettings.Containers; + if (!containers.ContainsKey(NotificationsContainer)) + { + localSettings.CreateContainer(NotificationsContainer, ApplicationDataCreateDisposition.Always); + } + if (!containers.ContainsKey(PrivacyContainer)) + { + localSettings.CreateContainer(PrivacyContainer, ApplicationDataCreateDisposition.Always); + } + if (!containers.ContainsKey(AppearanceContainer)) + { + localSettings.CreateContainer(AppearanceContainer, ApplicationDataCreateDisposition.Always); + } + if (!containers.ContainsKey(ChatsAndMediaContainer)) + { + localSettings.CreateContainer(ChatsAndMediaContainer, ApplicationDataCreateDisposition.Always); + } + if (!containers.ContainsKey(AdvancedContainer)) + { + localSettings.CreateContainer(AdvancedContainer, ApplicationDataCreateDisposition.Always); + } + } + + public static ShowNotificationTextSettings ShowNotificationTextSetting + { + get + { + return (ShowNotificationTextSettings)GetSetting(Containers[NotificationsContainer], + ShowNotificationText, (int)ShowNotificationTextSettings.NameAndMessage); + } + set + { + Containers[NotificationsContainer].Values[ShowNotificationText] = (int)value; + } + } + + public static bool BlockScreenshotsSetting + { + get + { + return GetSetting(Containers[PrivacyContainer], BlockScreenshots, false); + } + set + { + Containers[PrivacyContainer].Values[BlockScreenshots] = value; + } + } + + public static bool EnableReadReceiptsSetting + { + get + { + return GetSetting(Containers[PrivacyContainer], EnableReadReceipts, true); + } + set + { + Containers[PrivacyContainer].Values[EnableReadReceipts] = value; + } + } + + public static bool SpellCheckSetting + { + get + { + return GetSetting(Containers[ChatsAndMediaContainer], SpellCheck, true); + } + set + { + Containers[ChatsAndMediaContainer].Values[SpellCheck] = value; + } + } + + public static bool SendMessageWithEnterSetting + { + get + { + return GetSetting(Containers[ChatsAndMediaContainer], SendMessageWithEnter, true); + } + set + { + Containers[ChatsAndMediaContainer].Values[SendMessageWithEnter] = value; + } + } + + private static T GetSetting(ApplicationDataContainer container, string key, T defaultValue) + { + if (container.Values.ContainsKey(key)) + { + return (T)container.Values[key]; + } + else + { + container.Values[key] = defaultValue; + return defaultValue; + } + } + } +} diff --git a/Signal-Windows.Lib/IncomingMessages.cs b/Signal-Windows.Lib/IncomingMessages.cs new file mode 100644 index 0000000..7fcb7a9 --- /dev/null +++ b/Signal-Windows.Lib/IncomingMessages.cs @@ -0,0 +1,693 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using libsignal.messages.multidevice; +using libsignalservice; +using libsignalservice.crypto; +using libsignalservice.messages; +using libsignalservice.messages.multidevice; +using libsignalservice.push; +using libsignalservice.util; +using Microsoft.Extensions.Logging; +using Signal_Windows.Models; +using Signal_Windows.Storage; +using static libsignalservice.SignalServiceMessagePipe; + +namespace Signal_Windows.Lib +{ + class IncomingMessages : IMessagePipeCallback + { + private readonly ILogger Logger = LibsignalLogging.CreateLogger(); + private readonly CancellationToken Token; + private readonly SignalServiceMessagePipe Pipe; + private readonly SignalServiceMessageReceiver MessageReceiver; + + public IncomingMessages(CancellationToken token, SignalServiceMessagePipe pipe, SignalServiceMessageReceiver messageReceiver) + { + Token = token; + Pipe = pipe; + MessageReceiver = messageReceiver; + } + + public async Task HandleIncomingMessages() + { + Logger.LogDebug("HandleIncomingMessages()"); + while (!Token.IsCancellationRequested) + { + try + { + await Pipe.ReadBlockingAsync(this); + } + catch (OperationCanceledException) + { + break; + } + catch (Exception e) + { + var line = new StackTrace(e, true).GetFrames()[0].GetFileLineNumber(); + Logger.LogWarning("HandleIncomingMessages() failed: {0} occurred ({1}):\n{2}", e.GetType(), e.Message, e.StackTrace); + } + } + Logger.LogInformation("HandleIncomingMessages() finished"); + } + + public async Task OnMessageAsync(SignalServiceMessagePipeMessage message) + { + Logger.LogTrace("OnMessage() locking"); + await SignalLibHandle.Instance.SemaphoreSlim.WaitAsync(Token); + Logger.LogTrace("OnMessage() locked"); + try + { + if (message is SignalServiceEnvelope envelope) + { + List messages = new List(); + if (envelope.IsReceipt()) + { + SignalMessage update = SignalDBContext.IncreaseReceiptCountLocked(envelope); + if (update != null) + { + await SignalLibHandle.Instance.DispatchMessageUpdate(update); + } + } + else + { + await HandleMessage(envelope); + } + } + else if (message is SignalServiceMessagePipeEmptyMessage) + { + SignalLibHandle.Instance.DispatchPipeEmptyMessage(); + } + } + catch (Exception e) + { + Logger.LogError("OnMessage failed: {0}\n{1}", e.Message, e.StackTrace); + if (e.InnerException != null) + { + Logger.LogError("InnerException: {0}\n{1}", e.InnerException.Message, e.InnerException.StackTrace); + } + } + finally + { + SignalLibHandle.Instance.SemaphoreSlim.Release(); + Logger.LogTrace("OnMessage() released"); + } + } + + private async Task HandleMessage(SignalServiceEnvelope envelope) + { + var cipher = new SignalServiceCipher(new SignalServiceAddress(SignalLibHandle.Instance.Store.OwnGuid, SignalLibHandle.Instance.Store.Username), new Store(), LibUtils.GetCertificateValidator()); + // TODO: Starting to get messages of an unknown type which causes Decrypt to return null, so handle the null case for now. + var content = cipher.Decrypt(envelope); + long timestamp = Util.CurrentTimeMillis(); + + if (content == null) + { + //TODO callmessages + Logger.LogWarning("HandleMessage() received unrecognized message"); + return; + } + if (content.Message != null) + { + SignalServiceDataMessage message = content.Message; + if (message.EndSession) + { + await HandleSessionResetMessage(envelope, content, message, false, timestamp); + } + else if (message.IsGroupUpdate()) + { + if (message.Group.Type == SignalServiceGroup.GroupType.UPDATE) + { + await HandleGroupUpdateMessage(envelope, content, message, false, timestamp); + } + else if (message.Group.Type == SignalServiceGroup.GroupType.QUIT) + { + await HandleGroupLeaveMessage(envelope, content, message, false, timestamp); + } + else if (message.Group.Type == SignalServiceGroup.GroupType.REQUEST_INFO) + { + Logger.LogWarning("Received REQUEST_INFO request"); + } + } + else if (message.ExpirationUpdate) + { + await HandleExpirationUpdateMessage(envelope, content, message, false, timestamp); + } + else + { + await HandleSignalMessage(envelope, content, message, false, timestamp); + } + } + else if (content.SynchronizeMessage != null) + { + if (content.SynchronizeMessage.Sent != null) + { + var syncMessage = content.SynchronizeMessage.Sent; + var dataMessage = syncMessage.Message; + + if (dataMessage.EndSession) + { + await HandleSessionResetMessage(envelope, content, dataMessage, true, timestamp); + } + else if (dataMessage.IsGroupUpdate()) + { + if (dataMessage.Group.Type == SignalServiceGroup.GroupType.UPDATE) + { + await HandleGroupUpdateMessage(envelope, content, dataMessage, true, timestamp); + } + else if (dataMessage.Group.Type == SignalServiceGroup.GroupType.QUIT) + { + await HandleGroupLeaveMessage(envelope, content, dataMessage, true, timestamp); + } + else if (dataMessage.Group.Type == SignalServiceGroup.GroupType.REQUEST_INFO) + { + Logger.LogWarning("Received synced REQUEST_INFO request"); + } + } + else if (dataMessage.ExpirationUpdate) + { + await HandleExpirationUpdateMessage(envelope, content, dataMessage, true, timestamp); + } + else + { + await HandleSignalMessage(envelope, content, dataMessage, true, timestamp); + } + } + else if (content.SynchronizeMessage.Reads != null) + { + var readMessages = content.SynchronizeMessage.Reads; + foreach (var readMessage in readMessages) + { + try + { + await HandleSyncedReadMessage(readMessage); + } + catch (Exception e) + { + Logger.LogError("HandleReadMessage failed: {0}\n{1}", e.Message, e.StackTrace); + } + } + } + else if (content.SynchronizeMessage.BlockedList != null) + { + List blockedNumbers = content.SynchronizeMessage.BlockedList.Addresses; + await HandleBlockedNumbers(blockedNumbers); + } + else if (content.SynchronizeMessage.Groups != null) + { + Logger.LogInformation("HandleMessage() handling groups sync message from device {0}", envelope.GetSourceDevice()); + int read; + var avatarBuffer = new byte[4096]; + var groups = content.SynchronizeMessage.Groups; + using (var tmpFile = LibUtils.CreateTmpFile("groups_sync")) + { + var plaintextStream = await MessageReceiver.RetrieveAttachmentAsync(groups.AsPointer(), tmpFile, + 10000, Token); + var deviceGroupsStream = new DeviceGroupsInputStream(plaintextStream); + var groupsList = new List<(SignalGroup, List)>(); + DeviceGroup g; + while ((g = deviceGroupsStream.Read()) != null) + { + if (g.Avatar != null) + { + SignalServiceAttachmentStream ssas = g.Avatar.AsStream(); + while ((read = ssas.InputStream.Read(avatarBuffer, 0, avatarBuffer.Length)) > 0) + { + + } + } + var group = new SignalGroup() + { + ThreadDisplayName = g.Name, + ThreadId = Base64.EncodeBytes(g.Id), + GroupMemberships = new List(), + CanReceive = true, + ExpiresInSeconds = g.ExpirationTimer != null ? g.ExpirationTimer.Value : 0 + }; + groupsList.Add((group, g.Members)); + } + List dbGroups = await SignalDBContext.InsertOrUpdateGroups(groupsList); + await SignalLibHandle.Instance.DispatchAddOrUpdateConversations(dbGroups); + } + } + else if (content.SynchronizeMessage.Contacts != null && content.SynchronizeMessage.Contacts.Complete) //TODO incomplete updates + { + Logger.LogInformation("HandleMessage() handling contacts sync message from device {0}", envelope.GetSourceDevice()); + int read; + var avatarBuffer = new byte[4096]; + ContactsMessage contacts = content.SynchronizeMessage.Contacts; + using (var tmpFile = LibUtils.CreateTmpFile("contacts_sync")) + { + var plaintextStream = await MessageReceiver.RetrieveAttachmentAsync( + contacts.Contacts.AsPointer(), tmpFile, 10000, Token); + var deviceContactsStream = new DeviceContactsInputStream(plaintextStream); + List contactsList = new List(); + DeviceContact c; + while ((c = deviceContactsStream.Read()) != null) + { + if (c.Avatar != null) + { + SignalServiceAttachmentStream ssas = c.Avatar.AsStream(); + while ((read = ssas.InputStream.Read(avatarBuffer, 0, avatarBuffer.Length)) > 0) + { + + } + } + SignalContact contact = new SignalContact() + { + ThreadDisplayName = c.Name, + ThreadId = c.Address.E164, + ThreadGuid = c.Address.Uuid, + Color = c.Color, + CanReceive = true, + ExpiresInSeconds = c.ExpirationTimer != null ? c.ExpirationTimer.Value : 0 + }; + contactsList.Add(contact); + } + var dbContacts = SignalDBContext.InsertOrUpdateContacts(contactsList); + await SignalLibHandle.Instance.DispatchAddOrUpdateConversations(dbContacts); + } + } + } + else if (content.ReadMessage != null) + { + SignalServiceReceiptMessage receiptMessage = content.ReadMessage; + Logger.LogTrace("HandleMessage() received ReceiptMessage (type={0}, when={1})", receiptMessage.ReceiptType, receiptMessage.When); + } + else + { + //TODO callmessages + Logger.LogWarning("HandleMessage() received unrecognized message"); + } + } + + private async Task HandleSyncedReadMessage(ReadMessage readMessage) + { + var updatedConversation = SignalDBContext.UpdateMessageRead(readMessage.Timestamp); + await SignalLibHandle.Instance.DispatchMessageRead(updatedConversation); + } + + private async Task HandleExpirationUpdateMessage(SignalServiceEnvelope envelope, SignalServiceContent content, SignalServiceDataMessage message, bool isSync, long timestamp) + { + SignalMessageDirection type; + SignalContact author; + SignalMessageStatus status; + string prefix; + SignalConversation conversation; + long composedTimestamp; + + if (isSync) + { + var sent = content.SynchronizeMessage.Sent; + type = SignalMessageDirection.Synced; + status = SignalMessageStatus.Confirmed; + composedTimestamp = sent.Timestamp; + author = null; + prefix = "You have"; + if (message.Group != null) + { + conversation = await SignalDBContext.GetOrCreateGroupLocked(Base64.EncodeBytes(message.Group.GroupId), 0); + } + else + { + conversation = await SignalDBContext.GetOrCreateContactLocked(sent.Destination.E164, sent.Destination.Uuid); + } + } + else + { + status = 0; + type = SignalMessageDirection.Incoming; + author = await SignalDBContext.GetOrCreateContactLocked(content.Sender.E164, content.Sender.Uuid); + prefix = $"{author.ThreadDisplayName} has"; + composedTimestamp = envelope.GetTimestamp(); + if (message.Group != null) + { + conversation = await SignalDBContext.GetOrCreateGroupLocked(Base64.EncodeBytes(message.Group.GroupId), 0); + } + else + { + conversation = await SignalDBContext.GetOrCreateContactLocked(content.Sender.E164, content.Sender.Uuid); + } + } + conversation.ExpiresInSeconds = (uint)message.ExpiresInSeconds; + SignalDBContext.UpdateExpiresInLocked(conversation); + string finalMessage; + if (message.ExpiresInSeconds == 0) + { + finalMessage = $"{prefix} has turned off disappearing messages."; + } + else + { + finalMessage = $"{prefix} set disappearing message time to {message.ExpiresInSeconds} seconds."; + } + // Update conversations to reflect the new expires in seconds + await SignalLibHandle.Instance.DispatchAddOrUpdateConversation(conversation, null); + SignalMessage sm = new SignalMessage() + { + Direction = type, + Type = SignalMessageType.ExpireUpdate, + Status = status, + Author = author, + Content = new SignalMessageContent() { Content = finalMessage }, + ThreadId = conversation.ThreadId, + ThreadGuid = conversation.ThreadGuid, + DeviceId = (uint)envelope.GetSourceDevice(), + Receipts = 0, + ComposedTimestamp = composedTimestamp, + ReceivedTimestamp = timestamp, + }; + await SignalLibHandle.Instance.SaveAndDispatchSignalMessage(sm, null, conversation); + } + + private async Task HandleSessionResetMessage(SignalServiceEnvelope envelope, SignalServiceContent content, SignalServiceDataMessage dataMessage, bool isSync, long timestamp) + { + SignalMessageDirection type; + SignalContact author; + SignalMessageStatus status; + SignalConversation conversation; + string prefix; + string conversationId; + Guid? conversationGuid; + long composedTimestamp; + + if (isSync) + { + var sent = content.SynchronizeMessage.Sent; + type = SignalMessageDirection.Synced; + status = SignalMessageStatus.Confirmed; + composedTimestamp = sent.Timestamp; + author = null; + prefix = "You have"; + conversationId = sent.Destination.E164; + conversationGuid = sent.Destination.Uuid; + } + else + { + status = 0; + type = SignalMessageDirection.Incoming; + author = await SignalDBContext.GetOrCreateContactLocked(content.Sender.E164, content.Sender.Uuid); + prefix = $"{author.ThreadDisplayName} has"; + composedTimestamp = envelope.GetTimestamp(); + conversationId = content.Sender.E164; + conversationGuid = content.Sender.Uuid; + } + LibsignalDBContext.DeleteAllSessions(conversationId); + conversation = await SignalDBContext.GetOrCreateContactLocked(conversationId, conversationGuid); + + SignalMessage sm = new SignalMessage() + { + Direction = type, + Type = SignalMessageType.SessionReset, + Status = status, + Author = author, + Content = new SignalMessageContent() { Content = $"{prefix} reset the session." }, + ThreadId = conversationId, + ThreadGuid = conversationGuid, + DeviceId = (uint)envelope.GetSourceDevice(), + Receipts = 0, + ComposedTimestamp = composedTimestamp, + ReceivedTimestamp = timestamp, + }; + await SignalLibHandle.Instance.SaveAndDispatchSignalMessage(sm, null, conversation); + } + + /// + /// Handles a list of blocked numbers. This will update the database to match the + /// blocked numbers list. + /// + /// The list of blocked numbers. + private async Task HandleBlockedNumbers(List blockedNumbers) + { + List blockedE164 = blockedNumbers.Select(a => a.E164).ToList(); + List blockedContacts = new List(); + List contacts = SignalDBContext.GetAllContactsLocked(); + foreach (var contact in contacts) + { + if (blockedE164.Contains(contact.ThreadId)) + { + if (!contact.Blocked) + { + contact.Blocked = true; + SignalDBContext.UpdateBlockStatus(contact); + blockedContacts.Add(contact); + } + } + else + { + if (contact.Blocked) + { + contact.Blocked = false; + SignalDBContext.UpdateBlockStatus(contact); + } + } + } + await SignalLibHandle.Instance.DispatchHandleBlockedContacts(blockedContacts); + } + + private async Task HandleGroupLeaveMessage(SignalServiceEnvelope envelope, SignalServiceContent content, SignalServiceDataMessage dataMessage, bool isSync, long timestamp) + { + SignalServiceGroup sentGroup = dataMessage.Group; + if (sentGroup != null) + { + string groupid = Base64.EncodeBytes(sentGroup.GroupId); + SignalGroup group = await SignalDBContext.GetOrCreateGroupLocked(groupid, 0); + if (isSync) + { + SignalContact author = await SignalDBContext.GetOrCreateContactLocked(content.Sender.E164, content.Sender.Uuid); + SignalMessage sm = new SignalMessage() + { + Direction = SignalMessageDirection.Incoming, + Type = SignalMessageType.GroupLeave, + Status = SignalMessageStatus.Received, + Author = author, + Content = new SignalMessageContent() { Content = $"You have left the group." }, + ThreadId = groupid, + DeviceId = (uint)envelope.GetSourceDevice(), + Receipts = 0, + ComposedTimestamp = envelope.GetTimestamp(), + ReceivedTimestamp = timestamp, + }; + SignalConversation updatedConversation = SignalDBContext.RemoveMemberFromGroup(groupid, author, sm); + await SignalLibHandle.Instance.DispatchAddOrUpdateConversation(updatedConversation, sm); + } + else + { + SignalContact author = await SignalDBContext.GetOrCreateContactLocked(content.Sender.E164, content.Sender.Uuid); + SignalMessage sm = new SignalMessage() + { + Direction = SignalMessageDirection.Incoming, + Type = SignalMessageType.GroupLeave, + Status = SignalMessageStatus.Received, + Author = author, + Content = new SignalMessageContent() { Content = $"{author.ThreadDisplayName} has left the group." }, + ThreadId = groupid, + DeviceId = (uint)envelope.GetSourceDevice(), + Receipts = 0, + ComposedTimestamp = envelope.GetTimestamp(), + ReceivedTimestamp = timestamp, + }; + SignalConversation updatedConversation = SignalDBContext.RemoveMemberFromGroup(groupid, author, sm); + await SignalLibHandle.Instance.DispatchAddOrUpdateConversation(updatedConversation, sm); + } + } + else + { + Logger.LogError("HandleGroupLeaveMessage() received group update without group info"); + } + } + + private async Task HandleGroupUpdateMessage(SignalServiceEnvelope envelope, SignalServiceContent content, SignalServiceDataMessage dataMessage, bool isSync, long timestamp) + { + if (dataMessage.Group != null) //TODO check signal Android: group messages have different types! + { + SignalServiceGroup group = dataMessage.Group; + string groupid = Base64.EncodeBytes(group.GroupId); + SignalGroup g = new SignalGroup(); + string displayname = "Unknown group"; + string avatarfile = null; + if (group.Name != null) + { + displayname = group.Name; + } + var dbgroup = SignalDBContext.InsertOrUpdateGroupLocked(groupid, displayname, avatarfile, true, timestamp); + if (group.Members != null) + { + foreach (var member in group.Members) + { + SignalDBContext.InsertOrUpdateGroupMembershipLocked(dbgroup.Id, (await SignalDBContext.GetOrCreateContactLocked(member.E164, member.Uuid)).Id); + } + } + + /* insert message into conversation */ + SignalMessageDirection type; + SignalContact author; + SignalMessageStatus status; + string prefix; + long composedTimestamp; + + if (isSync) + { + var sent = content.SynchronizeMessage.Sent; + type = SignalMessageDirection.Synced; + status = SignalMessageStatus.Confirmed; + composedTimestamp = sent.Timestamp; + author = null; + prefix = "You have"; + } + else + { + status = 0; + type = SignalMessageDirection.Incoming; + author = await SignalDBContext.GetOrCreateContactLocked(content.Sender.E164, content.Sender.Uuid); + prefix = $"{author.ThreadDisplayName} has"; + composedTimestamp = envelope.GetTimestamp(); + } + + SignalMessage sm = new SignalMessage() + { + Direction = type, + Type = SignalMessageType.GroupUpdate, + Status = status, + Author = author, + Content = new SignalMessageContent() { Content = $"{prefix} updated the group." }, + ThreadId = groupid, + DeviceId = (uint)envelope.GetSourceDevice(), + Receipts = 0, + ComposedTimestamp = composedTimestamp, + ReceivedTimestamp = timestamp + }; + SignalDBContext.SaveMessageLocked(sm); + dbgroup.MessagesCount += 1; + if (sm.Direction == SignalMessageDirection.Incoming) + { + dbgroup.UnreadCount += 1; + } + else + { + dbgroup.UnreadCount = 0; + dbgroup.LastSeenMessageIndex = dbgroup.MessagesCount; + } + dbgroup.LastMessage = sm; + await SignalLibHandle.Instance.DispatchAddOrUpdateConversation(dbgroup, sm); + } + else + { + Logger.LogError("HandleGroupUpdateMessage() received group update without group info"); + } + } + + private async Task HandleSignalMessage(SignalServiceEnvelope envelope, SignalServiceContent content, SignalServiceDataMessage dataMessage, bool isSync, long timestamp) + { + SignalMessageDirection type; + SignalContact author; + SignalMessageStatus status; + SignalConversation conversation; + long composedTimestamp; + string body = dataMessage.Body ?? ""; + + if (dataMessage.Group != null) + { + var rawId = dataMessage.Group.GroupId; + var threadId = Base64.EncodeBytes(rawId); + conversation = await SignalDBContext.GetOrCreateGroupLocked(threadId, timestamp); + if (!conversation.CanReceive) + { + SignalServiceGroup group = new SignalServiceGroup(SignalServiceGroup.GroupType.REQUEST_INFO, + rawId, null, null, null); + SignalServiceDataMessage requestInfoMessage = new SignalServiceDataMessage(Util.CurrentTimeMillis(), + group, null, null); + SignalLibHandle.Instance.OutgoingQueue.Add(new SignalServiceDataMessageSendable(requestInfoMessage, + envelope.GetSourceAddress())); + } + composedTimestamp = envelope.GetTimestamp(); + } + else + { + if (isSync) + { + var sent = content.SynchronizeMessage.Sent; + conversation = await SignalDBContext.GetOrCreateContactLocked(sent.Destination.E164, sent.Destination.Uuid); + composedTimestamp = sent.Timestamp; + } + else + { + conversation = await SignalDBContext.GetOrCreateContactLocked(content.Sender.E164, content.Sender.Uuid); + composedTimestamp = envelope.GetTimestamp(); + } + } + + if (isSync) + { + type = SignalMessageDirection.Synced; + status = SignalMessageStatus.Confirmed; + author = null; + } + else + { + status = 0; + type = SignalMessageDirection.Incoming; + author = await SignalDBContext.GetOrCreateContactLocked(content.Sender.E164, content.Sender.Uuid); + } + + if (author != null && author.Blocked) + { + // Don't save blocked messages + return; + } + + List attachments = new List(); + SignalMessage message = new SignalMessage() + { + Direction = type, + Status = status, + Author = author, + Content = new SignalMessageContent() { Content = body.Truncate(2000) }, + ThreadId = conversation.ThreadId, + ThreadGuid = conversation.ThreadGuid, + DeviceId = (uint)envelope.GetSourceDevice(), + Receipts = 0, + ComposedTimestamp = composedTimestamp, + ReceivedTimestamp = timestamp, + AttachmentsCount = (uint)attachments.Count, + Attachments = attachments + }; + if (dataMessage.Attachments != null) + { + var receivedAttachments = dataMessage.Attachments; + foreach (var receivedAttachment in receivedAttachments) + { + var pointer = receivedAttachment.AsPointer(); + SignalAttachment sa = new SignalAttachment() + { + Message = message, + Status = (uint)SignalAttachmentStatus.Default, + SentFileName = pointer.FileName, + ContentType = receivedAttachment.ContentType, + Key = pointer.Key, + Relay = null, + CdnNumber = pointer.CdnNumber, + Size = (long)pointer.Size, + Digest = pointer.Digest + }; + + if (pointer.RemoteId.V2.HasValue) + { + sa.StorageId = (ulong)pointer.RemoteId.V2.Value; + } + else + { + sa.V3StorageId = pointer.RemoteId.V3; + } + attachments.Add(sa); + } + + // Make sure to update attachments count + message.AttachmentsCount = (uint)attachments.Count; + } + await SignalLibHandle.Instance.SaveAndDispatchSignalMessage(message, null, conversation); + } + } +} diff --git a/Signal-Windows/Migrations/LibsignalDB/20170806145530_ls1.Designer.cs b/Signal-Windows.Lib/Migrations/LibsignalDB/20170806145530_ls1.Designer.cs similarity index 100% rename from Signal-Windows/Migrations/LibsignalDB/20170806145530_ls1.Designer.cs rename to Signal-Windows.Lib/Migrations/LibsignalDB/20170806145530_ls1.Designer.cs diff --git a/Signal-Windows/Migrations/LibsignalDB/20170806145530_ls1.cs b/Signal-Windows.Lib/Migrations/LibsignalDB/20170806145530_ls1.cs similarity index 100% rename from Signal-Windows/Migrations/LibsignalDB/20170806145530_ls1.cs rename to Signal-Windows.Lib/Migrations/LibsignalDB/20170806145530_ls1.cs diff --git a/Signal-Windows.Lib/Migrations/LibsignalDB/20210311062557_m2.Designer.cs b/Signal-Windows.Lib/Migrations/LibsignalDB/20210311062557_m2.Designer.cs new file mode 100644 index 0000000..66826bd --- /dev/null +++ b/Signal-Windows.Lib/Migrations/LibsignalDB/20210311062557_m2.Designer.cs @@ -0,0 +1,115 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Signal_Windows.Storage; +using Signal_Windows.Models; + +namespace Signal_Windows.Migrations.LibsignalDB +{ + [DbContext(typeof(LibsignalDBContext))] + [Migration("20210311062557_m2")] + partial class m2 + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "1.1.5"); + + modelBuilder.Entity("Signal_Windows.Models.SignalIdentity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("IdentityKey"); + + b.Property("Username"); + + b.Property("VerifiedStatus"); + + b.HasKey("Id"); + + b.HasIndex("Username"); + + b.ToTable("Identities"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalPreKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Key"); + + b.HasKey("Id"); + + b.HasIndex("Id"); + + b.ToTable("PreKeys"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DeviceId"); + + b.Property("Session"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("Username"); + + b.ToTable("Sessions"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalSignedPreKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Key"); + + b.HasKey("Id"); + + b.ToTable("SignedPreKeys"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalStore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DeviceId"); + + b.Property("IdentityKeyPair"); + + b.Property("NextSignedPreKeyId"); + + b.Property("OwnGuid"); + + b.Property("Password"); + + b.Property("PreKeyIdOffset"); + + b.Property("Registered"); + + b.Property("RegistrationId"); + + b.Property("SignalingKey"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.ToTable("Store"); + }); + } + } +} diff --git a/Signal-Windows.Lib/Migrations/LibsignalDB/20210311062557_m2.cs b/Signal-Windows.Lib/Migrations/LibsignalDB/20210311062557_m2.cs new file mode 100644 index 0000000..2c2191c --- /dev/null +++ b/Signal-Windows.Lib/Migrations/LibsignalDB/20210311062557_m2.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Signal_Windows.Migrations.LibsignalDB +{ + public partial class m2 : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "OwnGuid", + table: "Store", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "OwnGuid", + table: "Store"); + } + } +} diff --git a/Signal-Windows/Migrations/LibsignalDB/LibsignalDBContextModelSnapshot.cs b/Signal-Windows.Lib/Migrations/LibsignalDB/LibsignalDBContextModelSnapshot.cs similarity index 81% rename from Signal-Windows/Migrations/LibsignalDB/LibsignalDBContextModelSnapshot.cs rename to Signal-Windows.Lib/Migrations/LibsignalDB/LibsignalDBContextModelSnapshot.cs index 897667a..72be80b 100644 --- a/Signal-Windows/Migrations/LibsignalDB/LibsignalDBContextModelSnapshot.cs +++ b/Signal-Windows.Lib/Migrations/LibsignalDB/LibsignalDBContextModelSnapshot.cs @@ -1,6 +1,10 @@ -using Microsoft.EntityFrameworkCore; +using System; +using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; using Signal_Windows.Storage; +using Signal_Windows.Models; namespace Signal_Windows.Migrations.LibsignalDB { @@ -10,11 +14,11 @@ partial class LibsignalDBContextModelSnapshot : ModelSnapshot protected override void BuildModel(ModelBuilder modelBuilder) { modelBuilder - .HasAnnotation("ProductVersion", "1.1.2"); + .HasAnnotation("ProductVersion", "1.1.5"); modelBuilder.Entity("Signal_Windows.Models.SignalIdentity", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd(); b.Property("IdentityKey"); @@ -32,7 +36,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Signal_Windows.Models.SignalPreKey", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd(); b.Property("Key"); @@ -46,7 +50,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Signal_Windows.Models.SignalSession", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd(); b.Property("DeviceId"); @@ -66,7 +70,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Signal_Windows.Models.SignalSignedPreKey", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd(); b.Property("Key"); @@ -78,7 +82,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Signal_Windows.Models.SignalStore", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd(); b.Property("DeviceId"); @@ -87,6 +91,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("NextSignedPreKeyId"); + b.Property("OwnGuid"); + b.Property("Password"); b.Property("PreKeyIdOffset"); @@ -105,4 +111,4 @@ protected override void BuildModel(ModelBuilder modelBuilder) }); } } -} \ No newline at end of file +} diff --git a/Signal-Windows/Migrations/SignalDB/20170806163624_s1.cs b/Signal-Windows.Lib/Migrations/SignalDB/20170806163624_s1.cs similarity index 100% rename from Signal-Windows/Migrations/SignalDB/20170806163624_s1.cs rename to Signal-Windows.Lib/Migrations/SignalDB/20170806163624_s1.cs diff --git a/Signal-Windows/Migrations/SignalDB/20170806163624_s1.designer.cs b/Signal-Windows.Lib/Migrations/SignalDB/20170806163624_s1.designer.cs similarity index 100% rename from Signal-Windows/Migrations/SignalDB/20170806163624_s1.designer.cs rename to Signal-Windows.Lib/Migrations/SignalDB/20170806163624_s1.designer.cs diff --git a/Signal-Windows/Migrations/SignalDB/20170825082857_m2.Designer.cs b/Signal-Windows.Lib/Migrations/SignalDB/20170825082857_m2.Designer.cs similarity index 100% rename from Signal-Windows/Migrations/SignalDB/20170825082857_m2.Designer.cs rename to Signal-Windows.Lib/Migrations/SignalDB/20170825082857_m2.Designer.cs diff --git a/Signal-Windows/Migrations/SignalDB/20170825082857_m2.cs b/Signal-Windows.Lib/Migrations/SignalDB/20170825082857_m2.cs similarity index 100% rename from Signal-Windows/Migrations/SignalDB/20170825082857_m2.cs rename to Signal-Windows.Lib/Migrations/SignalDB/20170825082857_m2.cs diff --git a/Signal-Windows/Migrations/SignalDB/20170901124533_m3.Designer.cs b/Signal-Windows.Lib/Migrations/SignalDB/20170901124533_m3.Designer.cs similarity index 100% rename from Signal-Windows/Migrations/SignalDB/20170901124533_m3.Designer.cs rename to Signal-Windows.Lib/Migrations/SignalDB/20170901124533_m3.Designer.cs diff --git a/Signal-Windows/Migrations/SignalDB/20170901124533_m3.cs b/Signal-Windows.Lib/Migrations/SignalDB/20170901124533_m3.cs similarity index 100% rename from Signal-Windows/Migrations/SignalDB/20170901124533_m3.cs rename to Signal-Windows.Lib/Migrations/SignalDB/20170901124533_m3.cs diff --git a/Signal-Windows.Lib/Migrations/SignalDB/20180203062229_m4.Designer.cs b/Signal-Windows.Lib/Migrations/SignalDB/20180203062229_m4.Designer.cs new file mode 100644 index 0000000..4aa924b --- /dev/null +++ b/Signal-Windows.Lib/Migrations/SignalDB/20180203062229_m4.Designer.cs @@ -0,0 +1,256 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Signal_Windows.Storage; +using Signal_Windows.Models; + +namespace Signal_Windows.Migrations +{ + [DbContext(typeof(SignalDBContext))] + [Migration("20180203062229_m4")] + partial class m4 + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "1.1.4"); + + modelBuilder.Entity("Signal_Windows.Models.GroupMembership", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ContactId"); + + b.Property("GroupId"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.HasIndex("GroupId"); + + b.ToTable("GroupMemberships"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ContentType"); + + b.Property("Digest"); + + b.Property("FileName"); + + b.Property("Key"); + + b.Property("MessageId"); + + b.Property("Relay"); + + b.Property("SentFileName"); + + b.Property("Size"); + + b.Property("Status"); + + b.Property("StorageId"); + + b.HasKey("Id"); + + b.HasIndex("MessageId"); + + b.ToTable("Attachments"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalConversation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AvatarFile"); + + b.Property("CanReceive"); + + b.Property("Discriminator") + .IsRequired(); + + b.Property("Draft"); + + b.Property("ExpiresInSeconds"); + + b.Property("LastActiveTimestamp"); + + b.Property("LastMessageId"); + + b.Property("LastSeenMessageId"); + + b.Property("LastSeenMessageIndex"); + + b.Property("MessagesCount"); + + b.Property("ThreadDisplayName"); + + b.Property("ThreadId"); + + b.Property("UnreadCount"); + + b.HasKey("Id"); + + b.HasIndex("LastMessageId"); + + b.HasIndex("LastSeenMessageId"); + + b.HasIndex("ThreadId"); + + b.ToTable("SignalConversation"); + + b.HasDiscriminator("Discriminator").HasValue("SignalConversation"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalEarlyReceipt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DeviceId"); + + b.Property("Timestamp"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("Timestamp"); + + b.HasIndex("Username"); + + b.ToTable("EarlyReceipts"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AttachmentsCount"); + + b.Property("AuthorId"); + + b.Property("ComposedTimestamp"); + + b.Property("Contentrowid"); + + b.Property("DeviceId"); + + b.Property("Direction"); + + b.Property("ExpiresAt"); + + b.Property("Read"); + + b.Property("Receipts"); + + b.Property("ReceivedTimestamp"); + + b.Property("Status"); + + b.Property("ThreadId"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("Contentrowid"); + + b.HasIndex("ThreadId"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalMessageContent", b => + { + b.Property("rowid") + .ValueGeneratedOnAdd(); + + b.Property("Content"); + + b.HasKey("rowid"); + + b.ToTable("Messages_fts"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalContact", b => + { + b.HasBaseType("Signal_Windows.Models.SignalConversation"); + + b.Property("Color"); + + b.ToTable("SignalContact"); + + b.HasDiscriminator().HasValue("SignalContact"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalGroup", b => + { + b.HasBaseType("Signal_Windows.Models.SignalConversation"); + + + b.ToTable("SignalGroup"); + + b.HasDiscriminator().HasValue("SignalGroup"); + }); + + modelBuilder.Entity("Signal_Windows.Models.GroupMembership", b => + { + b.HasOne("Signal_Windows.Models.SignalContact", "Contact") + .WithMany("GroupMemberships") + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Signal_Windows.Models.SignalGroup", "Group") + .WithMany("GroupMemberships") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalAttachment", b => + { + b.HasOne("Signal_Windows.Models.SignalMessage", "Message") + .WithMany("Attachments") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalConversation", b => + { + b.HasOne("Signal_Windows.Models.SignalMessage", "LastMessage") + .WithMany() + .HasForeignKey("LastMessageId"); + + b.HasOne("Signal_Windows.Models.SignalMessage", "LastSeenMessage") + .WithMany() + .HasForeignKey("LastSeenMessageId"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalMessage", b => + { + b.HasOne("Signal_Windows.Models.SignalContact", "Author") + .WithMany() + .HasForeignKey("AuthorId"); + + b.HasOne("Signal_Windows.Models.SignalMessageContent", "Content") + .WithMany() + .HasForeignKey("Contentrowid"); + }); + } + } +} diff --git a/Signal-Windows.Lib/Migrations/SignalDB/20180203062229_m4.cs b/Signal-Windows.Lib/Migrations/SignalDB/20180203062229_m4.cs new file mode 100644 index 0000000..aee4bde --- /dev/null +++ b/Signal-Windows.Lib/Migrations/SignalDB/20180203062229_m4.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Signal_Windows.Migrations +{ + public partial class m4 : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Digest", + table: "Attachments", + nullable: true); + + migrationBuilder.AddColumn( + name: "Size", + table: "Attachments", + nullable: false, + defaultValue: 0L); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Digest", + table: "Attachments"); + + migrationBuilder.DropColumn( + name: "Size", + table: "Attachments"); + } + } +} diff --git a/Signal-Windows.Lib/Migrations/SignalDB/20180211071131_m5.Designer.cs b/Signal-Windows.Lib/Migrations/SignalDB/20180211071131_m5.Designer.cs new file mode 100644 index 0000000..bf2b5ac --- /dev/null +++ b/Signal-Windows.Lib/Migrations/SignalDB/20180211071131_m5.Designer.cs @@ -0,0 +1,258 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Signal_Windows.Storage; +using Signal_Windows.Models; + +namespace Signal_Windows.Migrations +{ + [DbContext(typeof(SignalDBContext))] + [Migration("20180211071131_m5")] + partial class m5 + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "1.1.4"); + + modelBuilder.Entity("Signal_Windows.Models.GroupMembership", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ContactId"); + + b.Property("GroupId"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.HasIndex("GroupId"); + + b.ToTable("GroupMemberships"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ContentType"); + + b.Property("Digest"); + + b.Property("FileName"); + + b.Property("Guid"); + + b.Property("Key"); + + b.Property("MessageId"); + + b.Property("Relay"); + + b.Property("SentFileName"); + + b.Property("Size"); + + b.Property("Status"); + + b.Property("StorageId"); + + b.HasKey("Id"); + + b.HasIndex("MessageId"); + + b.ToTable("Attachments"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalConversation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AvatarFile"); + + b.Property("CanReceive"); + + b.Property("Discriminator") + .IsRequired(); + + b.Property("Draft"); + + b.Property("ExpiresInSeconds"); + + b.Property("LastActiveTimestamp"); + + b.Property("LastMessageId"); + + b.Property("LastSeenMessageId"); + + b.Property("LastSeenMessageIndex"); + + b.Property("MessagesCount"); + + b.Property("ThreadDisplayName"); + + b.Property("ThreadId"); + + b.Property("UnreadCount"); + + b.HasKey("Id"); + + b.HasIndex("LastMessageId"); + + b.HasIndex("LastSeenMessageId"); + + b.HasIndex("ThreadId"); + + b.ToTable("SignalConversation"); + + b.HasDiscriminator("Discriminator").HasValue("SignalConversation"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalEarlyReceipt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DeviceId"); + + b.Property("Timestamp"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("Timestamp"); + + b.HasIndex("Username"); + + b.ToTable("EarlyReceipts"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AttachmentsCount"); + + b.Property("AuthorId"); + + b.Property("ComposedTimestamp"); + + b.Property("Contentrowid"); + + b.Property("DeviceId"); + + b.Property("Direction"); + + b.Property("ExpiresAt"); + + b.Property("Read"); + + b.Property("Receipts"); + + b.Property("ReceivedTimestamp"); + + b.Property("Status"); + + b.Property("ThreadId"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("Contentrowid"); + + b.HasIndex("ThreadId"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalMessageContent", b => + { + b.Property("rowid") + .ValueGeneratedOnAdd(); + + b.Property("Content"); + + b.HasKey("rowid"); + + b.ToTable("Messages_fts"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalContact", b => + { + b.HasBaseType("Signal_Windows.Models.SignalConversation"); + + b.Property("Color"); + + b.ToTable("SignalContact"); + + b.HasDiscriminator().HasValue("SignalContact"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalGroup", b => + { + b.HasBaseType("Signal_Windows.Models.SignalConversation"); + + + b.ToTable("SignalGroup"); + + b.HasDiscriminator().HasValue("SignalGroup"); + }); + + modelBuilder.Entity("Signal_Windows.Models.GroupMembership", b => + { + b.HasOne("Signal_Windows.Models.SignalContact", "Contact") + .WithMany("GroupMemberships") + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Signal_Windows.Models.SignalGroup", "Group") + .WithMany("GroupMemberships") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalAttachment", b => + { + b.HasOne("Signal_Windows.Models.SignalMessage", "Message") + .WithMany("Attachments") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalConversation", b => + { + b.HasOne("Signal_Windows.Models.SignalMessage", "LastMessage") + .WithMany() + .HasForeignKey("LastMessageId"); + + b.HasOne("Signal_Windows.Models.SignalMessage", "LastSeenMessage") + .WithMany() + .HasForeignKey("LastSeenMessageId"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalMessage", b => + { + b.HasOne("Signal_Windows.Models.SignalContact", "Author") + .WithMany() + .HasForeignKey("AuthorId"); + + b.HasOne("Signal_Windows.Models.SignalMessageContent", "Content") + .WithMany() + .HasForeignKey("Contentrowid"); + }); + } + } +} diff --git a/Signal-Windows.Lib/Migrations/SignalDB/20180211071131_m5.cs b/Signal-Windows.Lib/Migrations/SignalDB/20180211071131_m5.cs new file mode 100644 index 0000000..dad83a6 --- /dev/null +++ b/Signal-Windows.Lib/Migrations/SignalDB/20180211071131_m5.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Signal_Windows.Migrations +{ + public partial class m5 : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Guid", + table: "Attachments", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Guid", + table: "Attachments"); + } + } +} diff --git a/Signal-Windows.Lib/Migrations/SignalDB/20180521001340_m6.Designer.cs b/Signal-Windows.Lib/Migrations/SignalDB/20180521001340_m6.Designer.cs new file mode 100644 index 0000000..689ef7e --- /dev/null +++ b/Signal-Windows.Lib/Migrations/SignalDB/20180521001340_m6.Designer.cs @@ -0,0 +1,260 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Signal_Windows.Storage; +using Signal_Windows.Models; + +namespace Signal_Windows.Migrations +{ + [DbContext(typeof(SignalDBContext))] + [Migration("20180521001340_m6")] + partial class m6 + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "1.1.4"); + + modelBuilder.Entity("Signal_Windows.Models.GroupMembership", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ContactId"); + + b.Property("GroupId"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.HasIndex("GroupId"); + + b.ToTable("GroupMemberships"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ContentType"); + + b.Property("Digest"); + + b.Property("FileName"); + + b.Property("Guid"); + + b.Property("Key"); + + b.Property("MessageId"); + + b.Property("Relay"); + + b.Property("SentFileName"); + + b.Property("Size"); + + b.Property("Status"); + + b.Property("StorageId"); + + b.HasKey("Id"); + + b.HasIndex("MessageId"); + + b.ToTable("Attachments"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalConversation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AvatarFile"); + + b.Property("CanReceive"); + + b.Property("Discriminator") + .IsRequired(); + + b.Property("Draft"); + + b.Property("ExpiresInSeconds"); + + b.Property("LastActiveTimestamp"); + + b.Property("LastMessageId"); + + b.Property("LastSeenMessageId"); + + b.Property("LastSeenMessageIndex"); + + b.Property("MessagesCount"); + + b.Property("ThreadDisplayName"); + + b.Property("ThreadId"); + + b.Property("UnreadCount"); + + b.HasKey("Id"); + + b.HasIndex("LastMessageId"); + + b.HasIndex("LastSeenMessageId"); + + b.HasIndex("ThreadId"); + + b.ToTable("SignalConversation"); + + b.HasDiscriminator("Discriminator").HasValue("SignalConversation"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalEarlyReceipt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DeviceId"); + + b.Property("Timestamp"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("Timestamp"); + + b.HasIndex("Username"); + + b.ToTable("EarlyReceipts"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AttachmentsCount"); + + b.Property("AuthorId"); + + b.Property("ComposedTimestamp"); + + b.Property("Contentrowid"); + + b.Property("DeviceId"); + + b.Property("Direction"); + + b.Property("ExpiresAt"); + + b.Property("Read"); + + b.Property("Receipts"); + + b.Property("ReceivedTimestamp"); + + b.Property("Status"); + + b.Property("ThreadId"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("Contentrowid"); + + b.HasIndex("ThreadId"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalMessageContent", b => + { + b.Property("rowid") + .ValueGeneratedOnAdd(); + + b.Property("Content"); + + b.HasKey("rowid"); + + b.ToTable("Messages_fts"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalContact", b => + { + b.HasBaseType("Signal_Windows.Models.SignalConversation"); + + b.Property("Blocked"); + + b.Property("Color"); + + b.ToTable("SignalContact"); + + b.HasDiscriminator().HasValue("SignalContact"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalGroup", b => + { + b.HasBaseType("Signal_Windows.Models.SignalConversation"); + + + b.ToTable("SignalGroup"); + + b.HasDiscriminator().HasValue("SignalGroup"); + }); + + modelBuilder.Entity("Signal_Windows.Models.GroupMembership", b => + { + b.HasOne("Signal_Windows.Models.SignalContact", "Contact") + .WithMany("GroupMemberships") + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Signal_Windows.Models.SignalGroup", "Group") + .WithMany("GroupMemberships") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalAttachment", b => + { + b.HasOne("Signal_Windows.Models.SignalMessage", "Message") + .WithMany("Attachments") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalConversation", b => + { + b.HasOne("Signal_Windows.Models.SignalMessage", "LastMessage") + .WithMany() + .HasForeignKey("LastMessageId"); + + b.HasOne("Signal_Windows.Models.SignalMessage", "LastSeenMessage") + .WithMany() + .HasForeignKey("LastSeenMessageId"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalMessage", b => + { + b.HasOne("Signal_Windows.Models.SignalContact", "Author") + .WithMany() + .HasForeignKey("AuthorId"); + + b.HasOne("Signal_Windows.Models.SignalMessageContent", "Content") + .WithMany() + .HasForeignKey("Contentrowid"); + }); + } + } +} diff --git a/Signal-Windows.Lib/Migrations/SignalDB/20180521001340_m6.cs b/Signal-Windows.Lib/Migrations/SignalDB/20180521001340_m6.cs new file mode 100644 index 0000000..9a36317 --- /dev/null +++ b/Signal-Windows.Lib/Migrations/SignalDB/20180521001340_m6.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Signal_Windows.Migrations +{ + public partial class m6 : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Blocked", + table: "SignalConversation", + nullable: false, + defaultValue: false); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Blocked", + table: "SignalConversation"); + } + } +} diff --git a/Signal-Windows.Lib/Migrations/SignalDB/20210305185855_m7.Designer.cs b/Signal-Windows.Lib/Migrations/SignalDB/20210305185855_m7.Designer.cs new file mode 100644 index 0000000..203fe90 --- /dev/null +++ b/Signal-Windows.Lib/Migrations/SignalDB/20210305185855_m7.Designer.cs @@ -0,0 +1,262 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Signal_Windows.Storage; +using Signal_Windows.Models; + +namespace Signal_Windows.Migrations +{ + [DbContext(typeof(SignalDBContext))] + [Migration("20210305185855_m7")] + partial class m7 + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "1.1.5"); + + modelBuilder.Entity("Signal_Windows.Models.GroupMembership", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ContactId"); + + b.Property("GroupId"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.HasIndex("GroupId"); + + b.ToTable("GroupMemberships"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ContentType"); + + b.Property("Digest"); + + b.Property("FileName"); + + b.Property("Guid"); + + b.Property("Key"); + + b.Property("MessageId"); + + b.Property("Relay"); + + b.Property("SentFileName"); + + b.Property("Size"); + + b.Property("Status"); + + b.Property("StorageId"); + + b.HasKey("Id"); + + b.HasIndex("MessageId"); + + b.ToTable("Attachments"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalConversation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AvatarFile"); + + b.Property("CanReceive"); + + b.Property("Discriminator") + .IsRequired(); + + b.Property("Draft"); + + b.Property("DraftFileTokens"); + + b.Property("ExpiresInSeconds"); + + b.Property("LastActiveTimestamp"); + + b.Property("LastMessageId"); + + b.Property("LastSeenMessageId"); + + b.Property("LastSeenMessageIndex"); + + b.Property("MessagesCount"); + + b.Property("ThreadDisplayName"); + + b.Property("ThreadId"); + + b.Property("UnreadCount"); + + b.HasKey("Id"); + + b.HasIndex("LastMessageId"); + + b.HasIndex("LastSeenMessageId"); + + b.HasIndex("ThreadId"); + + b.ToTable("SignalConversation"); + + b.HasDiscriminator("Discriminator").HasValue("SignalConversation"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalEarlyReceipt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DeviceId"); + + b.Property("Timestamp"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("Timestamp"); + + b.HasIndex("Username"); + + b.ToTable("EarlyReceipts"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AttachmentsCount"); + + b.Property("AuthorId"); + + b.Property("ComposedTimestamp"); + + b.Property("Contentrowid"); + + b.Property("DeviceId"); + + b.Property("Direction"); + + b.Property("ExpiresAt"); + + b.Property("Read"); + + b.Property("Receipts"); + + b.Property("ReceivedTimestamp"); + + b.Property("Status"); + + b.Property("ThreadId"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("Contentrowid"); + + b.HasIndex("ThreadId"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalMessageContent", b => + { + b.Property("rowid") + .ValueGeneratedOnAdd(); + + b.Property("Content"); + + b.HasKey("rowid"); + + b.ToTable("Messages_fts"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalContact", b => + { + b.HasBaseType("Signal_Windows.Models.SignalConversation"); + + b.Property("Blocked"); + + b.Property("Color"); + + b.ToTable("SignalContact"); + + b.HasDiscriminator().HasValue("SignalContact"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalGroup", b => + { + b.HasBaseType("Signal_Windows.Models.SignalConversation"); + + + b.ToTable("SignalGroup"); + + b.HasDiscriminator().HasValue("SignalGroup"); + }); + + modelBuilder.Entity("Signal_Windows.Models.GroupMembership", b => + { + b.HasOne("Signal_Windows.Models.SignalContact", "Contact") + .WithMany("GroupMemberships") + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Signal_Windows.Models.SignalGroup", "Group") + .WithMany("GroupMemberships") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalAttachment", b => + { + b.HasOne("Signal_Windows.Models.SignalMessage", "Message") + .WithMany("Attachments") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalConversation", b => + { + b.HasOne("Signal_Windows.Models.SignalMessage", "LastMessage") + .WithMany() + .HasForeignKey("LastMessageId"); + + b.HasOne("Signal_Windows.Models.SignalMessage", "LastSeenMessage") + .WithMany() + .HasForeignKey("LastSeenMessageId"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalMessage", b => + { + b.HasOne("Signal_Windows.Models.SignalContact", "Author") + .WithMany() + .HasForeignKey("AuthorId"); + + b.HasOne("Signal_Windows.Models.SignalMessageContent", "Content") + .WithMany() + .HasForeignKey("Contentrowid"); + }); + } + } +} diff --git a/Signal-Windows.Lib/Migrations/SignalDB/20210305185855_m7.cs b/Signal-Windows.Lib/Migrations/SignalDB/20210305185855_m7.cs new file mode 100644 index 0000000..b35b222 --- /dev/null +++ b/Signal-Windows.Lib/Migrations/SignalDB/20210305185855_m7.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Signal_Windows.Migrations +{ + public partial class m7 : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DraftFileTokens", + table: "SignalConversation", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "DraftFileTokens", + table: "SignalConversation"); + } + } +} diff --git a/Signal-Windows.Lib/Migrations/SignalDB/20210311062417_m8.Designer.cs b/Signal-Windows.Lib/Migrations/SignalDB/20210311062417_m8.Designer.cs new file mode 100644 index 0000000..75ff1ea --- /dev/null +++ b/Signal-Windows.Lib/Migrations/SignalDB/20210311062417_m8.Designer.cs @@ -0,0 +1,270 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Signal_Windows.Storage; +using Signal_Windows.Models; + +namespace Signal_Windows.Migrations +{ + [DbContext(typeof(SignalDBContext))] + [Migration("20210311062417_m8")] + partial class m8 + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "1.1.5"); + + modelBuilder.Entity("Signal_Windows.Models.GroupMembership", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ContactId"); + + b.Property("GroupId"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.HasIndex("GroupId"); + + b.ToTable("GroupMemberships"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CdnNumber"); + + b.Property("ContentType"); + + b.Property("Digest"); + + b.Property("FileName"); + + b.Property("Guid"); + + b.Property("Key"); + + b.Property("MessageId"); + + b.Property("Relay"); + + b.Property("SentFileName"); + + b.Property("Size"); + + b.Property("Status"); + + b.Property("StorageId"); + + b.Property("V3StorageId"); + + b.HasKey("Id"); + + b.HasIndex("MessageId"); + + b.ToTable("Attachments"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalConversation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AvatarFile"); + + b.Property("CanReceive"); + + b.Property("Discriminator") + .IsRequired(); + + b.Property("Draft"); + + b.Property("DraftFileTokens"); + + b.Property("ExpiresInSeconds"); + + b.Property("LastActiveTimestamp"); + + b.Property("LastMessageId"); + + b.Property("LastSeenMessageId"); + + b.Property("LastSeenMessageIndex"); + + b.Property("MessagesCount"); + + b.Property("ThreadDisplayName"); + + b.Property("ThreadGuid"); + + b.Property("ThreadId"); + + b.Property("UnreadCount"); + + b.HasKey("Id"); + + b.HasIndex("LastMessageId"); + + b.HasIndex("LastSeenMessageId"); + + b.HasIndex("ThreadId"); + + b.ToTable("SignalConversation"); + + b.HasDiscriminator("Discriminator").HasValue("SignalConversation"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalEarlyReceipt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DeviceId"); + + b.Property("Timestamp"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("Timestamp"); + + b.HasIndex("Username"); + + b.ToTable("EarlyReceipts"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AttachmentsCount"); + + b.Property("AuthorId"); + + b.Property("ComposedTimestamp"); + + b.Property("Contentrowid"); + + b.Property("DeviceId"); + + b.Property("Direction"); + + b.Property("ExpiresAt"); + + b.Property("Read"); + + b.Property("Receipts"); + + b.Property("ReceivedTimestamp"); + + b.Property("Status"); + + b.Property("ThreadGuid"); + + b.Property("ThreadId"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("Contentrowid"); + + b.HasIndex("ThreadId"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalMessageContent", b => + { + b.Property("rowid") + .ValueGeneratedOnAdd(); + + b.Property("Content"); + + b.HasKey("rowid"); + + b.ToTable("Messages_fts"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalContact", b => + { + b.HasBaseType("Signal_Windows.Models.SignalConversation"); + + b.Property("Blocked"); + + b.Property("Color"); + + b.ToTable("SignalContact"); + + b.HasDiscriminator().HasValue("SignalContact"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalGroup", b => + { + b.HasBaseType("Signal_Windows.Models.SignalConversation"); + + + b.ToTable("SignalGroup"); + + b.HasDiscriminator().HasValue("SignalGroup"); + }); + + modelBuilder.Entity("Signal_Windows.Models.GroupMembership", b => + { + b.HasOne("Signal_Windows.Models.SignalContact", "Contact") + .WithMany("GroupMemberships") + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Signal_Windows.Models.SignalGroup", "Group") + .WithMany("GroupMemberships") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalAttachment", b => + { + b.HasOne("Signal_Windows.Models.SignalMessage", "Message") + .WithMany("Attachments") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalConversation", b => + { + b.HasOne("Signal_Windows.Models.SignalMessage", "LastMessage") + .WithMany() + .HasForeignKey("LastMessageId"); + + b.HasOne("Signal_Windows.Models.SignalMessage", "LastSeenMessage") + .WithMany() + .HasForeignKey("LastSeenMessageId"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalMessage", b => + { + b.HasOne("Signal_Windows.Models.SignalContact", "Author") + .WithMany() + .HasForeignKey("AuthorId"); + + b.HasOne("Signal_Windows.Models.SignalMessageContent", "Content") + .WithMany() + .HasForeignKey("Contentrowid"); + }); + } + } +} diff --git a/Signal-Windows.Lib/Migrations/SignalDB/20210311062417_m8.cs b/Signal-Windows.Lib/Migrations/SignalDB/20210311062417_m8.cs new file mode 100644 index 0000000..c32aa2b --- /dev/null +++ b/Signal-Windows.Lib/Migrations/SignalDB/20210311062417_m8.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Signal_Windows.Migrations +{ + public partial class m8 : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ThreadGuid", + table: "Messages", + nullable: true); + + migrationBuilder.AddColumn( + name: "ThreadGuid", + table: "SignalConversation", + nullable: true); + + migrationBuilder.AddColumn( + name: "CdnNumber", + table: "Attachments", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "V3StorageId", + table: "Attachments", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ThreadGuid", + table: "Messages"); + + migrationBuilder.DropColumn( + name: "ThreadGuid", + table: "SignalConversation"); + + migrationBuilder.DropColumn( + name: "CdnNumber", + table: "Attachments"); + + migrationBuilder.DropColumn( + name: "V3StorageId", + table: "Attachments"); + } + } +} diff --git a/Signal-Windows/Migrations/SignalDB/SignalDBContextModelSnapshot.cs b/Signal-Windows.Lib/Migrations/SignalDB/SignalDBContextModelSnapshot.cs similarity index 90% rename from Signal-Windows/Migrations/SignalDB/SignalDBContextModelSnapshot.cs rename to Signal-Windows.Lib/Migrations/SignalDB/SignalDBContextModelSnapshot.cs index d1fbedc..2c1a060 100644 --- a/Signal-Windows/Migrations/SignalDB/SignalDBContextModelSnapshot.cs +++ b/Signal-Windows.Lib/Migrations/SignalDB/SignalDBContextModelSnapshot.cs @@ -14,7 +14,7 @@ partial class SignalDBContextModelSnapshot : ModelSnapshot protected override void BuildModel(ModelBuilder modelBuilder) { modelBuilder - .HasAnnotation("ProductVersion", "1.1.2"); + .HasAnnotation("ProductVersion", "1.1.5"); modelBuilder.Entity("Signal_Windows.Models.GroupMembership", b => { @@ -39,10 +39,16 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Id") .ValueGeneratedOnAdd(); + b.Property("CdnNumber"); + b.Property("ContentType"); + b.Property("Digest"); + b.Property("FileName"); + b.Property("Guid"); + b.Property("Key"); b.Property("MessageId"); @@ -51,10 +57,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("SentFileName"); + b.Property("Size"); + b.Property("Status"); b.Property("StorageId"); + b.Property("V3StorageId"); + b.HasKey("Id"); b.HasIndex("MessageId"); @@ -76,6 +86,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Draft"); + b.Property("DraftFileTokens"); + b.Property("ExpiresInSeconds"); b.Property("LastActiveTimestamp"); @@ -90,6 +102,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ThreadDisplayName"); + b.Property("ThreadGuid"); + b.Property("ThreadId"); b.Property("UnreadCount"); @@ -146,7 +160,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Direction"); - b.Property("ExpiresAt"); + b.Property("ExpiresAt"); b.Property("Read"); @@ -156,6 +170,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Status"); + b.Property("ThreadGuid"); + b.Property("ThreadId"); b.Property("Type"); @@ -187,6 +203,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.HasBaseType("Signal_Windows.Models.SignalConversation"); + b.Property("Blocked"); + b.Property("Color"); b.ToTable("SignalContact"); diff --git a/Signal-Windows/Models/PhoneContact.cs b/Signal-Windows.Lib/Models/PhoneContact.cs similarity index 66% rename from Signal-Windows/Models/PhoneContact.cs rename to Signal-Windows.Lib/Models/PhoneContact.cs index 209fd44..602822e 100644 --- a/Signal-Windows/Models/PhoneContact.cs +++ b/Signal-Windows.Lib/Models/PhoneContact.cs @@ -1,20 +1,16 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Signal_Windows.Controls; using Windows.UI.Xaml.Media; namespace Signal_Windows.Models { + // Not a database model. public class PhoneContact { public string Id { get; set; } public string Name { get; set; } public string PhoneNumber { get; set; } + public Guid? SignalGuid { get; set; } public ImageSource Photo { get; set; } public bool OnSignal { get; set; } - public AddContactListElement View; } } diff --git a/Signal-Windows.Lib/Models/SignalAttachment.cs b/Signal-Windows.Lib/Models/SignalAttachment.cs new file mode 100644 index 0000000..450b84a --- /dev/null +++ b/Signal-Windows.Lib/Models/SignalAttachment.cs @@ -0,0 +1,85 @@ +using System.ComponentModel.DataAnnotations.Schema; +using libsignalservice.messages; +using libsignalservice.util; +using Windows.UI.Xaml.Controls; + +namespace Signal_Windows.Models +{ + // Database model + public class SignalAttachment + { + public long Id { get; set; } + public long MessageId { get; set; } + public SignalMessage Message { get; set; } + public string FileName { get; set; } + public string SentFileName { get; set; } + public string ContentType { get; set; } + public SignalAttachmentStatus Status { get; set; } + public byte[] Key { get; set; } + public string Relay { get; set; } + public int CdnNumber { get; set; } + + /// + /// The Signal attachment pointer V2 remote id. + /// + public ulong StorageId { get; set; } + + /// + /// The Signal attachment pointer V3 remote id. + /// + public string V3StorageId { get; set; } + public byte[] Digest { get; set; } + public long Size { get; set; } + public string Guid { get; set; } + + [NotMapped] + public Image AttachmentImage { get; set; } + + public SignalServiceAttachmentPointer ToAttachmentPointer() + { + if (StorageId != 0) + { + return new SignalServiceAttachmentPointer(CdnNumber, + new SignalServiceAttachmentRemoteId((long)StorageId), + ContentType, + Key, + (uint)Util.ToIntExact(Size), + null, + 0, + 0, + Digest, + FileName, + false, + null, + null, + Util.CurrentTimeMillis()); + } + else + { + return new SignalServiceAttachmentPointer(CdnNumber, + new SignalServiceAttachmentRemoteId(V3StorageId), + ContentType, + Key, + (uint)Util.ToIntExact(Size), + null, + 0, + 0, + Digest, + FileName, + false, + null, + null, + Util.CurrentTimeMillis()); + } + } + } + + public enum SignalAttachmentStatus + { + Default = 0, + Finished = 1, + InProgress = 2, + Failed = 3, + Failed_Permanently = 4 + } +} \ No newline at end of file diff --git a/Signal-Windows.Lib/Models/SignalAttachmentContainer.cs b/Signal-Windows.Lib/Models/SignalAttachmentContainer.cs new file mode 100644 index 0000000..57e0db0 --- /dev/null +++ b/Signal-Windows.Lib/Models/SignalAttachmentContainer.cs @@ -0,0 +1,25 @@ +using libsignalservice; +using Microsoft.Extensions.Logging; +using Signal_Windows.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Signal_Windows.Lib.Models +{ + // Not a database model + public class SignalAttachmentContainer + { + public SignalAttachment Attachment; + public int AttachmentIndex; + public int MessageIndex; + public SignalAttachmentContainer(SignalAttachment attachment, int attachmentIndex, int messageIndex) + { + Attachment = attachment; + AttachmentIndex = attachmentIndex; + MessageIndex = messageIndex; + } + } +} diff --git a/Signal-Windows/Models/SignalContact.cs b/Signal-Windows.Lib/Models/SignalContact.cs similarity index 59% rename from Signal-Windows/Models/SignalContact.cs rename to Signal-Windows.Lib/Models/SignalContact.cs index 71b9305..4e7e451 100644 --- a/Signal-Windows/Models/SignalContact.cs +++ b/Signal-Windows.Lib/Models/SignalContact.cs @@ -2,16 +2,11 @@ namespace Signal_Windows.Models { + // Database model public class SignalContact : SignalConversation { - public SignalContact() - { - ThreadDisplayName = "Anonymous"; - Color = ""; - ThreadId = ""; - } - public string Color { get; set; } + public bool Blocked { get; set; } public List GroupMemberships { get; set; } } } \ No newline at end of file diff --git a/Signal-Windows.Lib/Models/SignalConversation.cs b/Signal-Windows.Lib/Models/SignalConversation.cs new file mode 100644 index 0000000..d705333 --- /dev/null +++ b/Signal-Windows.Lib/Models/SignalConversation.cs @@ -0,0 +1,85 @@ +using System; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Signal_Windows.Models +{ + // Database model + public abstract class SignalConversation + { + public long Id { get; set; } + + /// + /// The E.164 number of the conversation. This may be null. This was originally the only identifier needed. + /// + public string ThreadId { get; set; } + + /// + /// The Guid of the conversation. This may be null. This was added once Signal started supporting UUIDs. + /// + public Guid? ThreadGuid { get; set; } + public string ThreadDisplayName { get; set; } + public long LastActiveTimestamp { get; set; } + public string Draft { get; set; } + public string DraftFileTokens { get; set; } // comma seperated list of tokens + public string AvatarFile { get; set; } + public long MessagesCount { get; set; } + public uint UnreadCount { get; set; } + public bool CanReceive { get; set; } + public uint ExpiresInSeconds { get; set; } + public long? LastMessageId { get; set; } + public SignalMessage LastMessage { get; set; } + public long LastSeenMessageIndex { get; set; } + public SignalMessage LastSeenMessage { get; set; } + [NotMapped] public string DisplayedColor { get; set; } + public Action UpdateUI; + + public SignalConversation Clone() + { + if (this is SignalContact contact) + { + return new SignalContact() + { + Id = Id, + ThreadId = ThreadId, + ThreadGuid = ThreadGuid, + ThreadDisplayName = ThreadDisplayName, + LastActiveTimestamp = LastActiveTimestamp, + Draft = Draft, + AvatarFile = AvatarFile, + MessagesCount = MessagesCount, + UnreadCount = UnreadCount, + CanReceive = CanReceive, + ExpiresInSeconds = ExpiresInSeconds, + LastMessageId = LastMessageId, + LastMessage = LastMessage, + LastSeenMessageIndex = LastSeenMessageIndex, + LastSeenMessage = LastSeenMessage, + Color = contact.Color + }; + } + else + { + return new SignalGroup() + { + Id = Id, + ThreadId = ThreadId, + ThreadGuid = ThreadGuid, + ThreadDisplayName = ThreadDisplayName, + LastActiveTimestamp = LastActiveTimestamp, + Draft = Draft, + AvatarFile = AvatarFile, + MessagesCount = MessagesCount, + UnreadCount = UnreadCount, + CanReceive = CanReceive, + ExpiresInSeconds = ExpiresInSeconds, + LastMessageId = LastMessageId, + LastMessage = LastMessage, + LastSeenMessageIndex = LastSeenMessageIndex, + LastSeenMessage = LastSeenMessage + }; + } + + } + } +} \ No newline at end of file diff --git a/Signal-Windows/Models/SignalEarlyReceipt.cs b/Signal-Windows.Lib/Models/SignalEarlyReceipt.cs similarity index 88% rename from Signal-Windows/Models/SignalEarlyReceipt.cs rename to Signal-Windows.Lib/Models/SignalEarlyReceipt.cs index f0d659e..63de6f8 100644 --- a/Signal-Windows/Models/SignalEarlyReceipt.cs +++ b/Signal-Windows.Lib/Models/SignalEarlyReceipt.cs @@ -1,5 +1,6 @@ namespace Signal_Windows.Models { + // Database model public class SignalEarlyReceipt { public ulong Id { get; set; } diff --git a/Signal-Windows/Models/SignalGroup.cs b/Signal-Windows.Lib/Models/SignalGroup.cs similarity index 92% rename from Signal-Windows/Models/SignalGroup.cs rename to Signal-Windows.Lib/Models/SignalGroup.cs index aa9ed41..aabca95 100644 --- a/Signal-Windows/Models/SignalGroup.cs +++ b/Signal-Windows.Lib/Models/SignalGroup.cs @@ -2,6 +2,7 @@ namespace Signal_Windows.Models { + // Database model public class SignalGroup : SignalConversation { public List GroupMemberships { get; set; } diff --git a/Signal-Windows/Models/SignalIdentity.cs b/Signal-Windows.Lib/Models/SignalIdentity.cs similarity index 90% rename from Signal-Windows/Models/SignalIdentity.cs rename to Signal-Windows.Lib/Models/SignalIdentity.cs index 58757ab..467edf1 100644 --- a/Signal-Windows/Models/SignalIdentity.cs +++ b/Signal-Windows.Lib/Models/SignalIdentity.cs @@ -1,5 +1,6 @@ namespace Signal_Windows.Models { + // Database model public class SignalIdentity { public long Id { get; set; } diff --git a/Signal-Windows/Models/SignalMessage.cs b/Signal-Windows.Lib/Models/SignalMessage.cs similarity index 67% rename from Signal-Windows/Models/SignalMessage.cs rename to Signal-Windows.Lib/Models/SignalMessage.cs index 36c09d4..b04ec38 100644 --- a/Signal-Windows/Models/SignalMessage.cs +++ b/Signal-Windows.Lib/Models/SignalMessage.cs @@ -1,10 +1,10 @@ -using Signal_Windows.Controls; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System; namespace Signal_Windows.Models { + // Database model public class SignalMessage { public long Id { get; set; } @@ -12,7 +12,17 @@ public class SignalMessage public SignalMessageType Type { get; set; } public SignalMessageStatus Status { get; set; } public SignalMessageContent Content { get; set; } + + /// + /// The E.164 number of the contact that sent this message. This may be null. + /// public string ThreadId { get; set; } + + /// + /// The Guid of the contact that sent this message. This may be null. This was added once Signal started + /// supporting UUIDs. + /// + public Guid? ThreadGuid { get; set; } public long? AuthorId { get; set; } public SignalContact Author { get; set; } public uint DeviceId { get; set; } @@ -20,9 +30,9 @@ public class SignalMessage public uint Receipts { get; set; } public long ReceivedTimestamp { get; set; } public long ComposedTimestamp { get; set; } - public uint ExpiresAt { get; set; } + public long ExpiresAt { get; set; } public uint AttachmentsCount { get; set; } - public List Attachments { get; set; } + public List Attachments { get; set; } = new List(); } public enum SignalMessageType @@ -31,7 +41,8 @@ public enum SignalMessageType GroupUpdate = 1, SessionReset = 2, ExpireUpdate = 3, - IdentityKeyChange = 4 + IdentityKeyChange = 4, + GroupLeave = 5 } public enum SignalMessageDirection diff --git a/Signal-Windows/Models/SignalMessageContent.cs b/Signal-Windows.Lib/Models/SignalMessageContent.cs similarity index 87% rename from Signal-Windows/Models/SignalMessageContent.cs rename to Signal-Windows.Lib/Models/SignalMessageContent.cs index d45e3a4..9ce034a 100644 --- a/Signal-Windows/Models/SignalMessageContent.cs +++ b/Signal-Windows.Lib/Models/SignalMessageContent.cs @@ -2,6 +2,7 @@ namespace Signal_Windows.Models { + // Database model public class SignalMessageContent { [Key] diff --git a/Signal-Windows/Models/SignalPreKey.cs b/Signal-Windows.Lib/Models/SignalPreKey.cs similarity index 83% rename from Signal-Windows/Models/SignalPreKey.cs rename to Signal-Windows.Lib/Models/SignalPreKey.cs index 54d40d3..2fa935b 100644 --- a/Signal-Windows/Models/SignalPreKey.cs +++ b/Signal-Windows.Lib/Models/SignalPreKey.cs @@ -1,5 +1,6 @@ namespace Signal_Windows.Models { + // Database model public class SignalPreKey { public long Id { get; set; } diff --git a/Signal-Windows/Models/SignalSession.cs b/Signal-Windows.Lib/Models/SignalSession.cs similarity index 88% rename from Signal-Windows/Models/SignalSession.cs rename to Signal-Windows.Lib/Models/SignalSession.cs index 2124676..417de9e 100644 --- a/Signal-Windows/Models/SignalSession.cs +++ b/Signal-Windows.Lib/Models/SignalSession.cs @@ -1,5 +1,6 @@ namespace Signal_Windows.Models { + // Database model public class SignalSession { public long Id { get; set; } diff --git a/Signal-Windows/Models/SignalSignedPreKey.cs b/Signal-Windows.Lib/Models/SignalSignedPreKey.cs similarity index 84% rename from Signal-Windows/Models/SignalSignedPreKey.cs rename to Signal-Windows.Lib/Models/SignalSignedPreKey.cs index cfac421..3b30f4e 100644 --- a/Signal-Windows/Models/SignalSignedPreKey.cs +++ b/Signal-Windows.Lib/Models/SignalSignedPreKey.cs @@ -1,5 +1,6 @@ namespace Signal_Windows.Models { + // Database model public class SignalSignedPreKey { public long Id { get; set; } diff --git a/Signal-Windows/Models/SignalStore.cs b/Signal-Windows.Lib/Models/SignalStore.cs similarity index 63% rename from Signal-Windows/Models/SignalStore.cs rename to Signal-Windows.Lib/Models/SignalStore.cs index 0466e05..9c25dea 100644 --- a/Signal-Windows/Models/SignalStore.cs +++ b/Signal-Windows.Lib/Models/SignalStore.cs @@ -1,10 +1,22 @@ -namespace Signal_Windows.Models +using System; + +namespace Signal_Windows.Models { + // Database model public class SignalStore { public long Id { get; set; } public uint DeviceId { get; set; } + + /// + /// Our phone number. + /// public string Username { get; set; } + + /// + /// Our Signal UUID. + /// + public Guid? OwnGuid { get; set; } public string Password { get; set; } public string SignalingKey { get; set; } public uint PreKeyIdOffset { get; set; } diff --git a/Signal-Windows.Lib/OutgoingMessages.cs b/Signal-Windows.Lib/OutgoingMessages.cs new file mode 100644 index 0000000..b7682fb --- /dev/null +++ b/Signal-Windows.Lib/OutgoingMessages.cs @@ -0,0 +1,254 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using libsignalservice; +using libsignalservice.crypto; +using libsignalservice.messages; +using libsignalservice.messages.multidevice; +using libsignalservice.push; +using libsignalservice.push.exceptions; +using libsignalservice.util; +using libsignalservicedotnet.crypto; +using Microsoft.Extensions.Logging; +using Signal_Windows.Models; +using Signal_Windows.Storage; +using Windows.Storage; + +namespace Signal_Windows.Lib +{ + interface ISendable + { + SignalMessageStatus Status { set; } + Task Send(SignalServiceMessageSender messageSender, CancellationToken token); + } + + class SignalServiceSyncMessageSendable : ISendable + { + public SignalMessageStatus Status { get; set; } + private readonly SignalServiceSyncMessage SyncMessage; + + public SignalServiceSyncMessageSendable(SignalServiceSyncMessage message) + { + SyncMessage = message; + } + + public async Task Send(SignalServiceMessageSender messageSender, CancellationToken token) + { + await messageSender.SendMessageAsync(SyncMessage, null, token); + } + } + + class SignalServiceDataMessageSendable : ISendable + { + public SignalMessageStatus Status { get; set; } + private readonly SignalServiceDataMessage DataMessage; + private readonly SignalServiceAddress Recipient; + + public SignalServiceDataMessageSendable(SignalServiceDataMessage dataMessage, SignalServiceAddress recipient) + { + DataMessage = dataMessage; + Recipient = recipient; + } + + public async Task Send(SignalServiceMessageSender messageSender, CancellationToken token) + { + await messageSender.SendMessageAsync(Recipient, null, DataMessage, token); + } + } + + class SignalMessageSendable : ISendable + { + private readonly ILogger Logger = LibsignalLogging.CreateLogger(); + public readonly SignalMessage OutgoingSignalMessage; + + public SignalMessageSendable(SignalMessage message) + { + OutgoingSignalMessage = message; + } + + public SignalMessageStatus Status { set => OutgoingSignalMessage.Status = value; } + + public async Task Send(SignalServiceMessageSender messageSender, CancellationToken token) + { + List outgoingAttachmentsList = null; + if (OutgoingSignalMessage.Attachments != null && OutgoingSignalMessage.Attachments.Count > 0) + { + outgoingAttachmentsList = new List(); + foreach (var attachment in OutgoingSignalMessage.Attachments) + { + try + { + var file = await ApplicationData.Current.LocalCacheFolder.GetFileAsync(@"Attachments\" + attachment.Id + ".plain"); + var stream = (await file.OpenAsync(FileAccessMode.ReadWrite, StorageOpenOptions.None)).AsStream(); + outgoingAttachmentsList.Add(SignalServiceAttachment.NewStreamBuilder() + .WithContentType(attachment.ContentType) + .WithStream(stream) + .WithLength(stream.Length) + .WithFileName(attachment.SentFileName) + .Build()); + } + catch (Exception e) + { + Logger.LogError($"HandleOutgoingMessages() failed to add attachment {attachment.Id}: {e.Message}\n{e.StackTrace}"); + } + } + } + + SignalServiceDataMessage message = new SignalServiceDataMessage(OutgoingSignalMessage.ComposedTimestamp, + outgoingAttachmentsList, OutgoingSignalMessage.Content.Content, (int)OutgoingSignalMessage.ExpiresAt); + + UpdateExpiresAt(OutgoingSignalMessage); + DisappearingMessagesManager.QueueForDeletion(OutgoingSignalMessage); + + if (!OutgoingSignalMessage.ThreadId.EndsWith("=")) + { + if (!token.IsCancellationRequested) + { + await messageSender.SendMessageAsync(new SignalServiceAddress(OutgoingSignalMessage.ThreadGuid, OutgoingSignalMessage.ThreadId), null, message, token); + OutgoingSignalMessage.Status = SignalMessageStatus.Confirmed; + } + } + else + { + List recipients = new List(); + SignalGroup g = await SignalDBContext.GetOrCreateGroupLocked(OutgoingSignalMessage.ThreadId, 0); + foreach (GroupMembership sc in g.GroupMemberships) + { + if (sc.Contact.ThreadId != SignalLibHandle.Instance.Store.Username) + { + recipients.Add(new SignalServiceAddress(null, sc.Contact.ThreadId)); + } + } + message.Group = new SignalServiceGroup(SignalServiceGroup.GroupType.DELIVER, Base64.Decode(g.ThreadId), + null, null, null); + if (!token.IsCancellationRequested) + { + var uaps = new List(); + foreach (var _ in recipients) + { + uaps.Add(null); + } + await messageSender.SendMessageAsync(recipients, uaps, false, message, token); + OutgoingSignalMessage.Status = SignalMessageStatus.Confirmed; + } + } + } + + /// + /// Updates a message ExpiresAt to be a timestamp instead of a relative value. + /// + /// The message to update + private void UpdateExpiresAt(SignalMessage message) + { + // We update here instead of earlier because we only want to start the timer once the message is actually sent. + long messageExpiration; + if (message.ExpiresAt == 0) + { + messageExpiration = 0; + } + else + { + messageExpiration = Util.CurrentTimeMillis() + (long)TimeSpan.FromSeconds(message.ExpiresAt).TotalMilliseconds; + } + message.ExpiresAt = messageExpiration; + } + } + + class OutgoingMessages + { + private readonly ILogger Logger = LibsignalLogging.CreateLogger(); + private readonly CancellationToken Token; + private readonly SignalLibHandle Handle; + private readonly SignalStore Store; + private readonly SignalServiceMessagePipe Pipe; + + public OutgoingMessages(CancellationToken token, SignalServiceMessagePipe pipe, SignalStore store, SignalLibHandle handle) + { + Token = token; + Pipe = pipe; + Store = store; + Handle = handle; + } + + public async Task HandleOutgoingMessages() + { + Logger.LogDebug("HandleOutgoingMessages()"); + try + { + var messageSender = new SignalServiceMessageSender(LibUtils.ServiceConfiguration, Store.OwnGuid, + Store.Username, Store.Password, (int)Store.DeviceId, new Store(), LibUtils.USER_AGENT, + LibUtils.HttpClient, Store.DeviceId != 1, + true, // true means we're using the Attachment V3 API + Pipe, null, null); + while (!Token.IsCancellationRequested) + { + ISendable sendable = null; + try + { + sendable = Handle.OutgoingQueue.Take(Token); + Logger.LogTrace($"Sending {sendable.GetType().Name}"); + await sendable.Send(messageSender, Token); + } + catch (OperationCanceledException) { return; } + catch (EncapsulatedExceptions exceptions) + { + sendable.Status = SignalMessageStatus.Confirmed; + Logger.LogError("HandleOutgoingMessages() encountered libsignal exceptions"); + IList identityExceptions = exceptions.UntrustedIdentityExceptions; + if (exceptions.NetworkExceptions.Count > 0) + { + sendable.Status = SignalMessageStatus.Failed_Network; + } + if (identityExceptions.Count > 0) + { + sendable.Status = SignalMessageStatus.Failed_Identity; + } + foreach (UntrustedIdentityException e in identityExceptions) + { + // TODO: Not sure what to do with this. + //await SendMessage(recipients, message); + //UpdateExpiresAt(outgoingSignalMessage); + //DisappearingMessagesManager.QueueForDeletion(outgoingSignalMessage); + //outgoingSignalMessage.Status = SignalMessageStatus.Confirmed; + await Handle.HandleOutgoingKeyChangeLocked(e.Identifier, Base64.EncodeBytes(e.IdentityKey.serialize())); + } + } + catch (RateLimitException) + { + Logger.LogError("HandleOutgoingMessages() could not send due to rate limits"); + sendable.Status = SignalMessageStatus.Failed_Ratelimit; + } + catch (UntrustedIdentityException e) + { + Logger.LogError("HandleOutgoingMessages() could not send due to untrusted identities"); + sendable.Status = SignalMessageStatus.Failed_Identity; + await Handle.HandleOutgoingKeyChangeLocked(e.Identifier, Base64.EncodeBytes(e.IdentityKey.serialize())); + } + catch (Exception e) + { + var line = new StackTrace(e, true).GetFrames()[0].GetFileLineNumber(); + Logger.LogError("HandleOutgoingMessages() failed in line {0}: {1}\n{2}", line, e.Message, e.StackTrace); + sendable.Status = SignalMessageStatus.Failed_Unknown; + } + await Handle.HandleMessageSentLocked(sendable); + } + Logger.LogInformation("HandleOutgoingMessages() stopping: cancellation was requested"); + } + catch (OperationCanceledException) + { + Logger.LogInformation("HandleOutgoingMessages() stopping: cancellation was requested (OperationCancelledException)"); + } + catch (Exception e) + { + Logger.LogError($"HandleOutgoingMessages() failed: {e.Message}\n{e.StackTrace}"); + } + finally + { + Logger.LogInformation("HandleOutgoingMessages() finished"); + } + } + } +} diff --git a/Signal-Windows.Lib/Properties/AssemblyInfo.cs b/Signal-Windows.Lib/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..e29db11 --- /dev/null +++ b/Signal-Windows.Lib/Properties/AssemblyInfo.cs @@ -0,0 +1,29 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Signal-Windows.Lib")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Signal-Windows.Lib")] +[assembly: AssemblyCopyright("Copyright © 2017")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: ComVisible(false)] \ No newline at end of file diff --git a/Signal-Windows.Lib/Properties/Signal_Windows.Lib.rd.xml b/Signal-Windows.Lib/Properties/Signal_Windows.Lib.rd.xml new file mode 100644 index 0000000..6da05d0 --- /dev/null +++ b/Signal-Windows.Lib/Properties/Signal_Windows.Lib.rd.xml @@ -0,0 +1,33 @@ + + + + + + + + + diff --git a/Signal-Windows.Lib/Settings/AppConfig.cs b/Signal-Windows.Lib/Settings/AppConfig.cs new file mode 100644 index 0000000..f28cbe8 --- /dev/null +++ b/Signal-Windows.Lib/Settings/AppConfig.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; +using Windows.ApplicationModel; + +namespace Signal_Windows.Lib.Settings +{ + public class AppConfig + { + private readonly IConfigurationRoot configurationRoot; + + public AppConfig() + { + string jsonSettingsFilePath = + $@"{Package.Current.InstalledLocation.Path}\Signal-Windows.Lib\Settings\"; + + bool useStaging = false; + if (useStaging) + { + jsonSettingsFilePath += "appsettings.json"; + } + else + { + jsonSettingsFilePath += "appsettings.production.json"; + } + + IConfigurationBuilder builder = new ConfigurationBuilder() + .Add(new LocalConfigurationSource(jsonSettingsFilePath)); + + configurationRoot = builder.Build(); + } + + public SignalSettings GetSignalSettings() + { + return new SignalSettings(GetSection(nameof(SignalSettings.ServiceUrl)), + new List() { GetSection("CdnUrl1") }, + new List() { GetSection("CdnUrl2") }, + GetSection(nameof(SignalSettings.ContactDiscoveryServiceUrl)), + GetSection(nameof(SignalSettings.ContactDiscoveryServiceEnclaveId))); + } + + private T GetSection(string key) + { + return configurationRoot.GetSection(key).Get(); + } + } +} diff --git a/Signal-Windows.Lib/Settings/LocalConfigurationProvider.cs b/Signal-Windows.Lib/Settings/LocalConfigurationProvider.cs new file mode 100644 index 0000000..b0bd455 --- /dev/null +++ b/Signal-Windows.Lib/Settings/LocalConfigurationProvider.cs @@ -0,0 +1,30 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Newtonsoft.Json.Linq; +using Windows.Storage; + +namespace Signal_Windows.Lib.Settings +{ + internal class LocalConfigurationProvider : ConfigurationProvider + { + public LocalConfigurationProvider(LocalConfigurationSource localConfigurationSource) + { + var appSettingsFile = WaitAndGet(StorageFile.GetFileFromPathAsync($@"{localConfigurationSource.JsonSettingsFilePath}").AsTask()); + JObject o = JObject.Parse(WaitAndGet(FileIO.ReadTextAsync(appSettingsFile).AsTask())); + foreach (JProperty token in o["signalSettings"]) + { + if (token.Value.Type == JTokenType.String) + { + Data.Add(token.Name, (string)token.Value); + } + } + } + + private T WaitAndGet(Task t) + { + t.Wait(); + return t.Result; + } + } +} diff --git a/Signal-Windows.Lib/Settings/LocalConfigurationSource.cs b/Signal-Windows.Lib/Settings/LocalConfigurationSource.cs new file mode 100644 index 0000000..bfa319c --- /dev/null +++ b/Signal-Windows.Lib/Settings/LocalConfigurationSource.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.Configuration; + +namespace Signal_Windows.Lib.Settings +{ + internal class LocalConfigurationSource : IConfigurationSource + { + public string JsonSettingsFilePath { get; } + + public LocalConfigurationSource(string jsonSettingsFilePath) + { + JsonSettingsFilePath = jsonSettingsFilePath; + } + + public IConfigurationProvider Build(IConfigurationBuilder builder) + { + return new LocalConfigurationProvider(this); + } + } +} diff --git a/Signal-Windows.Lib/Settings/SignalSettings.cs b/Signal-Windows.Lib/Settings/SignalSettings.cs new file mode 100644 index 0000000..47c05f0 --- /dev/null +++ b/Signal-Windows.Lib/Settings/SignalSettings.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace Signal_Windows.Lib.Settings +{ + public sealed class SignalSettings + { + public string ServiceUrl { get; private set; } + public List Cdn1Urls { get; private set; } + public List Cdn2Urls { get; private set; } + public string ContactDiscoveryServiceUrl { get; private set; } + public string ContactDiscoveryServiceEnclaveId { get; private set; } + + public SignalSettings(string serviceUrl, + List cdn1Urls, + List cdn2Urls, + string contactDiscoveryServiceUrl, + string contactDiscoveryServiceEnclaveId) + { + ServiceUrl = serviceUrl; + Cdn1Urls = cdn1Urls; + Cdn2Urls = cdn2Urls; + ContactDiscoveryServiceUrl = contactDiscoveryServiceUrl; + ContactDiscoveryServiceEnclaveId = contactDiscoveryServiceEnclaveId; + } + } +} diff --git a/Signal-Windows.Lib/Settings/appsettings.json b/Signal-Windows.Lib/Settings/appsettings.json new file mode 100644 index 0000000..14758bf --- /dev/null +++ b/Signal-Windows.Lib/Settings/appsettings.json @@ -0,0 +1,9 @@ +{ + "signalSettings": { + "ServiceUrl": "https://textsecure-service-staging.whispersystems.org", + "CdnUrl1": "https://cdn-staging.signal.org", + "CdnUrl2": "https://cdn2-staging.signal.org", + "ContactDiscoveryServiceUrl": "https://api-staging.directory.signal.org", + "ContactDiscoveryServiceEnclaveId": "c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15" + } +} diff --git a/Signal-Windows.Lib/Settings/appsettings.production.json b/Signal-Windows.Lib/Settings/appsettings.production.json new file mode 100644 index 0000000..7d3841d --- /dev/null +++ b/Signal-Windows.Lib/Settings/appsettings.production.json @@ -0,0 +1,9 @@ +{ + "signalSettings": { + "ServiceUrl": "https://textsecure-service.whispersystems.org", + "CdnUrl1": "https://cdn.signal.org", + "CdnUrl2": "https://cdn2.signal.org", + "ContactDiscoveryServiceUrl": "https://api.directory.signal.org", + "ContactDiscoveryServiceEnclaveId": "c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15" + } +} diff --git a/Signal-Windows.Lib/Signal-Windows.Lib.csproj b/Signal-Windows.Lib/Signal-Windows.Lib.csproj new file mode 100644 index 0000000..5ce7abe --- /dev/null +++ b/Signal-Windows.Lib/Signal-Windows.Lib.csproj @@ -0,0 +1,235 @@ + + + + + Debug + AnyCPU + {1934FD82-A5EA-4B71-B915-A1826593CB6E} + Library + Properties + Signal_Windows.Lib + Signal-Windows.Lib + en-US + UAP + 10.0.16299.0 + 10.0.15063.0 + 14 + 512 + {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + prompt + 4 + + + x86 + true + bin\x86\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + x86 + false + prompt + + + x86 + bin\x86\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + x86 + false + prompt + + + ARM + true + bin\ARM\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + ARM + false + prompt + + + ARM + bin\ARM\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + ARM + false + prompt + + + x64 + true + bin\x64\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + x64 + false + prompt + + + x64 + bin\x64\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + x64 + false + prompt + + + PackageReference + + + + + 20210311062557_m2.cs + + + + 20210305185855_m7.cs + + + + 20210311062417_m8.cs + + + + + + + + + 20180521001340_m6.cs + + + + + + + + + + + 20170806145530_ls1.cs + + + + + 20170806163624_s1.cs + + + + 20170825082857_m2.cs + + + + 20170901124533_m3.cs + + + + 20180203062229_m4.cs + + + + 20180211071131_m5.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + 2.14.0 + + + 1.1.5 + + + 1.1.5 + + + 1.1.5 + + + 1.1.5 + + + 1.1.2 + + + 6.2.12 + + + 6.1.1 + + + + + Windows Mobile Extensions for the UWP + + + + + Always + + + Always + + + + 14.0 + + + + \ No newline at end of file diff --git a/Signal-Windows.Lib/SignalLibHandle.cs b/Signal-Windows.Lib/SignalLibHandle.cs new file mode 100644 index 0000000..a3396ad --- /dev/null +++ b/Signal-Windows.Lib/SignalLibHandle.cs @@ -0,0 +1,1258 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using libsignalservice; +using Windows.UI.Core; +using Signal_Windows.Models; +using Signal_Windows.Storage; +using Microsoft.Extensions.Logging; +using System.Threading; +using libsignalservice.util; +using System.Collections.Concurrent; +using libsignal; +using System.Diagnostics; +using Signal_Windows.Lib.Events; +using libsignalservice.messages; +using System.IO; +using Windows.Networking.BackgroundTransfer; +using Windows.Storage; +using Windows.Web; +using libsignalservice.push; +using libsignalservice.messages.multidevice; +using libsignalservice.crypto; + +namespace Signal_Windows.Lib +{ + public class AppendResult + { + public bool WasInstantlyRead { get; } + public AppendResult(bool wasInstantlyRead) + { + WasInstantlyRead = wasInstantlyRead; + } + } + + public interface ISignalFrontend + { + void AddOrUpdateConversation(SignalConversation updatedConversation, SignalMessage updateMessage); + AppendResult HandleMessage(SignalMessage message, SignalConversation updatedConversation); + void HandleUnreadMessage(SignalMessage message); + void HandleMessageRead(SignalConversation updatedConversation); + void HandleIdentitykeyChange(LinkedList messages); + void HandleMessageUpdate(SignalMessage updatedMessage); + void ReplaceConversationList(List conversations); + Task HandleAuthFailure(); + void HandleAttachmentStatusChanged(SignalAttachment sa); + void HandleBlockedContacts(List blockedContacts); + void HandleMessageDelete(SignalMessage messsage); + Task Release(); + } + + public interface ISignalLibHandle + { + //Frontend API + SignalStore Store { get; set; } + SignalServiceAccountManager AccountManager { get; set; } + + void RequestSync(); + Task SendMessage(string messageText, StorageFile attachment, SignalConversation conversation); + Task SendBlockedMessage(); + Task SetMessageRead(SignalMessage message); + void ResendMessage(SignalMessage message); + IEnumerable GetMessages(SignalConversation thread, int startIndex, int count); + Task SaveAndDispatchSignalConversation(SignalConversation updatedConversation, SignalMessage updateMessage); + void PurgeAccountData(); + Task Acquire(CoreDispatcher d, ISignalFrontend w); + Task Reacquire(); + void Release(); + bool AddFrontend(CoreDispatcher d, ISignalFrontend w); + Task RemoveFrontend(CoreDispatcher d); + + // Background API + event EventHandler SignalMessageEvent; + void BackgroundAcquire(); + void BackgroundRelease(); + + // Attachment API + void StartAttachmentDownload(SignalAttachment sa); + Task ExportAttachment(SignalAttachment sa); + //void AbortAttachmentDownload(SignalAttachment sa); TODO + } + + public static class SignalHelper + { + public static ISignalLibHandle CreateSignalLibHandle(bool headless) + { + return new SignalLibHandle(headless); + } + } + + internal class SignalLibHandle : ISignalLibHandle + { + internal static SignalLibHandle Instance; + public SignalStore Store { get; set; } + public SignalServiceAccountManager AccountManager { get; set; } + + private readonly ILogger Logger = LibsignalLogging.CreateLogger(); + public SemaphoreSlim SemaphoreSlim = new SemaphoreSlim(1, 1); + private readonly bool Headless; + private bool Running = false; + private bool LikelyHasValidStore = false; + private CancellationTokenSource CancelSource = new CancellationTokenSource(); + private Dictionary Frames = new Dictionary(); + private CoreDispatcher MainWindowDispatcher; + private ISignalFrontend MainWindow; + private Task IncomingMessagesTask; + private Task OutgoingMessagesTask; + private SignalServiceMessageReceiver MessageReceiver; + public BlockingCollection OutgoingQueue = new BlockingCollection(new ConcurrentQueue()); + private EventWaitHandle GlobalResetEvent; + private Dictionary Downloads = new Dictionary(); + + public event EventHandler SignalMessageEvent; + + #region frontend api + public SignalLibHandle(bool headless) + { + Headless = headless; + Instance = this; + } + + public bool AddFrontend(CoreDispatcher d, ISignalFrontend w) + { + Logger.LogTrace("AddFrontend() locking"); + SemaphoreSlim.Wait(CancelSource.Token); + try + { + Logger.LogTrace("AddFrontend() locked"); + if (Running && LikelyHasValidStore) + { + Logger.LogInformation("Registering frontend of dispatcher {0}", w.GetHashCode()); + Frames.Add(d, w); + w.ReplaceConversationList(GetConversations()); + DisappearingMessagesManager.AddFrontend(d, w); + return true; + } + else + { + Logger.LogInformation("Ignoring AddFrontend call"); + return false; + } + } + finally + { + SemaphoreSlim.Release(); + Logger.LogTrace("AddFrontend() released"); + } + } + + public async Task RemoveFrontend(CoreDispatcher d) + { + Logger.LogTrace("RemoveFrontend() locking"); + await SemaphoreSlim.WaitAsync(CancelSource.Token); + try + { + Logger.LogTrace("RemoveFrontend() locked"); + Logger.LogInformation("Unregistering frontend of dispatcher {0}", d.GetHashCode()); + DisappearingMessagesManager.RemoveFrontend(d); + Frames.Remove(d); + } + catch (Exception e) + { + Logger.LogCritical($"RemoveFrontend failed(): {e.Message} ({e.GetType()})\n{e.StackTrace}"); + } + finally + { + SemaphoreSlim.Release(); + Logger.LogTrace("RemoveFrontend() released"); + } + } + + public void PurgeAccountData() + { + Logger.LogTrace("PurgeAccountData() locking"); + SemaphoreSlim.Wait(CancelSource.Token); + Logger.LogTrace("PurgeAccountData() locked"); + LibsignalDBContext.PurgeAccountData(); + SemaphoreSlim.Release(); + Logger.LogTrace("PurgeAccountData() released"); + } + + public async Task Acquire(CoreDispatcher d, ISignalFrontend w) //TODO wrap trycatch dispatch auth failure + { + Logger.LogTrace("Acquire() locking"); + CancelSource = new CancellationTokenSource(); + SemaphoreSlim.Wait(CancelSource.Token); + try + { + GlobalResetEvent = LibUtils.OpenResetEventSet(); + LibUtils.Lock(); + GlobalResetEvent.Reset(); + MainWindowDispatcher = d; + MainWindow = w; + Logger.LogDebug("Acquire() locked (global and local)"); + var getConversationsTask = Task.Run(() => + { + return GetConversations(); // we want to display the conversations asap! + }); + Instance = this; + Frames.Add(d, w); + w.ReplaceConversationList(await getConversationsTask); + var failTask = Task.Run(() => + { + SignalDBContext.FailAllPendingMessages(); // TODO GetMessages needs to be protected by semaphoreslim as we fail defered + }); + Store = await Task.Run(() => + { + return LibsignalDBContext.GetSignalStore(); + }); + if (Store == null) + { + return false; + } + + await SetSingletonsAsync(Store); + LikelyHasValidStore = true; + InitNetwork(); + var recoverDownloadsTask = Task.Run(() => + { + RecoverDownloads().Wait(); + }); + await failTask; // has to complete before messages are loaded + await recoverDownloadsTask; + Running = true; + return true; + } + finally + { + SemaphoreSlim.Release(); + Logger.LogTrace("Acquire() released"); + } + } + + public void BackgroundAcquire() + { + CancelSource = new CancellationTokenSource(); + Instance = this; + SignalDBContext.FailAllPendingMessages(); + Store = LibsignalDBContext.GetSignalStore(); + InitNetwork(); + RecoverDownloads().Wait(); + Running = true; + } + + public async Task Reacquire() + { + Logger.LogTrace("Reacquire() locking"); + CancelSource = new CancellationTokenSource(); + SemaphoreSlim.Wait(CancelSource.Token); + Logger.LogTrace("Reacquire() locked"); + try + { + GlobalResetEvent = LibUtils.OpenResetEventSet(); + Running = true; + LibUtils.Lock(); + GlobalResetEvent.Reset(); + LibsignalDBContext.ClearSessionCache(); + Instance = this; + Logger.LogTrace($"Reacquire() updating {Frames.Count} frames"); + await Task.Run(async () => + { + List tasks = new List(); + foreach (var f in Frames) + { + Logger.LogTrace($"Reacquire() updating frame {f.Value}"); + var conversations = GetConversations(); + var taskCompletionSource = new TaskCompletionSource(); + Logger.LogTrace($"Invoking CoreDispatcher {f.Key.GetHashCode()}"); + await f.Key.RunAsync(CoreDispatcherPriority.Normal, () => + { + try + { + f.Value.ReplaceConversationList(conversations); + } + catch (Exception e) + { + Logger.LogError("Reacquire() ReplaceConversationList() failed: {0}\n{1}", e.Message, e.StackTrace); + } + finally + { + taskCompletionSource.SetResult(false); + } + }); + tasks.Add(taskCompletionSource.Task); + } + foreach (var t in tasks) + { + await t; + } + Logger.LogTrace($"Reacquire() recovering downloads"); + await RecoverDownloads(); + Store = LibsignalDBContext.GetSignalStore(); + if (Store != null) + { + LikelyHasValidStore = true; + } + }); + if (LikelyHasValidStore) + { + await SetSingletonsAsync(Store); + Logger.LogTrace($"Reacquire() initializing network"); + InitNetwork(); + } + } + catch (Exception e) + { + Logger.LogError($"Reacquire() failed: {e.Message}\n{e.StackTrace}"); + } + finally + { + SemaphoreSlim.Release(); + Logger.LogTrace("Reacquire() released"); + } + } + + public void Release() + { + //TODO invalidate view information + Logger.LogTrace("Release()"); + if (Running) + { + Logger.LogTrace("Release() locking"); + SemaphoreSlim.Wait(CancelSource.Token); + Logger.LogTrace("Release() locked"); + Running = false; + CancelSource.Cancel(); + IncomingMessagesTask?.Wait(); + OutgoingMessagesTask?.Wait(); + DispatchRelease().Wait(); + Instance = null; + Logger.LogTrace("Release() releasing global"); + LibUtils.Unlock(); + Logger.LogTrace("Release() releasing local"); + SemaphoreSlim.Release(); + Logger.LogTrace("Release() released"); + } + else + { + Logger.LogWarning("SignalLibHandle was already closed"); + } + } + + public void BackgroundRelease() + { + Running = false; + CancelSource.Cancel(); + IncomingMessagesTask?.Wait(); + OutgoingMessagesTask?.Wait(); + Instance = null; + } + + public async Task SendMessage(string messageText, StorageFile attachmentStorageFile, SignalConversation conversation) + { + await Task.Run(async () => + { + Logger.LogTrace("SendMessage() locking"); + SemaphoreSlim.Wait(CancelSource.Token); + Logger.LogTrace("SendMessage() locked"); + try + { + var now = Util.CurrentTimeMillis(); + var attachmentsList = new List(); + if (attachmentStorageFile != null) + { + attachmentsList.Add(new SignalAttachment() + { + ContentType = attachmentStorageFile.ContentType, + SentFileName = attachmentStorageFile.Name + }); + } + + SignalMessage message = new SignalMessage() + { + Author = null, + ComposedTimestamp = now, + ExpiresAt = conversation.ExpiresInSeconds, + Content = new SignalMessageContent() { Content = messageText }, + ThreadId = conversation.ThreadId, + ThreadGuid = conversation.ThreadGuid, + ReceivedTimestamp = now, + Direction = SignalMessageDirection.Outgoing, + Read = true, + Type = SignalMessageType.Normal, + Attachments = attachmentsList, + AttachmentsCount = (uint)attachmentsList.Count() + }; + await SaveAndDispatchSignalMessage(message, attachmentStorageFile, conversation); + OutgoingQueue.Add(new SignalMessageSendable(message)); + } + finally + { + SemaphoreSlim.Release(); + Logger.LogTrace("SendMessage() released"); + } + }); + } + + public void RequestSync() + { + try + { + Logger.LogTrace("RequestSync()"); + var contactsRequest = SignalServiceSyncMessage.ForRequest(new RequestMessage(new SyncMessage.Types.Request() + { + Type = SyncMessage.Types.Request.Types.Type.Contacts + })); + OutgoingQueue.Add(new SignalServiceSyncMessageSendable(contactsRequest)); + var groupsRequest = SignalServiceSyncMessage.ForRequest(new RequestMessage(new SyncMessage.Types.Request() + { + Type = SyncMessage.Types.Request.Types.Type.Groups + })); + OutgoingQueue.Add(new SignalServiceSyncMessageSendable(groupsRequest)); + } + catch (Exception e) + { + Logger.LogError("RequestSync() failed: {0}\n{1}", e.Message, e.StackTrace); + } + } + + /// + /// Marks and dispatches a message as read. Must not be called on a task which holds the handle lock. + /// + /// + public async Task SetMessageRead(SignalMessage message) + { + Logger.LogTrace("SetMessageRead() locking"); + await SemaphoreSlim.WaitAsync(CancelSource.Token); + try + { + Logger.LogTrace("SetMessageRead() locked"); + var updatedConversation = SignalDBContext.UpdateMessageRead(message.ComposedTimestamp); + UpdateMessageExpiration(message, updatedConversation.ExpiresInSeconds); + OutgoingQueue.Add(new SignalServiceSyncMessageSendable(SignalServiceSyncMessage.ForRead(new List() { + new ReadMessage(new SignalServiceAddress(message.Author.ThreadGuid, message.Author.ThreadId), message.ComposedTimestamp) + }))); + await DispatchMessageRead(updatedConversation); + } + catch (Exception e) + { + Logger.LogError("SetMessageRead() failed: {0}\n{1}", e.Message, e.StackTrace); + } + finally + { + SemaphoreSlim.Release(); + } + Logger.LogTrace("SetMessageRead() released"); + } + + public void ResendMessage(SignalMessage message) + { + OutgoingQueue.Add(new SignalMessageSendable(message)); + } + + public IEnumerable GetMessages(SignalConversation thread, int startIndex, int count) + { + return SignalDBContext.GetMessagesLocked(thread, startIndex, count); + } + + public async Task SaveAndDispatchSignalConversation(SignalConversation updatedConversation, SignalMessage updateMessage) + { + Logger.LogTrace("SaveAndDispatchSignalConversation() locking"); + await SemaphoreSlim.WaitAsync(CancelSource.Token); + try + { + SignalDBContext.InsertOrUpdateConversationLocked(updatedConversation); + await DispatchAddOrUpdateConversation(updatedConversation, updateMessage); + } + catch (Exception e) + { + Logger.LogError("SaveAndDispatchSignalConversation() failed: {0}\n{1}", e.Message, e.StackTrace); + } + finally + { + SemaphoreSlim.Release(); + Logger.LogTrace("SaveAndDispatchSignalConversation() released"); + } + } + + public async Task SendBlockedMessage() + { + List blockedContacts = SignalDBContext.GetAllContactsLocked().Where(c => c.Blocked).ToList(); + List blockedNumbers = new List(); + List blockedGroups = new List(); + foreach (var contact in blockedContacts) + { + blockedNumbers.Add(new SignalServiceAddress(contact.ThreadGuid, contact.ThreadId)); + } + var blockMessage = SignalServiceSyncMessage.ForBlocked(new BlockedListMessage(blockedNumbers, blockedGroups)); + OutgoingQueue.Add(new SignalServiceSyncMessageSendable(blockMessage)); + await DispatchHandleBlockedContacts(blockedContacts); + } + #endregion + + #region attachment api + public void StartAttachmentDownload(SignalAttachment sa) + { + //TODO lock, check if already downloading, start a new download if not exists + Task.Run(async () => + { + try + { + Logger.LogTrace("StartAttachmentDownload() locking"); + SemaphoreSlim.Wait(CancelSource.Token); + Logger.LogTrace("StartAttachmentDownload() locked"); + await TryScheduleAttachmentDownload(sa); + } + catch (Exception e) + { + Logger.LogError("StartAttachmentDownload failed: {0}\n{1}", e.Message, e.StackTrace); + } + finally + { + SemaphoreSlim.Release(); + Logger.LogTrace("StartAttachmentDownload() released"); + } + }); + } + + public async Task ExportAttachment(SignalAttachment sa) + { + try + { + Logger.LogTrace("ExportAttachment() locking"); + await SemaphoreSlim.WaitAsync(CancelSource.Token); + Logger.LogTrace("ExportAttachment() locked"); + var savePicker = new Windows.Storage.Pickers.FileSavePicker + { + SuggestedStartLocation = Windows.Storage.Pickers.PickerLocationId.Downloads, + SuggestedFileName = sa.SentFileName ?? "signal" + }; + savePicker.FileTypeChoices.Add("Any", new List() { "." }); + var target_file = await savePicker.PickSaveFileAsync(); + if (target_file != null) + { + CachedFileManager.DeferUpdates(target_file); + IStorageFile localCopy = await ApplicationData.Current.LocalCacheFolder.GetFileAsync($@"Attachments\{sa.Id}.plain"); + await localCopy.CopyAndReplaceAsync(target_file); + Windows.Storage.Provider.FileUpdateStatus status = await CachedFileManager.CompleteUpdatesAsync(target_file); + } + } + catch (Exception e) + { + Logger.LogError("ExportAttachment failed: {0}\n{1}", e.Message, e.StackTrace); + } + finally + { + SemaphoreSlim.Release(); + Logger.LogTrace("ExportAttachment() released"); + } + } + #endregion + + #region internal api + internal async Task DispatchHandleAuthFailure() + { + List operations = new List(); + foreach (var dispatcher in Frames.Keys) + { + var taskCompletionSource = new TaskCompletionSource(); + await dispatcher.RunAsync(CoreDispatcherPriority.Normal, async () => + { + try + { + await Frames[dispatcher].HandleAuthFailure(); + } + catch (Exception e) + { + Logger.LogError("DispatchHandleAuthFailure failed: {0}\n{1}", e.Message, e.StackTrace); + } + finally + { + taskCompletionSource.SetResult(false); + } + }); + operations.Add(taskCompletionSource.Task); + } + foreach (var t in operations) + { + await t; + } + } + + internal async Task SaveAndDispatchSignalMessage(SignalMessage message, StorageFile attachmentStorageFile, SignalConversation conversation) + { + conversation.MessagesCount += 1; + if (message.Direction == SignalMessageDirection.Incoming) + { + conversation.UnreadCount += 1; + } + else + { + conversation.UnreadCount = 0; + conversation.LastSeenMessageIndex = conversation.MessagesCount; + } + SignalDBContext.SaveMessageLocked(message); + conversation.LastMessage = message; + conversation.LastActiveTimestamp = message.ComposedTimestamp; + if (attachmentStorageFile != null) + { + StorageFolder plaintextFile = await ApplicationData.Current.LocalCacheFolder.CreateFolderAsync(@"Attachments\", CreationCollisionOption.OpenIfExists); + foreach (var attachment in message.Attachments) + { + Logger.LogTrace(@"Copying attachment to \Attachments\{0}.plain", attachment.Id.ToString()); + await attachmentStorageFile.CopyAsync(plaintextFile, attachment.Id.ToString() + ".plain", NameCollisionOption.ReplaceExisting); + } + } + await DispatchHandleMessage(message, conversation); + } + + internal async Task DispatchHandleIdentityKeyChange(LinkedList messages) + { + List operations = new List(); + foreach (var dispatcher in Frames.Keys) + { + var taskCompletionSource = new TaskCompletionSource(); + await dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => + { + try + { + Frames[dispatcher].HandleIdentitykeyChange(messages); + } + catch (Exception e) + { + Logger.LogError("DispatchHandleIdentityKeyChange() dispatch failed: {0}\n{1}", e.Message, e.StackTrace); + } + finally + { + taskCompletionSource.SetResult(false); + } + }); + operations.Add(taskCompletionSource.Task); + } + foreach (var t in operations) + { + await t; + } + } + + internal async Task DispatchAddOrUpdateConversations(IList newConversations) + { + List operations = new List(); + foreach (var dispatcher in Frames.Keys) + { + var taskCompletionSource = new TaskCompletionSource(); + await dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => + { + try + { + foreach (var contact in newConversations) + { + Frames[dispatcher].AddOrUpdateConversation(contact, null); + } + } + catch (Exception e) + { + Logger.LogError("DispatchAddOrUpdateConversations() dispatch failed: {0}\n{1}", e.Message, e.StackTrace); + } + finally + { + taskCompletionSource.SetResult(false); + } + }); + operations.Add(taskCompletionSource.Task); + } + foreach (var t in operations) + { + await t; + } + } + + internal async Task DispatchAddOrUpdateConversation(SignalConversation conversation, SignalMessage updateMessage) + { + List operations = new List(); + foreach (var dispatcher in Frames.Keys) + { + var taskCompletionSource = new TaskCompletionSource(); + await dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => + { + try + { + Frames[dispatcher].AddOrUpdateConversation(conversation, updateMessage); + } + catch (Exception e) + { + Logger.LogError("DispatchAddOrUpdateConversation() dispatch failed: {0}\n{1}", e.Message, e.StackTrace); + } + finally + { + taskCompletionSource.SetResult(false); + } + }); + operations.Add(taskCompletionSource.Task); + } + foreach (var t in operations) + { + await t; + } + } + + internal async Task DispatchHandleMessage(SignalMessage message, SignalConversation conversation) + { + List> operations = new List>(); + foreach (var dispatcher in Frames.Keys) + { + TaskCompletionSource taskCompletionSource = new TaskCompletionSource(); + await dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => + { + AppendResult ar = null; + try + { + ar = (Frames[dispatcher].HandleMessage(message, conversation)); + } + catch (Exception e) + { + Logger.LogError("DispatchHandleMessage() dispatch failed: {0}\n{1}", e.Message, e.StackTrace); + } + finally + { + taskCompletionSource.SetResult(ar); + } + }); + operations.Add(taskCompletionSource.Task); + } + SignalMessageEvent?.Invoke(this, new SignalMessageEventArgs(message, Events.SignalPipeMessageType.NormalMessage)); + if (message.Author != null) + { + bool wasInstantlyRead = false; + foreach (var b in operations) + { + AppendResult result = await b; + if (result != null && result.WasInstantlyRead) + { + UpdateMessageExpiration(message, conversation.ExpiresInSeconds); + var updatedConversation = SignalDBContext.UpdateMessageRead(message.ComposedTimestamp); + await DispatchMessageRead(updatedConversation); + wasInstantlyRead = true; + break; + } + } + if (!wasInstantlyRead) + { + await DispatchHandleUnreadMessage(message); + } + } + } + + internal async Task DispatchHandleUnreadMessage(SignalMessage message) + { + List operations = new List(); + foreach (var dispatcher in Frames.Keys) + { + TaskCompletionSource taskCompletionSource = new TaskCompletionSource(); + await dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => + { + try + { + Frames[dispatcher].HandleUnreadMessage(message); + } + catch (Exception e) + { + Logger.LogError("DispatchHandleUnreadMessage() dispatch failed: {0}\n{1}", e.Message, e.StackTrace); + } + finally + { + taskCompletionSource.SetResult(false); + } + }); + operations.Add(taskCompletionSource.Task); + } + foreach (var t in operations) + { + await t; + } + } + + internal async Task DispatchMessageRead(SignalConversation conversation) + { + List operations = new List(); + foreach (var dispatcher in Frames.Keys) + { + var taskCompletionSource = new TaskCompletionSource(); + await dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => + { + try + { + Frames[dispatcher].HandleMessageRead(conversation); + } + catch (Exception e) + { + Logger.LogError("DispatchMessageRead() dispatch failed: {0}\n{1}", e.Message, e.StackTrace); + } + finally + { + taskCompletionSource.SetResult(false); + } + }); + operations.Add(taskCompletionSource.Task); + } + foreach (var waitHandle in operations) + { + await waitHandle; + } + } + + internal void UpdateMessageExpiration(SignalMessage message, uint conversationExpireTimeSeconds) + { + if (message.Type == Signal_Windows.Models.SignalMessageType.Normal && message.ExpiresAt == 0) + { + long messageExpiration; + if (conversationExpireTimeSeconds == 0) + { + messageExpiration = 0; + } + else + { + messageExpiration = Util.CurrentTimeMillis() + (long)TimeSpan.FromSeconds(conversationExpireTimeSeconds).TotalMilliseconds; + } + + if (messageExpiration > 0) + { + message.ExpiresAt = messageExpiration; + SignalDBContext.UpdateMessageExpiresAt(message); + DisappearingMessagesManager.QueueForDeletion(message); + } + } + } + + internal void DispatchPipeEmptyMessage() + { + SignalMessageEvent?.Invoke(this, new SignalMessageEventArgs(null, Events.SignalPipeMessageType.PipeEmptyMessage)); + } + + internal async Task HandleMessageSentLocked(ISendable msg) + { + if (msg is SignalMessageSendable smSendable) + { + Logger.LogTrace("HandleMessageSentLocked() locking"); + await SemaphoreSlim.WaitAsync(CancelSource.Token); + Logger.LogTrace("HandleMessageSentLocked() locked"); + var updated = SignalDBContext.UpdateMessageStatus(smSendable.OutgoingSignalMessage); + await DispatchMessageUpdate(updated); + SemaphoreSlim.Release(); + Logger.LogTrace("HandleMessageSentLocked() released"); + } + } + + internal async Task DispatchMessageUpdate(SignalMessage msg) + { + List operations = new List(); + foreach (var dispatcher in Frames.Keys) + { + var taskCompletionSource = new TaskCompletionSource(); + await dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => + { + try + { + Frames[dispatcher].HandleMessageUpdate(msg); + } + catch (Exception e) + { + Logger.LogError("DispatchMessageUpdate() dispatch failed: {0}\n{1}", e.Message, e.StackTrace); + } + finally + { + taskCompletionSource.SetResult(false); + } + }); + operations.Add(taskCompletionSource.Task); + } + foreach (var t in operations) + { + await t; + } + } + + internal async Task HandleOutgoingKeyChangeLocked(string user, string identity) + { + Logger.LogTrace("HandleOutgoingKeyChange() locking"); + await SemaphoreSlim.WaitAsync(CancelSource.Token); + try + { + Logger.LogTrace("HandleOutgoingKeyChange() locked"); + await LibsignalDBContext.SaveIdentityLocked(new SignalProtocolAddress(user, 1), identity); + } + catch (Exception e) + { + Logger.LogError("HandleOutgoingKeyChangeLocked() failed: {0}\n{1}", e.Message, e.StackTrace); + } + finally + { + SemaphoreSlim.Release(); + } + Logger.LogTrace("HandleOutgoingKeyChange() released"); + } + + /// + /// This will notify all windows of newly blocked numbers. + /// This does not save to the database. + /// + /// The list of blocked contacts + internal async Task DispatchHandleBlockedContacts(List blockedContacts) + { + List operations = new List(); + foreach (var dispatcher in Frames.Keys) + { + var taskCompletionSource = new TaskCompletionSource(); + await dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => + { + try + { + Frames[dispatcher].HandleBlockedContacts(blockedContacts); + } + catch (Exception e) + { + Logger.LogError("DispatchHandleBlockedContacts() dispatch failed: {0}\n{1}", e.Message, e.StackTrace); + } + finally + { + taskCompletionSource.SetResult(false); + } + }); + operations.Add(taskCompletionSource.Task); + } + foreach (var t in operations) + { + await t; + } + } + + internal async Task DispatchRelease() + { + List operations = new List(); + foreach (var dispatcher in Frames.Keys) + { + var taskCompletionSource = new TaskCompletionSource(); + await dispatcher.RunAsync(CoreDispatcherPriority.Normal, async () => + { + try + { + await Frames[dispatcher].Release(); + } + catch (Exception e) + { + Logger.LogError("DispatchRelease() dispatch failed: {0}\n{1}", e.Message, e.StackTrace); + } + finally + { + taskCompletionSource.SetResult(false); + } + }); + operations.Add(taskCompletionSource.Task); + } + foreach (var t in operations) + { + await t; + } + } + #endregion + + #region private + private async Task SetSingletonsAsync(SignalStore store) + { + // Setup SignalServiceAccountManager + AccountManager = CreateNewSignalServiceAccountManager(store); + if (await UpdateOwnGuid(store, AccountManager)) + { + AccountManager = CreateNewSignalServiceAccountManager(store); + } + } + + /// + /// Updates the store Signal UUID if it hasn't been set due to an upgrade from an older version. + /// + /// The SignalStore + /// The SignalServiceAccountManager + /// True if the UUID was set, false if it wasn't set + private async Task UpdateOwnGuid(SignalStore store, SignalServiceAccountManager accountManager) + { + if (!store.OwnGuid.HasValue) + { + Logger.LogInformation("Own Signal UUID not set, attempting to set."); + Guid ownGuid; + try + { + ownGuid = await accountManager.GetOwnUuidAsync(CancelSource.Token); + } + catch (Exception ex) + { + Logger.LogError(new EventId(), ex, "Failed to get own Signal UUID"); + return false; + } + + store.OwnGuid = ownGuid; + LibsignalDBContext.SaveOrUpdateSignalStore(store); + + Logger.LogInformation("Own Signal UUID now set"); + return true; + } + return false; + } + + private SignalServiceAccountManager CreateNewSignalServiceAccountManager(SignalStore store) + { + return new SignalServiceAccountManager(LibUtils.ServiceConfiguration, store.OwnGuid, store.Username, + store.Password, (int)store.DeviceId, LibUtils.USER_AGENT, LibUtils.HttpClient); + } + + private List GetConversations() + { + List conversations = new List(); + List contacts = SignalDBContext.GetAllContactsLocked(); + List groups = SignalDBContext.GetAllGroupsLocked(); + int amountContacts = contacts.Count; + int amountGroups = groups.Count; + int contactsIdx = 0; + int groupsIdx = 0; + while (contactsIdx < amountContacts || groupsIdx < amountGroups) + { + if (contactsIdx < amountContacts) + { + SignalConversation contact = contacts[contactsIdx]; + if (groupsIdx < amountGroups) + { + SignalConversation group = groups[groupsIdx]; + if (contact.LastActiveTimestamp > group.LastActiveTimestamp) + { + contactsIdx++; + conversations.Add(contact); + } + else + { + groupsIdx++; + conversations.Add(group); + } + } + else + { + contactsIdx++; + conversations.Add(contact); + } + } + else if (groupsIdx < amountGroups) + { + SignalConversation group = groups[groupsIdx]; + groupsIdx++; + conversations.Add(group); + } + } + return conversations; + } + + /// + /// Initializes the websocket connection handling. Must not not be called on a UI thread. Must not be called on a task which holds the handle lock. + /// + private void InitNetwork() + { + try + { + Logger.LogTrace("InitNetwork() sync context = {0}", SynchronizationContext.Current); + MessageReceiver = new SignalServiceMessageReceiver(LibUtils.ServiceConfiguration, new StaticCredentialsProvider(Store.OwnGuid, Store.Username, Store.Password, (int)Store.DeviceId), LibUtils.USER_AGENT, LibUtils.HttpClient); + Task.Run(async () => + { + try + { + var pipe = await MessageReceiver.CreateMessagePipeAsync(new SignalWebSocketFactory(), CancelSource.Token); + Logger.LogTrace("Starting IncomingMessagesTask"); + IncomingMessagesTask = Task.Run(() => new IncomingMessages(CancelSource.Token, pipe, MessageReceiver).HandleIncomingMessages()); + Logger.LogTrace("Starting OutgoingMessagesTask"); + OutgoingMessagesTask = Task.Run(() => new OutgoingMessages(CancelSource.Token, pipe, Store, this).HandleOutgoingMessages()); + } + catch (OperationCanceledException) + { + Logger.LogInformation("InitNetwork canceled"); + } + catch (Exception e) + { + Logger.LogError("InitNetwork failed: {0}\n{1}", e.Message, e.StackTrace); + await HandleAuthFailure(); + throw e; + } + }); + } + catch (Exception e) + { + Logger.LogError($"InitNetwork() failed: {e.Message}\n{e.StackTrace}"); + } + } + + /// + /// Dispatches the auth failure to all frontends and resets the frontend dict. Must not be called on a UI thread. Must not be called on a task which holds the handle lock. + /// + private async Task HandleAuthFailure() + { + Logger.LogError("HandleAuthFailure"); + Logger.LogTrace("HandleAuthFailure() locking"); + SemaphoreSlim.Wait(CancelSource.Token); + try + { + LikelyHasValidStore = false; + Running = false; + CancelSource.Cancel(); + await DispatchHandleAuthFailure(); + Frames.Clear(); + Frames.Add(MainWindowDispatcher, MainWindow); + } + finally + { + SemaphoreSlim.Release(); + Logger.LogTrace("HandleAuthFailure() released"); + } + } + + private async Task TryScheduleAttachmentDownload(SignalAttachment attachment) + { + if (Downloads.Count < 100) + { + if (attachment.Status != SignalAttachmentStatus.Finished && !Downloads.ContainsKey(attachment.Id)) + { + SignalServiceAttachmentPointer attachmentPointer = attachment.ToAttachmentPointer(); + IStorageFolder localFolder = ApplicationData.Current.LocalFolder; + StorageFile tmpDownload = await Task.Run(async () => + { + return await ApplicationData.Current.LocalCacheFolder.CreateFileAsync(@"Attachments\" + attachment.Id + ".cipher", CreationCollisionOption.ReplaceExisting); + }); + BackgroundDownloader downloader = new BackgroundDownloader(); + downloader.SetRequestHeader("Content-Type", "application/octet-stream"); + // this is the recommended way to call CreateDownload + // see https://docs.microsoft.com/en-us/uwp/api/windows.networking.backgroundtransfer.backgrounddownloader#Methods + DownloadOperation download = downloader.CreateDownload(new Uri(MessageReceiver.RetrieveAttachmentDownloadUrl(attachmentPointer)), tmpDownload); + attachment.Guid = download.Guid.ToString(); + SignalDBContext.UpdateAttachmentGuid(attachment); + Downloads.Add(attachment.Id, download); + var downloadSuccessfulHandler = Task.Run(async () => + { + Logger.LogInformation("Waiting for download {0}({1})", attachment.SentFileName, attachment.Id); + var t = await download.StartAsync(); + await HandleSuccessfullDownload(attachment, tmpDownload, download); + }); + } + } + } + + private async Task HandleSuccessfullDownload(SignalAttachment attachment, IStorageFile tmpDownload, DownloadOperation download) + { + try + { + SemaphoreSlim.Wait(CancelSource.Token); + StorageFile plaintextFile = await ApplicationData.Current.LocalCacheFolder.CreateFileAsync(@"Attachments\" + attachment.Id + ".plain", CreationCollisionOption.ReplaceExisting); + using (var tmpFileStream = (await tmpDownload.OpenAsync(FileAccessMode.ReadWrite)).AsStream()) + using (var plaintextFileStream = (await plaintextFile.OpenAsync(FileAccessMode.ReadWrite)).AsStream()) + { + Logger.LogInformation("Decrypting to {0}\\{1}", plaintextFile.Path, plaintextFile.Name); + DecryptAttachment(attachment.ToAttachmentPointer(), tmpFileStream, plaintextFileStream); + } + Logger.LogInformation("Deleting tmpFile {0}", tmpDownload.Name); + await tmpDownload.DeleteAsync(); + attachment.Status = SignalAttachmentStatus.Finished; + SignalDBContext.UpdateAttachmentStatus(attachment); + await DispatchAttachmentStatusChanged(download, attachment); + } + catch (Exception e) + { + Logger.LogError("HandleSuccessfullDownload failed: {0}\n{1}", e.Message, e.StackTrace); + } + finally + { + if (Downloads.ContainsKey(attachment.Id)) + { + Downloads.Remove(attachment.Id); + } + SemaphoreSlim.Release(); + } + } + + private void DecryptAttachment(SignalServiceAttachmentPointer pointer, Stream ciphertextFileStream, Stream plaintextFileStream) + { + Stream s = AttachmentCipherInputStream.CreateForAttachment(ciphertextFileStream, pointer.Size != null ? pointer.Size.Value : 0, pointer.Key, pointer.Digest); + s.CopyTo(plaintextFileStream); + } + + private async Task RecoverDownloads() + { + var downloads = await BackgroundDownloader.GetCurrentDownloadsAsync(); + foreach (DownloadOperation download in downloads) + { + try + { + SignalAttachment attachment = SignalDBContext.GetAttachmentByGuidNameLocked(download.Guid.ToString()); + if (attachment != null) + { + if (!Downloads.ContainsKey(attachment.Id)) + { + Logger.LogInformation("Creating attach task for attachment {0} ({1})", attachment.Id, download.Guid); + Downloads.Add(attachment.Id, download); + var t = Task.Run(async () => + { + await download.AttachAsync(); + await HandleSuccessfullDownload(attachment, download.ResultFile, download); + }); + } + else + { + Logger.LogInformation("Attachment {0} ({1}) already has a running task", attachment.Id, download.Guid); + } + } + else + { + Logger.LogInformation("Aborting unrecognized download {0}", download.Guid); + download.AttachAsync().Cancel(); + } + } + catch (Exception e) + { + Logger.LogError("TriageDownloads encountered an error: {0}\n{1}", e.Message, e.StackTrace); + } + } + } + + private async Task DispatchAttachmentStatusChanged(DownloadOperation op, SignalAttachment attachment) + { + try + { + List operations = new List(); + foreach (var dispatcher in Frames.Keys) + { + var taskCompletionSource = new TaskCompletionSource(); + await dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => + { + try + { + Frames[dispatcher].HandleAttachmentStatusChanged(attachment); + } + catch (Exception e) + { + Logger.LogError("DispatchAttachmentStatusChanged() dispatch failed: {0}\n{1}", e.Message, e.StackTrace); + } + finally + { + taskCompletionSource.SetResult(false); + } + }); + operations.Add(taskCompletionSource.Task); + } + foreach (var t in operations) + { + await t; + } + } + catch (Exception e) + { + Logger.LogError("DispatchAttachmentStatusChanged encountered an error: {0}\n{1}", e.Message, e.StackTrace); + } + } + #endregion + } +} diff --git a/Signal-Windows.Lib/SignalLogging.cs b/Signal-Windows.Lib/SignalLogging.cs new file mode 100644 index 0000000..b84bb0f --- /dev/null +++ b/Signal-Windows.Lib/SignalLogging.cs @@ -0,0 +1,224 @@ +using libsignalservice; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Windows.Storage; + +namespace Signal_Windows.Storage +{ + public class SignalLogging + { + public static void SetupLogging(bool ui) + { + if (ui) + { + var console = new SignalConsoleLoggerProvider(); + var file = new SignalFileLoggerProvider(ApplicationData.Current.LocalCacheFolder.Path + @"\Signal-Windows.ui.log", "UI"); + LibsignalLogging.LoggerFactory.AddProvider(console); + LibsignalLogging.LoggerFactory.AddProvider(file); + } + else + { + LibsignalLogging.LoggerFactory.AddProvider(new SignalFileLoggerProvider(ApplicationData.Current.LocalCacheFolder.Path + @"\Signal-Windows.bg.log", "BG")); + } + } + } + + class SignalConsoleLogger : ILogger + { + private readonly string ClassName; + public SignalConsoleLogger(string categoryName) + { + ClassName = categoryName; + } + + public IDisposable BeginScope(TState state) + { + throw new NotImplementedException(); + } + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + Debug.WriteLine(string.Format("{0:s} [{1}] [{2}] ", DateTime.UtcNow, logLevel, ClassName) + formatter(state, exception)); + } + } + + class SignalConsoleLoggerProvider : ILoggerProvider + { + public ILogger CreateLogger(string categoryName) + { + return new SignalConsoleLogger(categoryName); + } + + public void Dispose() + { + + } + } + + class SignalFileLogger : ILogger + { + private readonly string ClassName; + private readonly SignalFileLoggerProvider Provider; + + public SignalFileLogger(string categoryName, SignalFileLoggerProvider provider) + { + ClassName = categoryName; + Provider = provider; + } + + public IDisposable BeginScope(TState state) + { + throw new NotImplementedException(); + } + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + Provider.Log(string.Format("[{0}] [{1}] ", logLevel, ClassName) + formatter(state, exception)); + } + } + + public class SignalFileLoggerProvider : ILoggerProvider + { + private static readonly string UILog = ApplicationData.Current.LocalCacheFolder.Path + @"\Signal-Windows.ui.log"; + private static readonly string BGLog = ApplicationData.Current.LocalCacheFolder.Path + @"\Signal-Windows.bg.log"; + private readonly string Filename; + private readonly string OldFilename; + private readonly string Prefix; + private const int MaxLogSize = 256 * 1024; + private static readonly object Lock = new object(); + + public SignalFileLoggerProvider(string filename, string prefix) + { + Filename = filename; + OldFilename = Filename + ".old"; + Prefix = prefix; + } + + public void TruncateLog() + { + try + { + var length = new FileInfo(Filename).Length; + if (length > MaxLogSize) + { + if (File.Exists(OldFilename)) + { + File.Delete(OldFilename); + } + File.Move(Filename, OldFilename); + File.AppendAllText(Filename, $"{DateTime.UtcNow.ToString("s")} [SignalFileLoggerProvider] truncated log file\n"); + } + } + catch (Exception e) + { + Debug.WriteLine(string.Format("SignalFileLoggerProvider failed to truncate file: {0}", e)); + } + } + + public void Log(string line) + { + lock (Lock) + { + try + { + TruncateLog(); + File.AppendAllText(Filename, $"{DateTime.UtcNow.ToString("s")} [{Prefix}] {line}\n"); + } + catch (Exception e) + { + Debug.WriteLine(string.Format("SignalFileLoggerProvider failed to write: {0}", e)); + } + } + } + + public ILogger CreateLogger(string categoryName) + { + return new SignalFileLogger(categoryName, this); + } + + public void Dispose() + { + } + + public static void ForceAddUILog(string msg) + { + lock (Lock) + { + try + { + File.AppendAllText(UILog, msg); + } + catch (Exception e) + { + Debug.WriteLine(string.Format("SignalFileLoggerProvider failed to write: {0}", e)); + } + } + } + + public static void ForceAddBGLog(string msg) + { + lock (Lock) + { + try + { + File.AppendAllText(BGLog, msg); + } + catch (Exception e) + { + Debug.WriteLine(string.Format("SignalFileLoggerProvider failed to write: {0}", e)); + } + } + } + + public static void ExportUILog(StorageFile file) + { + lock(Lock) + { + FileIO.WriteTextAsync(file, "").AsTask().Wait(); + CachedFileManager.DeferUpdates(file); + var writer = file.OpenStreamForWriteAsync().Result; + try + { + var oldLog = File.OpenRead(ApplicationData.Current.LocalCacheFolder.Path + @"\Signal-Windows.ui.log.old"); + MoveFileContent(oldLog, writer); + oldLog.Dispose(); + } catch (Exception) { } + try + { + var newLog = File.OpenRead(ApplicationData.Current.LocalCacheFolder.Path + @"\Signal-Windows.ui.log"); + MoveFileContent(newLog, writer); + newLog.Dispose(); + } + catch (Exception) { } + Windows.Storage.Provider.FileUpdateStatus status = CachedFileManager.CompleteUpdatesAsync(file).AsTask().Result; + } + } + + private static void MoveFileContent(Stream source, Stream destination) + { + byte[] buffer = new byte[1024]; + int read; + while ((read = source.Read(buffer, 0, buffer.Length)) > 0) + { + destination.Write(buffer, 0, read); + } + destination.Flush(); + } + } +} diff --git a/Signal-Windows.Lib/SignalWebSocket.cs b/Signal-Windows.Lib/SignalWebSocket.cs new file mode 100644 index 0000000..7198c11 --- /dev/null +++ b/Signal-Windows.Lib/SignalWebSocket.cs @@ -0,0 +1,145 @@ +using libsignalservice; +using libsignalservice.push.exceptions; +using libsignalservice.websocket; +using Microsoft.Extensions.Logging; +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Windows.Networking.Sockets; +using Windows.Storage.Streams; + +namespace Signal_Windows.Lib +{ + public class SignalWebSocketFactory : ISignalWebSocketFactory + { + public ISignalWebSocket CreateSignalWebSocket(Uri uri, CancellationToken? token = null) + { + if (token == null) + { + token = CancellationToken.None; + } + + return new SignalWebSocket(uri, token.Value); + } + } + + class SignalWebSocket : ISignalWebSocket + { + private readonly ILogger Logger = LibsignalLogging.CreateLogger(); + private MessageWebSocket WebSocket; + private readonly SemaphoreSlim SemaphoreSlim = new SemaphoreSlim(1, 1); + private readonly Uri SignalWSUri; + private readonly CancellationToken Token; + public event EventHandler Closed; + public event EventHandler MessageReceived; + + public SignalWebSocket(Uri uri, CancellationToken token) + { + CreateMessageWebSocket(); + Token = token; + SignalWSUri = uri; + } + + private void CreateMessageWebSocket() + { + WebSocket = new MessageWebSocket(); + WebSocket.MessageReceived += WebSocket_MessageReceived; + WebSocket.Closed += WebSocket_Closed; + } + + private void WebSocket_Closed(IWebSocket sender, WebSocketClosedEventArgs args) + { + Closed?.Invoke(sender, new SignalWebSocketClosedEventArgs() { Code = args.Code, Reason = args.Reason }); + Logger.LogWarning("WebSocket_Closed() {0} ({1})", args.Code, args.Reason); + } + + private void WebSocket_MessageReceived(MessageWebSocket sender, MessageWebSocketMessageReceivedEventArgs args) + { + Logger.LogTrace("WebSocket_MessageReceived()"); + try + { + using (var data = args.GetDataStream()) + using (var buffer = new MemoryStream()) + { + data.AsStreamForRead().CopyTo(buffer); + MessageReceived.Invoke(sender, new SignalWebSocketMessageReceivedEventArgs() { Message = buffer.ToArray() }); + } + } + catch(Exception e) + { + Logger.LogError("WebSocket_MessageReceived failed: {0}\n{1}", e.Message, e.StackTrace); + Task.Run(ConnectAsync); + } + } + + public void Close(ushort code, string reason) + { + Logger.LogTrace("Closing SignalWebSocket connection"); + WebSocket.Close(code, reason); + } + + public async Task ConnectAsync() + { + Logger.LogTrace("ConnectAsync()"); + var locked = await SemaphoreSlim.WaitAsync(0, Token); // ensure no threads are reconnecting at the same time + if (locked) + { + while (!Token.IsCancellationRequested) + { + try + { + CreateMessageWebSocket(); + Logger.LogTrace("WebSocket.ConnectAsync()"); + await WebSocket.ConnectAsync(SignalWSUri).AsTask(Token); + SemaphoreSlim.Release(); + break; + } + catch (OperationCanceledException) { } + catch (Exception e) + { + if (e.Message.Contains("(403)")) + { + SemaphoreSlim.Release(); + throw new AuthorizationFailedException(403, "OWS server rejected authorization."); + } + Logger.LogError("ConnectAsync() failed: {0}\n{1}", e.Message, e.StackTrace); //System.Runtime.InteropServices.COMException (0x80072EE7) + await Task.Delay(10 * 1000); + } + } + } + else + { + Logger.LogTrace("ConnectAsync() not reconnecting: Reconnect in progress"); + } + } + + public void Dispose() + { + WebSocket.Dispose(); + } + + public async Task SendMessage(byte[] data) + { + Logger.LogTrace("SendMessage()"); + try + { + using (var dataWriter = new DataWriter(WebSocket.OutputStream)) + { + dataWriter.WriteBytes(data); + await dataWriter.StoreAsync(); + dataWriter.DetachStream(); + } + } + catch (OperationCanceledException) + { + Logger.LogTrace($"SendMessage() was cancelled"); + } + catch (Exception e) + { + Logger.LogError($"SendMessage() failed: {e.Message}\n{e.StackTrace}"); + var t = Task.Run(ConnectAsync); + } + } + } +} diff --git a/Signal-Windows.Lib/Storage/LibsignalDBContext.cs b/Signal-Windows.Lib/Storage/LibsignalDBContext.cs new file mode 100644 index 0000000..601a5df --- /dev/null +++ b/Signal-Windows.Lib/Storage/LibsignalDBContext.cs @@ -0,0 +1,679 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using libsignal; +using libsignal.ecc; +using libsignal.state; +using libsignal.util; +using libsignalservice; +using libsignalservice.push; +using libsignalservice.util; +using Microsoft.EntityFrameworkCore; +using Signal_Windows.Lib; +using Signal_Windows.Models; + +namespace Signal_Windows.Storage +{ + /// + /// Persistent state representing the . + /// + public sealed class LibsignalDBContext : DbContext + { + private static readonly object DBLock = new object(); + public DbSet Identities { get; set; } + public DbSet Store { get; set; } + public DbSet PreKeys { get; set; } + public DbSet SignedPreKeys { get; set; } + public DbSet Sessions { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseSqlite(@"Filename=..\LocalCache\Libsignal.db", x => x.SuppressForeignKeyEnforcement()); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasIndex(si => si.Username); + + modelBuilder.Entity() + .HasIndex(s => s.Username); + + modelBuilder.Entity() + .HasIndex(s => s.DeviceId); + + modelBuilder.Entity() + .HasIndex(pk => pk.Id); + } + + public static void Migrate() + { + lock (DBLock) + { + using (var ctx = new LibsignalDBContext()) + { + if (ctx.Database.GetPendingMigrations().Count() > 0) + { + ctx.Database.Migrate(); + } + } + } + } + + public static void PurgeAccountData() + { + lock (DBLock) + { + using (var ctx = new LibsignalDBContext()) + { + ctx.Database.ExecuteSqlCommand("DELETE FROM Store;"); + ctx.Database.ExecuteSqlCommand("DELETE FROM sqlite_sequence WHERE name = 'Store';"); + + ctx.Database.ExecuteSqlCommand("DELETE FROM SignedPreKeys;"); + ctx.Database.ExecuteSqlCommand("DELETE FROM sqlite_sequence WHERE name = 'SignedPreKeys';"); + + ctx.Database.ExecuteSqlCommand("DELETE FROM PreKeys;"); + ctx.Database.ExecuteSqlCommand("DELETE FROM sqlite_sequence WHERE name = 'PreKeys';"); + + ctx.Database.ExecuteSqlCommand("DELETE FROM Sessions;"); + ctx.Database.ExecuteSqlCommand("DELETE FROM sqlite_sequence WHERE name = 'Sessions';"); + } + } + } + + #region Identities + + public static string GetIdentityLocked(string number) + { + lock (DBLock) + { + using (var ctx = new LibsignalDBContext()) + { + var identity = ctx.Identities.LastOrDefault(i => i.Username == number); + if (identity == null) + { + return null; + } + return identity.IdentityKey; + } + } + } + + private static LinkedList InsertIdentityChangedMessages(string number) + { + long now = Util.CurrentTimeMillis(); + LinkedList messages = new LinkedList(); + using (var ctx = new SignalDBContext()) + { + SignalContact contact = SignalDBContext.GetSignalContactByThreadId(ctx, number); + if (contact != null) + { + string str = $"Your safety numbers with {contact.ThreadDisplayName} have changed."; + SignalMessage msg = new SignalMessage() + { + Author = contact, + ComposedTimestamp = now, + ReceivedTimestamp = now, + Direction = SignalMessageDirection.Incoming, + Type = SignalMessageType.IdentityKeyChange, + ThreadId = contact.ThreadId, + ThreadGuid = contact.ThreadGuid, + Content = new SignalMessageContent() { Content = str } + }; + contact.LastMessage = msg; + contact.MessagesCount += 1; + contact.UnreadCount += 1; + ctx.Messages.Add(msg); + messages.AddLast(msg); + var groups = ctx.GroupMemberships + .Where(gm => gm.ContactId == contact.Id) + .Include(gm => gm.Group); + foreach (var gm in groups) + { + msg = new SignalMessage() + { + Author = contact, + ComposedTimestamp = now, + ReceivedTimestamp = now, + Direction = SignalMessageDirection.Incoming, + Type = SignalMessageType.IdentityKeyChange, + ThreadId = gm.Group.ThreadId, + ThreadGuid = gm.Group.ThreadGuid, + Content = new SignalMessageContent() { Content = str } + }; + gm.Group.LastMessage = msg; + gm.Group.MessagesCount += 1; + gm.Group.UnreadCount += 1; + ctx.Messages.Add(msg); + messages.AddLast(msg); + } + } + ctx.SaveChanges(); + } + return messages; + } + + public static async Task SaveIdentityLocked(SignalProtocolAddress address, string identity) + { + LinkedList messages = null; + lock (DBLock) + { + using (var ctx = new LibsignalDBContext()) + { + var old = ctx.Identities + .Where(i => i.Username == address.Name) + .FirstOrDefault(); //could contain stale data + if (old == null) + { + ctx.Identities.Add(new SignalIdentity() + { + IdentityKey = identity, + Username = address.Name, + VerifiedStatus = VerifiedStatus.Default + }); + } + else if (old.IdentityKey != identity) + { + if (old.VerifiedStatus == VerifiedStatus.Verified) + { + old.VerifiedStatus = VerifiedStatus.Unverified; + } + old.IdentityKey = identity; + var oldSessions = ctx.Sessions + .Where(s => s.Username == address.Name); + foreach (var oldSession in oldSessions) + { + SessionRecord sessionRecord = new SessionRecord(Base64.Decode(oldSession.Session)); + sessionRecord.archiveCurrentState(); + oldSession.Session = Base64.EncodeBytes(sessionRecord.serialize()); + SessionsCache[GetSessionCacheIndex(address.Name, oldSession.DeviceId)] = sessionRecord; + } + messages = InsertIdentityChangedMessages(address.Name); + } + ctx.SaveChanges(); + } + } + if (messages != null) + { + await SignalLibHandle.Instance.DispatchHandleIdentityKeyChange(messages); + } + } + + internal static IdentityKey GetIdentityKey(SignalProtocolAddress address) + { + lock (DBLock) + { + using (var ctx = new LibsignalDBContext()) + { + return new IdentityKey(Base64.Decode(ctx.Identities + .Where(identity => identity.Username == address.Name) + .Single().IdentityKey), 0); + } + } + } + #endregion Identities + + #region Account + + public static SignalStore GetSignalStore() + { + lock (DBLock) + { + using (var ctx = new LibsignalDBContext()) + { + return ctx.Store + .AsNoTracking() + .SingleOrDefault(); + } + } + } + + public static void SaveOrUpdateSignalStore(SignalStore store) + { + lock (DBLock) + { + using (var ctx = new LibsignalDBContext()) + { + var old = ctx.Store.SingleOrDefault(); + if (old != null) + { + old.DeviceId = store.DeviceId; + old.IdentityKeyPair = store.IdentityKeyPair; + old.NextSignedPreKeyId = store.NextSignedPreKeyId; + old.Password = store.Password; + old.PreKeyIdOffset = store.PreKeyIdOffset; + old.Registered = store.Registered; + old.RegistrationId = store.RegistrationId; + old.SignalingKey = store.SignalingKey; + old.Username = store.Username; + } + else + { + ctx.Store.Add(store); + } + ctx.SaveChanges(); + } + } + } + + public static void UpdatePreKeyIdOffset(uint preKeyIdOffset) + { + lock (DBLock) + { + using (var ctx = new LibsignalDBContext()) + { + var s = ctx.Store.Single(); + s.PreKeyIdOffset = preKeyIdOffset; + ctx.SaveChanges(); + } + } + } + + public static void UpdateNextSignedPreKeyId(uint nextSignedPreKeyId) + { + lock (DBLock) + { + using (var ctx = new LibsignalDBContext()) + { + var s = ctx.Store.Single(); + s.NextSignedPreKeyId = nextSignedPreKeyId; + ctx.SaveChanges(); + } + } + } + + public static IdentityKeyPair GetIdentityKeyPair() + { + lock (DBLock) + { + using (var ctx = new LibsignalDBContext()) + { + var ikp = ctx.Store + .AsNoTracking() + .Single().IdentityKeyPair; + return new IdentityKeyPair(Base64.Decode(ikp)); + } + } + } + + public static uint GetLocalRegistrationId() + { + lock (DBLock) + { + using (var ctx = new LibsignalDBContext()) + { + return ctx.Store + .AsNoTracking() + .Single().RegistrationId; + } + } + } + + #endregion Account + + #region Sessions + public static void ClearSessionCache() + { + lock (DBLock) + { + SessionsCache.Clear(); + } + } + + private static string GetSessionCacheIndex(string username, uint deviceid) + { + return username + @"\" + deviceid; + } + + private static Dictionary SessionsCache = new Dictionary(); + + public static SessionRecord LoadSessionLocked(SignalProtocolAddress address) + { + lock (DBLock) + { + string index = GetSessionCacheIndex(address.Name, address.DeviceId); + if (SessionsCache.TryGetValue(index, out SessionRecord record)) + { + return record; + } + using (var ctx = new LibsignalDBContext()) + { + var session = ctx.Sessions + .Where(s => s.Username == address.Name && s.DeviceId == address.DeviceId) + .AsNoTracking() + .SingleOrDefault(); + if (session != null) + { + record = new SessionRecord(Base64.Decode(session.Session)); + } + else + { + record = new SessionRecord(); + } + SessionsCache[index] = record; + return record; + } + } + } + + public static List GetSubDeviceSessions(string name) + { + lock (DBLock) + { + using (var ctx = new LibsignalDBContext()) + { + var sessions = ctx.Sessions + .Where(se => se.Username == name) + .AsNoTracking() + .ToList(); + var s = new List(); + foreach (var session in sessions) + { + if (session.DeviceId != SignalServiceAddress.DEFAULT_DEVICE_ID) + { + s.Add(session.DeviceId); + } + } + return s; + } + } + } + + public static void StoreSession(SignalProtocolAddress address, SessionRecord record) + { + lock (DBLock) + { + string index = GetSessionCacheIndex(address.Name, address.DeviceId); + using (var ctx = new LibsignalDBContext()) + { + var session = ctx.Sessions + .Where(s => s.DeviceId == address.DeviceId && s.Username == address.Name) + .SingleOrDefault(); + if (session != null) + { + session.Session = Base64.EncodeBytes(record.serialize()); + } + else + { + ctx.Sessions.Add(new SignalSession() + { + DeviceId = address.DeviceId, + Session = Base64.EncodeBytes(record.serialize()), + Username = address.Name + }); + } + SessionsCache[index] = record; + ctx.SaveChanges(); + } + } + } + + public static bool ContainsSession(SignalProtocolAddress address) + { + lock (DBLock) + { + using (var ctx = new LibsignalDBContext()) + { + var session = ctx.Sessions + .Where(s => s.Username == address.Name && s.DeviceId == address.DeviceId) + .SingleOrDefault(); + if (session == null) + return false; + + SessionRecord sessionRecord = new SessionRecord(Base64.Decode(session.Session)); + return sessionRecord.getSessionState().hasSenderChain() && + sessionRecord.getSessionState().getSessionVersion() == libsignal.protocol.CiphertextMessage.CURRENT_VERSION; + } + } + } + + public static void DeleteSession(SignalProtocolAddress address) + { + lock (DBLock) + { + string index = GetSessionCacheIndex(address.Name, address.DeviceId); + SessionsCache.Remove(index); + using (var ctx = new LibsignalDBContext()) + { + var sessions = ctx.Sessions + .Where(s => s.Username == address.Name && s.DeviceId == address.DeviceId); + ctx.Sessions.RemoveRange(sessions); + ctx.SaveChanges(); + } + } + } + + public static void DeleteAllSessions(string name) + { + lock (DBLock) + { + using (var ctx = new LibsignalDBContext()) + { + var sessions = ctx.Sessions + .Where(s => s.Username == name) + .ToList(); + foreach (var session in sessions) + { + SessionsCache.Remove(GetSessionCacheIndex(name, session.DeviceId)); + } + ctx.Sessions.RemoveRange(sessions); + ctx.SaveChanges(); + } + } + } + + #endregion Sessions + + #region PreKeys + + public static PreKeyRecord LoadPreKey(uint preKeyId) + { + lock (DBLock) + { + using (var ctx = new LibsignalDBContext()) + { + var pk = ctx.PreKeys + .Where(p => p.Id == preKeyId) + .AsNoTracking() + .Single(); + return new PreKeyRecord(Base64.Decode(pk.Key)); + } + } + } + + public static void StorePreKey(uint preKeyId, PreKeyRecord record) + { + lock (DBLock) + { + using (var ctx = new LibsignalDBContext()) + { + ctx.PreKeys.Add(new SignalPreKey() + { + Id = preKeyId, + Key = Base64.EncodeBytes(record.serialize()) + }); + ctx.SaveChanges(); + } + } + } + + public static bool ContainsPreKey(uint preKeyId) + { + lock (DBLock) + { + using (var ctx = new LibsignalDBContext()) + { + return ctx.PreKeys + .Where(p => p.Id == preKeyId) + .AsNoTracking() + .SingleOrDefault() != null; + } + } + } + + public static void RemovePreKey(uint preKeyId) + { + lock (DBLock) + { + using (var ctx = new LibsignalDBContext()) + { + var preKey = ctx.PreKeys + .AsNoTracking() + .Where(b => b.Id == preKeyId) + .SingleOrDefault(); + if (preKey != null) + { + ctx.PreKeys.Remove(preKey); + ctx.SaveChanges(); + } + } + } + } + + public static SignedPreKeyRecord LoadSignedPreKey(uint signedPreKeyId) + { + lock (DBLock) + { + using (var ctx = new LibsignalDBContext()) + { + var preKeys = ctx.SignedPreKeys + .AsNoTracking() + .Where(b => b.Id == signedPreKeyId) + .Single(); + return new SignedPreKeyRecord(Base64.Decode(preKeys.Key)); + } + } + } + + public static List LoadSignedPreKeys() + { + lock (DBLock) + { + using (var ctx = new LibsignalDBContext()) + { + var preKeys = ctx.SignedPreKeys + .AsNoTracking() + .ToList(); + var v = new List(); + foreach (var preKey in preKeys) + { + v.Add(new SignedPreKeyRecord(Base64.Decode(preKey.Key))); + } + return v; + } + } + } + + public static void StoreSignedPreKey(uint signedPreKeyId, SignedPreKeyRecord record) + { + lock (DBLock) + { + using (var ctx = new LibsignalDBContext()) + { + ctx.SignedPreKeys.Add(new SignalSignedPreKey() + { + Id = signedPreKeyId, + Key = Base64.EncodeBytes(record.serialize()) + }); + ctx.SaveChanges(); + } + } + } + + public static bool ContainsSignedPreKey(uint signedPreKeyId) + { + lock (DBLock) + { + using (var ctx = new LibsignalDBContext()) + { + var old = ctx.SignedPreKeys.Where(k => k.Id == signedPreKeyId).SingleOrDefault(); + if (old != null) + { + return true; + } + return false; + } + } + } + + public static void RemoveSignedPreKey(uint id) + { + lock (DBLock) + { + using (var ctx = new LibsignalDBContext()) + { + var old = ctx.SignedPreKeys.Where(k => k.Id == id).SingleOrDefault(); + if (old != null) + { + ctx.SignedPreKeys.Remove(old); + ctx.SaveChanges(); + } + } + } + } + + public static async Task RefreshPreKeysAsync(SignalServiceAccountManager accountManager, CancellationToken token) //TODO wrap in extra lock? enforce reload? + { + List oneTimePreKeys = GeneratePreKeys(); + SignedPreKeyRecord signedPreKeyRecord = GenerateSignedPreKey(GetIdentityKeyPair()); + await accountManager.SetPreKeysAsync(GetIdentityKeyPair().getPublicKey(), signedPreKeyRecord, oneTimePreKeys, token); + } + + private static List GeneratePreKeys() + { + List records = new List(); + for (uint i = 1; i < LibUtils.PREKEY_BATCH_SIZE; i++) + { + uint preKeyId = (SignalLibHandle.Instance.Store.PreKeyIdOffset + i) % Medium.MAX_VALUE; + ECKeyPair keyPair = Curve.generateKeyPair(); + PreKeyRecord record = new PreKeyRecord(preKeyId, keyPair); + + StorePreKey(preKeyId, record); + records.Add(record); + } + UpdatePreKeyIdOffset((SignalLibHandle.Instance.Store.PreKeyIdOffset + LibUtils.PREKEY_BATCH_SIZE + 1) % Medium.MAX_VALUE); + return records; + } + + private static PreKeyRecord GetOrGenerateLastResortPreKey() + { + if (ContainsPreKey(Medium.MAX_VALUE)) + { + try + { + return LoadPreKey(Medium.MAX_VALUE); + } + catch (InvalidKeyIdException) + { + RemovePreKey(Medium.MAX_VALUE); + } + } + ECKeyPair keyPair = Curve.generateKeyPair(); + PreKeyRecord record = new PreKeyRecord(Medium.MAX_VALUE, keyPair); + StorePreKey(Medium.MAX_VALUE, record); + return record; + } + + private static SignedPreKeyRecord GenerateSignedPreKey(IdentityKeyPair identityKeyPair) + { + try + { + ECKeyPair keyPair = Curve.generateKeyPair(); + byte[] signature = Curve.calculateSignature(identityKeyPair.getPrivateKey(), keyPair.getPublicKey().serialize()); + SignedPreKeyRecord record = new SignedPreKeyRecord(SignalLibHandle.Instance.Store.NextSignedPreKeyId, (ulong)DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond, keyPair, signature); + + StoreSignedPreKey(SignalLibHandle.Instance.Store.NextSignedPreKeyId, record); + UpdateNextSignedPreKeyId((SignalLibHandle.Instance.Store.NextSignedPreKeyId + 1) % Medium.MAX_VALUE); + return record; + } + catch (InvalidKeyException e) + { + throw e; + } + } + + #endregion PreKeys + } +} diff --git a/Signal-Windows.Lib/Storage/SignalDBContext.cs b/Signal-Windows.Lib/Storage/SignalDBContext.cs new file mode 100644 index 0000000..42d89a6 --- /dev/null +++ b/Signal-Windows.Lib/Storage/SignalDBContext.cs @@ -0,0 +1,820 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using libsignalservice; +using libsignalservice.messages; +using libsignalservice.push; +using libsignalservice.util; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Signal_Windows.Lib; +using Signal_Windows.Models; + +namespace Signal_Windows.Storage +{ + public sealed class SignalDBContext : DbContext + { + private static readonly ILogger Logger = LibsignalLogging.CreateLogger(); + private static readonly object DBLock = new object(); + public DbSet Contacts { get; set; } + public DbSet Messages { get; set; } + public DbSet Attachments { get; set; } + public DbSet Groups { get; set; } + public DbSet GroupMemberships { get; set; } + public DbSet Messages_fts { get; set; } + public DbSet EarlyReceipts { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseSqlite(@"Filename=..\LocalCache\Signal.db", x => x.SuppressForeignKeyEnforcement()); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasIndex(m => m.ThreadId); + + modelBuilder.Entity() + .HasIndex(m => m.AuthorId); + + modelBuilder.Entity() + .HasIndex(a => a.MessageId); + + modelBuilder.Entity() + .HasIndex(gm => gm.ContactId); + + modelBuilder.Entity() + .HasIndex(gm => gm.GroupId); + + modelBuilder.Entity() + .HasIndex(er => er.Username); + + modelBuilder.Entity() + .HasIndex(er => er.DeviceId); + + modelBuilder.Entity() + .HasIndex(er => er.Timestamp); + + modelBuilder.Entity() + .HasOne(sc => sc.LastMessage); + + modelBuilder.Entity() + .HasIndex(sc => sc.ThreadId); + } + + public static void Migrate() + { + lock (DBLock) + { + using (var ctx = new SignalDBContext()) + { + if (ctx.Database.GetPendingMigrations().Count() > 0) + { + ctx.Database.Migrate(); + } + /* + var serviceProvider = ctx.GetInfrastructure(); + var loggerFactory = serviceProvider.GetService(); + loggerFactory.AddProvider(new SqlLoggerProvider()); + */ + } + } + } + + #region Messages + + public static void FailAllPendingMessages() + { + lock (DBLock) + { + using (var ctx = new SignalDBContext()) + { + var messages = ctx.Messages + .Where(m => m.Direction == SignalMessageDirection.Outgoing && m.Status == SignalMessageStatus.Pending).ToList(); + messages.ForEach(m => m.Status = SignalMessageStatus.Failed_Unknown); + ctx.SaveChanges(); + } + } + } + + public static void SaveMessageLocked(SignalMessage message) + { + lock (DBLock) + { + using (var ctx = new SignalDBContext()) + { + SaveMessage(ctx, message); + ctx.SaveChanges(); + } + } + } + + private static SignalConversation SaveMessage(SignalDBContext ctx, SignalMessage message) + { + if (message.Direction == SignalMessageDirection.Synced) + { + var receipts = ctx.EarlyReceipts + .Where(er => er.Timestamp == message.ComposedTimestamp) + .ToList(); + + message.Receipts = (uint)receipts.Count; + ctx.EarlyReceipts.RemoveRange(receipts); + if (message.Receipts > 0) + { + message.Status = SignalMessageStatus.Received; + } + } + if (message.Author != null) + { + message.Author = GetSignalContactByThreadId(ctx, message.Author.ThreadId); + } + SignalConversation conversation = GetSignalConversationByThreadId(ctx, message.ThreadId); + conversation.LastActiveTimestamp = message.ComposedTimestamp; + conversation.LastMessage = message; + conversation.MessagesCount += 1; + if (message.Author == null) + { + conversation.UnreadCount = 0; + conversation.LastSeenMessageIndex = conversation.MessagesCount; + } + else + { + conversation.UnreadCount += 1; + } + ctx.Messages.Add(message); + return conversation; + } + + public static IEnumerable GetMessagesLocked(SignalConversation thread, int startIndex, int count) + { + Logger.LogTrace("GetMessagesLocked() skip {0} take {1}", startIndex, count); + var messages = new List(); + lock (DBLock) + { + using (var ctx = new SignalDBContext()) + { + messages = ctx.Messages + .Where(m => m.ThreadId == thread.ThreadId) + .Include(m => m.Content) + .Include(m => m.Author) + .Include(m => m.Attachments) + .OrderBy(m => m.Id) + .Skip(startIndex) + .AsNoTracking() + .Take(count) + .ToList(); + } + } + Logger.LogTrace($"GetMessagesLocked() returning {messages.Count} messages"); + return messages; + } + + public static SignalMessage UpdateMessageStatus(SignalMessage outgoingSignalMessage) + { + SignalMessage m; + lock (DBLock) + { + using (var ctx = new SignalDBContext()) + { + m = ctx.Messages.Single(t => t.ComposedTimestamp == outgoingSignalMessage.ComposedTimestamp && t.Author == null); + if (m != null) + { + if (outgoingSignalMessage.Status == SignalMessageStatus.Confirmed) + { + if (m.Receipts > 0) + { + m.Status = SignalMessageStatus.Received; + } + else + { + m.Status = SignalMessageStatus.Confirmed; + } + } + else + { + m.Status = outgoingSignalMessage.Status; + } + ctx.SaveChanges(); + } + return m; + } + } + } + + public static SignalMessage IncreaseReceiptCountLocked(SignalServiceEnvelope envelope) + { + SignalMessage m; + bool set_mark = false; + lock (DBLock) + { + using (var ctx = new SignalDBContext()) + { + m = ctx.Messages.SingleOrDefault(t => t.ComposedTimestamp == envelope.GetTimestamp()); + if (m != null) + { + m.Receipts++; + if (m.Status == SignalMessageStatus.Confirmed) + { + m.Status = SignalMessageStatus.Received; + set_mark = true; + } + } + else + { + ctx.EarlyReceipts.Add(new SignalEarlyReceipt() + { + DeviceId = (uint)envelope.GetSourceDevice(), + Timestamp = envelope.GetTimestamp(), + Username = envelope.GetSourceE164() + }); + } + ctx.SaveChanges(); + } + } + return set_mark ? m : null; + } + + public static void UpdateMessageExpiresAt(SignalMessage message) + { + lock (DBLock) + { + using (var ctx = new SignalDBContext()) + { + var m = ctx.Messages.Single(t => t.Id == message.Id); + m.ExpiresAt = message.ExpiresAt; + ctx.SaveChanges(); + } + } + } + + /// + /// Gets messages older than the given timestamp. + /// + /// Timestamp in millis + /// Expired messages + public static List GetExpiredMessages(long timestampMillis) + { + lock (DBLock) + { + using (var ctx = new SignalDBContext()) + { + var messages = ctx.Messages + .Where(m => m.ExpiresAt > 0) + .Where(m => m.ExpiresAt < timestampMillis) + .Include(m => m.Attachments) + .Include(m => m.Content) + .AsNoTracking() + .ToList(); + return messages; + } + } + } + + public static void DeleteMessage(SignalMessage message) + { + lock (DBLock) + { + using (var ctx = new SignalDBContext()) + { + ctx.Remove(message); + SignalConversation conversation = ctx.Contacts + .Where(c => c.ThreadId == message.ThreadId) + .Single(); + conversation.MessagesCount -= 1; + conversation.LastMessage = null; + conversation.LastMessageId = null; + conversation.LastSeenMessage = null; + conversation.LastSeenMessageIndex = ctx.Messages + .Where(m => m.ThreadId == conversation.ThreadId) + .Count() - 1; + + // also delete fts message + SignalMessageContent ftsMessage = ctx.Messages_fts.Where(m => m == message.Content) + .Single(); + ctx.Remove(ftsMessage); + ctx.SaveChanges(); + } + } + } + + #endregion Messages + + #region Attachments + + public static SignalAttachment GetAttachmentByGuidNameLocked(string guid) + { + lock (DBLock) + { + using (var ctx = new SignalDBContext()) + { + return ctx.Attachments + .Where(a => a.Guid == guid) + .FirstOrDefault(); + } + } + } + + public static void DeleteAttachment(SignalAttachment attachment) + { + lock (DBLock) + { + using (var ctx = new SignalDBContext()) + { + ctx.Remove(attachment); + ctx.SaveChanges(); + } + } + } + + internal static void UpdateAttachmentGuid(SignalAttachment attachment) + { + lock (DBLock) + { + using (var ctx = new SignalDBContext()) + { + var savedAttachment = ctx.Attachments + .Where(a => a.Id == attachment.Id) + .First(); + savedAttachment.Guid = attachment.Guid; + ctx.SaveChanges(); + } + } + } + + internal static void UpdateAttachmentStatus(SignalAttachment attachment) + { + lock (DBLock) + { + using (var ctx = new SignalDBContext()) + { + var savedAttachment = ctx.Attachments + .Where(a => a.Id == attachment.Id) + .First(); + savedAttachment.Status = attachment.Status; + ctx.SaveChanges(); + } + } + } + + #endregion Attachments + + #region Conversations + + private static SignalConversation GetSignalConversationByThreadId(SignalDBContext ctx, string id) + { + if (!id.EndsWith("=")) + { + return GetSignalContactByThreadId(ctx, id); + } + else + { + return GetSignalGroupByThreadId(ctx, id); + } + } + + internal static SignalContact GetSignalContactByThreadId(SignalDBContext ctx, string id) + { + return ctx.Contacts + .Where(c => c.ThreadId == id) + .SingleOrDefault(); + } + + private static SignalGroup GetSignalGroupByThreadId(SignalDBContext ctx, string id) + { + return ctx.Groups + .Where(c => c.ThreadId == id) + .SingleOrDefault(); + } + + public static void UpdateExpiresInLocked(SignalConversation thread) + { + lock (DBLock) + { + using (var ctx = new SignalDBContext()) + { + var dbConversation = GetSignalConversationByThreadId(ctx, thread.ThreadId); + if (dbConversation != null) + { + dbConversation.ExpiresInSeconds = thread.ExpiresInSeconds; + ctx.SaveChanges(); + } + } + } + } + + internal static SignalConversation UpdateMessageRead(long timestamp) + { + SignalConversation conversation; + lock (DBLock) + { + using (var ctx = new SignalDBContext()) + { + var message = ctx.Messages + .Where(m => m.ComposedTimestamp == timestamp) + .First(); //TODO care about early reads or messages with the same timestamp sometime + conversation = GetSignalConversationByThreadId(ctx, message.ThreadId); + var currentLastSeenMessage = ctx.Messages + .Where(m => m.ThreadId == conversation.ThreadId) + .Skip((int)conversation.LastSeenMessageIndex - 1) + .Take(1) + .Single(); + if (message.Id > currentLastSeenMessage.Id) + { + var diff = (uint)ctx.Messages + .Where(m => m.ThreadId == conversation.ThreadId && m.Id <= message.Id && m.Id > currentLastSeenMessage.Id) + .Count(); + conversation.LastSeenMessageIndex += diff; + if (diff > conversation.UnreadCount) + { + throw new InvalidOperationException($"UpdateMessageRead encountered an inconsistent state: {diff} > {conversation.UnreadCount}"); + } + conversation.UnreadCount -= diff; + ctx.SaveChanges(); + } + } + } + return conversation; + } + + internal static async Task> InsertOrUpdateGroups(IList<(SignalGroup group, List members)> groups) + { + List refreshedGroups = new List(); + List newContacts = new List(); + lock (DBLock) + { + using (var ctx = new SignalDBContext()) + { + foreach (var (group, members) in groups) + { + try + { + var dbGroup = ctx.Groups + .Where(g => g.ThreadId == group.ThreadId) + .Include(g => g.GroupMemberships) + .Include(g => g.LastMessage) + .ThenInclude(m => m.Content) + .SingleOrDefault(); + if (dbGroup != null) + { + dbGroup.GroupMemberships.Clear(); + dbGroup.ThreadDisplayName = group.ThreadDisplayName; + dbGroup.CanReceive = group.CanReceive; + dbGroup.ExpiresInSeconds = group.ExpiresInSeconds; + } + else + { + dbGroup = group; + ctx.Groups.Add(dbGroup); + } + refreshedGroups.Add(dbGroup); + foreach (var member in members) + { + (var contact, var createdNew) = GetOrCreateContact(ctx, member.E164, member.Uuid); + dbGroup.GroupMemberships.Add(new GroupMembership() + { + Contact = contact, + Group = dbGroup + }); + if (createdNew) + { + newContacts.Add(contact); + } + } + } + catch (Exception e) + { + Logger.LogError("InsertOrUpdateGroups failed: {0}\n{1}", e.Message, e.StackTrace); + } + } + ctx.SaveChanges(); + } + } + foreach (var c in newContacts) + { + await SignalLibHandle.Instance.DispatchAddOrUpdateConversation(c, null); + } + return refreshedGroups; + } + + internal static IList InsertOrUpdateContacts(IList contacts) + { + List refreshedContacts = new List(); + lock (DBLock) + { + using (var ctx = new SignalDBContext()) + { + foreach (var contact in contacts) + { + var dbContact = ctx.Contacts + .Where(c => c.ThreadId == contact.ThreadId) + .Include(c => c.LastMessage) + .ThenInclude(m => m.Content) + .SingleOrDefault(); + if (dbContact != null) + { + refreshedContacts.Add(dbContact); + dbContact.ThreadDisplayName = contact.ThreadDisplayName; + dbContact.Color = contact.Color; + dbContact.CanReceive = contact.CanReceive; + dbContact.ExpiresInSeconds = contact.ExpiresInSeconds; + } + else + { + refreshedContacts.Add(contact); + ctx.Contacts.Add(contact); + } + } + ctx.SaveChanges(); + } + } + return refreshedContacts; + } + + #endregion Threads + + #region Groups + + public static SignalConversation RemoveMemberFromGroup(string groupId, SignalContact member, SignalMessage quitMessage) + { + lock (DBLock) + { + using (var ctx = new SignalDBContext()) + { + var dbgroup = ctx.Groups + .Where(g => g.ThreadId == groupId) + .Include(g => g.GroupMemberships) + .ThenInclude(gm => gm.Contact) + .Single(); + dbgroup.GroupMemberships.RemoveAll(gm => gm.Contact.Id == member.Id); + if (member.ThreadId == SignalLibHandle.Instance.Store.Username) + { + dbgroup.CanReceive = false; + } + var conv = SaveMessage(ctx, quitMessage); + ctx.SaveChanges(); + return conv; + } + } + } + + public static async Task GetOrCreateGroupLocked(string groupId, long timestamp, bool notify = true) + { + SignalGroup dbgroup; + bool createdNew = false; + lock (DBLock) + { + using (var ctx = new SignalDBContext()) + { + dbgroup = ctx.Groups + .Where(g => g.ThreadId == groupId) + .Include(g => g.GroupMemberships) + .ThenInclude(gm => gm.Contact) + .SingleOrDefault(); + if (dbgroup == null) + { + dbgroup = new SignalGroup() + { + ThreadId = groupId, + ThreadDisplayName = "Unknown group", + LastActiveTimestamp = timestamp, + AvatarFile = null, + UnreadCount = 0, + CanReceive = false, + GroupMemberships = new List() + }; + ctx.Add(dbgroup); + ctx.SaveChanges(); + createdNew = true; + } + } + } + if (createdNew && notify) + { + await SignalLibHandle.Instance.DispatchAddOrUpdateConversation(dbgroup, null); + } + return dbgroup; + } + + public static SignalGroup InsertOrUpdateGroupLocked(string groupId, string displayname, string avatarfile, bool canReceive, long timestamp) + { + SignalGroup dbgroup; + lock (DBLock) + { + using (var ctx = new SignalDBContext()) + { + dbgroup = ctx.Groups + .Where(g => g.ThreadId == groupId) + .Include(g => g.GroupMemberships) + .ThenInclude(gm => gm.Contact) + .SingleOrDefault(); + if (dbgroup == null) + { + dbgroup = new SignalGroup() + { + ThreadId = groupId, + ThreadDisplayName = displayname, + LastActiveTimestamp = timestamp, + AvatarFile = avatarfile, + UnreadCount = 0, + CanReceive = canReceive, + ExpiresInSeconds = 0, + GroupMemberships = new List() + }; + ctx.Add(dbgroup); + } + else + { + dbgroup.ThreadDisplayName = displayname; + dbgroup.LastActiveTimestamp = timestamp; + dbgroup.AvatarFile = avatarfile; + dbgroup.CanReceive = true; + } + ctx.SaveChanges(); + } + } + return dbgroup; + } + + public static void InsertOrUpdateGroupMembershipLocked(long groupid, long memberid) + { + lock (DBLock) + { + using (var ctx = new SignalDBContext()) + { + var old = ctx.GroupMemberships.Where(g => g.GroupId == groupid && g.ContactId == memberid).SingleOrDefault(); + if (old == null) + { + ctx.GroupMemberships.Add(new GroupMembership() + { + ContactId = memberid, + GroupId = groupid + }); + ctx.SaveChanges(); + } + } + } + } + + public static List GetAllGroupsLocked() + { + lock (DBLock) + { + using (var ctx = new SignalDBContext()) + { + return ctx.Groups + .OrderByDescending(g => g.LastActiveTimestamp) + .Include(g => g.GroupMemberships) + .ThenInclude(gm => gm.Contact) + .Include(g => g.LastMessage) + .ThenInclude(m => m.Content) + .AsNoTracking() + .ToList(); + } + } + } + + #endregion Groups + + #region Contacts + + public static List GetAllContactsLocked() + { + lock (DBLock) + { + using (var ctx = new SignalDBContext()) + { + return ctx.Contacts + .OrderByDescending(c => c.LastActiveTimestamp) + .Include(g => g.LastMessage) + .ThenInclude(m => m.Content) + .AsNoTracking() + .ToList(); + } + } + } + + /// + /// Gets a from the database using the specified E.164 or creates a new one if no + /// such contact exists already. + /// + /// The E.164 of the contact + /// The Signal UUID of the contact + /// The + public static async Task GetOrCreateContactLocked(string e164, Guid? guid) + { + SignalContact contact; + bool createdNew; + lock (DBLock) + { + using (var ctx = new SignalDBContext()) + { + (contact, createdNew) = GetOrCreateContact(ctx, e164, guid); + } + } + if (createdNew) + { + await SignalLibHandle.Instance.DispatchAddOrUpdateConversation(contact, null); + } + return contact; + } + + /// + /// Get a from the database using the specified E.164 or creates a new one if no + /// such contact exists already. + /// + /// The current DB context + /// The E.164 of the contact + /// The Signal UUID of the contact + /// A tuple of the and if the contact was newly created. + private static (SignalContact contact, bool createdNew) GetOrCreateContact(SignalDBContext ctx, string e164, Guid? guid) + { + bool createdNew = false; + SignalContact contact = GetSignalContactByThreadId(ctx, e164); + if (contact == null) + { + contact = new SignalContact() + { + ThreadId = e164, + ThreadGuid = guid, + ThreadDisplayName = e164, + CanReceive = true, + LastActiveTimestamp = Util.CurrentTimeMillis(), + Color = null + }; + ctx.Contacts.Add(contact); + ctx.SaveChanges(); + createdNew = true; + } + + // If we retrieve an existing contact and the Guid isn't set on it, set it now. + if (guid.HasValue && !contact.ThreadGuid.HasValue) + { + contact.ThreadGuid = guid; + ctx.SaveChanges(); + createdNew = true; + } + return (contact, createdNew); + } + + public static void InsertOrUpdateConversationLocked(SignalConversation conversation) + { + lock (DBLock) + { + using (var ctx = new SignalDBContext()) + { + InsertOrUpdateConversation(ctx, conversation); + ctx.SaveChanges(); + } + } + } + + private static void InsertOrUpdateConversation(SignalDBContext ctx, SignalConversation conversation) + { + var dbConversation = GetSignalConversationByThreadId(ctx, conversation.ThreadId); + if (dbConversation == null) + { + if (conversation is SignalContact dbContact) + { + ctx.Contacts.Add(dbContact); + } + else if (conversation is SignalGroup dbGroup) + { + ctx.Groups.Add(dbGroup); + } + } + else + { + dbConversation.ThreadId = conversation.ThreadId; + dbConversation.ThreadDisplayName = conversation.ThreadDisplayName; + dbConversation.CanReceive = conversation.CanReceive; + dbConversation.AvatarFile = conversation.AvatarFile; + dbConversation.Draft = conversation.Draft; + dbConversation.DraftFileTokens = conversation.DraftFileTokens; + dbConversation.UnreadCount = conversation.UnreadCount; + if (dbConversation is SignalContact dbContact && conversation is SignalContact contract) + { + dbContact.Color = contract.Color; + } + } + } + + public static void UpdateBlockStatus(SignalContact contact) + { + lock (DBLock) + { + using (var ctx = new SignalDBContext()) + { + var c = GetSignalContactByThreadId(ctx, contact.ThreadId); + if (c == null) + { + throw new Exception("UpdateBlockStatus() failed: Could not find contact!"); + } + c.Blocked = contact.Blocked; + ctx.SaveChanges(); + } + } + } + #endregion Contacts + } +} diff --git a/Signal-Windows/Storage/Store.cs b/Signal-Windows.Lib/Storage/Store.cs similarity index 84% rename from Signal-Windows/Storage/Store.cs rename to Signal-Windows.Lib/Storage/Store.cs index f78b108..98ae65b 100644 --- a/Signal-Windows/Storage/Store.cs +++ b/Signal-Windows.Lib/Storage/Store.cs @@ -19,7 +19,7 @@ public uint GetLocalRegistrationId() public bool SaveIdentity(SignalProtocolAddress address, IdentityKey identityKey) { - LibsignalDBContext.SaveIdentityLocked(address, Base64.encodeBytes(identityKey.serialize())); + LibsignalDBContext.SaveIdentityLocked(address, Base64.EncodeBytes(identityKey.serialize())).Wait(); //TODO wait is bad return true; } @@ -37,8 +37,9 @@ public bool IsTrustedIdentity(SignalProtocolAddress address, IdentityKey identit else { //TODO compare timestamps & firstUse, see Signal-Android impl - string identity = Base64.encodeBytes(identityKey.serialize()); - return savedIdentity == Base64.encodeBytes(identityKey.serialize()); + string identity = Base64.EncodeBytes(identityKey.serialize()); + bool isTrusted = savedIdentity == Base64.EncodeBytes(identityKey.serialize()); + return isTrusted; } } @@ -64,7 +65,7 @@ public void RemovePreKey(uint preKeyId) public SessionRecord LoadSession(SignalProtocolAddress address) { - return LibsignalDBContext.LoadSession(address); + return LibsignalDBContext.LoadSessionLocked(address); } public List GetSubDeviceSessions(string name) @@ -116,5 +117,10 @@ public void RemoveSignedPreKey(uint signedPreKeyId) { LibsignalDBContext.RemoveSignedPreKey(signedPreKeyId); } + + public IdentityKey GetIdentity(SignalProtocolAddress address) + { + return LibsignalDBContext.GetIdentityKey(address); + } } } \ No newline at end of file diff --git a/Signal-Windows.Lib/Util/LibUtils.cs b/Signal-Windows.Lib/Util/LibUtils.cs new file mode 100644 index 0000000..8b4646f --- /dev/null +++ b/Signal-Windows.Lib/Util/LibUtils.cs @@ -0,0 +1,166 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Threading; +using libsignal.ecc; +using libsignalmetadatadotnet.certificate; +using libsignalservice; +using libsignalservice.configuration; +using libsignalservice.util; +using Microsoft.Extensions.Logging; +using Signal_Windows.Lib.Settings; +using Windows.ApplicationModel; +using Windows.Storage; + +namespace Signal_Windows.Lib +{ + public class LibUtils + { + private static readonly ILogger Logger = LibsignalLogging.CreateLogger(); + public const string GlobalMutexName = "SignalWindowsPrivateMessenger_Mutex"; + public const string GlobalEventWaitHandleName = "SignalWindowsPrivateMessenger_EventWaitHandle"; + public static string UNIDENTIFIED_SENDER_TRUST_ROOT = "BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF"; + public static SignalServiceUrl[] ServiceUrls; + public static SignalCdnUrl[] Cdn1Urls; + public static SignalCdnUrl[] Cdn2Urls; + public static SignalContactDiscoveryUrl[] ContactDiscoveryUrls; + public static SignalServiceConfiguration ServiceConfiguration; + public static bool MainPageActive = false; + public static string USER_AGENT = "Signal-Windows"; + public static uint PREKEY_BATCH_SIZE = 100; + public static bool WindowActive = false; + public static Mutex GlobalLock; + public static HttpClient HttpClient; + public static AppConfig AppConfig; + public static SignalSettings SignalSettings; + + static LibUtils() + { + HttpClient = new HttpClient(); + AppConfig = new AppConfig(); + SignalSettings = AppConfig.GetSignalSettings(); + ServiceUrls = new SignalServiceUrl[] { new SignalServiceUrl(SignalSettings.ServiceUrl) }; + Cdn1Urls = new SignalCdnUrl[] { new SignalCdnUrl(SignalSettings.Cdn1Urls[0]) }; + Cdn2Urls = new SignalCdnUrl[] { new SignalCdnUrl(SignalSettings.Cdn2Urls[0]) }; + ContactDiscoveryUrls = new SignalContactDiscoveryUrl[] { new SignalContactDiscoveryUrl(SignalSettings.ContactDiscoveryServiceUrl) }; + ServiceConfiguration = new SignalServiceConfiguration(ServiceUrls, Cdn1Urls, Cdn2Urls, ContactDiscoveryUrls); + } + + private static SynchronizationContext GlobalLockContext; + + internal static void Lock() + { + Logger.LogTrace("System lock locking, sync context = {0}", SynchronizationContext.Current); + GlobalLock = new Mutex(false, GlobalMutexName, out bool createdNew); + GlobalLockContext = SynchronizationContext.Current; + try + { + GlobalLock.WaitOne(); + } + catch (AbandonedMutexException e) + { + Logger.LogWarning("System lock was abandoned! {0}", e.Message); + } + Logger.LogTrace("System lock locked"); + } + + public static bool Lock(int timeout) + { + GlobalLock = new Mutex(false, GlobalMutexName, out bool createdNew); + GlobalLockContext = SynchronizationContext.Current; + Logger.LogTrace("System lock locking with timeout, sync context = {0}", SynchronizationContext.Current); + bool success = false; + try + { + success = GlobalLock.WaitOne(timeout); + } + catch (AbandonedMutexException e) + { + Logger.LogWarning("System lock was abandoned! {0}", e.Message); + success = true; + } + Logger.LogTrace("System lock locked = {}", success); + return success; + } + + public static void Unlock() + { + Logger.LogTrace("System lock releasing, sync context = {0}", SynchronizationContext.Current); + try + { + if (GlobalLockContext != null) + { + GlobalLockContext.Post((a) => + { + GlobalLock.ReleaseMutex(); + }, null); + } + else + { + GlobalLock.ReleaseMutex(); + } + } + catch (Exception e) + { + Logger.LogWarning("System lock failed to unlock! {0}\n{1}", e.Message, e.StackTrace); + } + Logger.LogTrace("System lock released"); + } + + public static EventWaitHandle OpenResetEventSet() + { + Logger.LogTrace("OpenResetEventSet()"); + var handle = new EventWaitHandle(true, EventResetMode.ManualReset, GlobalEventWaitHandleName, out bool createdNew); + if (!createdNew) + { + Logger.LogTrace("OpenResetEventSet() setting old event"); + handle.Set(); + } + return handle; + } + + public static EventWaitHandle OpenResetEventUnset() + { + Logger.LogTrace("OpenResetEventUnset()"); + return new EventWaitHandle(false, EventResetMode.ManualReset, GlobalEventWaitHandleName, out bool createdNew); + } + + public static FileStream CreateTmpFile(string name) + { + return File.Open(ApplicationData.Current.LocalCacheFolder.Path + Path.AltDirectorySeparatorChar + name, FileMode.Create, FileAccess.ReadWrite); + } + + public static string GetAppStartMessage() + { + var version = Package.Current.Id.Version; + return + "-------------------------------------------------\n" + + String.Format(" Signal-Windows {0}.{1}.{2}.{3} starting\n", version.Major, version.Minor, version.Build, version.Revision) + + "-------------------------------------------------\n"; + } + + public static string GetBGStartMessage() + { + var version = Package.Current.Id.Version; + return + "-------------------------------------------------\n" + + String.Format(" Signal-Windows BG {0}.{1}.{2}.{3} starting\n", version.Major, version.Minor, version.Build, version.Revision) + + "-------------------------------------------------\n"; + } + + public static CertificateValidator GetCertificateValidator() + { + ECPublicKey unidentifiedSenderTrustRoot = Curve.decodePoint(Base64.Decode(UNIDENTIFIED_SENDER_TRUST_ROOT), 0); + return new CertificateValidator(unidentifiedSenderTrustRoot); + } + } + + public static class StringExt + { + public static string Truncate(this string value, int maxLength) // thanks to https://stackoverflow.com/a/2776689/1569755 + { + if (string.IsNullOrEmpty(value)) return value; + return value.Length <= maxLength ? value : value.Substring(0, maxLength); + } + } +} diff --git a/Signal-Windows.Lib/Util/NotificationsUtils.cs b/Signal-Windows.Lib/Util/NotificationsUtils.cs new file mode 100644 index 0000000..63c53a1 --- /dev/null +++ b/Signal-Windows.Lib/Util/NotificationsUtils.cs @@ -0,0 +1,156 @@ +using Microsoft.Toolkit.Uwp.Notifications; +using Signal_Windows.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Windows.Foundation.Metadata; +using Windows.UI.Notifications; + +namespace Signal_Windows.Lib +{ + public class NotificationsUtils + { + public static ToastNotifier Notifier = ToastNotificationManager.CreateToastNotifier(); + public static void Notify(SignalMessage message) + { + TryVibrate(true); + SendMessageNotification(message); + SendTileNotification(message); + } + + public static void TryVibrate(bool quick) + { + if (ApiInformation.IsTypePresent("Windows.Phone.Devices.Notification.VibrationDevice")) + { + Windows.Phone.Devices.Notification.VibrationDevice.GetDefault().Vibrate(TimeSpan.FromMilliseconds(quick ? 100 : 500)); + } + } + + public static void SendMessageNotification(SignalMessage message) + { + // notification tags can only be 16 chars (64 after creators update) + // https://docs.microsoft.com/en-us/uwp/api/Windows.UI.Notifications.ToastNotification#Windows_UI_Notifications_ToastNotification_Tag + string notificationId = message.ThreadId; + ToastBindingGeneric toastBinding = new ToastBindingGeneric() + { + AppLogoOverride = new ToastGenericAppLogo() + { + Source = "ms-appx:///Assets/LargeTile.scale-100.png", + HintCrop = ToastGenericAppLogoCrop.Circle + } + }; + + var notificationText = GetNotificationText(message); + foreach (var item in notificationText) + { + toastBinding.Children.Add(item); + } + + ToastContent toastContent = new ToastContent() + { + Launch = notificationId, + Visual = new ToastVisual() + { + BindingGeneric = toastBinding + }, + DisplayTimestamp = DateTimeOffset.FromUnixTimeMilliseconds(message.ReceivedTimestamp) + }; + + ToastNotification toastNotification = new ToastNotification(toastContent.GetXml()); + if (message.Author.ExpiresInSeconds > 0) + { + toastNotification.ExpirationTime = DateTime.Now.Add(TimeSpan.FromSeconds(message.Author.ExpiresInSeconds)); + } + toastNotification.Tag = notificationId; + Notifier.Show(toastNotification); + } + + private static IList GetNotificationText(SignalMessage message) + { + List text = new List(); + if (GlobalSettingsManager.ShowNotificationTextSetting == GlobalSettingsManager.ShowNotificationTextSettings.NameAndMessage) + { + text.Add(CreateToastTitle(message.Author.ThreadDisplayName)); + text.Add(CreateToastBody(message.Content.Content)); + } + else if (GlobalSettingsManager.ShowNotificationTextSetting == GlobalSettingsManager.ShowNotificationTextSettings.NameOnly) + { + text.Add(CreateToastTitle(message.Author.ThreadDisplayName)); + } + else if (GlobalSettingsManager.ShowNotificationTextSetting == GlobalSettingsManager.ShowNotificationTextSettings.NoNameOrMessage) + { + text.Add(CreateToastTitle("New message")); + } + else + { + text.Add(CreateToastTitle("New message")); + } + return text; + } + + private static AdaptiveText CreateToastTitle(string text) + { + return new AdaptiveText() + { + Text = text, + HintMaxLines = 1 + }; + } + + private static AdaptiveText CreateToastBody(string text) + { + return new AdaptiveText() + { + Text = text, + HintWrap = true + }; + } + + public static void SendTileNotification(SignalMessage message) + { + TileBindingContentAdaptive tileBindingContent = new TileBindingContentAdaptive() + { + /* + PeekImage = new TilePeekImage() + { + Source = "ms-appx:///Assets/gambino.png" + } + */ + }; + var notificationText = GetNotificationText(message); + foreach (var item in notificationText) + { + tileBindingContent.Children.Add(item); + } + + TileBinding tileBinding = new TileBinding() + { + Content = tileBindingContent + }; + + TileContent tileContent = new TileContent() + { + Visual = new TileVisual() + { + TileMedium = tileBinding, + TileWide = tileBinding, + TileLarge = tileBinding + } + }; + + TileNotification tileNotification = new TileNotification(tileContent.GetXml()); + if (message.Author.ExpiresInSeconds > 0) + { + tileNotification.ExpirationTime = DateTime.Now.Add(TimeSpan.FromSeconds(message.Author.ExpiresInSeconds)); + } + TileUpdateManager.CreateTileUpdaterForApplication().Update(tileNotification); + } + + public static void Withdraw(string threadId) + { + ToastNotificationManager.History.Remove(threadId); + } + } +} diff --git a/Signal-Windows.RC/Properties/AssemblyInfo.cs b/Signal-Windows.RC/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..4c320a0 --- /dev/null +++ b/Signal-Windows.RC/Properties/AssemblyInfo.cs @@ -0,0 +1,29 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Signal-Windows.RC")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Signal-Windows.RC")] +[assembly: AssemblyCopyright("Copyright © 2018")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: ComVisible(false)] \ No newline at end of file diff --git a/Signal-Windows.RC/Signal-Windows.RC.csproj b/Signal-Windows.RC/Signal-Windows.RC.csproj new file mode 100644 index 0000000..9eafd68 --- /dev/null +++ b/Signal-Windows.RC/Signal-Windows.RC.csproj @@ -0,0 +1,138 @@ + + + + + Debug + AnyCPU + {F8AF07FF-395F-4BF7-84F7-D16EDE7B00DF} + winmdobj + Properties + Signal_Windows.RC + Signal-Windows.RC + en-US + UAP + 10.0.16299.0 + 10.0.15063.0 + 14 + 512 + {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + false + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + prompt + 4 + + + x86 + true + bin\x86\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + x86 + false + prompt + + + x86 + bin\x86\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + x86 + false + prompt + + + ARM + true + bin\ARM\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + ARM + false + prompt + + + ARM + bin\ARM\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + ARM + false + prompt + + + x64 + true + bin\x64\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + x64 + false + prompt + + + x64 + bin\x64\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + x64 + false + prompt + + + PackageReference + + + + + + + + 6.2.12 + + + 6.1.1 + + + + + {1934fd82-a5ea-4b71-b915-a1826593cb6e} + Signal-Windows.Lib + + + + 14.0 + + + + \ No newline at end of file diff --git a/Signal-Windows.RC/SignalBackgroundTask.cs b/Signal-Windows.RC/SignalBackgroundTask.cs new file mode 100644 index 0000000..6a191aa --- /dev/null +++ b/Signal-Windows.RC/SignalBackgroundTask.cs @@ -0,0 +1,110 @@ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using libsignalservice; +using Microsoft.Extensions.Logging; +using Microsoft.Toolkit.Uwp.Notifications; +using Signal_Windows.Lib; +using Signal_Windows.Lib.Events; +using Signal_Windows.Models; +using Signal_Windows.Storage; +using Windows.ApplicationModel.Background; +using Windows.UI.Notifications; + +namespace Signal_Windows.RC +{ + public sealed class SignalBackgroundTask : IBackgroundTask + { + private const string TaskName = "SignalMessageBackgroundTask"; + private readonly ILogger Logger = LibsignalLogging.CreateLogger(); + private BackgroundTaskDeferral Deferral; + private ISignalLibHandle Handle; + private ToastNotifier ToastNotifier; + private AutoResetEvent ResetEvent = new AutoResetEvent(false); + private EventWaitHandle GlobalResetEvent; + + public void Run(IBackgroundTaskInstance taskInstance) + { + SignalFileLoggerProvider.ForceAddBGLog(LibUtils.GetBGStartMessage()); + SignalLogging.SetupLogging(false); + Deferral = taskInstance.GetDeferral(); + ToastNotifier = ToastNotificationManager.CreateToastNotifier(); + taskInstance.Canceled += OnCanceled; + bool locked = LibUtils.Lock(5000); + Logger.LogTrace("Locking global finished, locked = {0}", locked); + if (!locked) + { + Logger.LogWarning("App is running, background task shutting down"); + Deferral.Complete(); + return; + } + GlobalResetEvent = LibUtils.OpenResetEventUnset(); + Task.Run(() => + { + GlobalResetEvent.WaitOne(); + Logger.LogInformation("Background task received app startup signal"); + ResetEvent.Set(); + }); + try + { + Handle = SignalHelper.CreateSignalLibHandle(true); + Handle.SignalMessageEvent += Handle_SignalMessageEvent; + Handle.BackgroundAcquire(); + ResetEvent.WaitOne(); + } + catch (Exception e) + { + Logger.LogError("Background task failed: {0}\n{1}", e.Message, e.StackTrace); + } + finally + { + Logger.LogInformation("Background task shutting down"); + Handle.BackgroundRelease(); + LibUtils.Unlock(); + Deferral.Complete(); + } + } + + private void OnCanceled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason) + { + Logger.LogInformation("Background task received cancel request"); + ResetEvent.Set(); + } + + private void Handle_SignalMessageEvent(object sender, SignalMessageEventArgs e) + { + if (e.MessageType == Lib.Events.SignalPipeMessageType.NormalMessage) + { + NotificationsUtils.Notify(e.Message); + } + else if (e.MessageType == Lib.Events.SignalPipeMessageType.PipeEmptyMessage) + { + Logger.LogInformation("Background task has drained the pipe"); + ResetEvent.Set(); + } + } + + private IList GetNotificationText(string authorName, string content) + { + List text = new List(); + AdaptiveText title = new AdaptiveText() + { + Text = authorName, + HintMaxLines = 1 + }; + AdaptiveText messageText = new AdaptiveText() + { + Text = content, + HintWrap = true + }; + text.Add(title); + text.Add(messageText); + return text; + } + } +} diff --git a/Signal-Windows.sln b/Signal-Windows.sln index 7cd9252..1d44755 100644 --- a/Signal-Windows.sln +++ b/Signal-Windows.sln @@ -1,8 +1,15 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27004.2006 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30907.101 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Signal-Windows", "Signal-Windows\Signal-Windows.csproj", "{41736A64-5B66-44AF-879A-501192A46920}" + ProjectSection(ProjectDependencies) = postProject + {1934FD82-A5EA-4B71-B915-A1826593CB6E} = {1934FD82-A5EA-4B71-B915-A1826593CB6E} + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Signal-Windows.Lib", "Signal-Windows.Lib\Signal-Windows.Lib.csproj", "{1934FD82-A5EA-4B71-B915-A1826593CB6E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Signal-Windows.RC", "Signal-Windows.RC\Signal-Windows.RC.csproj", "{F8AF07FF-395F-4BF7-84F7-D16EDE7B00DF}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -38,6 +45,38 @@ Global {41736A64-5B66-44AF-879A-501192A46920}.Release|x86.ActiveCfg = Release|x86 {41736A64-5B66-44AF-879A-501192A46920}.Release|x86.Build.0 = Release|x86 {41736A64-5B66-44AF-879A-501192A46920}.Release|x86.Deploy.0 = Release|x86 + {1934FD82-A5EA-4B71-B915-A1826593CB6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1934FD82-A5EA-4B71-B915-A1826593CB6E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1934FD82-A5EA-4B71-B915-A1826593CB6E}.Debug|ARM.ActiveCfg = Debug|ARM + {1934FD82-A5EA-4B71-B915-A1826593CB6E}.Debug|ARM.Build.0 = Debug|ARM + {1934FD82-A5EA-4B71-B915-A1826593CB6E}.Debug|x64.ActiveCfg = Debug|x64 + {1934FD82-A5EA-4B71-B915-A1826593CB6E}.Debug|x64.Build.0 = Debug|x64 + {1934FD82-A5EA-4B71-B915-A1826593CB6E}.Debug|x86.ActiveCfg = Debug|x86 + {1934FD82-A5EA-4B71-B915-A1826593CB6E}.Debug|x86.Build.0 = Debug|x86 + {1934FD82-A5EA-4B71-B915-A1826593CB6E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1934FD82-A5EA-4B71-B915-A1826593CB6E}.Release|Any CPU.Build.0 = Release|Any CPU + {1934FD82-A5EA-4B71-B915-A1826593CB6E}.Release|ARM.ActiveCfg = Release|ARM + {1934FD82-A5EA-4B71-B915-A1826593CB6E}.Release|ARM.Build.0 = Release|ARM + {1934FD82-A5EA-4B71-B915-A1826593CB6E}.Release|x64.ActiveCfg = Release|x64 + {1934FD82-A5EA-4B71-B915-A1826593CB6E}.Release|x64.Build.0 = Release|x64 + {1934FD82-A5EA-4B71-B915-A1826593CB6E}.Release|x86.ActiveCfg = Release|x86 + {1934FD82-A5EA-4B71-B915-A1826593CB6E}.Release|x86.Build.0 = Release|x86 + {F8AF07FF-395F-4BF7-84F7-D16EDE7B00DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F8AF07FF-395F-4BF7-84F7-D16EDE7B00DF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F8AF07FF-395F-4BF7-84F7-D16EDE7B00DF}.Debug|ARM.ActiveCfg = Debug|ARM + {F8AF07FF-395F-4BF7-84F7-D16EDE7B00DF}.Debug|ARM.Build.0 = Debug|ARM + {F8AF07FF-395F-4BF7-84F7-D16EDE7B00DF}.Debug|x64.ActiveCfg = Debug|x64 + {F8AF07FF-395F-4BF7-84F7-D16EDE7B00DF}.Debug|x64.Build.0 = Debug|x64 + {F8AF07FF-395F-4BF7-84F7-D16EDE7B00DF}.Debug|x86.ActiveCfg = Debug|x86 + {F8AF07FF-395F-4BF7-84F7-D16EDE7B00DF}.Debug|x86.Build.0 = Debug|x86 + {F8AF07FF-395F-4BF7-84F7-D16EDE7B00DF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F8AF07FF-395F-4BF7-84F7-D16EDE7B00DF}.Release|Any CPU.Build.0 = Release|Any CPU + {F8AF07FF-395F-4BF7-84F7-D16EDE7B00DF}.Release|ARM.ActiveCfg = Release|ARM + {F8AF07FF-395F-4BF7-84F7-D16EDE7B00DF}.Release|ARM.Build.0 = Release|ARM + {F8AF07FF-395F-4BF7-84F7-D16EDE7B00DF}.Release|x64.ActiveCfg = Release|x64 + {F8AF07FF-395F-4BF7-84F7-D16EDE7B00DF}.Release|x64.Build.0 = Release|x64 + {F8AF07FF-395F-4BF7-84F7-D16EDE7B00DF}.Release|x86.ActiveCfg = Release|x86 + {F8AF07FF-395F-4BF7-84F7-D16EDE7B00DF}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Signal-Windows/App.xaml b/Signal-Windows/App.xaml index 878e080..78b948a 100644 --- a/Signal-Windows/App.xaml +++ b/Signal-Windows/App.xaml @@ -6,6 +6,6 @@ xmlns:vm="using:Signal_Windows.ViewModels" RequestedTheme="Light"> - + \ No newline at end of file diff --git a/Signal-Windows/App.xaml.cs b/Signal-Windows/App.xaml.cs index 25eb61d..9de3983 100644 --- a/Signal-Windows/App.xaml.cs +++ b/Signal-Windows/App.xaml.cs @@ -13,10 +13,18 @@ using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Navigation; -using Microsoft.QueryStringDotNET; using Windows.UI.Notifications; using Microsoft.Extensions.Logging; -using Windows.Foundation.Diagnostics; +using Signal_Windows.Lib; +using Windows.ApplicationModel.Core; +using Windows.UI.Core; +using Windows.UI.ViewManagement; +using System.Collections.Generic; +using System.Collections.Concurrent; +using Windows.ApplicationModel.Background; +using Windows.Networking.BackgroundTransfer; +using Windows.UI; +using libsignalservice.configuration; namespace Signal_Windows { @@ -25,17 +33,33 @@ namespace Signal_Windows /// sealed partial class App : Application { - private readonly ILogger Logger = LibsignalLogging.CreateLogger(); - public static string URL = "https://textsecure-service.whispersystems.org"; - public static SignalServiceUrl[] ServiceUrls = new SignalServiceUrl[] { new SignalServiceUrl(URL, null) }; + private static App Instance; + private static ILogger Logger = LibsignalLogging.CreateLogger(); public static StorageFolder LocalCacheFolder = ApplicationData.Current.LocalCacheFolder; - public static ViewModelLocator ViewModels = (ViewModelLocator)Current.Resources["Locator"]; - public static SignalStore Store; public static bool MainPageActive = false; - public static string USER_AGENT = "Signal-Windows"; public static uint PREKEY_BATCH_SIZE = 100; - public static bool WindowActive = false; - private Task Init; + public static ISignalLibHandle Handle = SignalHelper.CreateSignalLibHandle(false); + private Dictionary _Views = new Dictionary(); + public static int MainViewId; + private IBackgroundTaskRegistration backgroundTaskRegistration; + + private Dictionary Views + { + get + { + lock (_Views) + { + return _Views; + } + } + } + + static App() + { + // TODO enforce these have begun before initializing and ensure the logger is working + Task.Run(() => { SignalDBContext.Migrate(); }); + Task.Run(() => { LibsignalDBContext.Migrate(); }); + } /// /// Initialisiert das Singletonanwendungsobjekt. Dies ist die erste Zeile von erstelltem Code @@ -43,185 +67,350 @@ sealed partial class App : Application /// public App() { + SignalFileLoggerProvider.ForceAddUILog(LibUtils.GetAppStartMessage()); + Instance = this; SignalLogging.SetupLogging(true); this.InitializeComponent(); - this.Suspending += OnSuspending; - this.Resuming += OnResuming; this.UnhandledException += OnUnhandledException; - try - { - Init = Task.Run(() => - { - SignalDBContext.Migrate(); - LibsignalDBContext.Migrate(); - return LibsignalDBContext.GetSignalStore(); - }); - } - catch (Exception e) - { - var line = new StackTrace(e, true).GetFrames()[0].GetFileLineNumber(); - Logger.LogError("App() failed in line {0}: {1}\n{2}", line, e.Message, e.StackTrace); - } + this.Suspending += App_Suspending; + this.Resuming += App_Resuming; } - private void OnUnhandledException(object sender, UnhandledExceptionEventArgs ex) + public static SignalWindowsFrontend CurrentSignalWindowsFrontend(int id) { - Exception e = ex.Exception; - var frame = new StackTrace(e, true).GetFrames()[0]; - Logger.LogError("UnhandledException occured in {0}/{1}: {2}\n{3}", frame.GetFileName(), frame.GetFileLineNumber(), e.Message, e.StackTrace); + return Instance.Views[id]; } - /// - /// Wird aufgerufen, wenn die Anwendung durch den Endbenutzer normal gestartet wird. Weitere Einstiegspunkte - /// werden z. B. verwendet, wenn die Anwendung gestartet wird, um eine bestimmte Datei zu öffnen. - /// - /// Details über Startanforderung und -prozess. - protected override async void OnLaunched(LaunchActivatedEventArgs e) + private async void App_Resuming(object sender, object e) { - await OnLaunchedOrActivated(e); + Logger.LogInformation("Resuming"); + DisappearingMessagesManager.DeleteExpiredMessages(); + await Handle.Reacquire(); } - protected override async void OnActivated(IActivatedEventArgs args) + private async void App_Suspending(object sender, SuspendingEventArgs e) { - await OnLaunchedOrActivated(args, false); + Logger.LogInformation("Suspending"); + var def = e.SuspendingOperation.GetDeferral(); + await Task.Run(() => Handle.Release()); + def.Complete(); + Logger.LogDebug("Suspended"); } - /// - /// - /// - /// - /// - /// If OnLaunched this is true - /// If OnActivated this is false - /// - /// - private async Task OnLaunchedOrActivated(IActivatedEventArgs e, bool launched = true) + private void OnUnhandledException(object sender, UnhandledExceptionEventArgs ex) { - Logger.LogDebug(LocalCacheFolder.Path); - Window.Current.Activated += Current_Activated; - WindowActive = true; - Frame rootFrame = Window.Current.Content as Frame; + Exception e = ex.Exception; + Logger.LogError("UnhandledException {0} occurred ({1}):\n{2}", e.GetType(), e.Message, e.StackTrace); + } - // App-Initialisierung nicht wiederholen, wenn das Fenster bereits Inhalte enthält. - // Nur sicherstellen, dass das Fenster aktiv ist. - if (rootFrame == null) + protected override async void OnActivated(IActivatedEventArgs args) + { + Logger.LogInformation("OnActivated() {0}", args.GetType()); + if (args.Kind == ActivationKind.Protocol) { - // Frame erstellen, der als Navigationskontext fungiert und zum Parameter der ersten Seite navigieren - rootFrame = new Frame(); - - rootFrame.NavigationFailed += OnNavigationFailed; - - if (e.PreviousExecutionState == ApplicationExecutionState.Terminated) + ProtocolActivatedEventArgs protocolArgs = args as ProtocolActivatedEventArgs; + // We support multiple protocols so check what scheme was used to launch the app + if (protocolArgs.Uri.Scheme.ToLower() == "signalpassback") { - //TODO: Zustand von zuvor angehaltener Anwendung laden + if (protocolArgs.Data.ContainsKey("token")) + { + string signalCaptchaToken = (string)protocolArgs.Data["token"]; + var registerPageInstance = CurrentSignalWindowsFrontend(MainViewId).Locator.RegisterPageInstance; + registerPageInstance.CaptchaCode = signalCaptchaToken; + registerPageInstance.CaptchaWebViewEnabled = false; + CurrentSignalWindowsFrontend(MainViewId).Locator.CaptchaPageInstance.View.Frame.GoBack(); + } + else + { + Logger.LogError("App was launched with signalpassback:// but wasn't passed a token"); + } + return; } - - // Den Frame im aktuellen Fenster platzieren - Window.Current.Content = rootFrame; } - - if (launched) + DisappearingMessagesManager.DeleteExpiredMessages(); + if (args is ToastNotificationActivatedEventArgs toastArgs) { - LaunchActivatedEventArgs args = e as LaunchActivatedEventArgs; - if (args.PrelaunchActivated == false) + string requestedConversation = toastArgs.Argument; + bool createdMainWindow = await CreateMainWindow(requestedConversation); + if (!createdMainWindow) { - if (Windows.Foundation.Metadata.ApiInformation.IsTypePresent("Windows.UI.ViewManagement.StatusBar")) + if (args is IViewSwitcherProvider viewSwitcherProvider && viewSwitcherProvider.ViewSwitcher != null) { - var sb = Windows.UI.ViewManagement.StatusBar.GetForCurrentView(); - sb.BackgroundColor = Windows.UI.Color.FromArgb(1, 0x20, 0x90, 0xEA); - sb.BackgroundOpacity = 1; - sb.ForegroundColor = Windows.UI.Colors.White; - } - - if (rootFrame.Content == null) - { - // Wenn der Navigationsstapel nicht wiederhergestellt wird, zur ersten Seite navigieren - // und die neue Seite konfigurieren, indem die erforderlichen Informationen als Navigationsparameter - // übergeben werden - Store = await Init; - if (Store == null || !Store.Registered) + ActivationViewSwitcher switcher = viewSwitcherProvider.ViewSwitcher; + int currentId = toastArgs.CurrentlyShownApplicationViewId; + if (viewSwitcherProvider.ViewSwitcher.IsViewPresentedOnActivationVirtualDesktop(toastArgs.CurrentlyShownApplicationViewId)) { - rootFrame.Navigate(typeof(StartPage), args.Arguments); + var taskCompletionSource = new TaskCompletionSource(); + await Views[currentId].Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => + { + try + { + Logger.LogInformation("OnActivated() selecting conversation"); + Views[currentId].Locator.MainPageInstance.TrySelectConversation(requestedConversation); + } + catch (Exception e) + { + Logger.LogError("OnActivated() TrySelectConversation() failed: {0}\n{1}", e.Message, e.StackTrace); + } + finally + { + taskCompletionSource.SetResult(false); + } + }); + await taskCompletionSource.Task; + await viewSwitcherProvider.ViewSwitcher.ShowAsStandaloneAsync(currentId); } else { - rootFrame.Navigate(typeof(MainPage), args.Arguments); + await CreateSecondaryWindowOrShowMain(switcher, requestedConversation); } } + else + { + Logger.LogError("OnActivated() has no ViewSwitcher"); + } } } else { - if (e is ToastNotificationActivatedEventArgs) + Logger.LogError("unknown IActivatedEventArgs {0}", args); + } + } + + protected override async void OnLaunched(LaunchActivatedEventArgs e) + { + Logger.LogInformation("Launching (PreviousExecutionState={0})", e.PreviousExecutionState); + DisappearingMessagesManager.DeleteExpiredMessages(); + try + { + string taskName = "SignalMessageBackgroundTask"; + foreach (var task in BackgroundTaskRegistration.AllTasks) { - var args = e as ToastNotificationActivatedEventArgs; - QueryString queryString = QueryString.Parse(args.Argument); - if (!(rootFrame.Content is MainPage)) + if (task.Value.Name == taskName) { - rootFrame.Navigate(typeof(MainPage), queryString); + task.Value.Unregister(false); } } + + var builder = new BackgroundTaskBuilder + { + Name = taskName, + TaskEntryPoint = "Signal_Windows.RC.SignalBackgroundTask", + IsNetworkRequested = true + }; + builder.SetTrigger(new TimeTrigger(15, false)); + builder.AddCondition(new SystemCondition(SystemConditionType.InternetAvailable)); + var requestStatus = await BackgroundExecutionManager.RequestAccessAsync(); + if (requestStatus != BackgroundAccessStatus.DeniedBySystemPolicy || + requestStatus != BackgroundAccessStatus.DeniedByUser || + requestStatus != BackgroundAccessStatus.Unspecified) + { + backgroundTaskRegistration = builder.Register(); + } + else + { + Logger.LogWarning($"Unable to register background task: {requestStatus}"); + } + + backgroundTaskRegistration.Completed += BackgroundTaskRegistration_Completed; + } + catch (Exception ex) + { + Logger.LogError("Cannot setup bg task: {0}\n{1}", ex.Message, ex.StackTrace); + } + + bool createdMainWindow = await CreateMainWindow(null); + if (!createdMainWindow) + { + ActivationViewSwitcher switcher = e.ViewSwitcher; + int currentId = e.CurrentlyShownApplicationViewId; + if (!switcher.IsViewPresentedOnActivationVirtualDesktop(currentId)) + { + await CreateSecondaryWindowOrShowMain(e.ViewSwitcher, e.Arguments); + } + else + { + await switcher.ShowAsStandaloneAsync(currentId); + } } - TileUpdateManager.CreateTileUpdaterForApplication().Clear(); - // Sicherstellen, dass das aktuelle Fenster aktiv ist - Window.Current.Activate(); } - private void Current_Activated(object sender, Windows.UI.Core.WindowActivatedEventArgs e) + private async Task CreateSecondaryWindowOrShowMain(ActivationViewSwitcher switcher, string conversationId) { - if (e.WindowActivationState == Windows.UI.Core.CoreWindowActivationState.Deactivated) + Logger.LogInformation("CreateSecondaryWindow()"); + CoreApplicationView newView = CoreApplication.CreateNewView(); + int newViewId = 0; + var frontendCreationCompletionSource = new TaskCompletionSource(); + await newView.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { - WindowActive = false; + SignalWindowsFrontend swf = null; + try + { + Frame frame = new Frame(); + frame.Navigate(typeof(MainPage), conversationId); + Window.Current.Content = frame; + Window.Current.Activate(); + var currView = ApplicationView.GetForCurrentView(); + if (GlobalSettingsManager.BlockScreenshotsSetting) + { + currView.IsScreenCaptureEnabled = false; + } + currView.Consolidated += CurrView_Consolidated; + newViewId = currView.Id; + ViewModelLocator newVML = (ViewModelLocator)Resources["Locator"]; + SetupTopBar(); + swf = new SignalWindowsFrontend(newView.Dispatcher, newVML, newViewId); + } + catch (Exception e) + { + Logger.LogError("CreateSecondaryWindowOrShowMain() SignalWindowsFrontend creation failed: {0}\n{1}", e.Message, e.StackTrace); + } + finally + { + frontendCreationCompletionSource.SetResult(swf); + } + }); + var frontend = await frontendCreationCompletionSource.Task; + var addFrontendCompletionSource = new TaskCompletionSource(); + await newView.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => + { + bool addFrontendResult = false; + try + { + //AddFrontend blocks for the handle lock, but the new window is not yet registered, so nothing will be invoked + addFrontendResult = Handle.AddFrontend(frontend.Dispatcher, frontend); + } + catch (Exception e) + { + Logger.LogError("CreateSecondaryWindowOrShowMain() AddFrontend() failed: {0}\n{1}", e.Message, e.StackTrace); + } + finally + { + addFrontendCompletionSource.SetResult(addFrontendResult); + } + }); + var success = await addFrontendCompletionSource.Task; + if (success) + { + Views.Add(newViewId, frontend); + DisappearingMessagesManager.AddFrontend(frontend.Dispatcher, frontend); + await switcher.ShowAsStandaloneAsync(newViewId); + Logger.LogInformation("CreateSecondaryWindow() added view {0}", newViewId); } else { - WindowActive = true; + Logger.LogInformation("CreateSecondaryWindow() showing MainView {0}", MainViewId); + await switcher.ShowAsStandaloneAsync(MainViewId); } } - /// - /// Wird aufgerufen, wenn die Navigation auf eine bestimmte Seite fehlschlägt - /// - /// Der Rahmen, bei dem die Navigation fehlgeschlagen ist - /// Details über den Navigationsfehler - private void OnNavigationFailed(object sender, NavigationFailedEventArgs e) + private void SetupTopBar() { - throw new Exception("Failed to load Page " + e.SourcePageType.FullName); + Logger.LogTrace("SetupTopBar()"); + // mobile clients have a status bar + if (Windows.Foundation.Metadata.ApiInformation.IsTypePresent("Windows.UI.ViewManagement.StatusBar")) + { + var sb = StatusBar.GetForCurrentView(); + sb.BackgroundColor = Color.FromArgb(1, 0x20, 0x90, 0xEA); + sb.BackgroundOpacity = 1; + sb.ForegroundColor = Colors.White; + } + // desktop clients have a title bar + else if (Windows.Foundation.Metadata.ApiInformation.IsTypePresent("Windows.UI.ViewManagement.ApplicationView")) + { + var titleBar = ApplicationView.GetForCurrentView().TitleBar; + if (titleBar != null) + { + titleBar.ButtonBackgroundColor = Color.FromArgb(1, 0x20, 0x90, 0xEA); + titleBar.ButtonForegroundColor = Colors.White; + titleBar.BackgroundColor = Color.FromArgb(1, 0x20, 0x90, 0xEA); + titleBar.ForegroundColor = Colors.White; + } + else + { + Logger.LogError("TitleBar is null"); + } + } + else + { + Logger.LogError("Neither TitleBar nor StatusBar found"); + } } - /// - /// Wird aufgerufen, wenn die Ausführung der Anwendung angehalten wird. Der Anwendungszustand wird gespeichert, - /// ohne zu wissen, ob die Anwendung beendet oder fortgesetzt wird und die Speicherinhalte dabei - /// unbeschädigt bleiben. - /// - /// Die Quelle der Anhalteanforderung. - /// Details zur Anhalteanforderung. - private async void OnSuspending(object sender, SuspendingEventArgs e) + private async Task CreateMainWindow(string conversationId) { - var deferral = e.SuspendingOperation.GetDeferral(); - if (MainPageActive) + if (!(Window.Current.Content is Frame rootFrame)) { - if (ViewModels.MainPageInstance != null) + rootFrame = new Frame(); + rootFrame.NavigationFailed += OnNavigationFailed; + Window.Current.Content = rootFrame; + var currView = ApplicationView.GetForCurrentView(); + if (GlobalSettingsManager.BlockScreenshotsSetting) + { + currView.IsScreenCaptureEnabled = false; + } + var frontend = new SignalWindowsFrontend(Window.Current.Dispatcher, (ViewModelLocator)Resources["Locator"], currView.Id); + Views.Add(currView.Id, frontend); + DisappearingMessagesManager.AddFrontend(frontend.Dispatcher, frontend); + MainViewId = currView.Id; + SetupTopBar(); + + try + { + ApplicationViewSwitcher.DisableShowingMainViewOnActivation(); + ApplicationViewSwitcher.DisableSystemViewActivationPolicy(); + TileUpdateManager.CreateTileUpdaterForApplication().Clear(); + // We need to await here so that any exception that Acquire throws actually gets caught + var hasStoreRecord = await Handle.Acquire(frontend.Dispatcher, frontend); + if (hasStoreRecord) + { + rootFrame.Navigate(typeof(MainPage), conversationId); + } + else + { + Handle.Release(); + rootFrame.Navigate(typeof(StartPage)); + } + Window.Current.Activate(); + } + catch (Exception ex) { - await ViewModels.MainPageInstance.Shutdown(); + var line = new StackTrace(ex, true).GetFrames()[0].GetFileLineNumber(); + Logger.LogError("OnLaunchedOrActivated() could not load signal handle: {0}\n{1}", ex.Message, ex.StackTrace); + rootFrame.Navigate(typeof(StartPage)); } + return true; } - Logger.LogInformation("OnSuspending() shutdown successful"); - //TODO: Anwendungszustand speichern und alle Hintergrundaktivitäten beenden - deferral.Complete(); + return false; } - private async void OnResuming(object sender, object e) + private void BackgroundTaskRegistration_Completed(BackgroundTaskRegistration sender, BackgroundTaskCompletedEventArgs args) { - Logger.LogInformation("OnResuming()"); - if (ViewModels.MainPageInstance != null) - { - await ViewModels.MainPageInstance.Init(); - } - else + Debug.WriteLine("Background task completed"); + } + + private async void CurrView_Consolidated(ApplicationView sender, ApplicationViewConsolidatedEventArgs args) + { + Logger.LogTrace($"CurrView_Consolidated() sender.Id = {sender.Id}"); + sender.Consolidated -= CurrView_Consolidated; + var signalWindowsFrontend = Views[sender.Id]; + await Handle.RemoveFrontend(signalWindowsFrontend.Dispatcher); + Views.Remove(sender.Id); + DisappearingMessagesManager.RemoveFrontend(signalWindowsFrontend.Dispatcher); + if (sender.Id != MainViewId) { - Logger.LogError("OnResuming() failed"); + Window.Current.Close(); } } + + /// + /// Wird aufgerufen, wenn die Navigation auf eine bestimmte Seite fehlschlägt + /// + /// Der Rahmen, bei dem die Navigation fehlgeschlagen ist + /// Details über den Navigationsfehler + private void OnNavigationFailed(object sender, NavigationFailedEventArgs e) + { + throw new Exception("Failed to load Page " + e.SourcePageType.FullName); + } } -} \ No newline at end of file +} diff --git a/Signal-Windows/Certificates/api-directory-signal-org.crt b/Signal-Windows/Certificates/api-directory-signal-org.crt new file mode 100644 index 0000000..a4daafe --- /dev/null +++ b/Signal-Windows/Certificates/api-directory-signal-org.crt @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEMDCCAxigAwIBAgICEDowDQYJKoZIhvcNAQELBQAwgY0xCzAJBgNVBAYTAlVT +MRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMR0w +GwYDVQQKDBRPcGVuIFdoaXNwZXIgU3lzdGVtczEdMBsGA1UECwwUT3BlbiBXaGlz +cGVyIFN5c3RlbXMxEzARBgNVBAMMClRleHRTZWN1cmUwHhcNMTkwNjAxMDAwMDAw +WhcNMzEwMTA5MDMzNzEwWjCBgzELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlm +b3JuaWExHTAbBgNVBAoMFE9wZW4gV2hpc3BlciBTeXN0ZW1zMR0wGwYDVQQLDBRP +cGVuIFdoaXNwZXIgU3lzdGVtczEhMB8GA1UEAwwYYXBpLmRpcmVjdG9yeS5zaWdu +YWwub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz5QXsh6QPygd +gwIY86CbopBAng5zHHknvD3pX3vOBkt7Gd6IlZ+Jle/QFblaqTFPTuU/VX1oT4OI +c5ZTNb5g/LvKMTBRzEset9CeTjx5STRcmWRlPeu3AJPZZEOvCH3AN55GOOiF8FQp +qoFVIhSUFS17iuRr3iGLA0Khn0Ink0qJouQuBqfrx8AL+r5dfTfEqs4sxpS34rxy +5M8z7HrccxbdcBHkNfn/QRLVikmzpFIBhlMcd9C8orobx+9Zv1cTsyl7m95Ma6zm +/aAVT1nPfKi9t666kYvuTezkehbOCsPqTuGZipQ8620vWs4o0u6X+t9JJfYaTHHF +lAU+GuYzCQIDAQABo4GhMIGeMAkGA1UdEwQCMAAwLAYJYIZIAYb4QgENBB8WHU9w +ZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRlMB0GA1UdDgQWBBSvJRKESl+1u6wi +Vs7ju08VUdaFLzAfBgNVHSMEGDAWgBQBixjxP/s5GURuhYa+lGUypzI8kDAjBgNV +HREEHDAaghhhcGkuZGlyZWN0b3J5LnNpZ25hbC5vcmcwDQYJKoZIhvcNAQELBQAD +ggEBAFganu/WuRTlcn2NYQPBGjVLtFUmvxZ8Y0U9u3Vg+fj8hXkpC3IN0MlWslmK +EIFJTYUJKpUqvmCPuhjvsaUKCsF1ECaydzl6Tt6nQZmc74epLxDCprbClM8iLDZS ++0ojUZdF/fGjT16NnoUy1aT2BhpFsIQOZCqM40jf1sHWRSsvnojPu8/NzHWBuRjt +HKMJ/I9knakOywrd3htDQdySadU+7uwKRnX/adRpvr3sYi/4cR5sHuf6bAmL6eCB +iZ4yTkYTQ0sPjAEYCrC2HsQPfYMdAPPMWuMlxgRDJkYT9y18jb9FXF6xVf7HhPWQ +ZUmeym0sPsdNE2uKBEuo2YZXxrE= +-----END CERTIFICATE----- diff --git a/Signal-Windows/Certificates/api-staging-directory-signal-org.crt b/Signal-Windows/Certificates/api-staging-directory-signal-org.crt new file mode 100644 index 0000000..6241805 --- /dev/null +++ b/Signal-Windows/Certificates/api-staging-directory-signal-org.crt @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEQDCCAyigAwIBAgICEDswDQYJKoZIhvcNAQELBQAwgY0xCzAJBgNVBAYTAlVT +MRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMR0w +GwYDVQQKDBRPcGVuIFdoaXNwZXIgU3lzdGVtczEdMBsGA1UECwwUT3BlbiBXaGlz +cGVyIFN5c3RlbXMxEzARBgNVBAMMClRleHRTZWN1cmUwHhcNMTkwNjAxMDAwMDAw +WhcNMzEwMTA5MDMzODQ4WjCBizELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlm +b3JuaWExHTAbBgNVBAoMFE9wZW4gV2hpc3BlciBTeXN0ZW1zMR0wGwYDVQQLDBRP +cGVuIFdoaXNwZXIgU3lzdGVtczEpMCcGA1UEAwwgYXBpLXN0YWdpbmcuZGlyZWN0 +b3J5LnNpZ25hbC5vcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDC +W8XIRtpbvAfl20N8qTqGXCsSh6t56xc6JQqVxKp3D1c3Rmkeok34kyFoY0ASZ5We +whK4qb8lwh+XKWCwPrSD4PflfdwYE6Zj5HqRNtIkxpU2eNSnjTG20y1gZa/zvvRJ +624G9eXk/W3MdBPJm6XwmstwnApCljRFeK+sAAIz3wWF4xK4q1dlExSYo/+P5fYA +lYsIvr6xdG2jAwWryQijbcjXEeqNZZ0P7G3nomv3in3rqYJsQt5ooZl+/g4/pVlS +8eKIqeBSeyx2WFAZS8XLyZ8GYNNkQUsJtRSENlHsudBHo6zmX4vvfI3A3kl9D6yf +zdfn/dHnNFXjFz1cgnytAgMBAAGjgakwgaYwCQYDVR0TBAIwADAsBglghkgBhvhC +AQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYEFDwg +LNfkZMeCu59TURlhRALnil+/MB8GA1UdIwQYMBaAFAGLGPE/+zkZRG6Fhr6UZTKn +MjyQMCsGA1UdEQQkMCKCIGFwaS1zdGFnaW5nLmRpcmVjdG9yeS5zaWduYWwub3Jn +MA0GCSqGSIb3DQEBCwUAA4IBAQAQaTCZfjRiU8nPkphjca0jYXChN/jXDxpdX/fH +S7C6gSd6orAvAv9IXDuT76WJWJgQvk0pf+qiWghZUAYPUp/bSeF2ujIrPdzTdtoG +MZfgqu+gfyKJI0U/5KcZIAT29bWGE5XMQK3h4Z5yhWEWa7YTWpDP8TevxgjbkA6T +mrkT4qbH2pInb4iDFWyJQfJIPbKe2a57qEMkMpHsNlNGvUE3D1UWjEX/lJUv1mVm +fkuDurf+429N1RmnxPaCToKp1mwUHh/f/jPZOf1tbEqWbxHBm2vomuOk6ERHWwJc +emfnutybrXA9vUrW6ECAMYL36JxMf4wjWhUNdPZSQsbNufqA +-----END CERTIFICATE----- diff --git a/Signal-Windows/Certificates/cdn-signal-org.crt b/Signal-Windows/Certificates/cdn-signal-org.crt new file mode 100644 index 0000000..8d2b8a2 --- /dev/null +++ b/Signal-Windows/Certificates/cdn-signal-org.crt @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDoDCCAoigAwIBAgICEBcwDQYJKoZIhvcNAQELBQAwgY0xCzAJBgNVBAYTAlVT +MRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMR0w +GwYDVQQKDBRPcGVuIFdoaXNwZXIgU3lzdGVtczEdMBsGA1UECwwUT3BlbiBXaGlz +cGVyIFN5c3RlbXMxEzARBgNVBAMMClRleHRTZWN1cmUwHhcNMTkwMjE1MTczODE3 +WhcNMjkwMzEyMTgxOTUwWjB5MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZv +cm5pYTEdMBsGA1UECgwUT3BlbiBXaGlzcGVyIFN5c3RlbXMxHTAbBgNVBAsMFE9w +ZW4gV2hpc3BlciBTeXN0ZW1zMRcwFQYDVQQDDA5jZG4uc2lnbmFsLm9yZzCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM18LQzkZEfWlg4hw/dBL9paXMIB +8No3GZAgAy/09MbfinutJhogqyGsfD9QVwKOcVCvolxWRJxOVHLvfJ+j3Ojaizip +PSg4tJBcbN+9ZakhzUpPRoghEI4yiKrl0Sqi7vJNILC1JTYvkRytQ9n/4Jbs5Y2R +BnRT1TPYTV57UYEJTbpi7gEiAWGj2rth0iCCvOasx+qhZEdPOn1e6lwVwWKHe0Ic +TRfT2CWMW01KVLcW86+adINJC/1ymCCoUyAve8Qsdf59G5bmObwjQzhxFqFhHY7Q +FfbJcvl0n1Cn1eglY+a/RyEDs5oux7VcZ8aj6P5GLiya+i08XBOQs3AuHwECAwEA +AaMdMBswGQYDVR0RBBIwEIIOY2RuLnNpZ25hbC5vcmcwDQYJKoZIhvcNAQELBQAD +ggEBAG/PlhcSBiL6fGKTGRpaoycPg7hJ9ziHLiB+y0QyB5wqO5derbp7SMXlOZV+ +SdL63ngqyVoN0iuC4BM7lU8DJithuOT+DkdBUHAdejNgRNl0tgpxiKFhl81NV1bH +cDkHXtI6Eg31yWJKn5PkQX5bVICwoe1ebZJdERU+Uc4uf9IUgrJmkWNSNRRVNtXE +iyL7WEbG3MlOE7UNzIJWYeBa/F7AWNItLd5fu9hbJvGq/pLUxVuNeSr2mrSxLF/U +tUYOvxNSwLpLCNoS7wnv60ZtLmXBCZ8hswk/q79aWHy3ln5ByH72UEQs3psE2qao +Ov8CGulVWMPSRA2lUjj3NNE1CfI= +-----END CERTIFICATE----- diff --git a/Signal-Windows/Certificates/cdn-staging-signal-org.crt b/Signal-Windows/Certificates/cdn-staging-signal-org.crt new file mode 100644 index 0000000..ed7f02e --- /dev/null +++ b/Signal-Windows/Certificates/cdn-staging-signal-org.crt @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIELDCCAxSgAwIBAgICECcwDQYJKoZIhvcNAQELBQAwgY0xCzAJBgNVBAYTAlVT +MRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMR0w +GwYDVQQKDBRPcGVuIFdoaXNwZXIgU3lzdGVtczEdMBsGA1UECwwUT3BlbiBXaGlz +cGVyIFN5c3RlbXMxEzARBgNVBAMMClRleHRTZWN1cmUwHhcNMTkwNjAxMDAwMDAw +WhcNMzAwMTE4MjE0MzM1WjCBgTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlm +b3JuaWExHTAbBgNVBAoMFE9wZW4gV2hpc3BlciBTeXN0ZW1zMR0wGwYDVQQLDBRP +cGVuIFdoaXNwZXIgU3lzdGVtczEfMB0GA1UEAwwWY2RuLXN0YWdpbmcuc2lnbmFs +Lm9yZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMKxzAlGolPijtSn +AVevtsCKrc+4pPrMfc1Sg9VQQs45VWrDvVR7PyNP1ORYMqal0AywLjAuwhsC7BbN +y6xi17epy4UErlzqc6TMkxMboVKruBqZ8ehUDNNi0hdgxr3Cw0ZMKOXwvpfYZYIE +dn35a1Bn1OWhJtPk33YAFCz1C6yLQ0maVtAS/YTA7DjSvRJJLBiJoQt7bEA1Y+IV +oX8DaKY/wIBmRkMoZLWtpiC8V8W8mUMl0C8F196ncyBb/9q17gK5HIMWCg1RPJ78 +bbYSOSv7nf1uc1vsBLDgv3Ew2p8QEtsBY72WZPfpsS2uMyS8V3XxIxBfqYWVsXwX +ucm88Z8CAwEAAaOBnzCBnDAJBgNVHRMEAjAAMCwGCWCGSAGG+EIBDQQfFh1PcGVu +U1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQU2doVeQDguA6pSzLd +ZPAaqBivonEwHwYDVR0jBBgwFoAUAYsY8T/7ORlEboWGvpRlMqcyPJAwIQYDVR0R +BBowGIIWY2RuLXN0YWdpbmcuc2lnbmFsLm9yZzANBgkqhkiG9w0BAQsFAAOCAQEA +ND8s88nAFFrS1NV6O5GpNVk0nNVlA8qrqNtGwKCazPDwcBnVgCxEsgDz5o0SKe/B +IH7puRiOZnnGPSd7+XL1xlnq/EGtieFtg/4EQgZzL9E3kECegFsUIk7CT6TQgIu9 +30YJj4S0X2yT5CP5gy/WWiNw4lpRUVDQg+owBFlYPyMN3mBADfKOl8R9KXbpHK1c +qiIhDeYNyVA2csllzMR7bwCWP0ohYDaWAtNcCKkNg5uZKIAGCd4cqg+KgoLLy/Z4 +1IAoOmpDdX6r1iGfcFiiwJ5Y6gl6y41xobWarOf3F1yKpuyQGJRChUqPNW3koUde +alW238HKeaWkXNzQIq+66g== +-----END CERTIFICATE----- diff --git a/Signal-Windows/Certificates/cdn2-signal-org.crt b/Signal-Windows/Certificates/cdn2-signal-org.crt new file mode 100644 index 0000000..892bba8 --- /dev/null +++ b/Signal-Windows/Certificates/cdn2-signal-org.crt @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEHTCCAwWgAwIBAgICECwwDQYJKoZIhvcNAQELBQAwgY0xCzAJBgNVBAYTAlVT +MRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMR0w +GwYDVQQKDBRPcGVuIFdoaXNwZXIgU3lzdGVtczEdMBsGA1UECwwUT3BlbiBXaGlz +cGVyIFN5c3RlbXMxEzARBgNVBAMMClRleHRTZWN1cmUwHhcNMTkwNjAxMDAwMDAw +WhcNMzAwMzE4MTkxMTM2WjB6MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZv +cm5pYTEdMBsGA1UECgwUT3BlbiBXaGlzcGVyIFN5c3RlbXMxHTAbBgNVBAsMFE9w +ZW4gV2hpc3BlciBTeXN0ZW1zMRgwFgYDVQQDDA9jZG4yLnNpZ25hbC5vcmcwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC7RvcmiCpeLRMrj3ntVuglVD+E +uY9DvaeDRm0MF6/T/No2IGFqCNSxpvK821lmVVZyohWB3YhKmEQLEz31aNAGfwVz +RNp477zKYHh0PG//UTqayv6M5n0lPGmunV254plGG8028ZKW+kJRwEPJLPRj6F+1 +KbvEJLEXSvXF/wLYHoOcWcPNjJtgmWstmMPlANBQ7yZJ+a3IxU9wT40AhLOTtN5L +Xvfe9w19TUvVG5ZtJQFv66rjcLAHthmYTWPATUzf98uAnkzUXoj8eBthuSGczQ0T +KTgCHPT4aCQcns22x99RHTDfhbDz9SYhlCsiEEeEYQm6z2rkd/zRe4mvEZ+PAgMB +AAGjgZgwgZUwCQYDVR0TBAIwADAsBglghkgBhvhCAQ0EHxYdT3BlblNTTCBHZW5l +cmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYEFLD5Cz5EcyGADdxcHQXKLYOyBIri +MB8GA1UdIwQYMBaAFAGLGPE/+zkZRG6Fhr6UZTKnMjyQMBoGA1UdEQQTMBGCD2Nk +bjIuc2lnbmFsLm9yZzANBgkqhkiG9w0BAQsFAAOCAQEAMiI8izFaj6A77xiLb7x4 +FXqlj4XJaXFioL30YmHTP57CWCuShbFASvYIYIrcMrPI0imi/MojM7DTiW0PV47B +Ask9p6JTcjRNmsLr3Ig2SEE5rm9f813RoCx8Bzii9/afMezIuaL0Cjyh+6db+8BT +RUdoz+DaaxkvWM/xalMk6APZWoKcXqpJhRRGMa/SgFRprvEG7n6nip86NyneTdWL +/6HbDNfDlS2kwDs7ZEK//dt19BuXE6FZYYwWTRFP0L/j1JCT1RO0D5N4EzdmvCqm +s689AMCQQLamXsFqXBwsxDXSvrHK/KXicPvrCG7dN258NSDUYjwToFFEsc5s5MQq +5A== +-----END CERTIFICATE----- diff --git a/Signal-Windows/Certificates/cdn2-staging-signal-org.crt b/Signal-Windows/Certificates/cdn2-staging-signal-org.crt new file mode 100644 index 0000000..4bc6ba0 --- /dev/null +++ b/Signal-Windows/Certificates/cdn2-staging-signal-org.crt @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIELjCCAxagAwIBAgICEC0wDQYJKoZIhvcNAQELBQAwgY0xCzAJBgNVBAYTAlVT +MRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMR0w +GwYDVQQKDBRPcGVuIFdoaXNwZXIgU3lzdGVtczEdMBsGA1UECwwUT3BlbiBXaGlz +cGVyIFN5c3RlbXMxEzARBgNVBAMMClRleHRTZWN1cmUwHhcNMTkwNjAxMDAwMDAw +WhcNMzAwMzE4MTkxMjIwWjCBgjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlm +b3JuaWExHTAbBgNVBAoMFE9wZW4gV2hpc3BlciBTeXN0ZW1zMR0wGwYDVQQLDBRP +cGVuIFdoaXNwZXIgU3lzdGVtczEgMB4GA1UEAwwXY2RuMi1zdGFnaW5nLnNpZ25h +bC5vcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDG+ig3tXe9bp05 +Ob2l38jA0d5LyHWYjqlfMzsOsPCCGo60PcOLKJjsAAV/OfI8vdAe+LAI7sIfe2OV +heQPQnoG27+t89TCK+gH48bwyNOlQ+dgvwkS9hSw/6iNp0b2ClGXfoQjw7FOkpRV +ZRmQgDEL1bdLKgrqfkOUAi/lVcOE2/qrB+3GdKfeSb6WRV39buHmAe048I/WxbCg +smD+JEKfsN3lvO/qtwKhP2J/0shyyXrF1vfPxJNh9w+gtNTinxO+Pp0iKGDxEbel +6DdvPB3OIVVO6klTL/GP/pWNP5ieWoGh4lAjrPjhF3/IEHdr9lNzOtPLfQEVzEYc +FlPwbD0nAgMBAAGjgaAwgZ0wCQYDVR0TBAIwADAsBglghkgBhvhCAQ0EHxYdT3Bl +blNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYEFBnqJJohmL/bCRr1 +uJazBMg81WqKMB8GA1UdIwQYMBaAFAGLGPE/+zkZRG6Fhr6UZTKnMjyQMCIGA1Ud +EQQbMBmCF2NkbjItc3RhZ2luZy5zaWduYWwub3JnMA0GCSqGSIb3DQEBCwUAA4IB +AQC7Pvx3WYF2MjcQK9T1/ZqC2dKIYuZlYr3tgQJigFwO37I4A5Slj/I7+2K1Agyb +XAbdhp/x1O2Fi2pYDsXZ7Av34eE2boR6GnXuOw/qKlNrdwwUN226iIcDp7U1Ir/g +hdvG3jG8tllIN2nX+sePuFBar+ZnYu8JBAzEI6uY6zrea2qlojoxn3U1QapdXKP0 +ZUjJEp5/lanRlk9gQbO2Z5bh4x3tL+sWf5Jkb12ntEz3LegceVCb5xXAYb6FA/Yg +whswYl/qsmpOoM6XF6v7ttKrRFZzLb1SgyU98B5PwdIpIBN/5ajJiWfHbgTOPP39 +3M9+duJSRQ3KRnJ2wnPnfUi+ +-----END CERTIFICATE----- diff --git a/Signal-Windows/Certificates/textsecure-service-staging-whispersystems-org.crt b/Signal-Windows/Certificates/textsecure-service-staging-whispersystems-org.crt new file mode 100644 index 0000000..50c39d8 --- /dev/null +++ b/Signal-Windows/Certificates/textsecure-service-staging-whispersystems-org.crt @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIID+zCCAuOgAwIBAgICEBUwDQYJKoZIhvcNAQELBQAwgY0xCzAJBgNVBAYTAlVT +MRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMR0w +GwYDVQQKDBRPcGVuIFdoaXNwZXIgU3lzdGVtczEdMBsGA1UECwwUT3BlbiBXaGlz +cGVyIFN5c3RlbXMxEzARBgNVBAMMClRleHRTZWN1cmUwHhcNMTkwMjE1MTczODE3 +WhcNMjkwMzEyMTgxNzUwWjCBmDELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlm +b3JuaWExHTAbBgNVBAoMFE9wZW4gV2hpc3BlciBTeXN0ZW1zMR0wGwYDVQQLDBRP +cGVuIFdoaXNwZXIgU3lzdGVtczE2MDQGA1UEAwwtdGV4dHNlY3VyZS1zZXJ2aWNl +LXN0YWdpbmcud2hpc3BlcnN5c3RlbXMub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAq+R2DEZS27gxQO5dCnzmKpcKkR0qf3RNxiKjcXbvXy+Ev8AG +ENDfTeBUX7UV+qz+mOO58rlHXbDnHgPJE47UoOhbaWSfSMsmeflBDRfA7aO1k3Yn +TQB5jTxH2IEyZNOPaSOGWnqReMSlkcrzsfQMmwaNWxcBBquOnmnVrnQIH6ORUl5X +PrTyhak/va4jdxfbsZKthuWJ6WhfW3MCwI/hsKrwK3eT7mVG9widhRoIYLER7DTy +rqIv124quOjqsy8uqFIglCHGHeKtz8zbyh2dFtnwNQiDF/JPWAAZwHPpgeZgQET5 +s6nT5wG4Xio7uesrkih1acXiSCyWILNKmm10+wIDAQABo1gwVjBUBgNVHREETTBL +gi10ZXh0c2VjdXJlLXNlcnZpY2Utc3RhZ2luZy53aGlzcGVyc3lzdGVtcy5vcmeC +GnNlcnZpY2Utc3RhZ2luZy5zaWduYWwub3JnMA0GCSqGSIb3DQEBCwUAA4IBAQC3 +2VRSxXJio8b5WLHT5NVXMw+oxZ9MR5vpuWBTleLh3SUx0Ach0Qdsi1sqYNqXyI9I +UhlZFZNAiebe/t4BSGYa7H9Pfj0LQfKCWElHCLjAvVPod/fD4LPipyS0lhA0DjAu +FLmEBvIc6JxnM/KX4qJGfek+IvuHwXYMkIplq5N8f0G8uN3zV2s58fSgXcxYi2nk +VzS0CLcxcopDxxZCrW1PQihFdIEsi5/yL2C4sKI5luiBGdjv5/ei1mBk8jD7oszL +Etgz6o+I7uxjOny6+tAxchSeAJc0/y95EZSdSYwaEAen8F19SIAnrpRU4ljgQrG9 +yIchV8zNxKkUYpEt2D++ +-----END CERTIFICATE----- diff --git a/Signal-Windows/Certificates/textsecure-servicewhispersystemsorg.crt b/Signal-Windows/Certificates/textsecure-servicewhispersystemsorg.crt new file mode 100644 index 0000000..680a7e2 --- /dev/null +++ b/Signal-Windows/Certificates/textsecure-servicewhispersystemsorg.crt @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIID4zCCAsugAwIBAgICEBgwDQYJKoZIhvcNAQELBQAwgY0xCzAJBgNVBAYTAlVT +MRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMR0w +GwYDVQQKDBRPcGVuIFdoaXNwZXIgU3lzdGVtczEdMBsGA1UECwwUT3BlbiBXaGlz +cGVyIFN5c3RlbXMxEzARBgNVBAMMClRleHRTZWN1cmUwHhcNMTkwMjE1MTczODE3 +WhcNMjkwMzEyMTgyMDIwWjCBkDELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlm +b3JuaWExHTAbBgNVBAoMFE9wZW4gV2hpc3BlciBTeXN0ZW1zMR0wGwYDVQQLDBRP +cGVuIFdoaXNwZXIgU3lzdGVtczEuMCwGA1UEAwwldGV4dHNlY3VyZS1zZXJ2aWNl +LndoaXNwZXJzeXN0ZW1zLm9yZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAKzIEbXRRbfAosvPk4magHWzsHhwOzu7On7EA4xxqViHbN4ox4jl5Lh9mu6n +VW0eBvxc9zQKPG0ijgQJN/SV53jFwjqqtr4JYTsHzKs6bgHlYH6sW3XHxePj5JFK +SSXWY7lKNASVl5KkSmhaiYItEPExvSoPB9bNwupixZ5Ae0iIE/NYQA6yZXpQTY0d +BU0l1q0pQeXzLXqgJetThzSXr6j5soNO2KyRoMBNbI42fPUYvWRCOUfyUNI2fb3q +suZD+QQ7YKxl5hgDBU8oNCNN80sNWjhh5nFEOWGj5lxl1qYTkp3sWJJGYD6cuQDJ +1DrSKNbDUWnslIe+wvZfTx9+km0CAwEAAaNIMEYwRAYDVR0RBD0wO4IldGV4dHNl +Y3VyZS1zZXJ2aWNlLndoaXNwZXJzeXN0ZW1zLm9yZ4ISc2VydmljZS5zaWduYWwu +b3JnMA0GCSqGSIb3DQEBCwUAA4IBAQApay5HvPcMP+HE2vS3WOxL/ygG1o/q4zcO +/VYOfA7q2yiFN2FDF8lEcwEqcDMAz2+hGK/fXi2gaIYq6fp3fL9OtzIrXmUNCB2I +9PpuI4jj6xUtERecOXSaHE2C3TI3t7CIcvhbGU1OrJiDLbVFHE8RAetsJJyd2YWu +zBwd9U3oWS4ZNzjlwQLTOiJpoApSKmMlQ6OVfgdr6rRTI1ocw+q4/wDxcYEhiLoM +ljy42A/WrwXzyUMDkcAtZHTjkUAuSLivn434nLcYXalMUIW8sQNLksKTqVH26MKS +2t2HRVs4cwDfmtGzmWSLbgRBl/8Oquq5XLLNEUIM31NVcBUFpKhJ +-----END CERTIFICATE----- diff --git a/Signal-Windows/Controls/AddContactListElement.xaml.cs b/Signal-Windows/Controls/AddContactListElement.xaml.cs index 7e75722..2917f85 100644 --- a/Signal-Windows/Controls/AddContactListElement.xaml.cs +++ b/Signal-Windows/Controls/AddContactListElement.xaml.cs @@ -89,7 +89,6 @@ private void AddContactListElement_DataContextChanged(FrameworkElement sender, D { if (Model != null) { - Model.View = this; DisplayName = Model.Name; PhoneNumber = Model.PhoneNumber; ContactPhoto = Model.Photo; diff --git a/Signal-Windows/Controls/AddedAttachment.xaml b/Signal-Windows/Controls/AddedAttachment.xaml new file mode 100644 index 0000000..b740679 --- /dev/null +++ b/Signal-Windows/Controls/AddedAttachment.xaml @@ -0,0 +1,22 @@ + + + + + + + + + + + diff --git a/Signal-Windows/Controls/AddedAttachment.xaml.cs b/Signal-Windows/Controls/AddedAttachment.xaml.cs new file mode 100644 index 0000000..3602f8d --- /dev/null +++ b/Signal-Windows/Controls/AddedAttachment.xaml.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using Windows.Foundation; +using Windows.Foundation.Collections; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Data; +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Navigation; + +// The User Control item template is documented at https://go.microsoft.com/fwlink/?LinkId=234236 + +namespace Signal_Windows.Controls +{ + public sealed partial class AddedAttachment : UserControl + { + public event Action OnCancelAttachmentButtonClicked; + public AddedAttachment() + { + this.InitializeComponent(); + } + + public void ShowAttachment(string filename) + { + Visibility = Visibility.Visible; + AddedAttachmentFilename.Text = filename; + } + + public void HideAttachment() + { + Visibility = Visibility.Collapsed; + } + + private void CancelAttachmentButton_Click(object sender, RoutedEventArgs e) + { + OnCancelAttachmentButtonClicked?.Invoke(); + } + } +} diff --git a/Signal-Windows/Controls/Attachment.xaml b/Signal-Windows/Controls/Attachment.xaml new file mode 100644 index 0000000..a764083 --- /dev/null +++ b/Signal-Windows/Controls/Attachment.xaml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + diff --git a/Signal-Windows/Controls/Attachment.xaml.cs b/Signal-Windows/Controls/Attachment.xaml.cs new file mode 100644 index 0000000..97246ba --- /dev/null +++ b/Signal-Windows/Controls/Attachment.xaml.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using libsignalservice; +using Microsoft.Extensions.Logging; +using Signal_Windows.Models; +using Signal_Windows.Storage; +using Windows.Foundation; +using Windows.Foundation.Collections; +using Windows.Storage; +using Windows.UI.ViewManagement; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Data; +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Media.Imaging; +using Windows.UI.Xaml.Navigation; + +// The User Control item template is documented at https://go.microsoft.com/fwlink/?LinkId=234236 + +namespace Signal_Windows.Controls +{ + public sealed partial class Attachment : UserControl, INotifyPropertyChanged + { + private readonly ILogger Logger = LibsignalLogging.CreateLogger(); + public event PropertyChangedEventHandler PropertyChanged; + + private Uri imagePath; + public Uri ImagePath + { + get { return imagePath; } + set { imagePath = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ImagePath))); } + } + + private Symbol attachmentIcon; + public Symbol AttachmentIcon + { + get { return attachmentIcon; } + set { attachmentIcon = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(AttachmentIcon))); } + } + + public SignalAttachment Model + { + get { return DataContext as SignalAttachment; } + } + + public Attachment() + { + this.InitializeComponent(); + DataContextChanged += Attachment_DataContextChanged; + AttachmentImage.ImageFailed += AttachmentImage_ImageFailed; + } + + private void AttachmentImage_ImageFailed(object sender, ExceptionRoutedEventArgs e) + { + Logger.LogError("AttachmentImage_ImageFailed {0}", e.ErrorMessage); + } + + private void Attachment_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args) + { + if (Model != null) + { + if (Model.Status == SignalAttachmentStatus.Finished || Model.Message.Direction == SignalMessageDirection.Outgoing) + { + AttachmentImage.Visibility = Visibility.Visible; + AttachmentDownloadIcon.Visibility = Visibility.Collapsed; + if (IMAGE_TYPES.Contains(Model.ContentType)) + { + var path = ApplicationData.Current.LocalCacheFolder.Path + @"\Attachments\" + Model.Id + ".plain"; + ImagePath = new Uri(path); + } + } + else if (Model.Status == SignalAttachmentStatus.Default || Model.Status == SignalAttachmentStatus.Finished || Model.Status == SignalAttachmentStatus.Failed) + { + AttachmentImage.Visibility = Visibility.Collapsed; + AttachmentDownloadIcon.Visibility = Visibility.Visible; + } + } + } + + private void AttachmentDownloadIcon_Tapped(object sender, TappedRoutedEventArgs e) + { + App.Handle.StartAttachmentDownload(Model); + } + + private void AttachmentImage_Tapped(object sender, TappedRoutedEventArgs e) + { + if (IsDetailsPageEnabled) + { + App.CurrentSignalWindowsFrontend(ApplicationView.GetForCurrentView().Id).Locator.MainPageInstance.OpenAttachment(Model); + } + } + + public bool HandleUpdate(SignalAttachment sa) + { + DataContext = sa; + return Model.Status != SignalAttachmentStatus.Finished && Model.Status != SignalAttachmentStatus.Failed_Permanently; + } + + private bool IsDetailsPageEnabled => IMAGE_TYPES.Contains(Model.ContentType); + + private static HashSet IMAGE_TYPES = new HashSet() + { + "image/jpeg", + "image/png", + "image/gif", + "image/bmp" + }; + } +} diff --git a/Signal-Windows/Controls/Conversation.xaml b/Signal-Windows/Controls/Conversation.xaml index cc1594b..6ca468b 100644 --- a/Signal-Windows/Controls/Conversation.xaml +++ b/Signal-Windows/Controls/Conversation.xaml @@ -12,18 +12,73 @@ d:DesignWidth="400"> - - - - - - - - - - + - + @@ -36,6 +91,7 @@ +   •   @@ -45,7 +101,6 @@ - @@ -54,22 +109,24 @@ - + - + - - + + + + + + \ No newline at end of file diff --git a/Signal-Windows/Controls/Conversation.xaml.cs b/Signal-Windows/Controls/Conversation.xaml.cs index 7fcd7da..d10870c 100644 --- a/Signal-Windows/Controls/Conversation.xaml.cs +++ b/Signal-Windows/Controls/Conversation.xaml.cs @@ -1,4 +1,8 @@ +using libsignalservice; using libsignalservice.util; +using Microsoft.Extensions.Logging; +using Signal_Windows.Lib; +using Signal_Windows.Lib.Models; using Signal_Windows.Models; using Signal_Windows.Storage; using Signal_Windows.ViewModels; @@ -7,8 +11,18 @@ using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; +using System.Runtime.InteropServices.WindowsRuntime; using System.Threading.Tasks; +using Windows.ApplicationModel.DataTransfer; using Windows.Foundation; +using Windows.Graphics.Imaging; +using Windows.Storage; +using Windows.Storage.AccessCache; +using Windows.Storage.Pickers; +using Windows.Storage.Streams; +using Windows.System; +using Windows.UI.Core; +using Windows.UI.ViewManagement; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Input; @@ -18,13 +32,22 @@ namespace Signal_Windows.Controls { + public interface IMessageView + { + SignalMessage Model { get; set; } + void HandleUpdate(SignalMessage m); + FrameworkElement AsFrameworkElement(); + } + public sealed partial class Conversation : UserControl, INotifyPropertyChanged { + private readonly ILogger Logger = LibsignalLogging.CreateLogger(); public event PropertyChangedEventHandler PropertyChanged; - private bool SendingMessage = false; - private Dictionary OutgoingCache = new Dictionary(); private SignalConversation SignalConversation; - private VirtualizedCollection Collection; + public VirtualizedCollection Collection; + private CoreWindowActivationState ActivationState = CoreWindowActivationState.Deactivated; + private int LastMarkReadRequest; + private StorageFile SelectedFile; private string _ThreadDisplayName; @@ -58,6 +81,17 @@ public Visibility SeparatorVisibility set { _SeparatorVisiblity = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SeparatorVisibility))); } } + private bool _SendButtonEnabled; + public bool SendButtonEnabled + { + get { return _SendButtonEnabled; } + set + { + _SendButtonEnabled = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SendButtonEnabled))); + } + } + private Brush _HeaderBackground; public Brush HeaderBackground @@ -66,45 +100,76 @@ public Brush HeaderBackground set { _HeaderBackground = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HeaderBackground))); } } - private bool _SendEnabled; - public bool SendEnabled + public Brush SendButtonBackground + { + get { return Utils.Blue; } + } + + private bool blocked; + public bool Blocked { - get { return _SendEnabled; } + get { return blocked; } set { - _SendEnabled = value; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SendEnabled))); + blocked = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Blocked))); } } + public bool SendMessageVisible + { + get { return !Blocked; } + set { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SendMessageVisible))); } + } + + private bool spellCheckEnabled; + public bool SpellCheckEnabled + { + get { return spellCheckEnabled; } + set { spellCheckEnabled = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SpellCheckEnabled))); } + } + public Conversation() { this.InitializeComponent(); Displayname.Foreground = Utils.ForegroundIncoming; Separator.Foreground = Utils.ForegroundIncoming; Username.Foreground = Utils.ForegroundIncoming; - SendEnabled = false; + SendButtonEnabled = false; + Loaded += Conversation_Loaded; + Unloaded += Conversation_Unloaded; } - public MainPageViewModel GetMainPageVm() + private void Conversation_Unloaded(object sender, RoutedEventArgs e) { - return DataContext as MainPageViewModel; + Window.Current.Activated -= HandleWindowActivated; + } + + private void Conversation_Loaded(object sender, RoutedEventArgs e) + { + Window.Current.Activated += HandleWindowActivated; + } + + private void HandleWindowActivated(object sender, WindowActivatedEventArgs e) + { + Logger.LogTrace("HandleWindowActivated() new activation state {0}", e.WindowActivationState); + ActivationState = e.WindowActivationState; + MarkBottommostMessageRead(); } - public void Update(SignalConversation thread) + public MainPageViewModel GetMainPageVm() { - InputTextBox.IsEnabled = thread.CanReceive; - UpdateHeader(thread); + return DataContext as MainPageViewModel; } private void UpdateHeader(SignalConversation thread) { ThreadDisplayName = thread.ThreadDisplayName; ThreadUsername = thread.ThreadId; - if (thread is SignalContact) + if (thread is SignalContact contact) { - SignalContact contact = (SignalContact)thread; - HeaderBackground = Utils.GetBrushFromColor(contact.Color); + HeaderBackground = contact.Color != null ? Utils.GetBrushFromColor((contact.Color)) : + Utils.GetBrushFromColor(Utils.CalculateDefaultColor(contact.ThreadDisplayName)); if (ThreadUsername != ThreadDisplayName) { ThreadUsernameVisibility = Visibility.Visible; @@ -126,10 +191,38 @@ private void UpdateHeader(SignalConversation thread) public void Load(SignalConversation conversation) { + bool conversationThreadIdChanged = SignalConversation?.ThreadId != conversation?.ThreadId; SignalConversation = conversation; - InputTextBox.IsEnabled = false; + if (SignalConversation is SignalContact contact) + { + Blocked = contact.Blocked; + SendMessageVisible = !Blocked; + } + else + { + // Need to make sure to reset the Blocked and SendMessageVisible values in case + // a group chat is selected. Group chats can never be blocked. + Blocked = false; + SendMessageVisible = !Blocked; + } + LastMarkReadRequest = -1; + SendButtonEnabled = false; + + /* + * On app resume this method (Load()) gets called with the same conversation, but new object. + * Only load draft if it is acutally a different conversation, + * because on mobile app gets supended during file picking and + * the new conversation does not have the DraftFileTokens + */ + if (conversationThreadIdChanged) + { + // We don't need to wait for this + _ = LoadDraft(); + } + UserInputBar.FocusTextBox(); DisposeCurrentThread(); UpdateHeader(conversation); + SpellCheckEnabled = GlobalSettingsManager.SpellCheckSetting; /* * When selecting a small (~650 messages) conversation after a bigger (~1800 messages) one, @@ -139,16 +232,16 @@ public void Load(SignalConversation conversation) */ ConversationItemsControl.ItemsSource = new List(); UpdateLayout(); - Collection = new VirtualizedCollection(conversation); + Collection = new VirtualizedCollection(conversation); ConversationItemsControl.ItemsSource = Collection; UpdateLayout(); - InputTextBox.IsEnabled = conversation.CanReceive; + SendButtonEnabled = conversation.CanReceive; ScrollToUnread(); } public void DisposeCurrentThread() { - OutgoingCache.Clear(); + Collection?.Dispose(); } public T FindElementByName(FrameworkElement element, string sChildName) where T : FrameworkElement @@ -157,9 +250,7 @@ public T FindElementByName(FrameworkElement element, string sChildName) where var nChildCount = VisualTreeHelper.GetChildrenCount(element); for (int i = 0; i < nChildCount; i++) { - FrameworkElement child = VisualTreeHelper.GetChild(element, i) as FrameworkElement; - - if (child == null) + if (!(VisualTreeHelper.GetChild(element, i) is FrameworkElement child)) continue; if (child is T && child.Name.Equals(sChildName)) @@ -178,49 +269,69 @@ public T FindElementByName(FrameworkElement element, string sChildName) where public void UpdateMessageBox(SignalMessage updatedMessage) { - if (OutgoingCache.ContainsKey(updatedMessage.Id)) + if (Collection != null) { - var m = OutgoingCache[updatedMessage.Id]; - var item = (ListViewItem) ConversationItemsControl.ContainerFromIndex(Collection.GetVirtualIndex(m.Index)); - if (item != null) + IMessageView m = Collection.GetMessageByDbId(updatedMessage.Id); + if (m != null) { - var message = FindElementByName(item, "ListBoxItemContent"); - bool retain = message.HandleUpdate(updatedMessage); - if (!retain) - { - OutgoingCache.Remove(m.Index); - } + var attachment = FindElementByName(m.AsFrameworkElement(), "Attachment"); + m.HandleUpdate(updatedMessage); } } } - public void Append(SignalMessageContainer sm, bool forceScroll) + public void UpdateAttachment(SignalAttachment sa) { - var sourcePanel = (ItemsStackPanel)ConversationItemsControl.ItemsPanelRoot; - bool bottom = sourcePanel.LastVisibleIndex == Collection.Count - 2; /* -2 because we already incremented Count */ - Collection.Add(sm, true); - if (forceScroll || bottom) + if (Collection != null) + { + IMessageView m = Collection.GetMessageByDbId(sa.Message.Id); + if (m != null) + { + var attachment = FindElementByName(m.AsFrameworkElement(), "Attachment"); + attachment.HandleUpdate(sa); + } + } + } + + public AppendResult Append(IMessageView sm) + { + AppendResult result = new AppendResult(false); + bool bottom = GetBottommostIndex() == Collection.Count - 2; // -2 because we already incremented Count + Collection.Add(sm, sm.Model.Author == null); + if (bottom) { UpdateLayout(); ScrollToBottom(); + if (ActivationState != CoreWindowActivationState.Deactivated) + { + result = new AppendResult(true); + } } + return result; } - public void AddToOutgoingMessagesCache(SignalMessageContainer m) + public void HandleDeleteMesage(SignalMessage message) { - OutgoingCache[m.Message.Id] = m; + Collection.Remove(message); } - - private async void TextBox_KeyDown(object sender, KeyRoutedEventArgs e) + + private async void UserInputBar_OnEnterKeyPressed() { - if (e.Key == Windows.System.VirtualKey.Enter) + CoreWindow coreWindow = CoreWindow.GetForCurrentThread(); + bool shift = coreWindow.GetKeyState(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down); + bool control = coreWindow.GetKeyState(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down); + // If SendMessageWithEnterSetting is true then add new if shift key is pressed. + // If SendMessageWithEnterSetting is false then send message if control key is pressed. + if (GlobalSettingsManager.SendMessageWithEnterSetting ? shift : !control) { - // this fixes double send by enter repeat - if (!SendingMessage) + UserInputBar.AddLinefeed(); + } + else + { + bool sendMessageResult = await GetMainPageVm().SendMessage(UserInputBar.InputText, SelectedFile); + if (sendMessageResult) { - SendingMessage = true; - await GetMainPageVm().TextBox_KeyDown(sender, e); - SendingMessage = false; + ResetInput(); } } } @@ -234,15 +345,53 @@ private void ScrollToBottom() } } - private async void SendMessageButton_Click(object sender, RoutedEventArgs e) + + private async void UserInputBar_OnSendMessageButtonClicked() { - await GetMainPageVm().SendMessageButton_Click(InputTextBox); + if (string.IsNullOrEmpty(UserInputBar.InputText) && SelectedFile == null) + { + var filePicker = new FileOpenPicker(); + filePicker.FileTypeFilter.Add("*"); // Without this the file picker throws an exception, this is not documented + var file = await filePicker.PickSingleFileAsync(); + SetSelectedFile(file); + if (SelectedFile != null) + { + UserInputBar.FocusTextBox(); + } + } + else + { + bool sendMessageResult = await GetMainPageVm().SendMessage(UserInputBar.InputText, SelectedFile); + if (sendMessageResult) + { + ResetInput(); + } + } } - private void InputTextBox_TextChanged(object sender, TextChangedEventArgs e) + private static ScrollViewer GetScrollViewer(DependencyObject element) { - TextBox t = sender as TextBox; - SendEnabled = t.Text != string.Empty; + if (element is ScrollViewer) + { + return (ScrollViewer)element; + } + + for (int i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++) + { + var child = VisualTreeHelper.GetChild(element, i); + + var result = GetScrollViewer(child); + if (result == null) + { + continue; + } + else + { + return result; + } + } + + return null; } private void ScrollToUnread() @@ -262,40 +411,242 @@ private void ScrollToUnread() } } + private int GetBottommostIndex() + { + if (ConversationItemsControl.ItemsPanelRoot is ItemsStackPanel sourcePanel) + { + return sourcePanel.LastVisibleIndex; + } + else + { + Logger.LogError("GetBottommostIndex() ItemsPanelRoot is not a valid ItemsStackPanel ({0})", ConversationItemsControl.ItemsPanelRoot); + return -1; + } + } + private void ConversationSettingsButton_Click(object sender, RoutedEventArgs e) { if (SignalConversation is SignalContact) { - App.ViewModels.ConversationSettingsPageInstance.Contact = (SignalContact)SignalConversation; + App.CurrentSignalWindowsFrontend(ApplicationView.GetForCurrentView().Id).Locator.ConversationSettingsPageInstance.Contact = (SignalContact)SignalConversation; GetMainPageVm().View.Frame.Navigate(typeof(ConversationSettingsPage)); } } - } - public class MessageTemplateSelector : DataTemplateSelector - { - public DataTemplate NormalMessage { get; set; } - public DataTemplate UnreadMarker { get; set; } - public DataTemplate IdentityKeyChangeMessage { get; set; } + private void ScrollViewer_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e) + { + if (ActivationState != CoreWindowActivationState.Deactivated) + { + MarkBottommostMessageRead(); + } + } - protected override DataTemplate SelectTemplateCore(object item, DependencyObject container) + private void MarkBottommostMessageRead() { - FrameworkElement element = container as FrameworkElement; - if (item is SignalMessageContainer) + if (Collection != null) { - SignalMessageContainer smc = (SignalMessageContainer)item; - SignalMessage sm = smc.Message; - if (sm.Type == SignalMessageType.IdentityKeyChange) + int bottomIndex = GetBottommostIndex(); + int rawBottomIndex = Collection.GetRawIndex(bottomIndex); + long lastSeenIndex = SignalConversation.LastSeenMessageIndex; + if (lastSeenIndex <= rawBottomIndex && LastMarkReadRequest < rawBottomIndex) { - return IdentityKeyChangeMessage; + LastMarkReadRequest = rawBottomIndex; + var msg = ((IMessageView)Collection[bottomIndex]).Model; + if (msg.Author != null) + { + Task.Run(async () => + { + await App.Handle.SetMessageRead(msg); + }); + } } - return NormalMessage; } - if (item is SignalUnreadMarker) + } + + private async void UserInputBar_OnUnblockButtonClicked() + { + if (SignalConversation is SignalContact contact) + { + contact.Blocked = false; + Blocked = false; + SendMessageVisible = !Blocked; + SignalDBContext.UpdateBlockStatus(contact); + await Task.Run(() => + { + App.Handle.SendBlockedMessage(); + }); + } + } + + private void AddedAttachmentDisplay_OnCancelAttachmentButtonClicked() + { + SetSelectedFile(null); + } + + private void UpdateSendButtonIcon() + { + if (UserInputBar.InputText != string.Empty || SelectedFile != null) { - return UnreadMarker; + UserInputBar.SetSendButtonIcon(Symbol.Send); + } + else + { + UserInputBar.SetSendButtonIcon(Symbol.Attach); + } + } + + private void ResetInput() + { + SetSelectedFile(null); + UserInputBar.InputText = string.Empty; + UpdateSendButtonIcon(); + } + + /// + /// Saves the selected file in StorageApplicationPermissions store and saves conversation in database. + /// + /// Task that saves conversation in database. + public async Task SaveCurrentConversationInDatabase() + { + if (SignalConversation != null) + { + SignalConversation.Draft = UserInputBar.InputText; + if (SelectedFile == null) + { + if (!string.IsNullOrEmpty(SignalConversation.DraftFileTokens) && + StorageApplicationPermissions.FutureAccessList.ContainsItem(SignalConversation.DraftFileTokens)) + { + StorageApplicationPermissions.FutureAccessList.Remove(SignalConversation.DraftFileTokens); + } + SignalConversation.DraftFileTokens = null; + } + else + { + if (string.IsNullOrEmpty(SignalConversation.DraftFileTokens) || + !StorageApplicationPermissions.FutureAccessList.ContainsItem(SignalConversation.DraftFileTokens)) + { + SignalConversation.DraftFileTokens = StorageApplicationPermissions.FutureAccessList.Add(SelectedFile); + } + else + { + // Just reuse the old key + StorageApplicationPermissions.FutureAccessList.AddOrReplace(SignalConversation.DraftFileTokens, SelectedFile); + } + } + // SignalConversation can change while starting Thread. + SignalConversation conversationToSave = SignalConversation; + await Task.Run(() => + { + SignalDBContext.InsertOrUpdateConversationLocked(conversationToSave); + }); + } + } + + private async Task LoadDraft() + { + UserInputBar.InputText = SignalConversation.Draft ?? string.Empty; + UserInputBar.SetCursorPositionToEnd(); + SetSelectedFile(null); + try + { + StorageFile file = !string.IsNullOrWhiteSpace(SignalConversation.DraftFileTokens) && + StorageApplicationPermissions.FutureAccessList.ContainsItem(SignalConversation.DraftFileTokens) ? + await StorageApplicationPermissions.FutureAccessList.GetFileAsync(SignalConversation.DraftFileTokens) : null; + if (file != null) + { + SetSelectedFile(file); + } + } + catch (Exception e) + { + Logger.LogError("LoadDraft() load file failed: {0}\n{1}", e.Message, e.StackTrace); + } + } + + /// + /// Set file as attatchment in UI. + /// + /// Attatchment to set in UI and save + private void SetSelectedFile(StorageFile file) + { + SelectedFile = file; + if (SelectedFile == null) + { + AddedAttachmentDisplay.HideAttachment(); + } + else + { + AddedAttachmentDisplay.ShowAttachment(SelectedFile.Name); + } + UpdateSendButtonIcon(); + } + + private async void Grid_KeyDown(object sender, KeyRoutedEventArgs e) + { + if (e.Key == VirtualKey.V) + { + bool ctrl = CoreWindow.GetForCurrentThread().GetKeyState(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down); + if (ctrl) + { + var dataPackageView = Clipboard.GetContent(); + if (dataPackageView.Contains(StandardDataFormats.StorageItems)) + { + var pastedFiles = await dataPackageView.GetStorageItemsAsync(); + var pastedFile = pastedFiles[0]; + SetSelectedFile(pastedFile as StorageFile); + } + else if (dataPackageView.Contains(StandardDataFormats.Bitmap)) + { + RandomAccessStreamReference pastedBitmap = await dataPackageView.GetBitmapAsync(); + var pastedBitmapStream = await pastedBitmap.OpenReadAsync(); + var tmpFile = await ApplicationData.Current.TemporaryFolder.CreateFileAsync("Signal-Windows-Screenshot.png", CreationCollisionOption.GenerateUniqueName); + using (var tmpFileStream = await tmpFile.OpenAsync(FileAccessMode.ReadWrite)) + { + BitmapDecoder decoder = await BitmapDecoder.CreateAsync(pastedBitmapStream); + var pixels = await decoder.GetPixelDataAsync(); + BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, tmpFileStream); + encoder.SetPixelData(decoder.BitmapPixelFormat, + BitmapAlphaMode.Ignore, // Alpha is not used + decoder.OrientedPixelWidth, + decoder.OrientedPixelHeight, + decoder.DpiX, decoder.DpiY, + pixels.DetachPixelData()); + await encoder.FlushAsync(); + } + SetSelectedFile(tmpFile); + } + } + } + } + + private void Grid_DragOver(object sender, DragEventArgs e) + { + e.AcceptedOperation = DataPackageOperation.Copy; + e.DragUIOverride.IsCaptionVisible = false; + e.DragUIOverride.IsGlyphVisible = false; + } + + private async void Grid_Drop(object sender, DragEventArgs e) + { + if (e.DataView.Contains(StandardDataFormats.StorageItems)) + { + var storageItems = await e.DataView.GetStorageItemsAsync(); + var storageItem = storageItems[0]; + SetSelectedFile(storageItem as StorageFile); + } + } + + private void ConversationItemsControl_SizeChanged(object sender, SizeChangedEventArgs e) + { + var scrollbar = GetScrollViewer(this); + if (scrollbar != null) + { + var verticalDelta = e.PreviousSize.Height - e.NewSize.Height; + if (verticalDelta > 0) + { + scrollbar.ChangeView(null, scrollbar.VerticalOffset + verticalDelta, null); + } } - return null; } } -} \ No newline at end of file +} diff --git a/Signal-Windows/Controls/ConversationListElement.xaml b/Signal-Windows/Controls/ConversationListElement.xaml index 06ab6f9..91d55c2 100644 --- a/Signal-Windows/Controls/ConversationListElement.xaml +++ b/Signal-Windows/Controls/ConversationListElement.xaml @@ -3,6 +3,7 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:Signal_Windows.Controls" + xmlns:model="using:Signal_Windows.Models" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" @@ -17,16 +18,32 @@ - - + + + + + + + - - + + + - - + + + + + + + + + + + + \ No newline at end of file diff --git a/Signal-Windows/Controls/ConversationListElement.xaml.cs b/Signal-Windows/Controls/ConversationListElement.xaml.cs index f421b70..eb68830 100644 --- a/Signal-Windows/Controls/ConversationListElement.xaml.cs +++ b/Signal-Windows/Controls/ConversationListElement.xaml.cs @@ -1,4 +1,5 @@ using Signal_Windows.Models; +using Signal_Windows.Views; using System.ComponentModel; using System.Diagnostics; using Windows.UI.Xaml; @@ -32,7 +33,6 @@ public SignalConversation Model } private uint _UnreadCount; - public uint UnreadCount { get @@ -43,6 +43,26 @@ public uint UnreadCount { _UnreadCount = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(UnreadString))); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(UnreadStringVisibility))); + } + } + + public Visibility UnreadStringVisibility + { + get + { + if (UnreadCount > 0) + { + return Visibility.Visible; + } + else + { + return Visibility.Collapsed; + } + } + set + { + // we never set this } } @@ -59,10 +79,13 @@ public string UnreadString return ""; } } + set + { + // we never set this + } } private string _LastMessage = "@"; - public string LastMessage { get @@ -73,11 +96,29 @@ public string LastMessage { _LastMessage = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(LastMessage))); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(LastMessageVisibility))); } } - private Brush _FillBrush = Utils.Blue; + public Visibility LastMessageVisibility + { + get + { + if (string.IsNullOrEmpty(LastMessage)) + { + return Visibility.Collapsed; + } + else + { + return Visibility.Visible; + } + } + set + { + } + } + private Brush _FillBrush = Utils.Blue; public Brush FillBrush { get @@ -92,7 +133,6 @@ public Brush FillBrush } private string _Initials = string.Empty; - public string Initials { get @@ -113,28 +153,67 @@ public string LastMessageTimestamp set { _LastMessageTimestamp = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(LastMessageTimestamp))); } } + private bool blockedIconVisible; + public bool BlockedIconVisible + { + get { return blockedIconVisible; } + set { blockedIconVisible = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(BlockedIconVisible))); } + } + private void ThreadListItem_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args) { if (Model != null) { - Model.View = this; + var frame = Window.Current.Content as Frame; + if (frame != null) + { + if (frame.CurrentSourcePageType == typeof(BlockedContactsPage)) + { + UpdateBlockedContactElement(); + Model.UpdateUI = UpdateBlockedContactElement; + } + else + { + UpdateConversationDisplay(); + Model.UpdateUI = UpdateConversationDisplay; + } + } + } + } + + public void UpdateConversationDisplay() + { + if (Model != null) + { + if (Model is SignalContact contact) + { + BlockedIconVisible = contact.Blocked; + FillBrush = contact.Color != null ? Utils.GetBrushFromColor((contact.Color)) : + Utils.GetBrushFromColor(Utils.CalculateDefaultColor(Model.ThreadDisplayName)); + } + else + { + FillBrush = Utils.Blue; + } ConversationDisplayName.Text = Model.ThreadDisplayName; UnreadCount = Model.UnreadCount; LastMessage = Model.LastMessage?.Content.Content; Initials = Utils.GetInitials(Model.ThreadDisplayName); - FillBrush = Model is SignalContact ? Utils.GetBrushFromColor(((SignalContact)Model).Color) : Utils.Blue; LastMessageTimestamp = Utils.GetTimestamp(Model.LastActiveTimestamp); } } - public void UpdateConversationDisplay(SignalConversation thread) + public void UpdateBlockedContactElement() { - Model.ThreadDisplayName = thread.ThreadDisplayName; - Model.LastActiveTimestamp = thread.LastActiveTimestamp; - ConversationDisplayName.Text = thread.ThreadDisplayName; - UnreadCount = thread.UnreadCount; - LastMessage = Model.LastMessage?.Content.Content; - LastMessageTimestamp = Utils.GetTimestamp(Model.LastActiveTimestamp); + if (Model != null) + { + SignalContact contact = (SignalContact)Model; + FillBrush = contact.Color != null ? Utils.GetBrushFromColor((contact.Color)) : + Utils.GetBrushFromColor(Utils.CalculateDefaultColor(Model.ThreadDisplayName)); + ConversationDisplayName.Text = Model.ThreadDisplayName; + Initials = Utils.GetInitials(Model.ThreadDisplayName); + LastMessage = null; + } } } } \ No newline at end of file diff --git a/Signal-Windows/Controls/IdentityKeyChangeMessage.xaml.cs b/Signal-Windows/Controls/IdentityKeyChangeMessage.xaml.cs index 30ccf59..f7c298d 100644 --- a/Signal-Windows/Controls/IdentityKeyChangeMessage.xaml.cs +++ b/Signal-Windows/Controls/IdentityKeyChangeMessage.xaml.cs @@ -1,4 +1,5 @@ -using Signal_Windows.Models; +using Signal_Windows.Lib; +using Signal_Windows.Models; using System; using System.Collections.Generic; using System.IO; @@ -18,18 +19,20 @@ namespace Signal_Windows.Controls { - public sealed partial class IdentityKeyChangeMessage : UserControl + public sealed partial class IdentityKeyChangeMessage : UserControl, IMessageView { - public IdentityKeyChangeMessage() + public IdentityKeyChangeMessage(SignalMessage model) { this.InitializeComponent(); - DataContextChanged += IdentityKeyChangeMessage_DataContextChanged; + this.DataContextChanged += IdentityKeyChangeMessage_DataContextChanged; + Model = model; } - public SignalMessageContainer Model + + public SignalMessage Model { get { - return this.DataContext as SignalMessageContainer; + return this.DataContext as SignalMessage; } set { @@ -37,16 +40,26 @@ public SignalMessageContainer Model } } + public void HandleUpdate(SignalMessage m) + { + throw new NotImplementedException(); + } + private void IdentityKeyChangeMessage_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args) { if (Model != null) { - MessageTextBlock.Text = Model.Message.Content.Content; + MessageTextBlock.Text = Model.Content.Content; } else { MessageTextBlock.Text = "null"; } } + + public FrameworkElement AsFrameworkElement() + { + return this; + } } } diff --git a/Signal-Windows/Controls/Message.xaml b/Signal-Windows/Controls/Message.xaml index 8dc2810..e8ce145 100644 --- a/Signal-Windows/Controls/Message.xaml +++ b/Signal-Windows/Controls/Message.xaml @@ -16,8 +16,9 @@ - - + + + @@ -26,7 +27,7 @@ Send again - + diff --git a/Signal-Windows/Controls/Message.xaml.cs b/Signal-Windows/Controls/Message.xaml.cs index be6552b..a282cc5 100644 --- a/Signal-Windows/Controls/Message.xaml.cs +++ b/Signal-Windows/Controls/Message.xaml.cs @@ -1,24 +1,34 @@ +using Signal_Windows.Lib; using Signal_Windows.Models; using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; +using System.Text.RegularExpressions; using Windows.Storage; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Documents; using Windows.UI.Xaml.Media; // The User Control item template is documented at https://go.microsoft.com/fwlink/?LinkId=234236 namespace Signal_Windows.Controls { - public sealed partial class Message : UserControl + public sealed partial class Message : UserControl, IMessageView, INotifyPropertyChanged { - public SignalMessageContainer Model + // This is taken from https://gist.github.com/gruber/8891611 + // This is public domain: https://daringfireball.net/2010/07/improved_regex_for_matching_urls + private const string UrlRegexString = @"(?i)\b((?:https?:(?:/{1,3}|[a-z0-9%])|[a-z0-9.\-]+[.](?:com|net|org|edu|gov|mil|aero|asia|biz|cat|coop|info|int|jobs|mobi|museum|name|post|pro|tel|travel|xxx|ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cs|cu|cv|cx|cy|cz|dd|de|dj|dk|dm|do|dz|ec|ee|eg|eh|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|Ja|sk|sl|sm|sn|so|sr|ss|st|su|sv|sx|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|yu|za|zm|zw)/)(?:[^\s()<>{}\[\]]+|\([^\s()]*?\([^\s()]+\)[^\s()]*?\)|\([^\s]+?\))+(?:\([^\s()]*?\([^\s()]+\)[^\s()]*?\)|\([^\s]+?\)|[^\s`!()\[\]{};:'"".,<>?«»“”‘’])|(?:(? 0) + { + HasAttachment = true; + Attachment = Model.Attachments[0]; + } + } + } + + private void UpdateMessageTextBlock() + { + string messageText = Model.Content.Content; + var matches = urlRegex.Matches(messageText); + if (matches.Count == 0) + { + MessageContentTextBlock.Text = messageText; + } + else + { + MessageContentTextBlock.Inlines.Clear(); + int previousIndex = 0; + int currentIndex = 0; + foreach (Match match in matches) + { + // First create a Run of the text before the link + currentIndex = match.Index; + var length = currentIndex - previousIndex; + if (length > 0) + { + Run run = new Run + { + Text = messageText.Substring(previousIndex, currentIndex - previousIndex) + }; + MessageContentTextBlock.Inlines.Add(run); + } + + // Now add the hyperlink + string link = match.Value; + Hyperlink hyperlink = new Hyperlink(); + Run hyperlinkRun = new Run + { + Text = link + }; + try + { + hyperlink.NavigateUri = new Uri(link); + } + catch (Exception) + { + MessageContentTextBlock.Inlines.Add(new Run() + { + Text = link + }); + continue; + } + finally + { + previousIndex = currentIndex + match.Length; + currentIndex = previousIndex; + } + hyperlink.UnderlineStyle = UnderlineStyle.Single; + hyperlink.Inlines.Add(hyperlinkRun); + MessageContentTextBlock.Inlines.Add(hyperlink); + } + + // Then finish up by adding the rest of the message text to the TextBox + var restLength = messageText.Length - currentIndex; + if (restLength > 0) + { + Run restRun = new Run + { + Text = messageText.Substring(currentIndex, restLength) + }; + MessageContentTextBlock.Inlines.Add(restRun); + } } } @@ -102,14 +203,18 @@ private void MessageBox_DataContextChanged(FrameworkElement sender, DataContextC private void ResendTextBlock_Tapped(object sender, Windows.UI.Xaml.Input.TappedRoutedEventArgs e) { - App.ViewModels.MainPageInstance.OutgoingQueue.Add(Model.Message); + App.Handle.ResendMessage(Model); } - internal bool HandleUpdate(SignalMessage updatedMessage) + public void HandleUpdate(SignalMessage m) { - Model.Message.Status = updatedMessage.Status; + Model.Status = m.Status; UpdateUI(); - return updatedMessage.Status != SignalMessageStatus.Received; + } + + public FrameworkElement AsFrameworkElement() + { + return this; } } -} \ No newline at end of file +} diff --git a/Signal-Windows/Controls/UnreadMarker.xaml.cs b/Signal-Windows/Controls/UnreadMarker.xaml.cs index 994f518..4eda246 100644 --- a/Signal-Windows/Controls/UnreadMarker.xaml.cs +++ b/Signal-Windows/Controls/UnreadMarker.xaml.cs @@ -23,23 +23,6 @@ public sealed partial class UnreadMarker : UserControl public UnreadMarker() { this.InitializeComponent(); - DataContextChanged += UnreadMarker_DataContextChanged; - } - - public SignalUnreadMarker Model - { - get - { - return this.DataContext as SignalUnreadMarker; - } - } - - private void UnreadMarker_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args) - { - if (Model != null) - { - UnreadText.Text = Model.Text; - } } public void SetText(string text) diff --git a/Signal-Windows/Controls/UserInput.xaml b/Signal-Windows/Controls/UserInput.xaml new file mode 100644 index 0000000..9baf3b1 --- /dev/null +++ b/Signal-Windows/Controls/UserInput.xaml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Signal-Windows/Views/AttachmentDetailsPage.xaml.cs b/Signal-Windows/Views/AttachmentDetailsPage.xaml.cs new file mode 100644 index 0000000..2beca5a --- /dev/null +++ b/Signal-Windows/Views/AttachmentDetailsPage.xaml.cs @@ -0,0 +1,66 @@ +using Signal_Windows.Models; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using Windows.Foundation; +using Windows.Foundation.Collections; +using Windows.Storage; +using Windows.UI.Core; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Data; +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Navigation; + +// The Blank Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=234238 + +namespace Signal_Windows.Views +{ + /// + /// An empty page that can be used on its own or navigated to within a Frame. + /// + public sealed partial class AttachmentDetailsPage : Page + { + public ObservableCollection Attachments { get; set; } = new ObservableCollection(); + private SignalAttachment Attachment; + + public AttachmentDetailsPage() + { + this.InitializeComponent(); + } + + public string Path { get; set; } + + protected override void OnNavigatedTo(NavigationEventArgs e) + { + base.OnNavigatedTo(e); + Utils.EnableBackButton(BackButton_Click); + Attachments.Clear(); + Attachment = e.Parameter as SignalAttachment; + Attachments.Add($"{ApplicationData.Current.LocalCacheFolder.Path}/Attachments/{Attachment.Id}.plain"); + } + + protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) + { + base.OnNavigatingFrom(e); + Utils.DisableBackButton(BackButton_Click); + } + + private void BackButton_Click(object sender, BackRequestedEventArgs e) + { + Frame.GoBack(); + e.Handled = true; + } + + private async void ExportButton_Click(object sender, RoutedEventArgs e) + { + await App.Handle.ExportAttachment(Attachment); + } + } +} diff --git a/Signal-Windows/Views/BlockedContactsPage.xaml b/Signal-Windows/Views/BlockedContactsPage.xaml new file mode 100644 index 0000000..d8161a4 --- /dev/null +++ b/Signal-Windows/Views/BlockedContactsPage.xaml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + diff --git a/Signal-Windows/Views/BlockedContactsPage.xaml.cs b/Signal-Windows/Views/BlockedContactsPage.xaml.cs new file mode 100644 index 0000000..6e50779 --- /dev/null +++ b/Signal-Windows/Views/BlockedContactsPage.xaml.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using Signal_Windows.Models; +using Signal_Windows.ViewModels; +using Windows.Foundation; +using Windows.Foundation.Collections; +using Windows.UI.Core; +using Windows.UI.ViewManagement; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Data; +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Navigation; + +// The Blank Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=234238 + +namespace Signal_Windows.Views +{ + /// + /// An empty page that can be used on its own or navigated to within a Frame. + /// + public sealed partial class BlockedContactsPage : Page + { + public BlockedContactsPage() + { + this.InitializeComponent(); + } + + public BlockedContactsPageViewModel Vm + { + get { return (BlockedContactsPageViewModel)DataContext; } + } + + protected override void OnNavigatedTo(NavigationEventArgs e) + { + Utils.EnableBackButton(BackButton_Click); + Vm.OnNavigatedTo(); + } + + protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) + { + Utils.DisableBackButton(BackButton_Click); + } + + private void BackButton_Click(object sender, BackRequestedEventArgs e) + { + Frame.GoBack(); + e.Handled = true; + } + + private void BlockedContactsListView_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (e.AddedItems.Count == 1) + { + var contact = e.AddedItems[0] as SignalContact; + App.CurrentSignalWindowsFrontend(ApplicationView.GetForCurrentView().Id).Locator.ConversationSettingsPageInstance.Contact = contact; + Frame.Navigate(typeof(ConversationSettingsPage)); + } + } + } +} diff --git a/Signal-Windows/Views/CaptchaPage.xaml b/Signal-Windows/Views/CaptchaPage.xaml new file mode 100644 index 0000000..a2f419a --- /dev/null +++ b/Signal-Windows/Views/CaptchaPage.xaml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/Signal-Windows/Views/CaptchaPage.xaml.cs b/Signal-Windows/Views/CaptchaPage.xaml.cs new file mode 100644 index 0000000..1cf0b12 --- /dev/null +++ b/Signal-Windows/Views/CaptchaPage.xaml.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using Signal_Windows.ViewModels; +using Windows.Foundation; +using Windows.Foundation.Collections; +using Windows.UI.Core; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Data; +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Navigation; + +// The Blank Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=234238 + +namespace Signal_Windows.Views +{ + // Pretty much the only reason this page exists is because putting the WebView on the RegisterPage wouldn't + // correctly load the CAPTCHA part of the web page for me. + public sealed partial class CaptchaPage : Page + { + public CaptchaPageViewModel Vm + { + get + { + return (CaptchaPageViewModel)DataContext; + } + } + + public CaptchaPage() + { + this.InitializeComponent(); + Vm.View = this; + } + + protected override void OnNavigatedTo(NavigationEventArgs e) + { + base.OnNavigatedTo(e); + Vm.OnNavigatedTo(); + } + + protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) + { + base.OnNavigatingFrom(e); + Vm.OnNavigatingFrom(); + } + + private void refreshButton_Click(object sender, RoutedEventArgs e) + { + // KeyDown event doesn't work with WebView so just use a button to allow users to refresh the page + webView.Refresh(); + } + } +} diff --git a/Signal-Windows/Views/ChatsAndMediaSettingsPage.xaml b/Signal-Windows/Views/ChatsAndMediaSettingsPage.xaml new file mode 100644 index 0000000..56eb5ed --- /dev/null +++ b/Signal-Windows/Views/ChatsAndMediaSettingsPage.xaml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Signal-Windows/Views/GlobalSettingsPage.xaml.cs b/Signal-Windows/Views/GlobalSettingsPage.xaml.cs new file mode 100644 index 0000000..44f3d08 --- /dev/null +++ b/Signal-Windows/Views/GlobalSettingsPage.xaml.cs @@ -0,0 +1,83 @@ +using Signal_Windows.ViewModels; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using Windows.Foundation; +using Windows.Foundation.Collections; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Data; +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Navigation; + +// The Blank Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=234238 + +namespace Signal_Windows.Views +{ + /// + /// An empty page that can be used on its own or navigated to within a Frame. + /// + public sealed partial class GlobalSettingsPage : Page + { + public GlobalSettingsPage() + { + this.InitializeComponent(); + Vm.View = this; + } + + public GlobalSettingsPageViewModel Vm + { + get + { + return (GlobalSettingsPageViewModel)DataContext; + } + } + + protected override void OnNavigatedTo(NavigationEventArgs ev) + { + base.OnNavigatedTo(ev); + Utils.EnableBackButton(Vm.BackButton_Click); + Vm.OnNavigatedTo(); + } + + protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) + { + base.OnNavigatingFrom(e); + Utils.DisableBackButton(Vm.BackButton_Click); + } + + private void NotificationsButton_Click(object sender, RoutedEventArgs e) + { + Frame.Navigate(typeof(NotificationSettingsPage)); + } + + private void PrivacyButton_Click(object sender, RoutedEventArgs e) + { + Frame.Navigate(typeof(PrivacySettingsPage)); + } + + private void AppearanceButton_Click(object sender, RoutedEventArgs e) + { + Frame.Navigate(typeof(AppearanceSettingsPage)); + } + + private void ChatsAndMediaButton_Click(object sender, RoutedEventArgs e) + { + Frame.Navigate(typeof(ChatsAndMediaSettingsPage)); + } + + private void AdvancedButton_Click(object sender, RoutedEventArgs e) + { + Frame.Navigate(typeof(AdvancedSettingsPage)); + } + + private void AboutButton_Click(object sender, RoutedEventArgs e) + { + Frame.Navigate(typeof(AboutPage)); + } + } +} diff --git a/Signal-Windows/Views/LinkPage.xaml b/Signal-Windows/Views/LinkPage.xaml index 70f1ea8..504101c 100644 --- a/Signal-Windows/Views/LinkPage.xaml +++ b/Signal-Windows/Views/LinkPage.xaml @@ -35,7 +35,7 @@ This device's name will be - + diff --git a/Signal-Windows/Views/LinkPage.xaml.cs b/Signal-Windows/Views/LinkPage.xaml.cs index 9dea535..fa19bf4 100644 --- a/Signal-Windows/Views/LinkPage.xaml.cs +++ b/Signal-Windows/Views/LinkPage.xaml.cs @@ -1,4 +1,5 @@ using Signal_Windows.ViewModels; +using System.Threading.Tasks; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Navigation; using ZXing.Mobile; @@ -53,16 +54,20 @@ public void SetQR(string qr) QRCode.Source = writer.Write(qr); } - public void Finish(bool success) + public string GetDeviceName() + { + return DeviceName.Text; + } + + public async Task Finish(bool success) { if (success) { + var frontend = App.CurrentSignalWindowsFrontend(App.MainViewId); + await App.Handle.Reacquire(); + App.Handle.RequestSync(); Frame.Navigate(typeof(MainPage)); } } - - private void FinishButton_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e) - { - } } } \ No newline at end of file diff --git a/Signal-Windows/Views/MainPage.xaml b/Signal-Windows/Views/MainPage.xaml index 71d7a75..aced0f9 100644 --- a/Signal-Windows/Views/MainPage.xaml +++ b/Signal-Windows/Views/MainPage.xaml @@ -8,6 +8,7 @@ xmlns:viewmodels="using:Signal_Windows.ViewModels" xmlns:controls="using:Signal_Windows.Controls" mc:Ignorable="d" + NavigationCacheMode="Required" DataContext="{Binding MainPageInstance, Source={StaticResource Locator}}"> @@ -24,23 +25,33 @@ - + - - - + + + - - + + + + + + + + + + - + - + diff --git a/Signal-Windows/Views/MainPage.xaml.cs b/Signal-Windows/Views/MainPage.xaml.cs index 816bb5b..6077b2a 100644 --- a/Signal-Windows/Views/MainPage.xaml.cs +++ b/Signal-Windows/Views/MainPage.xaml.cs @@ -1,10 +1,16 @@ +using libsignalservice; +using Microsoft.Extensions.Logging; using Signal_Windows.Controls; +using Signal_Windows.Lib; +using Signal_Windows.Models; using Signal_Windows.ViewModels; using Signal_Windows.Views; using System; +using System.Diagnostics; using System.Threading.Tasks; using Windows.Foundation; using Windows.UI.Popups; +using Windows.UI.ViewManagement; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Input; @@ -19,49 +25,40 @@ namespace Signal_Windows /// public sealed partial class MainPage : Page { + private readonly ILogger Logger = LibsignalLogging.CreateLogger(); public MainPage() { this.InitializeComponent(); Vm.View = this; - Loaded += MainPage_Loaded; - Unloaded += MainPage_Unloaded; - } - - private void MainPage_Unloaded(object sender, RoutedEventArgs e) - { - Frame.SizeChanged -= Frame_SizeChanged; - } - - private void MainPage_Loaded(object sender, RoutedEventArgs e) - { - Frame.SizeChanged += Frame_SizeChanged; - SwitchToStyle(GetCurrentViewStyle()); - MainPanel.DisplayMode = SplitViewDisplayMode.CompactInline; } public void SwitchToStyle(PageStyle newStyle) { - if (newStyle == PageStyle.Narrow) + var frame = Window.Current.Content as Frame; + if (frame?.CurrentSourcePageType == typeof(MainPage)) { - if (Vm.SelectedThread != null) + if (newStyle == PageStyle.Narrow) { - Utils.EnableBackButton(Vm.BackButton_Click); - MainPanel.IsPaneOpen = false; - MainPanel.CompactPaneLength = 0; + if (Vm.SelectedThread != null) + { + Utils.EnableBackButton(Vm.BackButton_Click); + Vm.IsPaneOpen = false; + Vm.CompactPaneLength = 0; + } + else + { + Unselect(); + Vm.IsPaneOpen = true; + } } - else + else if (newStyle == PageStyle.Wide) { - Unselect(); - MainPanel.IsPaneOpen = true; + Utils.DisableBackButton(Vm.BackButton_Click); + Vm.IsPaneOpen = false; + Vm.CompactPaneLength = ContactsGrid.Width = 320; } + UpdateStyle(newStyle); } - else if (newStyle == PageStyle.Wide) - { - Utils.DisableBackButton(Vm.BackButton_Click); - MainPanel.IsPaneOpen = false; - MainPanel.CompactPaneLength = ContactsGrid.Width = 320; - } - UpdateStyle(newStyle); } private void UpdateStyle(PageStyle currentStyle) @@ -70,34 +67,39 @@ private void UpdateStyle(PageStyle currentStyle) { // TODO: When phone is in landscape mode this is incorrect and some stuff gets cut off, we need to // get the actual useable width (actualwidth - top icon bar - bottom control bar) - ContactsGrid.Width = ActualWidth; + ContactsGrid.Width = Frame.ActualWidth; if (Vm.SelectedThread == null) { - MainPanel.OpenPaneLength = ActualWidth; + Vm.OpenPaneLength = Frame.ActualWidth; } } else if (currentStyle == PageStyle.Wide) { - MainPanel.CompactPaneLength = MainPanel.OpenPaneLength = ContactsGrid.Width = 320; + Vm.CompactPaneLength = Vm.OpenPaneLength = ContactsGrid.Width = 320; } } public PageStyle GetCurrentViewStyle() { - return Utils.GetViewStyle(new Size(ActualWidth, ActualHeight)); + return Utils.GetViewStyle(new Size(Frame.ActualWidth, ActualHeight)); } - protected override async void OnNavigatedTo(NavigationEventArgs e) + protected override void OnNavigatedTo(NavigationEventArgs e) { - base.OnNavigatedTo(e); - await Vm.OnNavigatedTo(); + Vm.RequestedConversationId = e.Parameter as string; + SwitchToStyle(GetCurrentViewStyle()); + Vm.DisplayMode = SplitViewDisplayMode.CompactInline; + Frame.SizeChanged += Frame_SizeChanged; + Vm.TrySelectConversation(Vm.RequestedConversationId); } - protected override async void OnNavigatingFrom(NavigatingCancelEventArgs e) + protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) { - base.OnNavigatingFrom(e); - ContactsList.SelectedItem = null; - await Vm.OnNavigatingFrom(); + Frame.SizeChanged -= Frame_SizeChanged; + if (GetCurrentViewStyle() == PageStyle.Narrow) + { + Utils.DisableBackButton(Vm.BackButton_Click); + } } private void Frame_SizeChanged(object sender, SizeChangedEventArgs e) @@ -127,11 +129,6 @@ public Conversation Thread } } - private void ContactsList_SelectionChanged(object sender, SelectionChangedEventArgs e) - { - Vm.ContactsList_SelectionChanged(sender, e); - } - public static async Task NotifyNewIdentity(string user) { var title = "Safety Numbers Change"; @@ -155,10 +152,20 @@ public void ReselectTop() private void AddContactButton_Click(object sender, RoutedEventArgs e) { - App.ViewModels.AddContactPageInstance.MainPageVM = Vm; - //App.ViewModels.AddContactPageInstance.ContactName = ""; - //App.ViewModels.AddContactPageInstance.ContactNumber = ""; + var signalWindowsFrontend = App.CurrentSignalWindowsFrontend(ApplicationView.GetForCurrentView().Id); + signalWindowsFrontend.Locator.AddContactPageInstance.ContactName = ""; + signalWindowsFrontend.Locator.AddContactPageInstance.ContactNumber = ""; Frame.Navigate(typeof(AddContactPage)); } + + internal void Reload() + { + ConversationControl.Load(Vm.SelectedThread); + } + + private void GlobalSettingsButton_Click(object sender, RoutedEventArgs e) + { + Frame.Navigate(typeof(GlobalSettingsPage)); + } } } \ No newline at end of file diff --git a/Signal-Windows/Views/NotificationSettingsPage.xaml b/Signal-Windows/Views/NotificationSettingsPage.xaml new file mode 100644 index 0000000..c8088e4 --- /dev/null +++ b/Signal-Windows/Views/NotificationSettingsPage.xaml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Signal-Windows/Views/NotificationSettingsPage.xaml.cs b/Signal-Windows/Views/NotificationSettingsPage.xaml.cs new file mode 100644 index 0000000..568ff69 --- /dev/null +++ b/Signal-Windows/Views/NotificationSettingsPage.xaml.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using Signal_Windows.Lib; +using Signal_Windows.ViewModels; +using Windows.Foundation; +using Windows.Foundation.Collections; +using Windows.UI.Core; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Data; +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Navigation; + +// The Blank Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=234238 + +namespace Signal_Windows.Views +{ + /// + /// An empty page that can be used on its own or navigated to within a Frame. + /// + public sealed partial class NotificationSettingsPage : Page + { + public NotificationSettingsPage() + { + this.InitializeComponent(); + } + + public NotificationSettingsPageViewModel Vm + { + get + { + return (NotificationSettingsPageViewModel)DataContext; + } + } + + protected override void OnNavigatedTo(NavigationEventArgs e) + { + Utils.EnableBackButton(BackButton_Click); + Vm.OnNavigatedTo(); + } + + protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) + { + Utils.DisableBackButton(BackButton_Click); + } + + private void BackButton_Click(object sender, BackRequestedEventArgs e) + { + Frame.GoBack(); + e.Handled = true; + } + + private void ShowNotificationText_Checked(object sender, RoutedEventArgs e) + { + var radioButton = sender as RadioButton; + string tag = radioButton.Tag.ToString(); + if (tag == Vm.NameAndMessageTag) + { + if (GlobalSettingsManager.ShowNotificationTextSetting != GlobalSettingsManager.ShowNotificationTextSettings.NameAndMessage) + { + GlobalSettingsManager.ShowNotificationTextSetting = GlobalSettingsManager.ShowNotificationTextSettings.NameAndMessage; + Vm.NameAndMessageChecked = true; + } + } + else if (tag == Vm.NameOnlyTag) + { + if (GlobalSettingsManager.ShowNotificationTextSetting != GlobalSettingsManager.ShowNotificationTextSettings.NameOnly) + { + GlobalSettingsManager.ShowNotificationTextSetting = GlobalSettingsManager.ShowNotificationTextSettings.NameOnly; + Vm.NameOnlyChecked = true; + } + } + else if (tag == Vm.NoNameOrMessageTag) + { + if (GlobalSettingsManager.ShowNotificationTextSetting != GlobalSettingsManager.ShowNotificationTextSettings.NoNameOrMessage) + { + GlobalSettingsManager.ShowNotificationTextSetting = GlobalSettingsManager.ShowNotificationTextSettings.NoNameOrMessage; + Vm.NoNameOrMessageChecked = true; + } + } + } + } +} diff --git a/Signal-Windows/Views/PrivacySettingsPage.xaml b/Signal-Windows/Views/PrivacySettingsPage.xaml new file mode 100644 index 0000000..378d50b --- /dev/null +++ b/Signal-Windows/Views/PrivacySettingsPage.xaml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +