[Pkg-privacy-commits] [ricochet-im] 01/03: Imported Upstream version 1.1.1
Ximin Luo
infinity0 at debian.org
Wed Sep 30 16:03:35 UTC 2015
This is an automated email from the git hooks/post-receive script.
infinity0 pushed a commit to branch master
in repository ricochet-im.
commit 788547c77a6f80c4165ec6e67f4734458adf85dd
Author: Ximin Luo <infinity0 at pdeb1>
Date: Wed Sep 30 17:27:03 2015 +0200
Imported Upstream version 1.1.1
---
AUTHORS.md | 33 +
BUILDING.md | 105 ++
LICENSE | 233 ++++
README.md | 35 +
config.tests/mingw-64aslr/mingw-64aslr.pro | 3 +
config.tests/mingw-64aslr/test.cpp | 1 +
.../sanitize-ubsan-more/sanitize-ubsan-more.pro | 4 +
config.tests/sanitize-ubsan-more/test.cpp | 1 +
config.tests/sanitize-ubsan/sanitize-ubsan.pro | 4 +
config.tests/sanitize-ubsan/test.cpp | 1 +
config.tests/sanitize/sanitize.pro | 4 +
config.tests/sanitize/test.cpp | 1 +
.../stack-protector-strong.pro | 4 +
config.tests/stack-protector-strong/test.cpp | 1 +
config.tests/stack-protector/stack-protector.pro | 4 +
config.tests/stack-protector/test.cpp | 1 +
config.tests/vtable-verify/test.cpp | 1 +
config.tests/vtable-verify/vtable-verify.pro | 4 +
contrib/README.sandboxing | 49 +
contrib/ricochet-seccomp-amd64.policy | 111 ++
contrib/ricochet-seccomp-arm.policy | 111 ++
contrib/ricochet-seccomp-x86.policy | 111 ++
contrib/usr.bin.ricochet-apparmor | 56 +
doc/deprecated/protocol-1.0.txt | 396 ++++++
doc/design.md | 70 ++
doc/protocol.md | 537 ++++++++
hardened.pri | 46 +
icons/LICENSE | 45 +
icons/Ricochet.icns | Bin 0 -> 728302 bytes
icons/icons.qrc | 6 +
icons/ricochet.ico | Bin 0 -> 56790 bytes
icons/ricochet.png | Bin 0 -> 3325 bytes
icons/ricochet.svg | 1324 ++++++++++++++++++++
icons/ricochet_icons.json | 34 +
icons/ricochet_icons.ttf | Bin 0 -> 5200 bytes
packaging/installer/SetupModern11.bmp | Bin 0 -> 52574 bytes
packaging/installer/building.txt | 11 +
packaging/installer/installer.iss | 116 ++
packaging/installer/prepare.bat | 3 +
packaging/linux-static/content/QtMultimedia/qmldir | 4 +
packaging/linux-static/content/QtQuick.2/qmldir | 4 +
.../linux-static/content/QtQuick/Controls/qmldir | 4 +
.../linux-static/content/QtQuick/Dialogs/qmldir | 4 +
.../linux-static/content/QtQuick/Layouts/qmldir | 4 +
.../content/QtQuick/PrivateWidgets/qmldir | 4 +
.../linux-static/content/QtQuick/Window.2/qmldir | 4 +
packaging/linux-static/content/README | 49 +
packaging/linux-static/release.sh | 52 +
packaging/osx/release_osx.sh | 57 +
packaging/rpm/ricochet.spec | 66 +
protobuf.pri | 51 +
ricochet.pro | 264 ++++
sounds/LICENSE | 7 +
sounds/message.wav | Bin 0 -> 70704 bytes
sounds/online.wav | Bin 0 -> 217956 bytes
sounds/sounds.qrc | 6 +
src/Info.plist | 22 +
src/core/ContactIDValidator.cpp | 115 ++
src/core/ContactIDValidator.h | 70 ++
src/core/ContactUser.cpp | 536 ++++++++
src/core/ContactUser.h | 173 +++
src/core/ContactsManager.cpp | 208 +++
src/core/ContactsManager.h | 102 ++
src/core/ConversationModel.cpp | 318 +++++
src/core/ConversationModel.h | 117 ++
src/core/IdentityManager.cpp | 155 +++
src/core/IdentityManager.h | 79 ++
src/core/IncomingRequestManager.cpp | 372 ++++++
src/core/IncomingRequestManager.h | 161 +++
src/core/OutgoingContactRequest.cpp | 218 ++++
src/core/OutgoingContactRequest.h | 104 ++
src/core/UserIdentity.cpp | 230 ++++
src/core/UserIdentity.h | 122 ++
src/main.cpp | 341 +++++
src/protocol/AuthHiddenService.proto | 25 +
src/protocol/AuthHiddenServiceChannel.cpp | 351 ++++++
src/protocol/AuthHiddenServiceChannel.h | 76 ++
src/protocol/Channel.cpp | 328 +++++
src/protocol/Channel.h | 234 ++++
src/protocol/Channel_p.h | 99 ++
src/protocol/ChatChannel.cpp | 196 +++
src/protocol/ChatChannel.h | 77 ++
src/protocol/ChatChannel.proto | 18 +
src/protocol/Connection.cpp | 661 ++++++++++
src/protocol/Connection.h | 243 ++++
src/protocol/Connection_p.h | 96 ++
src/protocol/ContactRequestChannel.cpp | 292 +++++
src/protocol/ContactRequestChannel.h | 86 ++
src/protocol/ContactRequestChannel.proto | 35 +
src/protocol/ControlChannel.cpp | 286 +++++
src/protocol/ControlChannel.h | 75 ++
src/protocol/ControlChannel.proto | 52 +
src/protocol/OutboundConnector.cpp | 314 +++++
src/protocol/OutboundConnector.h | 110 ++
src/ricochet.desktop | 9 +
src/tor/AuthenticateCommand.cpp | 64 +
src/tor/AuthenticateCommand.h | 63 +
src/tor/GetConfCommand.cpp | 124 ++
src/tor/GetConfCommand.h | 77 ++
src/tor/HiddenService.cpp | 140 +++
src/tor/HiddenService.h | 99 ++
src/tor/ProtocolInfoCommand.cpp | 85 ++
src/tor/ProtocolInfoCommand.h | 78 ++
src/tor/SetConfCommand.cpp | 106 ++
src/tor/SetConfCommand.h | 78 ++
src/tor/TorControl.cpp | 690 ++++++++++
src/tor/TorControl.h | 134 ++
src/tor/TorControlCommand.cpp | 63 +
src/tor/TorControlCommand.h | 70 ++
src/tor/TorControlSocket.cpp | 176 +++
src/tor/TorControlSocket.h | 77 ++
src/tor/TorManager.cpp | 306 +++++
src/tor/TorManager.h | 92 ++
src/tor/TorProcess.cpp | 311 +++++
src/tor/TorProcess.h | 100 ++
src/tor/TorProcess_p.h | 79 ++
src/tor/TorSocket.cpp | 151 +++
src/tor/TorSocket.h | 97 ++
src/ui/ContactsModel.cpp | 194 +++
src/ui/ContactsModel.h | 85 ++
src/ui/LanguagesModel.cpp | 80 ++
src/ui/LanguagesModel.h | 65 +
src/ui/LinkedText.cpp | 85 ++
src/ui/LinkedText.h | 56 +
src/ui/MainWindow.cpp | 158 +++
src/ui/MainWindow.h | 77 ++
src/ui/qml/AboutPreferences.qml | 36 +
src/ui/qml/AddContactDialog.qml | 119 ++
src/ui/qml/AudioNotifications.qml | 18 +
src/ui/qml/Bubble.qml | 57 +
src/ui/qml/ChatMessageArea.qml | 53 +
src/ui/qml/ChatPage.qml | 161 +++
src/ui/qml/ChatWindow.qml | 48 +
src/ui/qml/ContactActions.qml | 64 +
src/ui/qml/ContactIDField.qml | 97 ++
src/ui/qml/ContactList.qml | 99 ++
src/ui/qml/ContactListDelegate.qml | 115 ++
src/ui/qml/ContactPreferences.qml | 176 +++
src/ui/qml/ContactRequestDialog.qml | 113 ++
src/ui/qml/ContactRequestFields.qml | 50 +
src/ui/qml/ContactWindow.js | 20 +
src/ui/qml/GeneralPreferences.qml | 54 +
src/ui/qml/LanguagePreferences.qml | 62 +
src/ui/qml/MainToolBar.qml | 87 ++
src/ui/qml/MainWindow.qml | 143 +++
src/ui/qml/MessageDelegate.qml | 179 +++
src/ui/qml/MessageDialogWrapper.qml | 13 +
src/ui/qml/NetworkSetupWizard.qml | 145 +++
src/ui/qml/OfflineStateItem.qml | 235 ++++
src/ui/qml/OpenBrowserDialog.qml | 92 ++
src/ui/qml/PageView.qml | 68 +
src/ui/qml/PreferencesDialog.qml | 65 +
src/ui/qml/PresenceIcon.qml | 23 +
src/ui/qml/StartupStatusPage.qml | 54 +
src/ui/qml/TorBootstrapStatus.qml | 76 ++
src/ui/qml/TorConfigurationPage.qml | 226 ++++
src/ui/qml/TorLogDisplay.qml | 17 +
src/ui/qml/TorPreferences.qml | 94 ++
src/ui/qml/TorStateWidget.qml | 37 +
src/ui/qml/UnreadCountBadge.qml | 22 +
src/ui/qml/dummy.qml | 7 +
src/ui/qml/main.qml | 142 +++
src/ui/qml/qml.qrc | 42 +
src/utils/CryptoKey.cpp | 420 +++++++
src/utils/CryptoKey.h | 94 ++
src/utils/PendingOperation.cpp | 84 ++
src/utils/PendingOperation.h | 87 ++
src/utils/SecureRNG.cpp | 146 +++
src/utils/SecureRNG.h | 51 +
src/utils/Settings.cpp | 553 ++++++++
src/utils/Settings.h | 257 ++++
src/utils/StringUtil.cpp | 118 ++
src/utils/StringUtil.h | 46 +
src/utils/Useful.h | 71 ++
tests/cryptokey/tst_cryptokey.cpp | 205 +++
tests/cryptokey/tst_cryptokey.pro | 24 +
tests/tests.pri | 8 +
tests/tests.pro | 2 +
translation/embedded.qrc | 21 +
translation/inno/Bulgarian.isl | 337 +++++
translation/inno/Swedish.isl | 339 +++++
translation/installer_bg.isl | 14 +
translation/installer_cs.isl | 14 +
translation/installer_da.isl | 14 +
translation/installer_de.isl | 14 +
translation/installer_en.isl | 14 +
translation/installer_es.isl | 14 +
translation/installer_fi.isl | 14 +
translation/installer_fr.isl | 14 +
translation/installer_it.isl | 14 +
translation/installer_nl_NL.isl | 14 +
translation/installer_pl.isl | 14 +
translation/installer_pt_BR.isl | 14 +
translation/installer_ru.isl | 14 +
translation/installer_sv.isl | 14 +
translation/installer_tr.isl | 14 +
translation/installer_uk.isl | 14 +
translation/ricochet_bg.ts | 694 ++++++++++
translation/ricochet_cs.ts | 694 ++++++++++
translation/ricochet_da.ts | 694 ++++++++++
translation/ricochet_de.ts | 694 ++++++++++
translation/ricochet_en.ts | 694 ++++++++++
translation/ricochet_es.ts | 694 ++++++++++
translation/ricochet_fi.ts | 694 ++++++++++
translation/ricochet_fil_PH.ts | 695 ++++++++++
translation/ricochet_fr.ts | 694 ++++++++++
translation/ricochet_it.ts | 694 ++++++++++
translation/ricochet_nl_NL.ts | 694 ++++++++++
translation/ricochet_pl.ts | 694 ++++++++++
translation/ricochet_pt_BR.ts | 694 ++++++++++
translation/ricochet_ru.ts | 694 ++++++++++
translation/ricochet_sv.ts | 694 ++++++++++
translation/ricochet_tr.ts | 706 +++++++++++
translation/ricochet_uk.ts | 694 ++++++++++
214 files changed, 33219 insertions(+)
diff --git a/AUTHORS.md b/AUTHORS.md
new file mode 100644
index 0000000..b7b612b
--- /dev/null
+++ b/AUTHORS.md
@@ -0,0 +1,33 @@
+### Development
+* John Brooks (@special) `<john.brooks at dereferenced.net>`
+
+### Sponsors
+* Blueprint for Free Speech - https://blueprintforfreespeech.net/
+
+### Translations
+* Bulgarian - ivopetkovcz
+* Czech - Einfach
+* Danish - Mikkel Kroman
+* Dutch - mijnheer, Meternalf
+* Finnish - reviewjolla
+* French - rike, Creaprog, CrumpyGat, Jordi, franck99
+* German - djsmith85, rike
+* Italian - HostFat, GIANNAT
+* Polish - Kacper Kołodziej
+* Portuguese (Brazil) - swperman
+* Russian - vla8752, qualte
+* Spanish - strel
+* Swedish - rawtaz
+* Tagalog - taskmaster
+* Turkish - cbolat, basarancaner
+* Ukrainian - l3rixon, nergal
+
+### Thanks
+* Helder Ribeiro (@obvio171) - "Ricochet"
+* Robin Burchell (@rburchell)
+* prof7bit (TorChat) - Inspiration
+* Patrick Gray - invisible.im
+* Suelette Dreyfus
+* HD Moore
+* The Grugq
+* Lawrence Eastland - "R" icon
diff --git a/BUILDING.md b/BUILDING.md
new file mode 100644
index 0000000..faa2814
--- /dev/null
+++ b/BUILDING.md
@@ -0,0 +1,105 @@
+## Building Ricochet
+
+These instructions are intended for people who wish to build or modify Ricochet from source. Most users should [download releases](https://github.com/ricochet-im/ricochet/releases) instead.
+
+Clone with git from `https://github.com/ricochet-im/ricochet.git`, or download source packages [on github](https://github.com/ricochet-im/ricochet/releases). Then proceed to instructions for your platform.
+
+If you're interested in helping to package Ricochet for common Linux platforms, please get in touch!
+
+## Hints
+
+Add `CONFIG+=debug` or `CONFIG+=release` to the qmake command for a debug or release build. Debug builds enable logging to standard output, and shouldn't be used in sensitive environments.
+
+By default, Ricochet will be portable, and configuration is stored in a folder named `config` next to the binary. Add `DEFINES+=RICOCHET_NO_PORTABLE` to the qmake command for a system-wide installation using platform configuration paths instead.
+
+## Linux
+
+You will need:
+ * Qt >= 5.1.0
+ * OpenSSL (libcrypto)
+ * pkg-config
+ * Protocol Buffers (libprotobuf, protoc)
+
+#### Fedora
+```sh
+yum install make gcc-c++ protobuf-devel protobuf-compiler openssl-devel
+yum install qt5-qtbase qt5-qttools-devel qt5-qttools qt5-qtquickcontrols qt5-qtdeclarative qt5-qtbase-devel qt5-qtbase-gui qt5-qtdeclarative-devel qt5-qtmultimedia-devel
+yum install tor # or build your own
+```
+#### Debian & Ubuntu
+```sh
+apt-get install build-essential libssl-dev pkg-config libprotobuf-dev protobuf-compiler
+apt-get install qt5-qmake qt5-default qtbase5-dev qttools5-dev-tools qtdeclarative5-dev qtmultimedia5-dev
+apt-get install qml-module-qtquick-controls qml-module-qtquick-dialogs
+apt-get install tor # or build your own
+```
+
+If the `qml-module-qtquick` packages aren't available, try `qtdeclarative5-controls-plugin` instead.
+
+#### Qt SDK
+The [Qt SDK](https://qt-project.org/downloads) is available for most Linux systems and includes an IDE as well as all Qt dependencies.
+
+To build, simply run:
+```sh
+qmake # qmake-qt5 for some platforms
+make
+```
+
+For a system-wide installation, use:
+```sh
+qmake DEFINES+=RICOCHET_NO_PORTABLE
+make
+make install # as root
+```
+
+You must have a `tor` binary installed on the system (in $PATH), or placed next to the `ricochet` binary.
+
+In portable mode (default), all configuration is stored in a folder called `config` with the binary. When installed, the platform's user configuration path is used instead.
+
+The [buildscripts](https://github.com/ricochet-im/buildscripts) repository contains a set of scripts to build a fully static Ricochet on a clean Debian system. These are used to create the generic linux binary packages.
+
+#### Hardening
+Ricochet will use aggressive compiler hardening flags if available. `qmake` will print the results of these tests on first run, or when run with `CONFIG+=recheck`.
+
+To take full advantage of the sanitizer options, you may need to install `libasan` and `libubsan`.
+
+## OS X
+
+You will need:
+ * Xcode (for toolchain)
+ * Qt 5 - preferably the [Qt SDK](https://qt-project.org/downloads)
+ * Protocol Buffers (libprotobuf, protoc) - `brew install protobuf`
+
+You can either load `ricochet.pro` in Qt Creator and build normally, or build command-line with:
+```sh
+/path/to/qtsdk/5.3/clang_64/bin/qmake
+make
+```
+
+You also need a `tor` binary in $PATH or inside the build's `ricochet.app/Contents/MacOS` folder. The easiest solution is to use `brew install tor`. If you copy the `tor` binary, you will need to keep it up to date.
+
+Normally, configuration will be stored in a `config.ricochet` folder, in the same location as `ricochet.app`. However, if the bundle is installed to `/Applications`, the system location `~/Library/Application Support/Ricochet` is used instead. You can force that behavior by adding `DEFINES+=RICOCHET_NO_PORTABLE` to the qmake command.
+
+The `packaging/osx/release_osx.sh` script demonstrates how to build a redistributable app bundle.
+
+## Windows
+
+Building for Windows is difficult. You will need:
+ * Visual Studio C++ or MinGW
+ * Qt 5 - preferably the [Qt SDK](https://qt-project.org/downloads)
+ * OpenSSL (including libs and headers)
+ * Protocol Buffers >= 2.6.0
+
+Compile OpenSSL and protobuf first, according to their instructions.
+
+After installing the Qt SDK, open the `ricochet.pro` project in Qt Creator. Before building, you must click the 'Projects' tab on the left side, and under 'Build Steps', modify 'Additional arguments' to add:
+
+```
+ OPENSSLDIR=C:\path\to\openssl\ PROTOBUFDIR=C:\path\to\protobuf
+```
+
+Use the 'Build -> Run qmake' menu to test your changes.
+
+You also need a `tor.exe` binary, placed in the same folder as `ricochet.exe`.
+
+The windows installer can be built using Inno Setup. See `packaging\installer` for more information.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..1cb2bb9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,233 @@
+ Ricochet - https://ricochet.im/
+Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+
+ * Neither the names of the copyright owners nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+-------------------------------------------------------------------------------
+
+Tor is a registered trademark of The Tor Project, Inc.
+Ricochet is not in any way affiliated with or endorsed by The Tor Project.
+
+For more information about Tor, see https://www.torproject.org/.
+Tor is distributed under this license:
+
+Copyright (c) 2001-2004, Roger Dingledine
+Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson
+Copyright (c) 2007-2010, The Tor Project, Inc.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+
+ * Neither the names of the copyright owners nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+src/common/strlcat.c and src/common/strlcpy.c by Todd C. Miller are licensed
+under the following license:
+
+ * Copyright (c) 1998 Todd C. Miller <Todd.Miller at courtesan.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. The name of the author may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
+ * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+-------------------------------------------------------------------------------
+
+The Qt Toolkit is Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies)
+and other contributors.
+
+Qt is licensed under the GNU Lesser General Public License version 2.1,
+available online at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html,
+and a special exception, the text of which is distributed with Qt.
+
+This library is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+details.
+
+-------------------------------------------------------------------------------
+
+Ricochet "R" icon by Lawrence Eastland, CC-BY-SA 4.0. See icons/LICENSE.
+
+-------------------------------------------------------------------------------
+
+/* ====================================================================
+ * Copyright (c) 1998-2008 The OpenSSL Project. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * 3. All advertising materials mentioning features or use of this
+ * software must display the following acknowledgment:
+ * "This product includes software developed by the OpenSSL Project
+ * for use in the OpenSSL Toolkit. (http://www.openssl.org/)"
+ *
+ * 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to
+ * endorse or promote products derived from this software without
+ * prior written permission. For written permission, please contact
+ * openssl-core at openssl.org.
+ *
+ * 5. Products derived from this software may not be called "OpenSSL"
+ * nor may "OpenSSL" appear in their names without prior written
+ * permission of the OpenSSL Project.
+ *
+ * 6. Redistributions of any form whatsoever must retain the following
+ * acknowledgment:
+ * "This product includes software developed by the OpenSSL Project
+ * for use in the OpenSSL Toolkit (http://www.openssl.org/)"
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY
+ * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR
+ * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * ====================================================================
+ *
+ * This product includes cryptographic software written by Eric Young
+ * (eay at cryptsoft.com). This product includes software written by Tim
+ * Hudson (tjh at cryptsoft.com).
+ *
+ */
+
+ Original SSLeay License
+ -----------------------
+
+/* Copyright (C) 1995-1998 Eric Young (eay at cryptsoft.com)
+ * All rights reserved.
+ *
+ * This package is an SSL implementation written
+ * by Eric Young (eay at cryptsoft.com).
+ * The implementation was written so as to conform with Netscapes SSL.
+ *
+ * This library is free for commercial and non-commercial use as long as
+ * the following conditions are aheared to. The following conditions
+ * apply to all code found in this distribution, be it the RC4, RSA,
+ * lhash, DES, etc., code; not just the SSL code. The SSL documentation
+ * included with this distribution is covered by the same copyright terms
+ * except that the holder is Tim Hudson (tjh at cryptsoft.com).
+ *
+ * Copyright remains Eric Young's, and as such any Copyright notices in
+ * the code are not to be removed.
+ * If this package is used in a product, Eric Young should be given attribution
+ * as the author of the parts of the library used.
+ * This can be in the form of a textual message at program startup or
+ * in documentation (online or textual) provided with the package.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. All advertising materials mentioning features or use of this software
+ * must display the following acknowledgement:
+ * "This product includes cryptographic software written by
+ * Eric Young (eay at cryptsoft.com)"
+ * The word 'cryptographic' can be left out if the rouines from the library
+ * being used are not cryptographic related :-).
+ * 4. If you include any Windows specific code (or a derivative thereof) from
+ * the apps directory (application code) you must include an acknowledgement:
+ * "This product includes software written by Tim Hudson (tjh at cryptsoft.com)"
+ *
+ * THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ * The licence and distribution terms for any publically available version or
+ * derivative of this code cannot be changed. i.e. this code cannot simply be
+ * copied and put under another distribution licence
+ * [including the GNU Public Licence.]
+ */
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..bedd43e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,35 @@
+### Anonymous metadata-resistant instant messaging that just works
+Ricochet is an experiment with a different kind of instant messaging that **doesn't trust anyone** with your identity, your contact list, or your communications.
+
+* You can chat without exposing your identity (or IP address) to *anyone*
+* Nobody can discover who your contacts are or when you talk (*metadata-free!*)
+* There are no servers or operators to compromise that could access your information
+* It's cross-platform and easy for non-technical users
+
+### How it works
+Ricochet is a peer-to-peer instant messaging system built on Tor [hidden services](https://www.torproject.org/docs/hidden-services.html.en). Your login is your hidden service address, and contacts connect to you (not an intermediate server) through Tor. The rendezvous system makes it extremely hard for anyone to learn your identity from your address.
+
+Ricochet is not affiliated with or endorsed by The Tor Project.
+
+For more information, you can [read about Tor](https://www.torproject.org/about/overview.html.en) and [learn about Ricochet's design](https://github.com/ricochet-im/ricochet/blob/master/doc/design.md) or [protocol](https://github.com/ricochet-im/ricochet/blob/master/doc/protocol.md) (or the [old protocol](https://github.com/ricochet-im/ricochet/blob/master/doc/deprecated/protocol-1.0.txt)). Everything is [open-source](https://github.com/ricochet-im/ricochet/blob/master/LICENSE) and open [...]
+
+### Experimental
+This software is an experiment. It hasn't been audited or formally reviewed by anyone. Security and anonymity are difficult topics, and you should carefully evaluate your risks and exposure with any software. *Do not rely on Ricochet for your safety* unless you have more trust in my work than it deserves. That said, I believe it does more to try to protect your privacy than any similar software.
+
+### Downloads
+
+Ricochet is available for Windows, OS X (10.7 or later), and as a generic Linux binary package. Visit the [releases page](https://github.com/ricochet-im/ricochet/releases) for the latest version and changelog.
+
+All releases and signatures are also available at https://ricochet.im/releases/.
+
+Binaries are PGP signed by `9032 CAE4 CBFA 933A 5A21 45D5 FF97 C53F 183C 045D`.
+
+### Building from source
+See [BUILDING](https://github.com/ricochet-im/ricochet/blob/master/BUILDING.md) for Linux, OS X, and Windows build instructions.
+
+### Other
+Bugs can be reported on the [issue tracker](https://github.com/ricochet-im/ricochet/issues). Translations can be contributed on [Transifex](https://www.transifex.com/projects/p/ricochet/).
+
+You can contact me at `ricochet:rs7ce36jsj24ogfw` or `john.brooks at dereferenced.net`.
+
+You should support [The Tor Project](https://www.torproject.org/donate/donate.html.en), [EFF](https://www.eff.org/), and [run a Tor relay](https://www.torproject.org/docs/tor-relay-debian.html.en).
diff --git a/config.tests/mingw-64aslr/mingw-64aslr.pro b/config.tests/mingw-64aslr/mingw-64aslr.pro
new file mode 100644
index 0000000..944b72a
--- /dev/null
+++ b/config.tests/mingw-64aslr/mingw-64aslr.pro
@@ -0,0 +1,3 @@
+include(../../hardened.pri)
+QMAKE_LFLAGS += $$HARDENED_MINGW_64ASLR_FLAGS
+SOURCES += test.cpp
diff --git a/config.tests/mingw-64aslr/test.cpp b/config.tests/mingw-64aslr/test.cpp
new file mode 100644
index 0000000..76e8197
--- /dev/null
+++ b/config.tests/mingw-64aslr/test.cpp
@@ -0,0 +1 @@
+int main() { return 0; }
diff --git a/config.tests/sanitize-ubsan-more/sanitize-ubsan-more.pro b/config.tests/sanitize-ubsan-more/sanitize-ubsan-more.pro
new file mode 100644
index 0000000..eb5faaf
--- /dev/null
+++ b/config.tests/sanitize-ubsan-more/sanitize-ubsan-more.pro
@@ -0,0 +1,4 @@
+include(../../hardened.pri)
+QMAKE_CXXFLAGS += $$HARDENED_SANITIZE_UBSAN_MORE_FLAGS
+QMAKE_LFLAGS += $$HARDENED_SANITIZE_UBSAN_MORE_FLAGS
+SOURCES += test.cpp
diff --git a/config.tests/sanitize-ubsan-more/test.cpp b/config.tests/sanitize-ubsan-more/test.cpp
new file mode 100644
index 0000000..76e8197
--- /dev/null
+++ b/config.tests/sanitize-ubsan-more/test.cpp
@@ -0,0 +1 @@
+int main() { return 0; }
diff --git a/config.tests/sanitize-ubsan/sanitize-ubsan.pro b/config.tests/sanitize-ubsan/sanitize-ubsan.pro
new file mode 100644
index 0000000..494a5ec
--- /dev/null
+++ b/config.tests/sanitize-ubsan/sanitize-ubsan.pro
@@ -0,0 +1,4 @@
+include(../../hardened.pri)
+QMAKE_CXXFLAGS += $$HARDENED_SANITIZE_UBSAN_FLAGS
+QMAKE_LFLAGS += $$HARDENED_SANITIZE_UBSAN_FLAGS
+SOURCES += test.cpp
diff --git a/config.tests/sanitize-ubsan/test.cpp b/config.tests/sanitize-ubsan/test.cpp
new file mode 100644
index 0000000..76e8197
--- /dev/null
+++ b/config.tests/sanitize-ubsan/test.cpp
@@ -0,0 +1 @@
+int main() { return 0; }
diff --git a/config.tests/sanitize/sanitize.pro b/config.tests/sanitize/sanitize.pro
new file mode 100644
index 0000000..8a57c91
--- /dev/null
+++ b/config.tests/sanitize/sanitize.pro
@@ -0,0 +1,4 @@
+include(../../hardened.pri)
+QMAKE_CXXFLAGS += $$HARDENED_SANITIZE_FLAGS
+QMAKE_LFLAGS += $$HARDENED_SANITIZE_FLAGS
+SOURCES += test.cpp
diff --git a/config.tests/sanitize/test.cpp b/config.tests/sanitize/test.cpp
new file mode 100644
index 0000000..76e8197
--- /dev/null
+++ b/config.tests/sanitize/test.cpp
@@ -0,0 +1 @@
+int main() { return 0; }
diff --git a/config.tests/stack-protector-strong/stack-protector-strong.pro b/config.tests/stack-protector-strong/stack-protector-strong.pro
new file mode 100644
index 0000000..a937328
--- /dev/null
+++ b/config.tests/stack-protector-strong/stack-protector-strong.pro
@@ -0,0 +1,4 @@
+include(../../hardened.pri)
+QMAKE_CXXFLAGS += $$HARDENED_STACK_PROTECTOR_STRONG_FLAGS
+QMAKE_LFLAGS += $$HARDENED_STACK_PROTECTOR_STRONG_FLAGS
+SOURCES += test.cpp
diff --git a/config.tests/stack-protector-strong/test.cpp b/config.tests/stack-protector-strong/test.cpp
new file mode 100644
index 0000000..76e8197
--- /dev/null
+++ b/config.tests/stack-protector-strong/test.cpp
@@ -0,0 +1 @@
+int main() { return 0; }
diff --git a/config.tests/stack-protector/stack-protector.pro b/config.tests/stack-protector/stack-protector.pro
new file mode 100644
index 0000000..7c92274
--- /dev/null
+++ b/config.tests/stack-protector/stack-protector.pro
@@ -0,0 +1,4 @@
+include(../../hardened.pri)
+QMAKE_CXXFLAGS += $$HARDENED_STACK_PROTECTOR_FLAGS
+QMAKE_LFLAGS += $$HARDENED_STACK_PROTECTOR_FLAGS
+SOURCES += test.cpp
diff --git a/config.tests/stack-protector/test.cpp b/config.tests/stack-protector/test.cpp
new file mode 100644
index 0000000..76e8197
--- /dev/null
+++ b/config.tests/stack-protector/test.cpp
@@ -0,0 +1 @@
+int main() { return 0; }
diff --git a/config.tests/vtable-verify/test.cpp b/config.tests/vtable-verify/test.cpp
new file mode 100644
index 0000000..76e8197
--- /dev/null
+++ b/config.tests/vtable-verify/test.cpp
@@ -0,0 +1 @@
+int main() { return 0; }
diff --git a/config.tests/vtable-verify/vtable-verify.pro b/config.tests/vtable-verify/vtable-verify.pro
new file mode 100644
index 0000000..b2af369
--- /dev/null
+++ b/config.tests/vtable-verify/vtable-verify.pro
@@ -0,0 +1,4 @@
+include(../../hardened.pri)
+QMAKE_CXXFLAGS += $$HARDENED_VTABLE_VERIFY_FLAGS
+QMAKE_LFLAGS += $$HARDENED_VTABLE_VERIFY_FLAGS
+SOURCES += test.cpp
diff --git a/contrib/README.sandboxing b/contrib/README.sandboxing
new file mode 100644
index 0000000..ac8ef9d
--- /dev/null
+++ b/contrib/README.sandboxing
@@ -0,0 +1,49 @@
+This document explains some of the sandboxing contributions available for
+Ricochet.
+
+Debian GNU/Linux sandboxing:
+
+usr.bin.ricochet-apparmor is a basic AppArmor policy to be installed in
+/etc/apparmor.d/; it may be enabled like so:
+
+ cp usr.bin.ricochet-apparmor /etc/apparmor.d/usr.bin.ricochet-apparmor
+ aa-enforce /etc/apparmor.d/usr.bin.ricochet-apparmor
+
+the minijail tool originally from the ChromeOS project is an easy way to
+use seccomp as a generic wrapper around any program. We use it to add seccomp
+support to ricochet without using the more useful or privileged modes. If
+minijail is supported for your platform, congratulations. If not, it may be
+built and used like so:
+
+ apt-get install libcap-dev
+ git clone https://chromium.googlesource.com/chromiumos/platform/minijail
+ export CC=gcc
+ make
+ ./minijail0 -h
+
+ricochet-seccomp-amd64.policy is a loose seccomp filter policy to be used
+with the minijail tool originally from the ChromeOS project:
+
+ minijail0 -n -S ricochet-seccomp-amd64.policy /usr/bin/ricochet
+
+ricochet-seccomp-x86.policy and ricochet-seccomp-arm.policy are the base
+policies for the x86 and arm architectures. They may need tweaking before
+they are useful. They need testing - only the amd64 code has been used
+extensively.
+
+It is also possible to run Ricochet inside of xpra like so:
+
+ xpra start :23 --start-child ricochet --exit-with-children \
+ --no-pulseaudio \
+ --no-microphone \
+ --no-sharing \
+ --no-xsettings \
+ --no-mdns \
+ --no-notifications \
+ --no-bell \
+ --no-opengl=no \
+ --no-daemon && xpra attach :23 --no-tray --title=@title@
+
+It is also possible to combine the AppArmor protection as well as the xpra and
+minijail commands. This allows for a reasonable start for sandboxing Ricochet
+from the host system where it is running on Debian GNU/Linux.
diff --git a/contrib/ricochet-seccomp-amd64.policy b/contrib/ricochet-seccomp-amd64.policy
new file mode 100644
index 0000000..96e7261
--- /dev/null
+++ b/contrib/ricochet-seccomp-amd64.policy
@@ -0,0 +1,111 @@
+accept4: 1
+access: 1
+alarm: 1
+arch_prctl: 1
+bind: 1
+brk: 1
+capget: 1
+capset: 1
+chdir: 1
+chmod: 1
+chown: 1
+clock_getres: 1
+clone: 1
+close: 1
+connect: 1
+dup: 1
+dup2: 1
+epoll_create: 1
+epoll_ctl: 1
+epoll_wait: 1
+eventfd2: 1
+execve: 1
+exit: 1
+exit_group: 1
+faccessat: 1
+fadvise64: 1
+fchmod: 1
+fchown: 1
+fcntl: 1
+fdatasync: 1
+flock: 1
+fstat: 1
+fstatfs: 1
+ftruncate: 1
+futex: 1
+getcwd: 1
+getdents: 1
+getegid: 1
+geteuid: 1
+getgid: 1
+getpeername: 1
+getpgrp: 1
+getpid: 1
+getppid: 1
+getresgid: 1
+getresuid: 1
+getrlimit: 1
+getrusage: 1
+getsockname: 1
+getsockopt: 1
+getuid: 1
+ioctl: 1
+kill: 1
+listen: 1
+lseek: 1
+lstat: 1
+madvise: 1
+mkdir: 1
+mmap: 1
+mprotect: 1
+mremap: 1
+munmap: 1
+nanosleep: 1
+open: 1
+openat: 1
+pipe: 1
+pipe2: 1
+poll: 1
+prctl: 1
+pselect6: 1
+read: 1
+readlink: 1
+recvfrom: 1
+recvmsg: 1
+rename: 1
+rmdir: 1
+rt_sigaction: 1
+rt_sigprocmask: 1
+rt_sigreturn: 1
+sched_getaffinity: 1
+sched_yield: 1
+select: 1
+sendmsg: 1
+sendto: 1
+setgid: 1
+setgroups: 1
+setpgid: 1
+setresgid: 1
+setresuid: 1
+setrlimit: 1
+set_robust_list: 1
+setsid: 1
+setsockopt: 1
+set_tid_address: 1
+setuid: 1
+shmat: 1
+shmctl: 1
+shmdt: 1
+shmget: 1
+shutdown: 1
+socket: 1
+socketpair: 1
+stat: 1
+statfs: 1
+tgkill: 1
+umask: 1
+uname: 1
+unlink: 1
+wait4: 1
+write: 1
+writev: 1
diff --git a/contrib/ricochet-seccomp-arm.policy b/contrib/ricochet-seccomp-arm.policy
new file mode 100644
index 0000000..96e7261
--- /dev/null
+++ b/contrib/ricochet-seccomp-arm.policy
@@ -0,0 +1,111 @@
+accept4: 1
+access: 1
+alarm: 1
+arch_prctl: 1
+bind: 1
+brk: 1
+capget: 1
+capset: 1
+chdir: 1
+chmod: 1
+chown: 1
+clock_getres: 1
+clone: 1
+close: 1
+connect: 1
+dup: 1
+dup2: 1
+epoll_create: 1
+epoll_ctl: 1
+epoll_wait: 1
+eventfd2: 1
+execve: 1
+exit: 1
+exit_group: 1
+faccessat: 1
+fadvise64: 1
+fchmod: 1
+fchown: 1
+fcntl: 1
+fdatasync: 1
+flock: 1
+fstat: 1
+fstatfs: 1
+ftruncate: 1
+futex: 1
+getcwd: 1
+getdents: 1
+getegid: 1
+geteuid: 1
+getgid: 1
+getpeername: 1
+getpgrp: 1
+getpid: 1
+getppid: 1
+getresgid: 1
+getresuid: 1
+getrlimit: 1
+getrusage: 1
+getsockname: 1
+getsockopt: 1
+getuid: 1
+ioctl: 1
+kill: 1
+listen: 1
+lseek: 1
+lstat: 1
+madvise: 1
+mkdir: 1
+mmap: 1
+mprotect: 1
+mremap: 1
+munmap: 1
+nanosleep: 1
+open: 1
+openat: 1
+pipe: 1
+pipe2: 1
+poll: 1
+prctl: 1
+pselect6: 1
+read: 1
+readlink: 1
+recvfrom: 1
+recvmsg: 1
+rename: 1
+rmdir: 1
+rt_sigaction: 1
+rt_sigprocmask: 1
+rt_sigreturn: 1
+sched_getaffinity: 1
+sched_yield: 1
+select: 1
+sendmsg: 1
+sendto: 1
+setgid: 1
+setgroups: 1
+setpgid: 1
+setresgid: 1
+setresuid: 1
+setrlimit: 1
+set_robust_list: 1
+setsid: 1
+setsockopt: 1
+set_tid_address: 1
+setuid: 1
+shmat: 1
+shmctl: 1
+shmdt: 1
+shmget: 1
+shutdown: 1
+socket: 1
+socketpair: 1
+stat: 1
+statfs: 1
+tgkill: 1
+umask: 1
+uname: 1
+unlink: 1
+wait4: 1
+write: 1
+writev: 1
diff --git a/contrib/ricochet-seccomp-x86.policy b/contrib/ricochet-seccomp-x86.policy
new file mode 100644
index 0000000..96e7261
--- /dev/null
+++ b/contrib/ricochet-seccomp-x86.policy
@@ -0,0 +1,111 @@
+accept4: 1
+access: 1
+alarm: 1
+arch_prctl: 1
+bind: 1
+brk: 1
+capget: 1
+capset: 1
+chdir: 1
+chmod: 1
+chown: 1
+clock_getres: 1
+clone: 1
+close: 1
+connect: 1
+dup: 1
+dup2: 1
+epoll_create: 1
+epoll_ctl: 1
+epoll_wait: 1
+eventfd2: 1
+execve: 1
+exit: 1
+exit_group: 1
+faccessat: 1
+fadvise64: 1
+fchmod: 1
+fchown: 1
+fcntl: 1
+fdatasync: 1
+flock: 1
+fstat: 1
+fstatfs: 1
+ftruncate: 1
+futex: 1
+getcwd: 1
+getdents: 1
+getegid: 1
+geteuid: 1
+getgid: 1
+getpeername: 1
+getpgrp: 1
+getpid: 1
+getppid: 1
+getresgid: 1
+getresuid: 1
+getrlimit: 1
+getrusage: 1
+getsockname: 1
+getsockopt: 1
+getuid: 1
+ioctl: 1
+kill: 1
+listen: 1
+lseek: 1
+lstat: 1
+madvise: 1
+mkdir: 1
+mmap: 1
+mprotect: 1
+mremap: 1
+munmap: 1
+nanosleep: 1
+open: 1
+openat: 1
+pipe: 1
+pipe2: 1
+poll: 1
+prctl: 1
+pselect6: 1
+read: 1
+readlink: 1
+recvfrom: 1
+recvmsg: 1
+rename: 1
+rmdir: 1
+rt_sigaction: 1
+rt_sigprocmask: 1
+rt_sigreturn: 1
+sched_getaffinity: 1
+sched_yield: 1
+select: 1
+sendmsg: 1
+sendto: 1
+setgid: 1
+setgroups: 1
+setpgid: 1
+setresgid: 1
+setresuid: 1
+setrlimit: 1
+set_robust_list: 1
+setsid: 1
+setsockopt: 1
+set_tid_address: 1
+setuid: 1
+shmat: 1
+shmctl: 1
+shmdt: 1
+shmget: 1
+shutdown: 1
+socket: 1
+socketpair: 1
+stat: 1
+statfs: 1
+tgkill: 1
+umask: 1
+uname: 1
+unlink: 1
+wait4: 1
+write: 1
+writev: 1
diff --git a/contrib/usr.bin.ricochet-apparmor b/contrib/usr.bin.ricochet-apparmor
new file mode 100644
index 0000000..714ae24
--- /dev/null
+++ b/contrib/usr.bin.ricochet-apparmor
@@ -0,0 +1,56 @@
+# AppArmor Ricochet profile for Debian GNU/Linux
+# This profile is Free Software and released under the same license as Ricochet
+# itself.
+#
+# Copyleft 2015 Jacob Appelbaum <jacob at appelbaum.net>
+#
+#include <tunables/global>
+
+/usr/bin/ricochet {
+ #include <abstractions/kde>
+ #include <abstractions/nameservice>
+ #include <abstractions/audio>
+
+ # Allow TCP connections
+ network inet stream,
+ network inet6 stream,
+
+ /usr/lib/** mr,
+
+ # Allow Ricochet to exec pulseaudio
+ # This makes me very sad...
+ # as it seems that you can't isolate playing and recording :(
+ /usr/bin/pulseaudio ixr,
+
+ # Allow Ricochet to exec tor
+ /usr/bin/tor ixr,
+ /usr/share/tor/geoip r,
+ /usr/share/tor/geoip6 r,
+ # Tor in turn needs various things
+ /proc/sys/kernel/random/uuid r,
+ /sys/devices/system/cpu/ r,
+
+ # Allow Ricochet to read itself
+ /usr/bin/ricochet r,
+ /proc/[0-9]*/cmdline r,
+
+ # Allow Ricochet to generate audio
+ owner /{dev,run}/shm/pulse-shm* m,
+
+ # Allow Ricochet to draw the UX
+ /sys/devices/pci[0-9]*/**/uevent r,
+ /run/udev/data/* r,
+
+ # Allow Ricochet to load GTK themes
+ /usr/share/themes/* r,
+ /usr/share/themes/**/* r,
+
+ # Allow Ricochet to look up all your machine's PII
+ # Why does it need this stuff? BAD NEWS BEARS
+ /etc/machine-id r,
+ /var/lib/dbus/machine-id r,
+ /etc/udev/udev.conf r,
+
+ owner @{HOME}/.local/share/Ricochet/ rw,
+ owner @{HOME}/.local/share/Ricochet/** rwmk,
+}
diff --git a/doc/deprecated/protocol-1.0.txt b/doc/deprecated/protocol-1.0.txt
new file mode 100644
index 0000000..fd2a956
--- /dev/null
+++ b/doc/deprecated/protocol-1.0.txt
@@ -0,0 +1,396 @@
+This file aims to document the low-level details of communication and protocol
+in Ricochet.
+
+Unless otherwise noted, this document describes protocol version 0. Section 1
+(protocol negotiation) is intended to be invariant, as it is responsible for
+negotiating the protocol version.
+
+Table of contents:
+ 0. Conventions in this document
+ 1. The hidden service layer
+ 2. Protocol negotiation
+ 2.1. Introduction syntax
+ 3. Connection purpose and authentication
+ 3.1. Authenticated contact connections
+ 4. Connection management
+ 4.1. Command connections
+ 4.2. Data connections
+ 5. Message processing
+ 6. Commands and replies
+ 6.1. Length field
+ 6.2. Command field
+ 6.3. State field
+ 6.4. Identifier field
+ 6.5. Data
+ 7. Defined commands
+ 7.1. 0x00 - Ping
+ 7.2. 0x01 - Get connection secret
+ 7.3. 0x10 - Chat message
+ 8. Contact request connections
+
+0. Conventions in this document
+
+ 'Client' refers to the peer creating the connection
+ 'Server' refers to the peer receiving an incoming connection. After
+ authentication, this distinction is irrelevant.
+
+1. The hidden service layer
+
+[TBD: describe use of hidden services and the properties they make available]
+
+ Connections are made to port 9878 of the hidden service.
+
+2. Protocol negotiation
+
+ Immediately after establishing a connection, the client must send an
+ introduction. The server is expected to expire any connection on which
+ this introduction does not arrive in a reasonable amount of time.
+
+2.1. Introduction syntax
+
+ The client must send the following sequence:
+ 0x49
+ 0x4D
+ nVersions [1 octet]
+ versions [nVersions bytes]
+
+ 'nVersions' describes the number of version fields to follow. Each version
+ field is a single octet, where each value identifies an incompatible
+ protocol. The value 0xFF is reserved to indicate failure in a later reply.
+
+ In response, once the entire introduction has been received, the server
+ responds with a single octet containing the protocol version chosen from
+ the list provided by the client. This should be the highest mutually
+ supported version. If there is no mutual version, the server sends 0xff
+ (which is not a valid version value), and terminates the connection.
+
+ Implementations MUST NOT expect any more than 4 octets (the introduction
+ with one version) to arrive before attempting to process them. The client
+ MUST NOT expect more than one octet (the response version) before
+ processing that data, regardless of any data which may immediately follow.
+
+3. Connection purpose and authentication
+
+ Immediately after version negotiation has finished (i.e. the server has
+ responded), the client must send a connection purpose field, which defines
+ the type of connection. This is a single octet, with the following
+ currently recognized values:
+
+ 0x00 Contact command connection
+ 0x01 Contact data connection
+ 0x80 Contact request
+
+ Any value below 0x20 (non-inclusive) shares the same immediate usage for
+ authentication. The 0x00 value indicates a command connection (4.),
+ while all others are auxiliary connections. The protocol version must be
+ increased when introducing a new purpose.
+
+3.1. Authenticated contact connections
+
+ For connections with a purpose value below 0x20, the following
+ authentication process is used to prove the identity of existing
+ contacts.
+
+ After sending the purpose, the client must follow with a 16-octet
+ pre-exchanged secret. This secret is specific to each peer, and is defined
+ during the contact request (or shortly thereafter). Because of the
+ hidden service layer pre-authenticating the server end of the connection,
+ and the existing layers of encryption, this value is sent in plaintext.
+
+ The server looks up this value and matches it to any known contacts.
+ It then sends a one-octet response code; the value 0x00 for this code
+ indicates success, while all other values are assumed to be failure.
+ Any failure response may be followed by the server immediately closing
+ the connection, or may be followed by additional information. No response
+ other than 0x00 will result in the connection continuing to exist. The
+ following responses are currently defined:
+
+ 0x00 Success
+ 0x01 General failure; immediately closes the connection
+ 0x02 Unrecognized secret
+
+ After a success reply, the connection transitions into message parsing
+ and may be used bidirectionally as desired. The "unrecognized secret"
+ response should be considered a permanent failure and disable future
+ connection attempts without issuing a new contact request.
+
+4. Connection management
+
+4.1. Command connections
+
+ Connections initiated with a purpose of 0x00 (see section 3.) are command
+ connections, over which the protocol defined in section 5 through 7 is
+ used. Generally, there may only be one command connection with a peer,
+ and a new connection is considered to replace any existing connection
+ (causing failure of all incomplete commands).
+
+ A race is possible when two peers connect simultaniously and each
+ establish a command connection. To resolve this situation, the following
+ rules are used:
+
+ - If the remote peer establishes a command connection prior to
+ sending authentication data for an outgoing attempt, the outgoing
+ attempt is aborted and the peer's connection is used.
+ - If the existing connection is more than 30 seconds old, measured
+ from the time authentication is sent, it is replaced and closed.
+ - Otherwise, both peers close the connection for which the server's
+ onion-formatted hostname is considered less by a strcmp function.
+
+4.2. Data connections
+
+ All commands and general communication take place over the command
+ connection. It's important that command connections keep low latency
+ regardless of throughput, so they are not suitable for transferring
+ large data.
+
+ Peers may establish data connections with a purpose of 0x01 for the
+ transfer of larger amounts of data. These transfers must first be
+ negotiated on the command connection, where the receiving peer assigns
+ a 4 octet identifier for the blob of data.
+
+ Either peer may use a data connection regardless of why it was
+ originally established. Connections should not be closed by expiration
+ if a pending identifier has been issued for the peer's use. It should
+ be possible for the sending peer in a transfer to indicate that the
+ recipient should establish a data connection, to avoid relying on
+ the connectivity of both peers.
+
+ Data arrives in the following form:
+
+ identifier [4 octets]
+ length [64-bit unsigned big endian integer]
+ data [length octets]
+
+ Unexpected identifiers may be ignored or result in closing the
+ data connection.
+
+5. Message processing
+
+ Most communication takes place over the command or message protocol, used
+ on authenticated connections with contacts. This is a sequence of messages,
+ each of which is a command or a reply.
+
+ All commands must generate a reply, unless otherwise specifically defined.
+ This is done to allow intelligent handling of various error situations, and
+ to enable better internal API for managing outstanding commands. Replies
+ have a concept of finality; a command may generate many replies, but must
+ have exactly one final reply, which obviously must be the last.
+
+ Each command also has an identifier; this is a 16-bit value unique to the
+ sender of the command, which serves to relate replies back to the original
+ command. As a result of this, there is no requirement that replies be
+ immediate, and they may even be interleaved with other replies. The
+ identifier may be considered unused immediately after a final reply arrives
+ with that identifier set. Identifiers are specific to a single connection.
+ 0 is reserved for identifiers, and should not be used.
+
+ One last distinction is between two types of messages that have very
+ different behavior for processing. Buffered messages (which are the
+ majority) include their length, which may not exceed 65,540 octets
+ inclusive of the header and the length itself, meaning that they may
+ have a maximum payload of 65,534 octets. As the name implies, buffered
+ messages are buffered until all data has arrived, and processed once.
+
+6. Commands and replies
+
+ The syntax of a message (a command or reply) is:
+
+ length [16-bit big endian integer]
+ command [1 octet]
+ state [1 octet]
+ identifier [16-bit big endian integer]
+ data [length-1 octets, or unspecified]
+
+ These fields will be addressed in order.
+
+6.1. Length field
+
+ The length is a 16-bit big endian integer defining the size in bytes of the
+ data payload in the message. This length does not include the header of the
+ message. It may be 0 for messages with no associated data.
+
+6.2. Command field
+
+ The command is a one octet identifier for the command that should be
+ performed. When adding commands, great care should be taken to avoid
+ collisions with any other implementation. Command values below 0x80 should
+ be reserved for definition in this official document, while those above
+ are open for third party usage with appropriate precautions. A feature
+ inspection command may be introduced in the future to handle this issue.
+
+ For details on existing commands, see section 7.
+
+6.3. State field
+
+ The state field can be thought of as a subdivision of the command. It
+ enables different usages of the same command, and is also used to indicate
+ replies and reply status. The state is one octet, which is to be
+ interpreted as follows, where 0 is the most significant bit (0x80), and 7
+ is the least significant (0x01).
+
+ Bit 0 (0x80) indicates if the message is a reply.
+
+ If bit 0 is NOT set, the message is a command:
+ Bit 1 (0x40) is reserved, and should have a value of 1.
+ Bits 2 through 7 are available for command-specific usage.
+
+ If bit 0 IS set:
+ Bit 1 (0x40) indicates if the reply is a final reply. No more replies
+ may arrive with the same identifier for that command after the final
+ reply.
+
+ Bit 2 (0x20) indicates if the command was successful. This bit should
+ be set for any reply which does not indicate failure, but is largely
+ intepreted by the command itself.
+
+ IF bit 2 is NOT set (i.e. the command failed), all values with bit 3
+ (0x10) set are reserved for protocol usage as defined below.
+
+ All other bits (3-7 for success, 4-7 for failure) are available for
+ command-specific usage.
+
+ The following chart describes these values and their meaning:
+ 0x00 to 0x3F Reserved (unused)
+ 0x40 to 0x7F Command
+ 0x80 to 0x8F Reply Intermediate Failure Available
+ 0x90 to 0x9F Reply Intermediate Failure Reserved
+ 0xA0 to 0xBF Reply Intermediate Successful
+ 0xC0 to 0xCF Reply Final Failure Available
+ 0xD0 to 0xDF Reply Final Failure Reserved
+ 0xE0 to 0xFF Reply Final Successful
+
+6.4. Identifier field
+
+ The identifier is a 16-bit big endian integer, selected when sending the
+ command. It is unique to the peer and connection. It is illegal to send a
+ command with the same identifier as a command for which a final reply has
+ not arrived. It is illegal to send a reply with an identifier that does not
+ match an outstanding command from the peer.
+
+ The identifier 0 is reserved.
+
+6.5. Data
+
+ The data is arbitrary data, specific to the command (and possibly state).
+ Its length is defined by the length field, which may be 0.
+
+7. Defined commands
+
+ This is a comprehensive listing of all commands implemented in the version
+ of Ricochet to which this document corrosponds, and potentially those used
+ by other applications that are considered reserved for that purpose.
+
+7.1. 0x00 - Ping
+
+ The ping command has no data, and results in a single, final, successful
+ reply with a command-specific state of 0 (0xE0). It has no other effect.
+
+7.2. 0x01 - Get connection secret
+
+ The command has no data. If successful, the peer will send a single, final
+ reply with a command state of 0 (0xE0), and the 16-byte secret that is
+ used to authenticate the local (command outgoing) end when establishing a
+ connection.
+
+ This is a dirty trick used during contact requests to allow the requesting
+ end of the request to discover the secret that it should use.
+
+7.3. 0x10 - Chat message
+
+ The command sends the following data:
+
+ timeDelta 32-bit signed big-endian integer; seconds
+ before now that the message was written. A
+ negative value (indicating the future) may be
+ rejected.
+ lastReceived 16-bit big-endian integer; command identifier
+ of the last chat message that had been received
+ from the peer when this message was sent.
+ length 16-big unsigned big-endian integer; length in
+ octets of the text field
+ text UTF8-encoded message text of 'length' octets.
+
+ If successful, a single, final reply will be sent.
+
+8. Contact request connections
+
+ Contact requests are indicated by a connection with a purpose of 0x80.
+ Immediately after receiving the purpose, the server will respond with a
+ 16-octet randomly generated cookie.
+
+ Once the cookie is received, the client (i.e. the peer that is sending a
+ request) must send the following structure:
+
+ length 16-bit big-endian unsigned integer; length in
+ octets of the entire request message, inclusive
+ of itself.
+ serverHostname 16 octets, base32-encoded onion hostname of the
+ intended recipient of the request.
+ serverCookie 16 octets, the cookie provided by the server
+ connSecret 16 octets, random data that the recipient can
+ use to authenticate for a contact connection.
+ pubKeyLength 16-bit big-endian unsigned integer; length in
+ octets of the pubKey field
+ pubKey PEM-encoded hidden service public key of
+ pubKeyLength octets
+ nicknameLength 16-bit big-endian unsigned integer; length in
+ octets of the nickname field
+ nickname UTF-8 encoded string of nicknameLength octets;
+ suggested nickname for this contact
+ messageLength 16-bit big-endian unsigned integer; length in
+ octets of the message field
+ message UTF-8 encoded string of messageLength octets;
+ freeform message to be shown with the request
+ signatureLength 16-bit big-endian unsigned integer; length in
+ octets of the signature field
+ signature RSA signature of the SHA256 digest of the fields
+ from serverHostname to message inclusive.
+
+ Upon receiving the request, the server must verify that it is correct:
+
+ - length must be greater than 58
+ - serverHostname must equal the onion hostname that received this request
+ - serverCookie must equal the random cookie sent by the server
+ - pubKey must be a parseable and valid RSA public key
+ - at least one of nickname or message must be non-empty
+ - signature must be a valid signature by pubKey of the previous fields,
+ excluding the length.
+
+ Verifying serverHostname and serverCookie prevents replay and forwarding
+ attacks on the request. The signature authenticates that the origin of the
+ request is able to publish the hidden service represented by pubKey. The
+ requesting service's hostname can be calculated from pubKey.
+
+ After the request, the server will respond with a response code. All
+ responses with the exception of 0x00 (acknowledge) and 0x01 (accept) are
+ considered errors. The following are currently-defined responses:
+
+ 0x00 Acknowledged (see below)
+ 0x01 Accepted (see below)
+ 0x40 Rejected by user
+ 0x80 Syntax error
+ 0x81 Verification error
+ 0x82 Request requires either a nickname or a message
+
+ All of the error responses (that is, any with a value greater than 0x01)
+ will be followed closing the connection. In the case of user rejection,
+ and potentially others, the contact ID used for this request may be added
+ to a blacklist, causing any further requests to be immediately rejected
+ without notifying the user.
+
+ The acknowledged response is a special case; this indicates that the
+ request was valid and has been processed, but that no response has yet been
+ made. After the acknowledged response, the client should leave open the
+ connection and wait for another response code to arrive. However, this is
+ not a requirement.
+
+ Immediately after the accepted response, both peers are expected to
+ transition the connection to an established and authenticated command
+ connection (i.e., they begin the message protocol).
+
+ The requesting peer MUST be willing to accept normal contact connections,
+ authenticated with the connSecret field from the request, at any time after
+ the request has been sent. If such a connection is established, the
+ requesting peer should treat that as implicitly accepting the request. This
+ allows the request to be accepted if the request connection has since been
+ lost.
diff --git a/doc/design.md b/doc/design.md
new file mode 100644
index 0000000..0c5ea5d
--- /dev/null
+++ b/doc/design.md
@@ -0,0 +1,70 @@
+# Technical design of Ricochet
+
+Ricochet is an instant messaging system designed around Tor hidden services. This document describes the goals and design of that system from a technical perspective.
+
+The reader should be familiar with [Tor](https://www.torproject.org/about/overview.html.en) and [hidden services](https://www.torproject.org/docs/hidden-services.html.en).
+
+## Goals for the project
+
+To implement a real-time messaging system with these properties:
+
+ * Users aren't personally identifiable by contacts or their address
+ * Communication is authenticated and private
+ * No person or server can access contact lists, message history, or other metadata
+ * Resist censorship and monitoring at the local network level
+ * Resist blacklisting or denial of service against users
+ * Accessible and understandable for non-technical users
+ * Reliability and interactivity comparable with traditional IM services
+
+## Introduction
+
+Each user identity is represented by a hidden service as its connection point. The identity is shared as a contact ID in the form `ricochet:qjj5g7bxwcvs3d7i`, which is unique and sufficient to connect to the service.
+
+When online, the user publishes a hidden service corresponding with the onion hostname in the contact ID, and accepts bidirectionally anonymous connections, which are authenticated as known contacts or used to receive contact requests.
+
+Known contacts use a [customized protocol](https://github.com/ricochet-im/ricochet/blob/master/doc/protocol.md) over these connections for basic instant messaging features.
+
+## Contact requests
+
+Like classic instant messaging systems, you can request to add a user to your contacts using their contact ID. That request must be accepted before messages can be sent or received.
+
+A request is made by connecting to the service, indicating that the connection is for a contact request, and providing information including the sender's contact ID. The sender attempts periodically to make a connection for the contact request when online.
+
+The request includes:
+
+ * The hidden service hostname of the recipient hidden service
+ * A random cookie provided at the start of the connection by the recipient
+ * A random secret the recipient can use to authenticate normal connections
+ * The full public key corresponding to the hidden service for the sender's identity
+ * (Optionally) A nickname and short introductory message
+ * An RSA signature of the above with the same public key
+
+The recipient can calculate the sender's contact ID based on the public key, and authenticate it by verifying the signature on the request. This proves that the sender can publish the hidden service represented by their contact ID.
+
+The recipient user has the choice to accept or reject that request. A rejected public key may be added to a blacklist and rejected automatically from future requests.
+
+For more detail, see the [protocol documentation](https://github.com/ricochet-im/ricochet/blob/master/doc/protocol.md#contact-request-channel).
+
+## Contact connections
+
+When online, ricochet periodically attempts to make connections to all contacts. If the connection attempt succeeds, it is kept open and that contact is considered online. Only one connection is needed per contact, and it does not matter which end initiated the connection.
+
+The hidden service layer conveniently provides confidentiality, ephemerality, and authenticates the server side, so the application protocol is kept very simple. The client side of a connection authenticates with a pre-shared random secret established during or soon after the contact request.
+
+A simple command/reply based binary protocol is used to communicate. It attempts to offer some reliability for commands to recover from unstable connections. This was chosen over any existing protocol (such as XMPP) or implementation for simplicity and strict control over the surface exposed for attacks against security and anonymity.
+
+The protocol includes a version negotiation step for future expansion. A detailed description of the format and commands can be found in the [protocol documentation](https://github.com/ricochet-im/ricochet/blob/master/doc/protocol.md#introduction-and-version-negotiation).
+
+## UI and usability
+
+User interface is an extremely important and often under-considered aspect of security and anonymity. Less technical users should be able to easily learn how to use the software, and what they need to do to keep themselves safe.
+
+Ricochet's UI aims to be simple and familiar to users of other IM software. Knowledge of Tor and networking concepts aren't a requirement. It should be easy to do things the right way, hard to accidentally break them, and possible for technical users to tweak.
+
+Contributions in this area are very welcome, especially in translations and non-technical documentation and review.
+
+## Future development
+
+The design described here is close to the simplest implementation possible. The protocol has the potential to be extended to enable features like file transfer or even voice/video streaming. More advanced use of hidden services (e.g. authentication) can mitigate the risks of publishing a publicly connectable service. Separate services or more elaborate designs can be used to prevent attacks by non-contacts. Future development in Tor can improve the cryptography and principles behind hidd [...]
+
+Ideas, suggestions, and bugs are welcome on the [issue tracker](https://github.com/ricochet-im/ricochet/issues). Patches are welcome via pull request.
diff --git a/doc/protocol.md b/doc/protocol.md
new file mode 100644
index 0000000..ac8f09e
--- /dev/null
+++ b/doc/protocol.md
@@ -0,0 +1,537 @@
+## Overview
+
+Ricochet is a peer-to-peer instant messaging system built on anonymity networks. This document
+defines the communication protocol between two Ricochet instances, as carried out over a Tor hidden
+service connection.
+
+The protocol is defined in three layers:
+
+The **connection layer** describes the use of an anonymized TCP-style connection for peer-to-peer
+communication.
+
+The **packet layer** separates the connection into a series of *packets* delivered to *channels*.
+This allows multiplexing different operations on the same connection, and packetizes data for
+channel-level parsing.
+
+The **channel layer** parses and handles packets according to the *channel type* and the state of
+that specific channel.
+
+### Connections
+
+> TODO: This is a brief explanation; we should reference a design/architecture document with more
+> details.
+
+##### Hidden services
+
+Ricochet uses Tor [hidden services][rend-spec] as a transport; the reader should be familiar with
+that architecture and the properties it provides. In particular:
+
+ * The hostname is calculated from a hash of the server's public key, and serves to authenticate the
+ server without relying on a third party
+ * Connections are encrypted end-to-end, using the server's key and a DHE handshake to provide
+ forward secrecy
+ * Both ends of a connection are anonymous in that neither peer should be able to identify or locate
+ the other, and no relay should be able to connect an identity to the requests it makes
+ * Impersonating a server without its private key requires an 80-bit SHA1 collision using a valid
+ RSA key
+
+> TODO: We should explore additional cryptography on top of what Tor offers; see
+> [issue 72](https://github.com/ricochet-im/ricochet/issues/72).
+
+##### Usage
+
+Each Ricochet instance publishes a hidden service, which serves as its identity and accepts
+connections from contacts. When it first comes online, it attempts to connect to the addresses of
+known contacts. If a connection is made, it is held open; a contact is considered online when there
+is an open connection. Connections are made on port 9878.
+
+> This solution isn't ideal; we'll be exploring better designs on top of hidden services to improve
+> scalability and anonymity properties.
+
+Only one active connection is needed for a contact. Connections are fully bidirectional and all
+behavior is equivalent regardless of which peer acts as server at the transport level.
+
+Ricochet does not use central servers; connections are made to services published directly by your
+contacts with no intermediary.
+
+Keeping open connections to unknown peers poses a risk for various attacks, including resource
+exhaustion. Clients must either authenticate or take other useful action (e.g. delivering a contact
+request) quickly. The server side of the connection should expire unknown connections.
+
+### Channels
+
+Channels divide up the connection to allow multiplexing, extensibility, and stateful behavior for
+*packets*.
+
+The **channel id** associates packets with an instance of a channel on the connection, which was
+previously created by an *OpenChannel* message.
+
+The **channel type** defines how packets are parsed and handled. Distinct features have separate
+channel types; for example, `im.ricochet.chat` and `im.ricochet.file-transfer`. By convention,
+these are in reverse-URI form.
+
+Channels exist within a connection. The channel ID is unique only within that connection, and all
+channels are closed when the connection is lost.
+
+Channels must be explicitly created with an *OpenChannel* message. The recipient of that message
+chooses to accept or reject the channel; for example, it may reject channel types it doesn't
+support, or won't allow this peer to access.
+
+Channel instances also provide a state for messages. For example, all operations associated with the
+transfer of one file take place on the same channel, and a second file transfer would use a second
+channel of the same type.
+
+Both peers may send packets to the same channel. Depending on the channel type, messages may be
+fully bidirectional or may be a command-response protocol.
+
+At the beginning of the connection, one channel exists automatically: the *control channel*. As a
+special case, it always has a channel ID of `0`. The control channel provides functionality for
+creating new channels and maintenance of the underlying connection.
+
+### Authentication
+
+Ricochet needs a variety of levels and types of authentication; known contacts might have a strong
+proof of identity, while a request from a new person comes with a different proof and an anti-spam
+"proof of work". Some features could allow unauthenticated use.
+
+To support these scenarios, there is no pre-protocol authentication step. Peers add credentials to
+their connection by opening and completing various types of authentication channels.
+
+The most common example is `im.ricochet.auth.hidden-service`: the peer creates a channel of this
+type and carries out its protocol to prove that it has the private key for a hidden service name.
+Afterwards, that peer can send a contact request, and the recipient is able to know the source of
+that request.
+
+Another hypothetical example is `example.hashcash`: the peer would complete a proof of work as
+evidence that it doesn't intend to spam the recipient.
+
+These credentials are associated with the connection. For example, you may decide to not allow an
+`im.ricochet.chat` channel unless the peer has completed `im.ricochet.auth.hidden-service`
+authentication for a known contact's address.
+
+The hidden service transport provides one special case: the server end of the connection is
+authenticated equivalent to `im.ricochet.auth.hidden-service` at the beginning of the connection,
+and must be given equivalent privileges.
+
+### Conventions
+
+Unless otherwise noted, these conventions and definitions are used for the protocol and this
+document:
+
+* *Peer* refers to either Ricochet instance on a connection
+* *Recipient* refers to the peer which received the message
+* Channels encode data using [protocol buffers][protobuf], with one protobuf message per packet
+* Unless the channel type specifies another mechanism, unknown/unparsable messages result in
+ closing the channel.
+* Protocol behavior which appears malicious or strange may trigger consequences
+* Strings are UTF-8 encoded and should be carefully validated and handled
+* Any reply may be artificially delayed, but order must be preserved
+
+## Specification
+
+### Introduction and version negotiation
+
+Immediately after establishing a connection, the client side must send an introduction message
+identifying versions of the protocol that it is able to use. The server responds with one of
+those versions, or an error indicating that no compatible version exists.
+
+This step exists to enable smoother protocol changes in the future, and for better compatibility
+with old clients.
+
+The client begins the connection by sending the following raw sequence of bytes:
+
+```
+0x49
+0x4D
+nVersions // One byte, number of supported protocol versions
+nVersions times:
+ version // One byte, protocol version number
+```
+
+The total size is 3 plus the number of supported versions bytes. The server side of the connection
+must respond with a single byte for the selected version number, or 0xFF if no suitable version
+is found.
+
+This document describes protocol version 1. Known versions are:
+```
+0 The Ricochet 1.0 protocol
+1 This document
+```
+
+If the negotiation is successful, the connection can be immediately used to begin exchanging messages
+(the packet layer, below).
+
+### Packet layer
+
+The base layer on the connection is a trivial packet structure:
+
+```
+uint16 size // Big endian, including the header bytes
+uint16 channel // Big endian, channel identifier
+bytes data // Content of the packet
+```
+
+The connection reads and buffers data until it has a full packet, then looks up the channel
+identifier within the list of open channels. If the channel is found, data is passed to it for
+parsing and handling.
+
+The only other functionality implemented at this layer is closing a channel. A channel is closed by
+sending a packet to that channel with 0 bytes of data. When a packet is received for an unknown
+channel, the recipient responds by closing that channel.
+
+Note that packets are limited to 65,535 bytes in size, including the 4-byte header. To avoid causing
+latency on low throughput connections, channels should keep packets as small as possible. If a
+channel type requires larger packets of data, it must define a way to reassemble them specific to
+that channel type.
+
+### Control channel
+
+The control channel is a special case: it is the only channel open from the beginning of a
+connection, and it is always assigned the channel identifier `0`. If the control channel is closed,
+the connection must also terminate.
+
+The control channel contains methods used for maintenance of the connection and the creation of
+other channels. It is a stateless series of protobuf-serialized `ControlMessage`, with one message
+encoded per packet. Both peers on the connection may send all messages.
+
+##### Packet
+```protobuf
+message Packet {
+ // Must contain exactly one field
+ optional OpenChannel open_channel = 1;
+ optional ChannelResult channel_result = 2;
+ optional KeepAlive keep_alive = 3;
+ optional EnableFeatures enable_features = 4;
+ optional FeaturesEnabled features_enabled = 5;
+}
+```
+
+All packets sent to the control channel must encode a *Packet*, with exactly one field.
+
+##### OpenChannel
+```protobuf
+message OpenChannel {
+ required int32 channel_identifier = 1; // Arbitrary unique identifier for this channel instance
+ required string channel_type = 2; // String identifying channel type; e.g. im.ricochet.chat
+
+ // It is valid to extend the OpenChannel message to add fields specific
+ // to the requested channel_type.
+ extensions 100 to max;
+}
+```
+
+Requests to open a channel of the type *channel_type*, using the identifier *channel_identifier* for
+packets. Additional data may be added in extensions to this message.
+
+The recipient of an OpenChannel message checks whether it supports the *channel_type*, if the
+*channel_identifier* is valid and unassigned, and the validity of any extension data. The recipient
+also checks whether this connection allows channels of this type; for example, if the peer is
+sufficiently authenticated.
+
+If the request is allowed, *channel_identifier* will be sent with packets destined for this channel
+within this connection. It is also used to associate the *ChannelResult* message with this request.
+There are several rules that must be followed when choosing or accepting a *channel_identifier*:
+
+* The client side of a connection may only open odd-numbered channels
+* The server side may only open even-numbered channels
+* The identifier must fit within the range of uint16
+* The identifier must not be used by an open channel
+* The identifier should increase for every OpenChannel message, wrapping if necessary. Identifiers
+ should not be re-used except after wrapping.
+
+The even/odd restrictions on *channel_identifier* prevent peers from racing to open a channel with
+the same id. Channels are tied to a specific connection, so there is no collision between connections.
+
+By convention, channel types are in reverse URI format, e.g. `im.ricochet.chat`.
+
+A *ChannelResult* message must always be generated in response. If the request is egregiously
+invalid, the connection may be terminated instead.
+
+##### ChannelResult
+```protobuf
+message ChannelResult {
+ required int32 channel_identifier = 1; // Matching the value from OpenChannel
+ required bool opened = 2; // If the channel is now open
+
+ enum CommonError {
+ GenericError = 0;
+ UnknownTypeError = 1;
+ UnauthorizedError = 2;
+ BadUsageError = 3;
+ FailedError = 4;
+ }
+
+ optional CommonError common_error = 3;
+ optional string error_message = 4;
+
+ // As with OpenChannel, it is valid to extend this message with fields specific
+ // to the channel type.
+ extensions 100 to max;
+}
+```
+
+Sent in response to an *OpenChannel* message, with the same *channel_identifier* value. If *opened*
+is true, the channel is now ready to accept packets tagged with that identifier.
+
+##### KeepAlive
+```protobuf
+message KeepAlive {
+ required bool response_requested = 1;
+}
+```
+
+A simple ping message. If *response_requested* is true, a *KeepAlive* message is generated in
+response with *response_requested* as false.
+
+##### EnableFeatures
+```protobuf
+message EnableFeatures {
+ repeated string feature = 1;
+ extensions 100 to max;
+}
+
+message FeaturesEnabled {
+ repeated string feature = 1;
+ extensions 100 to max;
+}
+```
+
+Simple feature negotiation. Either peer may send the *EnableFeatures* message with a list of
+strings representing protocol changes or features. The recipient must respond with *FeaturesEnabled*
+containing the subset of those strings it recognizes and has enabled.
+
+No such feature strings are currently defined, and the current implementation should always respond
+with an empty list.
+
+### Chat channel
+
+| Channel | Detail |
+| ------------------ | ------ |
+| **Channel type** | `im.ricochet.chat` |
+| **Purpose** | Sending text-based instant messages |
+| **Direction** | One-way: Only initiator of the channel sends commands, and recipient sends replies |
+| **Singleton** | Only one chat channel is created by each peer on the connection |
+| **Authentication** | Requires `im.ricochet.auth.hidden-service` as a known contact |
+
+A chat channel allows the initiator (the peer who created the channel) to send messages, and receive
+acknowledgement for those messages. The opposing peer should also create a chat channel to send its
+own chat messages. Acknowledgement must be on the same channel as the original message. One peer may
+not open more than one chat channel on the same connection.
+
+Two chat channels (one per peer) are used to avoid ambiguity on which peer creates a chat channel,
+or which channel would be used in a race situation.
+
+##### Packet
+```protobuf
+message Packet {
+ optional ChatMessage chat_message = 1;
+ optional ChatAcknowledge chat_acknowledge = 2;
+}
+```
+
+##### ChatMessage
+```protobuf
+message ChatMessage {
+ required string message_text = 1;
+ optional uint32 message_id = 2; // Random ID for ack
+ optional int64 time_delta = 3; // Delta in seconds between now and when message was written
+}
+```
+
+A *message_id* of zero (or omitted) indicates that the recipient doesn't expect acknowledgement.
+
+If *message_id* is non-zero, the recipient should acknowledge receiving this message by sending
+*ChatAcknowledge*. Unacknowledged messages may be re-sent with the same *message_id*, and the
+recipient should drop duplicate messages with an identical non-zero *message_id*, after sending an
+acknowledgement.
+
+Sometimes, messages may be delayed or potentially lost across connections over a short period of time.
+In order to allow messages to be re-sent after a lost connection, clients should try to avoid choosing
+message ids from a recent connection (with the same peer) at the start of a new connection. For
+example, that can be done by randomizing the first message id for a channel.
+
+The *time_delta* field is a delta in seconds between when the message was composed and when it is being
+transmitted. For messages that are sent immediately, it should be 0 or omitted. If a message was written
+and couldn't be sent immediately (due to a connection failure, for example), the *time_delta* should be
+an approximation of when it was composed. A positive value does not make any sense, as it would indicate
+a message composed in the future.
+
+##### ChatAcknowledge
+```protobuf
+message ChatAcknowledge {
+ optional uint32 message_id = 1;
+ optional bool accepted = 2 [default = true];
+}
+```
+
+Acknowledge receipt of a *ChatMessage*.
+
+### Contact request channel
+
+| Channel | Detail |
+| ------------------ | ------ |
+| **Channel type** | `im.ricochet.contact.request` |
+| **Purpose** | Introduce a new client and ask for user approval to send messages |
+| **Direction** | One-way: Only initiator of the channel sends commands, and recipient sends replies |
+| **Singleton** | One instance created only by the client side of a connection |
+| **Authentication** | Requires `im.ricochet.auth.hidden-service` |
+
+Contact requests are sent to introduce oneself to the recipient and ask for further contact,
+including being put on the recipient's persistent contact list.
+
+The requesting client must have authenticated using `im.ricochet.auth.hidden-service` to prove
+ownership of a hidden service name.
+
+The recipient isn't required to immediately respond to a request. If the request is approved, the
+recipient may connect to the requesting client, and that is treated as implicitly accepting the
+request.
+
+##### OpenChannel
+```protobuf
+extend Control.OpenChannel {
+ optional ContactRequest contact_request = 200;
+}
+
+extend Control.ChannelResult {
+ optional Response response = 201;
+}
+```
+
+The OpenChannel message on a contact request channel must include the `contact_request` extension. A
+successful ChannelResult must include the `response` extension.
+
+If the response finishes the request, the channel will be closed immediately. Otherwise, the channel
+remains open to wait for another *Response* message (e.g. going from Pending to Accepted).
+
+##### ContactRequest
+```protobuf
+// Sent only as an attachment to OpenChannel
+message ContactRequest {
+ optional string nickname = 1;
+ optional string message_text = 2;
+}
+```
+
+Deliver a contact request, usually with a message and nickname attached. The "identity" of the
+request is proven through `im.ricochet.auth.hidden-service` authentication.
+
+The request is sent as an extension on the *OpenChannel* message.
+
+##### Response
+```protobuf
+message Response {
+ enum Status {
+ Undefined = 0; // Not valid in transmitted messages
+ Pending = 1;
+ Accepted = 2;
+ Rejected = 3;
+ Error = 4;
+ }
+
+ required Status status = 1;
+ optional string error_message = 2;
+}
+```
+
+Indicates the status of a contact request. The *Pending* status may be followed by another
+*ContactRequestResponse* with a final status. All other statuses must be followed by closing the
+channel, and may also close the connection. Closing the channel or the connection does not imply
+having a response - for example, the recipient may decide to time out the connection while it is
+waiting in the *Pending* state.
+
+The initial *Response* is sent as an extension to the *ChannelResult* message when the channel is
+opened. If that response is final, the channel is closed immediately after. Otherwise, the channel
+remains open, and the only valid message is another *Response*.
+
+If a request is *Rejected*, the requesting client must not send that request again, unless the user
+has manually cancelled the previous request and made a new one. Recipients should automatically
+reject excessive or abusive requests.
+
+If an *Error* occurs, the requesting client may only request again if it believes the error is
+solved. Recipients should automatically reject requests after repetitive errors.
+
+### AuthHiddenService
+
+| Channel | Detail |
+| ------------------ | ------ |
+| **Channel type** | `im.ricochet.auth.hidden-service` |
+| **Purpose** | Authenticate as the owner of a Tor hidden service |
+| **Direction** | One-way: Only initiator of the channel sends commands, and recipient sends replies |
+| **Singleton** | One instance created only by the client side of a connection |
+| **Authentication** | No prior authentication required |
+
+The `im.ricochet.auth.hidden-service` channel is used to prove ownership of a hidden service name by
+demonstrating ownership of a matching private key. This is used to authenticate as a known contact,
+or to prove ownership of a service name before sending a contact request.
+
+As a result of the transport, the server side of a hidden service connection is considered to have
+automatically completed `im.ricochet.auth.hidden-service` authentication, and must be allowed
+equivalent access.
+
+##### Packet
+```protobuf
+extend OpenChannel {
+ optional bytes client_cookie = 7200; // 16 random bytes
+}
+
+extend ChannelResult {
+ optional bytes server_cookie = 7200; // 16 random bytes
+}
+
+message Packet {
+ optional Proof proof = 1;
+ optional Result result = 2;
+}
+```
+
+The channel is opened by the peer who wishes to authenticate itself. The *OpenChannel* message
+must contain a *client_cookie* of 16 bytes. A successful *ChannelResult* message must include
+the *server_cookie* field, with a randomly generated value used to prevent replayed authentication.
+
+##### Proof
+```protobuf
+message Proof {
+ optional bytes public_key = 1; // DER encoded RSA public key
+ optional bytes signature = 2; // RSA signature
+}
+```
+
+The proof is calculated as:
+
+```
+// + represents concatenation, and function is HMAC-SHA256(key, message)
+HMAC-SHA256(client_cookie + server_cookie,
+ client_hostname // base32-encoded client address, without .onion
+ + recipient_hostname // base32-encoded server address, without .onion
+)
+```
+
+This proof is signed with the hidden service's private key using PKCS #1 v2.0 (as per OpenSSL
+RSA_sign) to make *signature*.
+
+The recipient of this message must:
+
+* Reject any message with a public_key field too large or not correctly formed to be a DER-encoded
+ 1024-bit RSA public key
+* Reject any message with a signature field of an unexpected size
+* Decode the public_key, and calculate its 'onion' address per [rend-spec][rend-spec]
+* Build the proof message
+* Verify that *signature* is a valid signature of the proof by *public_key*
+
+##### Result
+```protobuf
+message Result {
+ required bool accepted = 1;
+ optional bool is_known_contact = 2;
+}
+```
+
+If authentication is successful as a known contact, whose connection will be allowed to remain open
+without any further purpose, the *is_known_contact* flag must be set as true. If this flag is not
+set, the authenticating client should assume that it is not authorized (except e.g. to send a
+contact request).
+
+After sending *Result*, the channel should be closed.
+
+[rend-spec]: https://gitweb.torproject.org/torspec.git/blob/HEAD:/rend-spec.txt
+[protobuf]: https://code.google.com/p/protobuf/
diff --git a/hardened.pri b/hardened.pri
new file mode 100644
index 0000000..1b6c3b0
--- /dev/null
+++ b/hardened.pri
@@ -0,0 +1,46 @@
+load(configure)
+# Define common variables; these are used by config tests _and_ the actual build
+
+# Supported in gcc 4.8+
+HARDENED_SANITIZE_FLAGS = -fsanitize=address
+# Supported in gcc 4.9+
+HARDENED_SANITIZE_UBSAN_FLAGS = -fsanitize=undefined -fsanitize=integer-divide-by-zero -fsanitize=bounds -fsanitize=alignment -fsanitize=float-divide-by-zero -fsanitize=float-cast-overflow -fno-sanitize-recover
+# Supported in gcc 5.0+
+HARDENED_SANITIZE_UBSAN_MORE_FLAGS = -fsanitize=vptr -fsanitize=object-size
+# vtable-verify requires some OS support; see https://bugzilla.novell.com/show_bug.cgi?id=877239
+HARDENED_VTABLE_VERIFY_FLAGS = -fvtable-verify=std
+
+HARDENED_STACK_PROTECTOR_STRONG_FLAGS = -fstack-protector-strong
+HARDENED_STACK_PROTECTOR_FLAGS = -fstack-protector --param=ssp-buffer-size=4
+
+HARDENED_MINGW_64ASLR_FLAGS = -Wl,--dynamicbase -Wl,--high-entropy-va
+
+# Run tests and apply options where possible
+CONFIG(hardened) {
+ # mingw is always PIC, and complains about the flag
+ !mingw:HARDEN_FLAGS = -fPIC
+
+ qtCompileTest(sanitize):HARDEN_FLAGS += $$HARDENED_SANITIZE_FLAGS
+ qtCompileTest(sanitize-ubsan):HARDEN_FLAGS += $$HARDENED_SANITIZE_UBSAN_FLAGS
+ qtCompileTest(sanitize-ubsan-more):HARDEN_FLAGS += $$HARDENED_SANITIZE_UBSAN_MORE_FLAGS
+ qtCompileTest(vtable-verify):HARDEN_FLAGS += $$HARDENED_VTABLE_VERIFY_FLAGS
+
+ qtCompileTest(stack-protector-strong) {
+ HARDEN_FLAGS += $$HARDENED_STACK_PROTECTOR_STRONG_FLAGS
+ } else {
+ qtCompileTest(stack-protector):HARDEN_FLAGS += $$HARDENED_STACK_PROTECTOR_FLAGS
+ }
+
+ mingw {
+ qtCompileTest(mingw-64aslr):QMAKE_LFLAGS *= $$HARDENED_MINGW_64ASLR_FLAGS
+ QMAKE_LFLAGS *= -Wl,--nxcompat -Wl,--dynamicbase
+ }
+
+ QMAKE_CXXFLAGS *= $$HARDEN_FLAGS
+ QMAKE_LFLAGS *= $$HARDEN_FLAGS
+
+ # _FORTIFY_SOURCE requires -O, so only use on release builds
+ CONFIG(release,debug|release):QMAKE_CXXFLAGS *= -D_FORTIFY_SOURCE=2
+ # Linux specific
+ unix:!macx:QMAKE_LFLAGS *= -pie -Wl,-z,relro,-z,now
+}
diff --git a/icons/LICENSE b/icons/LICENSE
new file mode 100644
index 0000000..bdb1fce
--- /dev/null
+++ b/icons/LICENSE
@@ -0,0 +1,45 @@
+Ricochet "R" icon
+Copyright 2014, Lawrence Eastland (lawrence at eyedesign.com.au)
+
+This work is licensed under the Creative Commons Attribution-ShareAlike 4.0
+International License. To view a copy of this license, visit
+http://creativecommons.org/licenses/by-sa/4.0/ or send a letter to
+Creative Commons, PO Box 1866, Mountain View, CA 94042, USA.
+
+
+## Font Awesome
+
+ Copyright (C) 2012 by Dave Gandy
+
+ Author: Dave Gandy
+ License: SIL ()
+ Homepage: http://fortawesome.github.com/Font-Awesome/
+
+
+## Web Symbols
+
+ Copyright (c) 2011 by Just Be Nice studio. All rights reserved.
+
+ Author: Just Be Nice studio
+ License: SIL (http://scripts.sil.org/OFL)
+ Homepage: http://www.justbenicestudio.com/
+
+
+## Entypo
+
+ Copyright (C) 2012 by Daniel Bruce
+
+ Author: Daniel Bruce
+ License: SIL (http://scripts.sil.org/OFL)
+ Homepage: http://www.entypo.com
+
+
+## Iconic
+
+ Copyright (C) 2012 by P.J. Onori
+
+ Author: P.J. Onori
+ License: SIL (http://scripts.sil.org/OFL)
+ Homepage: http://somerandomdude.com/work/iconic/
+
+
diff --git a/icons/Ricochet.icns b/icons/Ricochet.icns
new file mode 100644
index 0000000..b31d9e6
Binary files /dev/null and b/icons/Ricochet.icns differ
diff --git a/icons/icons.qrc b/icons/icons.qrc
new file mode 100644
index 0000000..b0548c1
--- /dev/null
+++ b/icons/icons.qrc
@@ -0,0 +1,6 @@
+<RCC>
+ <qresource prefix="/icons">
+ <file>ricochet.svg</file>
+ <file>ricochet_icons.ttf</file>
+ </qresource>
+</RCC>
diff --git a/icons/ricochet.ico b/icons/ricochet.ico
new file mode 100644
index 0000000..5c665b3
Binary files /dev/null and b/icons/ricochet.ico differ
diff --git a/icons/ricochet.png b/icons/ricochet.png
new file mode 100644
index 0000000..c41ef50
Binary files /dev/null and b/icons/ricochet.png differ
diff --git a/icons/ricochet.svg b/icons/ricochet.svg
new file mode 100644
index 0000000..5c2e137
--- /dev/null
+++ b/icons/ricochet.svg
@@ -0,0 +1,1324 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.1"
+ width="813.34375"
+ height="1149.8438"
+ id="svg3004"
+ xml:space="preserve"
+ inkscape:version="0.48.5 r10040"
+ sodipodi:docname="ricochet.svg"><sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1006"
+ inkscape:window-height="804"
+ id="namedview492"
+ showgrid="false"
+ inkscape:zoom="0.20524528"
+ inkscape:cx="416.41631"
+ inkscape:cy="574.92188"
+ inkscape:window-x="621"
+ inkscape:window-y="192"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="svg3004" /><metadata
+ id="metadata3010"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
+ id="defs3008"><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3026"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-64.005676,-175.8542,-175.8542,64.005691,603.71484,586.15527)"
+ spreadMethod="pad"><stop
+ id="stop3028"
+ style="stop-color:#b1e0e5;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3030"
+ style="stop-color:#00b0f0;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3046"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0,112.22852,112.22852,0,631.29932,565.89648)"
+ spreadMethod="pad"><stop
+ id="stop3048"
+ style="stop-color:#00b2d9;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3050"
+ style="stop-color:#337abe;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3066"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(77.404358,-134.06815,-134.06815,-77.404343,680.26367,581.37695)"
+ spreadMethod="pad"><stop
+ id="stop3068"
+ style="stop-color:#00b2d9;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3070"
+ style="stop-color:#337abe;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3086"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-64.005508,-175.85374,-175.85374,64.005524,247.5459,660.99707)"
+ spreadMethod="pad"><stop
+ id="stop3088"
+ style="stop-color:#b1e0e5;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3090"
+ style="stop-color:#00b0f0;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3106"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(294.5083,0,0,-294.5083,127.87695,696.85156)"
+ spreadMethod="pad"><stop
+ id="stop3108"
+ style="stop-color:#00b2d9;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3110"
+ style="stop-color:#337abe;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3126"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(171.14064,203.95746,203.95746,-171.14064,277.22754,487.20605)"
+ spreadMethod="pad"><stop
+ id="stop3128"
+ style="stop-color:#00b2d9;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3130"
+ style="stop-color:#337abe;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3146"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-64.005173,-175.85283,-175.85283,64.005188,247.54687,846.9873)"
+ spreadMethod="pad"><stop
+ id="stop3148"
+ style="stop-color:#b1e0e5;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3150"
+ style="stop-color:#00b0f0;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3166"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(294.5083,0,0,-294.5083,127.87695,882.8418)"
+ spreadMethod="pad"><stop
+ id="stop3168"
+ style="stop-color:#00b2d9;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3170"
+ style="stop-color:#337abe;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3186"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(119.17529,0,0,-119.17529,303.20996,775.17578)"
+ spreadMethod="pad"><stop
+ id="stop3188"
+ style="stop-color:#00b2d9;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3190"
+ style="stop-color:#337abe;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3206"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-64.005173,-175.85283,-175.85283,64.005188,371.22412,767.14355)"
+ spreadMethod="pad"><stop
+ id="stop3208"
+ style="stop-color:#ffffff;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3210"
+ style="stop-color:#00b2d9;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3226"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(17.498856,99.241028,99.241028,-17.498856,390.05859,753.37793)"
+ spreadMethod="pad"><stop
+ id="stop3228"
+ style="stop-color:#00b2d9;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3230"
+ style="stop-color:#337abe;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3246"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(77.403381,-134.06645,-134.06645,-77.403366,447.77344,762.36523)"
+ spreadMethod="pad"><stop
+ id="stop3248"
+ style="stop-color:#00b2d9;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3250"
+ style="stop-color:#337abe;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3266"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-64.005173,-175.85283,-175.85283,64.005188,247.54639,1032.478)"
+ spreadMethod="pad"><stop
+ id="stop3268"
+ style="stop-color:#b1e0e5;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3270"
+ style="stop-color:#00b0f0;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3286"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(294.5083,0,0,-294.5083,127.87695,1068.333)"
+ spreadMethod="pad"><stop
+ id="stop3288"
+ style="stop-color:#00b2d9;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3290"
+ style="stop-color:#337abe;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3306"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(119.17529,0,0,-119.17529,303.20996,960.6665)"
+ spreadMethod="pad"><stop
+ id="stop3308"
+ style="stop-color:#00b2d9;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3310"
+ style="stop-color:#337abe;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3326"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-64.005173,-175.85283,-175.85283,64.005188,247.5459,1218.4688)"
+ spreadMethod="pad"><stop
+ id="stop3328"
+ style="stop-color:#b1e0e5;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3330"
+ style="stop-color:#00b0f0;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3346"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(294.5083,0,0,-294.5083,127.87695,1254.3232)"
+ spreadMethod="pad"><stop
+ id="stop3348"
+ style="stop-color:#b1e0e5;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3350"
+ style="stop-color:#60b9de;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3366"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(133.21353,-23.489105,-23.489105,-133.21353,296.19043,1158.4014)"
+ spreadMethod="pad"><stop
+ id="stop3368"
+ style="stop-color:#00b2d9;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3370"
+ style="stop-color:#337abe;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3386"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-64.005341,-175.85329,-175.85329,64.005356,562.46484,983.88379)"
+ spreadMethod="pad"><stop
+ id="stop3388"
+ style="stop-color:#b1e0e5;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3390"
+ style="stop-color:#00b0f0;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3406"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-294.5083,-3.05e-5,-3.05e-5,294.5083,737.3042,1019.7383)"
+ spreadMethod="pad"><stop
+ id="stop3408"
+ style="stop-color:#b1e0e5;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3410"
+ style="stop-color:#60b9de;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3426"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(77.403381,-134.06645,-134.06645,-77.403366,639.01465,979.10498)"
+ spreadMethod="pad"><stop
+ id="stop3428"
+ style="stop-color:#00b2d9;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3430"
+ style="stop-color:#337abe;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3446"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-64.005173,-175.85283,-175.85283,64.005188,466.37793,1189.9673)"
+ spreadMethod="pad"><stop
+ id="stop3448"
+ style="stop-color:#b1e0e5;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3450"
+ style="stop-color:#00b0f0;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3466"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(294.5083,0,0,-294.5083,346.7085,1225.8223)"
+ spreadMethod="pad"><stop
+ id="stop3468"
+ style="stop-color:#b1e0e5;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3470"
+ style="stop-color:#60b9de;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3486"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(77.403381,-134.06645,-134.06645,-77.403366,542.92773,1185.189)"
+ spreadMethod="pad"><stop
+ id="stop3488"
+ style="stop-color:#00b2d9;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3490"
+ style="stop-color:#337abe;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3506"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-30.103607,-82.709,-82.709,30.103607,607.46094,1085.8174)"
+ spreadMethod="pad"><stop
+ id="stop3508"
+ style="stop-color:#b1e0e5;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3510"
+ style="stop-color:#00b0f0;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3526"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(138.51611,0,0,-138.51611,551.17627,1102.6812)"
+ spreadMethod="pad"><stop
+ id="stop3528"
+ style="stop-color:#b1e0e5;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3530"
+ style="stop-color:#60b9de;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3550"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-30.103775,-82.709457,-82.709457,30.103775,572.45996,698.28613)"
+ spreadMethod="pad"><stop
+ id="stop3552"
+ style="stop-color:#b1e0e5;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3554"
+ style="stop-color:#00b0f0;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3570"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(138.51611,0,0,-138.51611,516.17529,715.15039)"
+ spreadMethod="pad"><stop
+ id="stop3572"
+ style="stop-color:#00b2d9;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3574"
+ style="stop-color:#337abe;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3594"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-64.005341,-175.85329,-175.85329,64.005356,1395.6465,586.15527)"
+ spreadMethod="pad"><stop
+ id="stop3596"
+ style="stop-color:#b1e0e5;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3598"
+ style="stop-color:#00b0f0;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3614"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0,112.22852,112.22852,0,1423.2305,565.89648)"
+ spreadMethod="pad"><stop
+ id="stop3616"
+ style="stop-color:#00b2d9;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3618"
+ style="stop-color:#337abe;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3634"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(77.403381,-134.06645,-134.06645,-77.403366,1472.1953,581.37695)"
+ spreadMethod="pad"><stop
+ id="stop3636"
+ style="stop-color:#00b2d9;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3638"
+ style="stop-color:#337abe;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3654"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-64.005341,-175.85329,-175.85329,64.005356,1039.4785,660.99707)"
+ spreadMethod="pad"><stop
+ id="stop3656"
+ style="stop-color:#b1e0e5;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3658"
+ style="stop-color:#00b0f0;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3674"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(294.50781,0,0,-294.50781,919.80859,696.85156)"
+ spreadMethod="pad"><stop
+ id="stop3676"
+ style="stop-color:#00b2d9;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3678"
+ style="stop-color:#337abe;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3694"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(171.14064,203.95746,203.95746,-171.14064,1069.1582,487.20703)"
+ spreadMethod="pad"><stop
+ id="stop3696"
+ style="stop-color:#00b2d9;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3698"
+ style="stop-color:#337abe;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3714"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-64.005005,-175.85237,-175.85237,64.00502,1039.4766,846.98633)"
+ spreadMethod="pad"><stop
+ id="stop3716"
+ style="stop-color:#b1e0e5;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3718"
+ style="stop-color:#00b0f0;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3734"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(294.50781,0,0,-294.50781,919.80859,882.8418)"
+ spreadMethod="pad"><stop
+ id="stop3736"
+ style="stop-color:#00b2d9;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3738"
+ style="stop-color:#337abe;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3754"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(119.17578,0,0,-119.17578,1095.1406,775.17578)"
+ spreadMethod="pad"><stop
+ id="stop3756"
+ style="stop-color:#00b2d9;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3758"
+ style="stop-color:#337abe;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3774"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-64.005341,-175.85329,-175.85329,64.005356,1163.1553,767.14453)"
+ spreadMethod="pad"><stop
+ id="stop3776"
+ style="stop-color:#fee6da;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3778"
+ style="stop-color:#f89734;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3794"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(17.498856,99.241028,99.241028,-17.498856,1181.9902,753.37793)"
+ spreadMethod="pad"><stop
+ id="stop3796"
+ style="stop-color:#fdc98e;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3798"
+ style="stop-color:#f1623b;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3814"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(77.403381,-134.06645,-134.06645,-77.403366,1239.7051,762.36523)"
+ spreadMethod="pad"><stop
+ id="stop3816"
+ style="stop-color:#f89734;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3818"
+ style="stop-color:#f1623b;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3834"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-64.005005,-175.85237,-175.85237,64.00502,1039.4775,1032.4775)"
+ spreadMethod="pad"><stop
+ id="stop3836"
+ style="stop-color:#b1e0e5;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3838"
+ style="stop-color:#00b0f0;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3854"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(294.50781,0,0,-294.50781,919.80859,1068.333)"
+ spreadMethod="pad"><stop
+ id="stop3856"
+ style="stop-color:#00b2d9;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3858"
+ style="stop-color:#337abe;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3874"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(119.17578,0,0,-119.17578,1095.1406,960.6665)"
+ spreadMethod="pad"><stop
+ id="stop3876"
+ style="stop-color:#00b2d9;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3878"
+ style="stop-color:#337abe;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3894"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-64.005173,-175.85283,-175.85283,64.005188,1039.4785,1218.4688)"
+ spreadMethod="pad"><stop
+ id="stop3896"
+ style="stop-color:#b1e0e5;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3898"
+ style="stop-color:#00b0f0;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3914"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(294.50781,0,0,-294.50781,919.80859,1254.3232)"
+ spreadMethod="pad"><stop
+ id="stop3916"
+ style="stop-color:#b1e0e5;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3918"
+ style="stop-color:#60b9de;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3934"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(133.21449,-23.489273,-23.489273,-133.21449,1088.1211,1158.4014)"
+ spreadMethod="pad"><stop
+ id="stop3936"
+ style="stop-color:#00b2d9;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3938"
+ style="stop-color:#337abe;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3954"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-64.005341,-175.85329,-175.85329,64.005356,1354.3965,983.88379)"
+ spreadMethod="pad"><stop
+ id="stop3956"
+ style="stop-color:#b1e0e5;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3958"
+ style="stop-color:#00b0f0;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3974"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-294.50879,-3.05e-5,-3.05e-5,294.50879,1529.2354,1019.7383)"
+ spreadMethod="pad"><stop
+ id="stop3976"
+ style="stop-color:#b1e0e5;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3978"
+ style="stop-color:#60b9de;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient3994"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(77.40387,-134.06731,-134.06731,-77.403854,1430.9463,979.10449)"
+ spreadMethod="pad"><stop
+ id="stop3996"
+ style="stop-color:#00b2d9;stop-opacity:1"
+ offset="0" /><stop
+ id="stop3998"
+ style="stop-color:#337abe;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient4014"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-64.005173,-175.85283,-175.85283,64.005188,1258.3086,1189.9678)"
+ spreadMethod="pad"><stop
+ id="stop4016"
+ style="stop-color:#b1e0e5;stop-opacity:1"
+ offset="0" /><stop
+ id="stop4018"
+ style="stop-color:#00b0f0;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient4034"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(294.50879,0,0,-294.50879,1138.6396,1225.8223)"
+ spreadMethod="pad"><stop
+ id="stop4036"
+ style="stop-color:#b1e0e5;stop-opacity:1"
+ offset="0" /><stop
+ id="stop4038"
+ style="stop-color:#60b9de;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient4054"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(77.403381,-134.06645,-134.06645,-77.403366,1334.8584,1185.1885)"
+ spreadMethod="pad"><stop
+ id="stop4056"
+ style="stop-color:#00b2d9;stop-opacity:1"
+ offset="0" /><stop
+ id="stop4058"
+ style="stop-color:#337abe;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient4074"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-30.103943,-82.70993,-82.70993,30.103943,1399.3926,1085.8184)"
+ spreadMethod="pad"><stop
+ id="stop4076"
+ style="stop-color:#b1e0e5;stop-opacity:1"
+ offset="0" /><stop
+ id="stop4078"
+ style="stop-color:#00b0f0;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient4094"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(138.51562,0,0,-138.51562,1343.1074,1102.6812)"
+ spreadMethod="pad"><stop
+ id="stop4096"
+ style="stop-color:#b1e0e5;stop-opacity:1"
+ offset="0" /><stop
+ id="stop4098"
+ style="stop-color:#60b9de;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient4118"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-30.103775,-82.709457,-82.709457,30.103775,1364.3916,698.28613)"
+ spreadMethod="pad"><stop
+ id="stop4120"
+ style="stop-color:#b1e0e5;stop-opacity:1"
+ offset="0" /><stop
+ id="stop4122"
+ style="stop-color:#00b0f0;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient4138"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(138.5166,0,0,-138.5166,1308.1064,715.15039)"
+ spreadMethod="pad"><stop
+ id="stop4140"
+ style="stop-color:#00b2d9;stop-opacity:1"
+ offset="0" /><stop
+ id="stop4142"
+ style="stop-color:#337abe;stop-opacity:1"
+ offset="1" /></linearGradient><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient4728"
+ xlink:href="#linearGradient3634"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(77.403381,-134.06645,-134.06645,-77.403366,1472.1953,581.37695)"
+ spreadMethod="pad" /><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient5011"
+ xlink:href="#linearGradient3614"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0,112.22852,112.22852,0,1423.2305,565.89648)"
+ spreadMethod="pad" /><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient5013"
+ xlink:href="#linearGradient3634"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(77.403381,-134.06645,-134.06645,-77.403366,1472.1953,581.37695)"
+ spreadMethod="pad" /><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient5015"
+ xlink:href="#linearGradient3594"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-64.005341,-175.85329,-175.85329,64.005356,1395.6465,586.15527)"
+ spreadMethod="pad" /><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient5017"
+ xlink:href="#linearGradient3654"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-64.005341,-175.85329,-175.85329,64.005356,1039.4785,660.99707)"
+ spreadMethod="pad" /><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient5019"
+ xlink:href="#linearGradient3674"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(294.50781,0,0,-294.50781,919.80859,696.85156)"
+ spreadMethod="pad" /><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient5021"
+ xlink:href="#linearGradient3694"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(171.14064,203.95746,203.95746,-171.14064,1069.1582,487.20703)"
+ spreadMethod="pad" /><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient5023"
+ xlink:href="#linearGradient3714"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-64.005005,-175.85237,-175.85237,64.00502,1039.4766,846.98633)"
+ spreadMethod="pad" /><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient5025"
+ xlink:href="#linearGradient3734"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(294.50781,0,0,-294.50781,919.80859,882.8418)"
+ spreadMethod="pad" /><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient5027"
+ xlink:href="#linearGradient3754"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(119.17578,0,0,-119.17578,1095.1406,775.17578)"
+ spreadMethod="pad" /><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient5029"
+ xlink:href="#linearGradient3774"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-64.005341,-175.85329,-175.85329,64.005356,1163.1553,767.14453)"
+ spreadMethod="pad" /><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient5031"
+ xlink:href="#linearGradient3794"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(17.498856,99.241028,99.241028,-17.498856,1181.9902,753.37793)"
+ spreadMethod="pad" /><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient5033"
+ xlink:href="#linearGradient3814"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(77.403381,-134.06645,-134.06645,-77.403366,1239.7051,762.36523)"
+ spreadMethod="pad" /><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient5035"
+ xlink:href="#linearGradient3834"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-64.005005,-175.85237,-175.85237,64.00502,1039.4775,1032.4775)"
+ spreadMethod="pad" /><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient5037"
+ xlink:href="#linearGradient3854"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(294.50781,0,0,-294.50781,919.80859,1068.333)"
+ spreadMethod="pad" /><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient5039"
+ xlink:href="#linearGradient3874"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(119.17578,0,0,-119.17578,1095.1406,960.6665)"
+ spreadMethod="pad" /><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient5041"
+ xlink:href="#linearGradient3894"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-64.005173,-175.85283,-175.85283,64.005188,1039.4785,1218.4688)"
+ spreadMethod="pad" /><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient5043"
+ xlink:href="#linearGradient3914"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(294.50781,0,0,-294.50781,919.80859,1254.3232)"
+ spreadMethod="pad" /><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient5045"
+ xlink:href="#linearGradient3934"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(133.21449,-23.489273,-23.489273,-133.21449,1088.1211,1158.4014)"
+ spreadMethod="pad" /><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient5047"
+ xlink:href="#linearGradient3954"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-64.005341,-175.85329,-175.85329,64.005356,1354.3965,983.88379)"
+ spreadMethod="pad" /><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient5049"
+ xlink:href="#linearGradient3974"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-294.50879,-3.05e-5,-3.05e-5,294.50879,1529.2354,1019.7383)"
+ spreadMethod="pad" /><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient5051"
+ xlink:href="#linearGradient3994"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(77.40387,-134.06731,-134.06731,-77.403854,1430.9463,979.10449)"
+ spreadMethod="pad" /><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient5053"
+ xlink:href="#linearGradient4014"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-64.005173,-175.85283,-175.85283,64.005188,1258.3086,1189.9678)"
+ spreadMethod="pad" /><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient5055"
+ xlink:href="#linearGradient4034"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(294.50879,0,0,-294.50879,1138.6396,1225.8223)"
+ spreadMethod="pad" /><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient5057"
+ xlink:href="#linearGradient4054"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(77.403381,-134.06645,-134.06645,-77.403366,1334.8584,1185.1885)"
+ spreadMethod="pad" /><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient5059"
+ xlink:href="#linearGradient4074"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-30.103943,-82.70993,-82.70993,30.103943,1399.3926,1085.8184)"
+ spreadMethod="pad" /><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient5061"
+ xlink:href="#linearGradient4094"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(138.51562,0,0,-138.51562,1343.1074,1102.6812)"
+ spreadMethod="pad" /><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient5063"
+ xlink:href="#linearGradient4118"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-30.103775,-82.709457,-82.709457,30.103775,1364.3916,698.28613)"
+ spreadMethod="pad" /><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ id="linearGradient5065"
+ xlink:href="#linearGradient4138"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(138.5166,0,0,-138.5166,1308.1064,715.15039)"
+ spreadMethod="pad" /></defs><g
+ id="g4352"
+ transform="matrix(1.25,0,0,-1.25,-1149.7613,1638.0475)"><g
+ id="g4867"><g
+ id="g3602"><g
+ id="g3604"><g
+ id="g3610"><g
+ id="g3612"><path
+ inkscape:connector-curvature="0"
+ style="fill:url(#linearGradient5011);stroke:none"
+ id="path3620"
+ d="m 1451.309,565.896 -175.332,40 119.174,72.229 175.333,-40 -119.175,-72.229 z" /></g></g></g></g><g
+ id="g3630"><g
+ id="g3632"><path
+ inkscape:connector-curvature="0"
+ style="fill:url(#linearGradient5013);stroke:none"
+ id="path3640"
+ d="m 1451.309,565.896 0,-175.333 119.175,72.228 0,175.334 -119.175,-72.229 z" /></g></g><g
+ id="g3582"><g
+ id="g3584"><g
+ id="g3590"><g
+ id="g3592"><path
+ inkscape:connector-curvature="0"
+ style="fill:url(#linearGradient5015);stroke:none"
+ id="path3600"
+ d="m 1451.309,390.563 -175.332,40 0,175.333 175.332,-40 0,-175.333 z" /></g></g></g></g><g
+ id="g3642"><g
+ id="g3644"><g
+ id="g3650"><g
+ id="g3652"><path
+ inkscape:connector-curvature="0"
+ style="fill:url(#linearGradient5017);stroke:none"
+ id="path3660"
+ d="m 1095.141,465.404 -175.332,40 0,175.333 175.332,-40 0,-175.333 z" /></g></g></g></g><g
+ id="g3662"><g
+ id="g3664"><g
+ id="g3670"><g
+ id="g3672"><path
+ inkscape:connector-curvature="0"
+ style="fill:url(#linearGradient5019);stroke:none"
+ id="path3680"
+ d="m 1095.141,640.737 -175.332,40 119.174,72.229 175.333,-40 -119.175,-72.229 z" /></g></g></g></g><g
+ id="g3682"><g
+ id="g3684"><g
+ id="g3690"><g
+ id="g3692"><path
+ inkscape:connector-curvature="0"
+ style="fill:url(#linearGradient5021);stroke:none"
+ id="path3700"
+ d="m 1095.141,640.737 0,-175.333 119.175,72.229 0,175.333 -119.175,-72.229 z" /></g></g></g></g><g
+ id="g3702"><g
+ id="g3704"><g
+ id="g3710"><g
+ id="g3712"><path
+ inkscape:connector-curvature="0"
+ style="fill:url(#linearGradient5023);stroke:none"
+ id="path3720"
+ d="m 1095.141,651.395 -175.332,40 0,175.333 175.332,-40 0,-175.333 z" /></g></g></g></g><g
+ id="g3722"><g
+ id="g3724"><g
+ id="g3730"><g
+ id="g3732"><path
+ inkscape:connector-curvature="0"
+ style="fill:url(#linearGradient5025);stroke:none"
+ id="path3740"
+ d="m 1095.141,826.728 -175.332,40 119.174,72.228 175.333,-40 -119.175,-72.228 z" /></g></g></g></g><g
+ id="g3742"><g
+ id="g3744"><g
+ id="g3750"><g
+ id="g3752"><path
+ inkscape:connector-curvature="0"
+ style="fill:url(#linearGradient5027);stroke:none"
+ id="path3760"
+ d="m 1095.141,826.728 0,-175.333 119.175,72.228 0,175.333 -119.175,-72.228 z" /></g></g></g></g><g
+ id="g3762"><g
+ id="g3764"><g
+ id="g3770"><g
+ id="g3772"><path
+ inkscape:connector-curvature="0"
+ style="fill:url(#linearGradient5029);stroke:none"
+ id="path3780"
+ d="m 1218.818,571.552 -175.333,40 0,175.333 175.333,-40 0,-175.333 z" /></g></g></g></g><g
+ id="g3782"><g
+ id="g3784"><g
+ id="g3790"><g
+ id="g3792"><path
+ inkscape:connector-curvature="0"
+ style="fill:url(#linearGradient5031);stroke:none"
+ id="path3800"
+ d="m 1218.818,746.885 -175.333,40 119.175,72.228 175.334,-40 -119.176,-72.228 z" /></g></g></g></g><g
+ id="g3802"><g
+ id="g3804"><g
+ id="g3810"><g
+ id="g3812"><path
+ inkscape:connector-curvature="0"
+ style="fill:url(#linearGradient5033);stroke:none"
+ id="path3820"
+ d="m 1218.818,746.885 0,-175.333 119.176,72.228 0,175.333 -119.176,-72.228 z" /></g></g></g></g><g
+ id="g3822"><g
+ id="g3824"><g
+ id="g3830"><g
+ id="g3832"><path
+ inkscape:connector-curvature="0"
+ style="fill:url(#linearGradient5035);stroke:none"
+ id="path3840"
+ d="m 1095.141,836.886 -175.332,40 0,175.333 175.332,-40 0,-175.333 z" /></g></g></g></g><g
+ id="g3842"><g
+ id="g3844"><g
+ id="g3850"><g
+ id="g3852"><path
+ inkscape:connector-curvature="0"
+ style="fill:url(#linearGradient5037);stroke:none"
+ id="path3860"
+ d="m 1095.141,1012.219 -175.332,40 119.174,72.228 175.333,-40 -119.175,-72.228 z" /></g></g></g></g><g
+ id="g3862"><g
+ id="g3864"><g
+ id="g3870"><g
+ id="g3872"><path
+ inkscape:connector-curvature="0"
+ style="fill:url(#linearGradient5039);stroke:none"
+ id="path3880"
+ d="m 1095.141,1012.219 0,-175.333 119.175,72.228 0,175.333 -119.175,-72.228 z" /></g></g></g></g><g
+ id="g3882"><g
+ id="g3884"><g
+ id="g3890"><g
+ id="g3892"><path
+ inkscape:connector-curvature="0"
+ style="fill:url(#linearGradient5041);stroke:none"
+ id="path3900"
+ d="m 1095.141,1022.876 -175.332,40 0,175.333 175.332,-40 0,-175.333 z" /></g></g></g></g><g
+ id="g3902"><g
+ id="g3904"><g
+ id="g3910"><g
+ id="g3912"><path
+ inkscape:connector-curvature="0"
+ style="fill:url(#linearGradient5043);stroke:none"
+ id="path3920"
+ d="m 1095.141,1198.209 -175.332,40 119.174,72.229 175.333,-40 -119.175,-72.229 z" /></g></g></g></g><g
+ id="g3922"><g
+ id="g3924"><g
+ id="g3930"><g
+ id="g3932"><path
+ inkscape:connector-curvature="0"
+ style="fill:url(#linearGradient5045);stroke:none"
+ id="path3940"
+ d="m 1095.141,1198.209 0,-175.333 119.175,72.228 0,175.334 -119.175,-72.229 z" /></g></g></g></g><g
+ id="g3942"><g
+ id="g3944"><g
+ id="g3950"><g
+ id="g3952"><path
+ inkscape:connector-curvature="0"
+ style="fill:url(#linearGradient5047);stroke:none"
+ id="path3960"
+ d="m 1410.061,788.291 -175.334,40 0,175.333 175.334,-40 0,-175.333 z" /></g></g></g></g><g
+ id="g3962"><g
+ id="g3964"><g
+ id="g3970"><g
+ id="g3972"><path
+ inkscape:connector-curvature="0"
+ style="fill:url(#linearGradient5049);stroke:none"
+ id="path3980"
+ d="m 1410.061,963.624 -175.334,40 119.175,72.229 175.333,-40 -119.174,-72.229 z" /></g></g></g></g><g
+ id="g3982"><g
+ id="g3984"><g
+ id="g3990"><g
+ id="g3992"><path
+ inkscape:connector-curvature="0"
+ style="fill:url(#linearGradient5051);stroke:none"
+ id="path4000"
+ d="m 1410.061,963.624 0,-175.333 119.174,72.229 0,175.333 -119.174,-72.229 z" /></g></g></g></g><g
+ id="g4002"><g
+ id="g4004"><g
+ id="g4010"><g
+ id="g4012"><path
+ inkscape:connector-curvature="0"
+ style="fill:url(#linearGradient5053);stroke:none"
+ id="path4020"
+ d="m 1313.973,994.375 -175.333,40 0,175.333 175.333,-40 0,-175.333 z" /></g></g></g></g><g
+ id="g4022"><g
+ id="g4024"><g
+ id="g4030"><g
+ id="g4032"><path
+ inkscape:connector-curvature="0"
+ style="fill:url(#linearGradient5055);stroke:none"
+ id="path4040"
+ d="m 1313.973,1169.708 -175.333,40 119.174,72.229 175.334,-40 -119.175,-72.229 z" /></g></g></g></g><g
+ id="g4042"><g
+ id="g4044"><g
+ id="g4050"><g
+ id="g4052"><path
+ inkscape:connector-curvature="0"
+ style="fill:url(#linearGradient5057);stroke:none"
+ id="path4060"
+ d="m 1313.973,1169.708 0,-175.333 119.175,72.229 0,175.333 -119.175,-72.229 z" /></g></g></g></g><g
+ id="g4062"><g
+ id="g4064"><g
+ id="g4070"><g
+ id="g4072"><path
+ inkscape:connector-curvature="0"
+ style="fill:url(#linearGradient5059);stroke:none"
+ id="path4080"
+ d="m 1425.572,993.824 -82.465,18.814 0,82.464 82.465,-18.813 0,-82.465 z" /></g></g></g></g><g
+ id="g4082"><g
+ id="g4084"><g
+ id="g4090"><g
+ id="g4092"><path
+ inkscape:connector-curvature="0"
+ style="fill:url(#linearGradient5061);stroke:none"
+ id="path4100"
+ d="m 1425.572,1076.289 -82.465,18.813 56.052,33.971 82.464,-18.813 -56.051,-33.971 z" /></g></g></g></g><g
+ id="g4102"
+ transform="translate(1425.5723,1076.2891)"><path
+ inkscape:connector-curvature="0"
+ style="fill:#00b2d9;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path4104"
+ d="m 0,0 0,-82.465 56.051,33.972 0,82.464 L 0,0 z" /></g><g
+ id="g4106"><g
+ id="g4108"><g
+ id="g4114"><g
+ id="g4116"><path
+ inkscape:connector-curvature="0"
+ style="fill:url(#linearGradient5063);stroke:none"
+ id="path4124"
+ d="m 1390.57,606.293 -82.464,18.813 0,82.464 82.464,-18.812 0,-82.465 z" /></g></g></g></g><g
+ id="g4126"><g
+ id="g4128"><g
+ id="g4134"><g
+ id="g4136"><path
+ inkscape:connector-curvature="0"
+ style="fill:url(#linearGradient5065);stroke:none"
+ id="path4144"
+ d="m 1390.57,688.758 -82.464,18.812 56.052,33.972 82.465,-18.813 -56.053,-33.971 z" /></g></g></g></g><g
+ id="g4146"
+ transform="translate(1390.5703,688.7578)"><path
+ inkscape:connector-curvature="0"
+ style="fill:#00b2d9;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path4148"
+ d="m 0,0 0,-82.465 56.053,33.972 0,82.464 L 0,0 z" /></g></g></g></svg>
\ No newline at end of file
diff --git a/icons/ricochet_icons.json b/icons/ricochet_icons.json
new file mode 100644
index 0000000..ea47a53
--- /dev/null
+++ b/icons/ricochet_icons.json
@@ -0,0 +1,34 @@
+{
+ "name": "ricochet_icons",
+ "css_prefix_text": "icon-",
+ "css_use_suffix": false,
+ "hinting": true,
+ "units_per_em": 1000,
+ "ascent": 850,
+ "glyphs": [
+ {
+ "uid": "178053298e3e5b03551d754d4b9acd8b",
+ "css": "doc-inv",
+ "code": 59392,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "d10920db2e79c997c5e783279291970c",
+ "css": "dot-3",
+ "code": 59398,
+ "src": "entypo"
+ },
+ {
+ "uid": "dbd39eb5a1d67beb54cfcb535e840e0f",
+ "css": "plus-4",
+ "code": 59408,
+ "src": "iconic"
+ },
+ {
+ "uid": "26aba7edd46944209b4961670675a813",
+ "css": "cog-1",
+ "code": 59395,
+ "src": "websymbols"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/icons/ricochet_icons.ttf b/icons/ricochet_icons.ttf
new file mode 100644
index 0000000..1df43d7
Binary files /dev/null and b/icons/ricochet_icons.ttf differ
diff --git a/packaging/installer/SetupModern11.bmp b/packaging/installer/SetupModern11.bmp
new file mode 100644
index 0000000..9ecefc7
Binary files /dev/null and b/packaging/installer/SetupModern11.bmp differ
diff --git a/packaging/installer/building.txt b/packaging/installer/building.txt
new file mode 100644
index 0000000..4f22664
--- /dev/null
+++ b/packaging/installer/building.txt
@@ -0,0 +1,11 @@
+Building the installer requires Inno Setup; see http://www.jrsoftware.org/isinfo.php
+
+Place tor.exe and ricochet.exe in this directory. You will also need the CRT
+DLLs from the version of MSVC used, e.g. msvcp110.dll and msvcr110.dll.
+
+In console:
+
+PATH=%PATH%;C:\Path\To\Qt\bin
+prepare.bat
+
+Then build from installer.iss to create the installer package.
diff --git a/packaging/installer/installer.iss b/packaging/installer/installer.iss
new file mode 100644
index 0000000..caa2568
--- /dev/null
+++ b/packaging/installer/installer.iss
@@ -0,0 +1,116 @@
+#define ExeVersion GetFileVersion(AddBackslash(SourcePath) + "ricochet.exe")
+
+[Setup]
+OutputBaseFilename=Ricochet
+AppName=Ricochet
+RestartIfNeededByRun=false
+PrivilegesRequired=lowest
+DefaultDirName={localappdata}\Ricochet\
+DisableProgramGroupPage=true
+DisableDirPage=false
+DisableReadyPage=false
+DefaultGroupName=Ricochet
+AppID={{B700250B-D3E2-407F-A534-8818EB8E3D93}
+AppVersion={#ExeVersion}
+UninstallDisplayName=Ricochet
+Uninstallable=not IsPortableInstall
+VersionInfoDescription=Ricochet
+VersionInfoProductName=Ricochet
+WizardImageFile=SetupModern11.bmp
+ShowLanguageDialog=no
+SetupIconFile=..\..\icons\ricochet.ico
+[Files]
+Source: ricochet.exe; DestDir: {app}; DestName: ricochet.exe; Flags: replacesameversion
+Source: ..\..\LICENSE; DestDir: {app}
+Source: tor.exe; DestDir: {app}; Flags: replacesameversion uninsrestartdelete
+Source: Qt\*; DestDir: {app}; Flags: recursesubdirs
+Source: MSVCP120.DLL; DestDir: {app}; Flags: skipifsourcedoesntexist
+Source: MSVCR120.DLL; DestDir: {app}; Flags: skipifsourcedoesntexist
+[Icons]
+Name: {group}\Ricochet; Filename: {app}\ricochet.exe; WorkingDir: {app}; Comment: {cm:AppTitle}; Check: not IsPortableInstall
+Name: {group}\{cm:UninstallShortcut}; Filename: {uninstallexe}; Check: not IsPortableInstall
+[UninstallDelete]
+Name: {app}\config; Type: filesandordirs
+[Run]
+Filename: {app}\ricochet.exe; WorkingDir: {app}; Description: {cm:RunShortcut}; Flags: postinstall nowait
+[Languages]
+Name: "bg"; MessagesFile: "..\..\translation\inno\Bulgarian.isl,..\..\translation\installer_bg.isl"
+Name: "cs"; MessagesFile: "compiler:Languages\Czech.isl,..\..\translation\installer_cs.isl"
+Name: "da"; MessagesFile: "compiler:Languages\Danish.isl,..\..\translation\installer_da.isl"
+Name: "de"; MessagesFile: "compiler:Languages\German.isl,..\..\translation\installer_de.isl"
+Name: "en"; MessagesFile: "compiler:Default.isl,..\..\translation\installer_en.isl"
+Name: "es"; MessagesFile: "compiler:Languages\Spanish.isl,..\..\translation\installer_es.isl"
+Name: "fi"; MessagesFile: "compiler:Languages\Finnish.isl,..\..\translation\installer_fi.isl"
+Name: "fr"; MessagesFile: "compiler:Languages\French.isl,..\..\translation\installer_fr.isl"
+Name: "it"; MessagesFile: "compiler:Languages\Italian.isl,..\..\translation\installer_it.isl"
+Name: "nl_NL"; MessagesFile: "compiler:Languages\Dutch.isl,..\..\translation\installer_nl_NL.isl"
+Name: "pt_BR"; MessagesFile: "compiler:Languages\BrazilianPortuguese.isl,..\..\translation\installer_pt_BR.isl"
+Name: "ru"; MessagesFile: "compiler:Languages\Russian.isl,..\..\translation\installer_ru.isl"
+Name: "sv"; MessagesFile: "..\..\translation\inno\Swedish.isl,..\..\translation\installer_sv.isl"
+Name: "tr"; MessagesFile: "compiler:Languages\Turkish.isl,..\..\translation\installer_tr.isl"
+Name: "uk"; MessagesFile: "compiler:Languages\Ukrainian.isl,..\..\translation\installer_uk.isl"
+Name: "pl"; MessagesFile: "compiler:Languages\Polish.isl,..\..\translation\installer_pl.isl"
+
+[Code]
+// http://www.vincenzo.net/isxkb/index.php?title=Obtaining_the_application's_version
+// http://www.vincenzo.net/isxkb/index.php?title=Uninstall_user_files
+
+var
+ PortablePage: TInputOptionWizardPage;
+
+procedure InitializeWizard;
+begin
+ PortablePage := CreateInputOptionPage(wpWelcome, CustomMessage('PortableTitle'), CustomMessage('PortableDesc'),
+ CustomMessage('PortableText') + #13#13, True, False);
+ PortablePage.Add(CustomMessage('PortableOptInstall'));
+ PortablePage.Add(CustomMessage('PortableOptExtract'));
+ PortablePage.Values[0] := True;
+end;
+
+function IsPortableInstall(): Boolean;
+begin
+ Result := PortablePage.Values[1];
+end;
+
+procedure CurPageChanged(CurPageID: Integer);
+var
+ s: String;
+ DefaultPortableDir: String;
+ DefaultInstallDir: String;
+begin
+ if CurPageID = wpSelectDir then begin
+ DefaultPortableDir := ExtractFilePath(ExpandConstant('{srcexe}')) + 'Ricochet';
+ DefaultInstallDir := ExpandConstant('{localappdata}') + '\Ricochet';
+
+ if IsPortableInstall() then begin
+ WizardForm.NextButton.Caption := CustomMessage('BtnExtract');
+ WizardForm.SelectDirLabel.Caption := CustomMessage('ExtractDirText');
+ WizardForm.PageDescriptionLabel.Caption := CustomMessage('ExtractDirDesc');
+ if WizardForm.DirEdit.Text = DefaultInstallDir then
+ WizardForm.DirEdit.Text := DefaultPortableDir;
+ end else begin
+ WizardForm.NextButton.Caption := SetupMessage(msgButtonInstall);
+ s := SetupMessage(msgSelectDirLabel3);
+ StringChangeEx(s, '[name]', 'Ricochet', True);
+ WizardForm.SelectDirLabel.Caption := s;
+ s := SetupMessage(msgSelectDirDesc);
+ StringChangeEx(s, '[name]', 'Ricochet', True);
+ WizardForm.PageDescriptionLabel.Caption := s;
+ if WizardForm.DirEdit.Text = DefaultPortableDir then
+ WizardForm.DirEdit.Text := DefaultInstallDir;
+ end;
+ end;
+end;
+
+function ShouldSkipPage(PageID: Integer): Boolean;
+begin
+ if (PageID = wpSelectDir) and (not IsPortableInstall()) then
+ Result := True
+ else if (PageID = wpReady) and (IsPortableInstall()) then
+ Result := True
+ else
+ Result := False;
+end;
+
+
+
diff --git a/packaging/installer/prepare.bat b/packaging/installer/prepare.bat
new file mode 100644
index 0000000..fa5b503
--- /dev/null
+++ b/packaging/installer/prepare.bat
@@ -0,0 +1,3 @@
+if exist "Qt\" rd /q /s Qt
+windeployqt --qmldir ..\..\src\ui\qml --dir Qt ricochet.exe
+if exist "Qt\qmltooling" rd /q /s Qt\qmltooling
diff --git a/packaging/linux-static/content/QtMultimedia/qmldir b/packaging/linux-static/content/QtMultimedia/qmldir
new file mode 100644
index 0000000..1fcd488
--- /dev/null
+++ b/packaging/linux-static/content/QtMultimedia/qmldir
@@ -0,0 +1,4 @@
+module QtMultimedia
+plugin declarative_multimedia
+classname Qt5MultimediaQuick
+typeinfo plugins.qmltypes
diff --git a/packaging/linux-static/content/QtQuick.2/qmldir b/packaging/linux-static/content/QtQuick.2/qmldir
new file mode 100644
index 0000000..4a79c82
--- /dev/null
+++ b/packaging/linux-static/content/QtQuick.2/qmldir
@@ -0,0 +1,4 @@
+module QtQuick
+plugin qtquick2plugin
+classname QtQuick2Plugin
+typeinfo plugins.qmltypes
diff --git a/packaging/linux-static/content/QtQuick/Controls/qmldir b/packaging/linux-static/content/QtQuick/Controls/qmldir
new file mode 100644
index 0000000..f51b055
--- /dev/null
+++ b/packaging/linux-static/content/QtQuick/Controls/qmldir
@@ -0,0 +1,4 @@
+module QtQuick.Controls
+plugin qtquickcontrolsplugin
+classname QtQuickControlsPlugin
+typeinfo plugins.qmltypes
diff --git a/packaging/linux-static/content/QtQuick/Dialogs/qmldir b/packaging/linux-static/content/QtQuick/Dialogs/qmldir
new file mode 100644
index 0000000..b4ae1a0
--- /dev/null
+++ b/packaging/linux-static/content/QtQuick/Dialogs/qmldir
@@ -0,0 +1,4 @@
+module QtQuick.Dialogs
+plugin dialogplugin
+classname QtQuick2DialogsPlugin
+typeinfo plugins.qmltypes
diff --git a/packaging/linux-static/content/QtQuick/Layouts/qmldir b/packaging/linux-static/content/QtQuick/Layouts/qmldir
new file mode 100644
index 0000000..6a260f0
--- /dev/null
+++ b/packaging/linux-static/content/QtQuick/Layouts/qmldir
@@ -0,0 +1,4 @@
+module QtQuick.Layouts
+plugin qquicklayoutsplugin
+classname QtQuickLayoutsPlugin
+typeinfo plugins.qmltypes
diff --git a/packaging/linux-static/content/QtQuick/PrivateWidgets/qmldir b/packaging/linux-static/content/QtQuick/PrivateWidgets/qmldir
new file mode 100644
index 0000000..da63c98
--- /dev/null
+++ b/packaging/linux-static/content/QtQuick/PrivateWidgets/qmldir
@@ -0,0 +1,4 @@
+module QtQuick.PrivateWidgets
+plugin widgetsplugin
+classname QtQuick2PrivateWidgetsPlugin
+typeinfo plugins.qmltypes
diff --git a/packaging/linux-static/content/QtQuick/Window.2/qmldir b/packaging/linux-static/content/QtQuick/Window.2/qmldir
new file mode 100644
index 0000000..c9d1e5a
--- /dev/null
+++ b/packaging/linux-static/content/QtQuick/Window.2/qmldir
@@ -0,0 +1,4 @@
+module QtQuick.Window
+plugin windowplugin
+classname QtQuick2WindowPlugin
+typeinfo plugins.qmltypes
diff --git a/packaging/linux-static/content/README b/packaging/linux-static/content/README
new file mode 100644
index 0000000..5b206a4
--- /dev/null
+++ b/packaging/linux-static/content/README
@@ -0,0 +1,49 @@
+Ricochet - https://ricochet.im/
+
+This is a pre-built static distribution of Ricochet intended to run on most
+Linux distributions. For source or other packages, visit the website above.
+
+You do not need to run or configure an instance of 'tor'. It is bundled with
+this software, and will be configured on the first run. There is not currently
+any option to use a system-wide instance of tor.
+
+Once you run Ricochet, configuration will be stored in a 'config' directory
+inside this folder. To upgrade, extract the new version to the same location.
+
+For more information, see: https://ricochet.im/
+Bug tracker: https://github.com/ricochet-im/ricochet/issues
+Contact:
+ ricochet:rs7ce36jsj24ogfw
+ john.brooks at dereferenced.net
+
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: GnuPG
+
+mQENBFJ8UIcBCACm782I6UllTMl0X2ssFpTkIT/irLEWHJ2rnf2qFXUU3En9E+cR
+jFpoutkjXyHPAcNedF5UHMG0lRkk3Xw7HIZlplMFLExR5UalF6j4nHBYMOw0h9pW
+y5GxwcyuCal9R0RCGuSTi0pdQe6R7zj+ccay/njGgv3zA6M4b8XA5iklSfHvhMzQ
+5lukjgyd54m8cVBvrHYOXJZQWZGOqnd+znIUm7bdCahAg5+29PEJ1S8NSMRofF0K
+O7VBkv9niSZHGf4sFUxJgBzJosTsIoXzhpUGjPpfV246VN3UlQsACczCe+xW76P0
+Ou3r4gjjOM3qpUarzMosRR9oXO0xTIjoT9h1ABEBAAG0KkpvaG4gQnJvb2tzIDxq
+b2huLmJyb29rc0BkZXJlZmVyZW5jZWQubmV0PokBPQQTAQoAJwUCUnxQhwIbAwUJ
+B4YfgAULCQgHAwUVCgkICwUWAgMBAAIeAQIXgAAKCRD/l8U/GDwEXfh6B/9lbQGO
+4jfY9XkiMkQZflKG+pvAktufE0tJ+oBF6JbNZQqXKl5hQZ9DdSrN+B4mnBW9N7Lw
+8XAWP8Um4rXDH4ajrCp+rgMtvq2v32DSN5iZHLAjks3DF54q2plhBbH6Xq8u3wU8
+Brdm1kbZ+4qqRUmOEylfGG21Gzw4z1wfaw6bQaRbLu2eiKQOa/cvYYQUDlES+c4l
+csawaiXOIDx/yZkHjUvDeC8GcPSIHQMUBnM2UOb4YXYPjhH7qPlOqhj3PLkQHLAp
+IwMjN4hm646tTrzrAqU5puWFADYHAb1cKuNCOCkD5c2ihcHwsOtamoVcrRAKhOpc
+zC+VJLPzFWj0EG6uuQENBFJ8UIcBCADRSpXjJjqWdCn3imbNcKKW2rwpruceucFd
+9x8J6IfUhO1Ow3pTQtgY8nmPvley3fJ9MuAKUDvPZuEwoMCHgiFzrsfKfIKw4UGN
+4xpC/CdyMDlxXtGTNRA5Mw+44V5DNeGL7cHRgebUmCjPr5sVIq4KHnNlhrKbirhU
+KR9MC9oAxgln4zHi9br9KJP31GkClNYK26j2Hz3jOUwSF28fdNVJL1YRjH2vSXo7
+16rGcubmSe00+vXEdnhNcIGtIiiPWfBbQ3yTf02rw1JqNf/5oriUHucLGwrkK+Fs
+MI1no59We55PmCnu4gZfNSrp+3srTRtdnO5MAWzzjC9cfhBsCQ9TABEBAAGJASUE
+GAEKAA8FAlJ8UIcCGwwFCQeGH4AACgkQ/5fFPxg8BF0PjQgAloisPPYxnsobqpVY
+H1ka6h+AJLm3WeaKnEfnNujZ1CTb5bjx8UzN6wUVBDRyhHLKY31vaPbgzZVvMz2/
+Xs0KyvuOVk3m9gB98W1dB4RAoVbTJvVe5VxRTAKouabcS/ezXOrOmECnxRFIaPg2
+mpBMzcGkOQ/Pivy8hLDXjlQlS4I/ItiAXfXzWVsMWpH3TaVX5DDT1P/rI56C/R3t
+R6K8js9Gq2C3YNy+bPlVIUxwDH0g6A2DwieT+UaBfoIFPB9n0iXC3TJVyh8CrWrp
+LxeIb1pmuX1z/Rvltbdt+k7nz927zqF8hR2BI+DXymBCZ0zmJET4ROg0LBpiPblE
+gTkqzw==
+=xleo
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/packaging/linux-static/release.sh b/packaging/linux-static/release.sh
new file mode 100755
index 0000000..081fae4
--- /dev/null
+++ b/packaging/linux-static/release.sh
@@ -0,0 +1,52 @@
+#!/bin/bash
+
+# Create and package a clean, release-mode build of the application
+# Must be run from the git source directory.
+# Create a .packagingrc file to define:
+# TOR_BINARY Path to tor binary to copy into bundle.
+# Should be built with --enable-static-libevent
+# You can also set other environment, e.g. MAKEOPTS and PATH (for qmake)
+
+set -e
+
+if [ ! -d .git ] || [ ! -f ricochet.pro ]; then
+ echo "Must be run from source directory"
+ exit 1
+fi
+
+VERSION=`git describe --tags HEAD`
+
+. .packagingrc
+
+if [ -z "$TOR_BINARY" ] || [ ! -f "$TOR_BINARY" ]; then
+ echo "Missing TOR_BINARY: $TOR_BINARY"
+ exit 1
+fi
+
+rm -r build || true
+mkdir build
+cd build
+
+qmake CONFIG+=release ${QMAKEOPTS} ..
+make
+strip ricochet
+
+mkdir -p staging/ricochet
+# Copy binaries to staging area
+cp ricochet staging/ricochet/
+cp "$TOR_BINARY" staging/ricochet/
+# Copy extra files
+cp -r ../packaging/linux-static/content/* staging/ricochet/
+
+cd staging
+tar cfj ricochet-${VERSION}-static.tar.bz2 ricochet
+mv *.bz2 ..
+cd ..
+
+echo "---------"
+
+tar fjt *.bz2
+
+echo
+echo "Output: ./build/ricochet-${VERSION}-static.tar.bz2"
+
diff --git a/packaging/osx/release_osx.sh b/packaging/osx/release_osx.sh
new file mode 100755
index 0000000..91fcb15
--- /dev/null
+++ b/packaging/osx/release_osx.sh
@@ -0,0 +1,57 @@
+#!/bin/bash
+
+# Create and package a clean, release-mode build of the application
+# Must be run from the git source directory.
+# Create a .packagingrc file to define:
+# TOR_BINARY Path to tor binary to copy into bundle.
+# Should be built with --enable-static-libevent
+# You can also set other environment, e.g. MAKEOPTS and PATH (for qmake)
+
+# This script assumes Qt >= 5.4.0, for correct codesigning and macdeployqt behavior.
+
+set -e
+
+if [ ! -d .git ] || [ ! -f ricochet.pro ]; then
+ echo "Must be run from source directory"
+ exit 1
+fi
+
+. .packagingrc
+
+if [ -z "$TOR_BINARY" ] || [ ! -f "$TOR_BINARY" ]; then
+ echo "Missing TOR_BINARY: $TOR_BINARY"
+ exit 1
+fi
+
+rm -r build || true
+mkdir build
+cd build
+
+qmake CONFIG+=release ..
+make
+
+cp "$TOR_BINARY" ricochet.app/Contents/MacOS/
+strip ricochet.app/Contents/MacOS/tor
+strip ricochet.app/Contents/MacOS/ricochet
+
+macdeployqt ricochet.app -qmldir=../src/ui/qml/
+mv ricochet.app Ricochet.app
+
+# Code signing, if CODESIGN_ID is defined
+if [ ! -z "$CODESIGN_ID" ]; then
+ codesign --verbose --sign "$CODESIGN_ID" --deep Ricochet.app
+fi
+
+hdiutil create Ricochet.dmg -srcfolder Ricochet.app -format UDZO -volname Ricochet
+
+echo "---------"
+
+otool -L Ricochet.app/Contents/MacOS/ricochet
+otool -L Ricochet.app/Contents/MacOS/tor
+
+codesign -vvvv -d Ricochet.app
+spctl -vvvv --assess --type execute Ricochet.app
+
+echo
+echo "Output: ./build/Ricochet.dmg"
+
diff --git a/packaging/rpm/ricochet.spec b/packaging/rpm/ricochet.spec
new file mode 100644
index 0000000..a6232a8
--- /dev/null
+++ b/packaging/rpm/ricochet.spec
@@ -0,0 +1,66 @@
+Name: ricochet
+Version: 1.1.0
+Release: 1%{?dist}
+Summary: Anonymous peer-to-peer instant messaging
+
+License: BSD
+URL: https://ricochet.im/
+Source0: https://ricochet.im/releases/%{version}/ricochet-%{version}-src.tar.bz2
+
+BuildRequires: openssl-devel
+BuildRequires: protobuf-compiler
+BuildRequires: protobuf-devel
+BuildRequires: qt5-qtbase-devel
+BuildRequires: qt5-qtbase-gui
+BuildRequires: qt5-qtdeclarative-devel
+BuildRequires: qt5-qtmultimedia-devel
+BuildRequires: qt5-qtquickcontrols
+BuildRequires: qt5-qttools-devel
+Requires: openssl-libs
+Requires: protobuf
+Requires: qt5-qtbase
+Requires: qt5-qtbase-gui
+Requires: qt5-qtdeclarative
+Requires: qt5-qtquickcontrols
+Requires: qt5-qtmultimedia
+Requires: tor
+
+%description
+Ricochet is an experiment with a different kind of instant messaging that doesn't trust anyone with your identity, your contact list, or your communications.
+ * You can chat without exposing your identity (or IP address) to anyone
+ * Nobody can discover who your contacts are or when you talk (metadata-free!)
+ * There are no servers to compromise or operators to intimidate for your information
+ * It's cross-platform and easy for non-technical users
+
+
+%prep
+%setup -q
+
+
+%build
+qmake-qt5 DEFINES+=RICOCHET_NO_PORTABLE CONFIG+=release
+make -f Makefile.Release %{?_smp_mflags}
+
+
+%install
+make -f Makefile.Release install INSTALL_ROOT=%{buildroot}
+install -m 0644 -D -p LICENSE %{buildroot}/%{_docdir}/%{name}/LICENSE
+install -m 0644 -D -p AUTHORS.md %{buildroot}/%{_docdir}/%{name}/AUTHORS.md
+install -m 0644 -D -p README.md %{buildroot}/%{_docdir}/%{name}/README.md
+
+
+%files
+/usr/bin/ricochet
+/usr/share/applications/ricochet.desktop
+/usr/share/icons/hicolor/48x48/apps/ricochet.png
+/usr/share/icons/hicolor/scalable/apps/ricochet.svg
+%docdir %{_docdir}/%{name}
+%doc %{_docdir}/%{name}/LICENSE
+%doc %{_docdir}/%{name}/AUTHORS.md
+%doc %{_docdir}/%{name}/README.md
+
+
+
+%changelog
+* Mon Jul 27 2015 Peter Ludikovsky <peter at ludikovsky.name> 1.1.0-1
+- Initial RPM Package
diff --git a/protobuf.pri b/protobuf.pri
new file mode 100644
index 0000000..bf942ea
--- /dev/null
+++ b/protobuf.pri
@@ -0,0 +1,51 @@
+# Qt qmake integration with Google Protocol Buffers compiler protoc
+#
+# To compile protocol buffers with qt qmake, specify PROTOS variable and
+# include this file
+#
+# Based on:
+# https://vilimpoc.org/blog/2013/06/09/using-google-protocol-buffers-with-qmake/
+
+PROTOC = protoc
+
+unix {
+ PKG_CONFIG = $$pkgConfigExecutable()
+
+ !contains(QT_CONFIG, no-pkg-config) {
+ CONFIG += link_pkgconfig
+ PKGCONFIG += protobuf
+ } else {
+ # Some SDK builds (e.g. OS X 5.4.1) are no-pkg-config, so try to hack the linker flags in.
+ QMAKE_LFLAGS += $$system($$PKG_CONFIG --libs protobuf)
+ }
+
+ gcc|clang {
+ # Add -isystem for protobuf includes to suppress some loud compiler warnings in their headers
+ PROTOBUF_CFLAGS = $$system($$PKG_CONFIG --cflags protobuf)
+ PROTOBUF_CFLAGS ~= s/^(?!-I).*//g
+ PROTOBUF_CFLAGS ~= s/^-I(.*)/-isystem \\1/g
+ QMAKE_CXXFLAGS += $$PROTOBUF_CFLAGS
+ }
+}
+
+win32 {
+ isEmpty(PROTOBUFDIR):error(You must pass PROTOBUFDIR=path/to/protobuf to qmake on this platform)
+ INCLUDEPATH += $${PROTOBUFDIR}/include
+ LIBS += -L$${PROTOBUFDIR}/lib -lprotobuf
+ PROTOC = $${PROTOBUFDIR}/bin/protoc.exe
+}
+
+protobuf_decl.name = protobuf headers
+protobuf_decl.input = PROTOS
+protobuf_decl.output = ${QMAKE_FILE_IN_PATH}/${QMAKE_FILE_BASE}.pb.h
+protobuf_decl.commands = $$PROTOC --cpp_out=${QMAKE_FILE_IN_PATH} --proto_path=${QMAKE_FILE_IN_PATH} ${QMAKE_FILE_NAME}
+protobuf_decl.variable_out = HEADERS
+QMAKE_EXTRA_COMPILERS += protobuf_decl
+
+protobuf_impl.name = protobuf sources
+protobuf_impl.input = PROTOS
+protobuf_impl.output = ${QMAKE_FILE_IN_PATH}/${QMAKE_FILE_BASE}.pb.cc
+protobuf_impl.depends = ${QMAKE_FILE_IN_PATH}/${QMAKE_FILE_BASE}.pb.h
+protobuf_impl.commands = $$escape_expand(\n)
+protobuf_impl.variable_out = SOURCES
+QMAKE_EXTRA_COMPILERS += protobuf_impl
diff --git a/ricochet.pro b/ricochet.pro
new file mode 100644
index 0000000..67657ef
--- /dev/null
+++ b/ricochet.pro
@@ -0,0 +1,264 @@
+# Ricochet - https://ricochet.im/
+# Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#
+# * Neither the names of the copyright owners nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+lessThan(QT_MAJOR_VERSION,5)|lessThan(QT_MINOR_VERSION,1) {
+ error("Qt 5.1 or greater is required. You can build your own, or get the SDK at https://qt-project.org/downloads")
+}
+
+TARGET = ricochet
+TEMPLATE = app
+QT += core gui network quick widgets multimedia
+CONFIG += c++11
+
+VERSION = 1.1.1
+
+# Use CONFIG+=no-hardened to disable compiler hardening options
+!CONFIG(no-hardened) {
+ CONFIG += hardened
+ include(hardened.pri)
+}
+
+# Pass DEFINES+=RICOCHET_NO_PORTABLE for a system-wide installation
+
+CONFIG(release,debug|release):DEFINES += QT_NO_DEBUG_OUTPUT QT_NO_WARNING_OUTPUT
+
+contains(DEFINES, RICOCHET_NO_PORTABLE) {
+ unix:!macx {
+ target.path = /usr/bin
+ shortcut.path = /usr/share/applications
+ shortcut.files = src/ricochet.desktop
+ icon.path = /usr/share/icons/hicolor/48x48/apps/
+ icon.files = icons/ricochet.png
+ scalable_icon.path = /usr/share/icons/hicolor/scalable/apps/
+ scalable_icon.files = icons/ricochet.svg
+ INSTALLS += target shortcut icon scalable_icon
+
+ exists(tor) {
+ message(Adding bundled Tor to installations)
+ bundletor.path = /usr/lib/ricochet/tor/
+ bundletor.files = tor/*
+ INSTALLS += bundletor
+ DEFINES += BUNDLED_TOR_PATH=\\\"/usr/lib/ricochet/tor/\\\"
+ }
+ }
+}
+
+macx {
+ CONFIG += bundle force_debug_plist
+ QT += macextras
+
+ # Qt 5.4 introduces a bug that breaks QMAKE_INFO_PLIST when qmake has a relative path.
+ # Work around by copying Info.plist directly.
+ greaterThan(QT_MAJOR_VERSION,5)|greaterThan(QT_MINOR_VERSION,4) {
+ QMAKE_INFO_PLIST = src/Info.plist
+ } else:equals(QT_MAJOR_VERSION,5):lessThan(QT_MINOR_VERSION,4) {
+ QMAKE_INFO_PLIST = src/Info.plist
+ } else {
+ CONFIG += no_plist
+ QMAKE_POST_LINK += cp $${_PRO_FILE_PWD_}/src/Info.plist $${OUT_PWD}/$${TARGET}.app/Contents/;
+ }
+
+ exists(tor) {
+ # Copy the entire tor/ directory, which should contain tor/tor (the binary itself)
+ QMAKE_POST_LINK += cp -R $${_PRO_FILE_PWD_}/tor $${OUT_PWD}/$${TARGET}.app/Contents/MacOS/;
+ }
+
+ icons.files = icons/Ricochet.icns
+ icons.path = Contents/Resources/
+ QMAKE_BUNDLE_DATA += icons
+}
+
+CONFIG += debug_and_release
+
+# Create a pdb for release builds as well, to enable debugging
+win32-msvc2008|win32-msvc2010 {
+ QMAKE_CXXFLAGS_RELEASE += /Zi
+ QMAKE_LFLAGS_RELEASE += /DEBUG /OPT:REF,ICF
+}
+
+INCLUDEPATH += src
+
+unix {
+ !isEmpty(OPENSSLDIR) {
+ INCLUDEPATH += $${OPENSSLDIR}/include
+ LIBS += -L$${OPENSSLDIR}/lib -lcrypto
+ } else:macx:!packagesExist(libcrypto) {
+ # Fall back to the OS-provided 0.9.8 if no other libcrypto is present
+ LIBS += -lcrypto
+ } else {
+ CONFIG += link_pkgconfig
+ PKGCONFIG += libcrypto
+ }
+}
+win32 {
+ isEmpty(OPENSSLDIR):error(You must pass OPENSSLDIR=path/to/openssl to qmake on this platform)
+
+ INCLUDEPATH += $${OPENSSLDIR}/include
+
+ win32-g++ {
+ LIBS += -L$${OPENSSLDIR}/lib -lcrypto
+ } else {
+ LIBS += -L$${OPENSSLDIR}/lib -llibeay32
+ }
+
+ # required by openssl
+ LIBS += -lUser32 -lGdi32 -ladvapi32
+}
+
+DEFINES += QT_NO_CAST_FROM_ASCII QT_NO_CAST_TO_ASCII
+
+SOURCES += src/main.cpp \
+ src/ui/MainWindow.cpp \
+ src/ui/ContactsModel.cpp \
+ src/tor/TorControl.cpp \
+ src/tor/TorControlSocket.cpp \
+ src/tor/TorControlCommand.cpp \
+ src/tor/ProtocolInfoCommand.cpp \
+ src/tor/AuthenticateCommand.cpp \
+ src/tor/SetConfCommand.cpp \
+ src/utils/StringUtil.cpp \
+ src/core/ContactsManager.cpp \
+ src/core/ContactUser.cpp \
+ src/tor/GetConfCommand.cpp \
+ src/tor/HiddenService.cpp \
+ src/utils/CryptoKey.cpp \
+ src/utils/SecureRNG.cpp \
+ src/core/OutgoingContactRequest.cpp \
+ src/core/IncomingRequestManager.cpp \
+ src/core/ContactIDValidator.cpp \
+ src/core/UserIdentity.cpp \
+ src/core/IdentityManager.cpp \
+ src/core/ConversationModel.cpp \
+ src/tor/TorProcess.cpp \
+ src/tor/TorManager.cpp \
+ src/tor/TorSocket.cpp \
+ src/ui/LinkedText.cpp \
+ src/utils/Settings.cpp \
+ src/utils/PendingOperation.cpp \
+ src/ui/LanguagesModel.cpp
+
+HEADERS += src/ui/MainWindow.h \
+ src/ui/ContactsModel.h \
+ src/tor/TorControl.h \
+ src/tor/TorControlSocket.h \
+ src/tor/TorControlCommand.h \
+ src/tor/ProtocolInfoCommand.h \
+ src/tor/AuthenticateCommand.h \
+ src/tor/SetConfCommand.h \
+ src/utils/StringUtil.h \
+ src/core/ContactsManager.h \
+ src/core/ContactUser.h \
+ src/tor/GetConfCommand.h \
+ src/tor/HiddenService.h \
+ src/utils/CryptoKey.h \
+ src/utils/SecureRNG.h \
+ src/core/OutgoingContactRequest.h \
+ src/core/IncomingRequestManager.h \
+ src/core/ContactIDValidator.h \
+ src/core/UserIdentity.h \
+ src/core/IdentityManager.h \
+ src/core/ConversationModel.h \
+ src/tor/TorProcess.h \
+ src/tor/TorProcess_p.h \
+ src/tor/TorManager.h \
+ src/tor/TorSocket.h \
+ src/ui/LinkedText.h \
+ src/utils/Settings.h \
+ src/utils/PendingOperation.h \
+ src/ui/LanguagesModel.h
+
+SOURCES += src/protocol/Channel.cpp \
+ src/protocol/ControlChannel.cpp \
+ src/protocol/Connection.cpp \
+ src/protocol/OutboundConnector.cpp \
+ src/protocol/AuthHiddenServiceChannel.cpp \
+ src/protocol/ChatChannel.cpp \
+ src/protocol/ContactRequestChannel.cpp
+
+HEADERS += src/protocol/Channel.h \
+ src/protocol/Channel_p.h \
+ src/protocol/ControlChannel.h \
+ src/protocol/Connection.h \
+ src/protocol/Connection_p.h \
+ src/protocol/OutboundConnector.h \
+ src/protocol/AuthHiddenServiceChannel.h \
+ src/protocol/ChatChannel.h \
+ src/protocol/ContactRequestChannel.h
+
+include(protobuf.pri)
+PROTOS += src/protocol/ControlChannel.proto \
+ src/protocol/AuthHiddenService.proto \
+ src/protocol/ChatChannel.proto \
+ src/protocol/ContactRequestChannel.proto
+
+# QML
+RESOURCES += src/ui/qml/qml.qrc \
+ icons/icons.qrc \
+ sounds/sounds.qrc
+
+win32:RC_ICONS = icons/ricochet.ico
+OTHER_FILES += src/ui/qml/*
+lupdate_only {
+ SOURCES += src/ui/qml/*.qml
+}
+
+# Translations
+TRANSLATIONS += \
+ translation/ricochet_en.ts \
+ translation/ricochet_it.ts \
+ translation/ricochet_es.ts \
+ translation/ricochet_da.ts \
+ translation/ricochet_pl.ts \
+ translation/ricochet_pt_BR.ts \
+ translation/ricochet_de.ts \
+ translation/ricochet_bg.ts \
+ translation/ricochet_cs.ts \
+ translation/ricochet_fi.ts \
+ translation/ricochet_fr.ts \
+ translation/ricochet_ru.ts \
+ translation/ricochet_uk.ts \
+ translation/ricochet_tr.ts \
+ translation/ricochet_nl_NL.ts \
+ translation/ricochet_fil_PH.ts \
+ translation/ricochet_sv.ts
+
+isEmpty(QMAKE_LRELEASE) {
+ win32:QMAKE_LRELEASE = $$[QT_INSTALL_BINS]\lrelease.exe
+ else:QMAKE_LRELEASE = $$[QT_INSTALL_BINS]/lrelease
+}
+
+updateqm.input = TRANSLATIONS
+updateqm.output = ${QMAKE_FILE_PATH}/${QMAKE_FILE_BASE}.qm
+updateqm.commands = $$QMAKE_LRELEASE ${QMAKE_FILE_IN} -qm ${QMAKE_FILE_PATH}/${QMAKE_FILE_BASE}.qm
+updateqm.CONFIG += no_link target_predeps
+QMAKE_EXTRA_COMPILERS += updateqm
+
+RESOURCES += translation/embedded.qrc
diff --git a/sounds/LICENSE b/sounds/LICENSE
new file mode 100644
index 0000000..649a444
--- /dev/null
+++ b/sounds/LICENSE
@@ -0,0 +1,7 @@
+message.wav, online.wav
+Copyright 2015, qso (qso at users.sf.net)
+
+This work is licensed under the Creative Commons Attribution-ShareAlike 4.0
+International License. To view a copy of this license, visit
+http://creativecommons.org/licenses/by-sa/4.0/ or send a letter to
+Creative Commons, PO Box 1866, Mountain View, CA 94042, USA.
diff --git a/sounds/message.wav b/sounds/message.wav
new file mode 100644
index 0000000..85fb050
Binary files /dev/null and b/sounds/message.wav differ
diff --git a/sounds/online.wav b/sounds/online.wav
new file mode 100644
index 0000000..9b95dfc
Binary files /dev/null and b/sounds/online.wav differ
diff --git a/sounds/sounds.qrc b/sounds/sounds.qrc
new file mode 100644
index 0000000..cf9e0d5
--- /dev/null
+++ b/sounds/sounds.qrc
@@ -0,0 +1,6 @@
+<RCC>
+ <qresource prefix="/sounds">
+ <file>message.wav</file>
+ <file>online.wav</file>
+ </qresource>
+</RCC>
diff --git a/src/Info.plist b/src/Info.plist
new file mode 100644
index 0000000..07ca4d7
--- /dev/null
+++ b/src/Info.plist
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>NSPrincipalClass</key>
+ <string>NSApplication</string>
+ <key>CFBundleIconFile</key>
+ <string>Ricochet.icns</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleExecutable</key>
+ <string>ricochet</string>
+ <key>CFBundleIdentifier</key>
+ <string>im.ricochet</string>
+ <key>CFBundleName</key>
+ <string>Ricochet</string>
+ <key>NSSupportsAutomaticGraphicsSwitching</key>
+ <true/>
+</dict>
+</plist>
diff --git a/src/core/ContactIDValidator.cpp b/src/core/ContactIDValidator.cpp
new file mode 100644
index 0000000..0d09d1d
--- /dev/null
+++ b/src/core/ContactIDValidator.cpp
@@ -0,0 +1,115 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ContactIDValidator.h"
+
+static QRegularExpression regex(QStringLiteral("(torsion|ricochet):([a-z2-7]{16})"));
+
+ContactIDValidator::ContactIDValidator(QObject *parent)
+ : QRegularExpressionValidator(parent), m_uniqueIdentity(0)
+{
+ setRegularExpression(regex);
+}
+
+QValidator::State ContactIDValidator::validate(QString &text, int &pos) const
+{
+ Q_UNUSED(pos);
+ fixup(text);
+ if (text.isEmpty())
+ return QValidator::Intermediate;
+
+ QValidator::State re = QRegularExpressionValidator::validate(text, pos);
+ if (re != QValidator::Acceptable) {
+ if (re == QValidator::Invalid)
+ emit failed();
+ return re;
+ }
+
+ if (matchingContact(text) || matchesIdentity(text)) {
+ emit failed();
+ return QValidator::Invalid;
+ }
+
+ return re;
+}
+
+ContactUser *ContactIDValidator::matchingContact(const QString &text) const
+{
+ ContactUser *u = 0;
+ if (m_uniqueIdentity)
+ u = m_uniqueIdentity->contacts.lookupHostname(text);
+ return u;
+}
+
+bool ContactIDValidator::matchesIdentity(const QString &text) const
+{
+ return m_uniqueIdentity && m_uniqueIdentity->hostname() == hostnameFromID(text);
+}
+
+void ContactIDValidator::fixup(QString &text) const
+{
+ text = text.trimmed().toLower();
+}
+
+bool ContactIDValidator::isValidID(const QString &text)
+{
+ return regex.match(text).hasMatch();
+}
+
+QString ContactIDValidator::hostnameFromID(const QString &ID)
+{
+ QRegularExpressionMatch match = regex.match(ID);
+ if (!match.hasMatch())
+ return QString();
+
+ return match.captured(2) + QStringLiteral(".onion");
+}
+
+QString ContactIDValidator::idFromHostname(const QString &hostname)
+{
+ QString re = hostname;
+
+ if (re.size() != 16)
+ {
+ if (re.size() == 22 && re.toLower().endsWith(QLatin1String(".onion")))
+ re.chop(6);
+ else
+ return QString();
+ }
+
+ re.prepend(QStringLiteral("ricochet:"));
+
+ if (!isValidID(re))
+ return QString();
+ return re;
+}
+
diff --git a/src/core/ContactIDValidator.h b/src/core/ContactIDValidator.h
new file mode 100644
index 0000000..f75e8a4
--- /dev/null
+++ b/src/core/ContactIDValidator.h
@@ -0,0 +1,70 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef CONTACTIDVALIDATOR_H
+#define CONTACTIDVALIDATOR_H
+
+#include <QRegularExpressionValidator>
+#include "UserIdentity.h"
+
+class ContactIDValidator : public QRegularExpressionValidator
+{
+ Q_OBJECT
+ Q_DISABLE_COPY(ContactIDValidator)
+
+ Q_PROPERTY(UserIdentity* notContactOfIdentity READ notContactOfIdentity WRITE setNotContactOfIdentity)
+
+public:
+ ContactIDValidator(QObject *parent = 0);
+
+ static bool isValidID(const QString &text);
+ static QString hostnameFromID(const QString &ID);
+ static QString idFromHostname(const QString &hostname);
+ static QString idFromHostname(const QByteArray &hostname) { return idFromHostname(QString::fromLatin1(hostname)); }
+
+ UserIdentity *notContactOfIdentity() const { return m_uniqueIdentity; }
+ void setNotContactOfIdentity(UserIdentity *i) { m_uniqueIdentity = i; }
+
+ virtual void fixup(QString &text) const;
+ virtual State validate(QString &text, int &pos) const;
+
+ Q_INVOKABLE ContactUser *matchingContact(const QString &text) const;
+ Q_INVOKABLE bool matchesIdentity(const QString &text) const;
+
+signals:
+ void failed() const;
+
+protected:
+ UserIdentity *m_uniqueIdentity;
+};
+
+#endif // CONTACTIDVALIDATOR_H
diff --git a/src/core/ContactUser.cpp b/src/core/ContactUser.cpp
new file mode 100644
index 0000000..0266ecb
--- /dev/null
+++ b/src/core/ContactUser.cpp
@@ -0,0 +1,536 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ContactUser.h"
+#include "UserIdentity.h"
+#include "ContactsManager.h"
+#include "utils/SecureRNG.h"
+#include "utils/Useful.h"
+#include "core/ContactIDValidator.h"
+#include "core/OutgoingContactRequest.h"
+#include "core/ConversationModel.h"
+#include "tor/HiddenService.h"
+#include "protocol/OutboundConnector.h"
+#include <QtDebug>
+#include <QDateTime>
+#include <QTcpSocket>
+#include <QtEndian>
+
+ContactUser::ContactUser(UserIdentity *ident, int id, QObject *parent)
+ : QObject(parent)
+ , identity(ident)
+ , uniqueID(id)
+ , m_connection(0)
+ , m_outgoingSocket(0)
+ , m_status(Offline)
+ , m_lastReceivedChatID(0)
+ , m_contactRequest(0)
+ , m_settings(0)
+ , m_conversation(0)
+{
+ Q_ASSERT(uniqueID >= 0);
+
+ m_settings = new SettingsObject(QStringLiteral("contacts.%1").arg(uniqueID));
+ connect(m_settings, &SettingsObject::modified, this, &ContactUser::onSettingsModified);
+
+ m_conversation = new ConversationModel(this);
+ m_conversation->setContact(this);
+
+ loadContactRequest();
+ updateStatus();
+ updateOutgoingSocket();
+}
+
+ContactUser::~ContactUser()
+{
+ delete m_settings;
+}
+
+void ContactUser::loadContactRequest()
+{
+ if (m_contactRequest)
+ return;
+
+ if (m_settings->read("request.status") != QJsonValue::Undefined) {
+ m_contactRequest = new OutgoingContactRequest(this);
+ connect(m_contactRequest, &OutgoingContactRequest::statusChanged, this, &ContactUser::updateStatus);
+ connect(m_contactRequest, &OutgoingContactRequest::removed, this, &ContactUser::requestRemoved);
+ connect(m_contactRequest, &OutgoingContactRequest::accepted, this, &ContactUser::requestAccepted);
+ updateStatus();
+ }
+}
+
+ContactUser *ContactUser::addNewContact(UserIdentity *identity, int id)
+{
+ ContactUser *user = new ContactUser(identity, id);
+ user->settings()->write("whenCreated", QDateTime::currentDateTime());
+
+ return user;
+}
+
+void ContactUser::updateStatus()
+{
+ Status newStatus;
+ if (m_contactRequest) {
+ if (m_contactRequest->status() == OutgoingContactRequest::Error ||
+ m_contactRequest->status() == OutgoingContactRequest::Rejected)
+ {
+ newStatus = RequestRejected;
+ } else {
+ newStatus = RequestPending;
+ }
+ } else if (m_connection && m_connection->isConnected()) {
+ newStatus = Online;
+ } else if (settings()->read("rejected").toBool()) {
+ newStatus = RequestRejected;
+ } else if (settings()->read("sentUpgradeNotification").toBool()) {
+ newStatus = Outdated;
+ } else {
+ newStatus = Offline;
+ }
+
+ if (newStatus == m_status)
+ return;
+
+ m_status = newStatus;
+ emit statusChanged();
+
+ updateOutgoingSocket();
+}
+
+void ContactUser::onSettingsModified(const QString &key, const QJsonValue &value)
+{
+ Q_UNUSED(value);
+ if (key == QLatin1String("nickname"))
+ emit nicknameChanged();
+}
+
+void ContactUser::updateOutgoingSocket()
+{
+ if (m_status != Offline && m_status != RequestPending) {
+ if (m_outgoingSocket) {
+ m_outgoingSocket->disconnect(this);
+ m_outgoingSocket->abort();
+ m_outgoingSocket->deleteLater();
+ m_outgoingSocket = 0;
+ }
+ return;
+ }
+
+ // Refuse to make outgoing connections to the local hostname
+ if (hostname() == identity->hostname())
+ return;
+
+ if (m_outgoingSocket && m_outgoingSocket->status() == Protocol::OutboundConnector::Ready) {
+ BUG() << "Called updateOutgoingSocket with an existing socket in Ready. This should've been deleted.";
+ m_outgoingSocket->disconnect(this);
+ m_outgoingSocket->deleteLater();
+ m_outgoingSocket = 0;
+ }
+
+ if (!m_outgoingSocket) {
+ m_outgoingSocket = new Protocol::OutboundConnector(this);
+ m_outgoingSocket->setAuthPrivateKey(identity->hiddenService()->cryptoKey());
+ connect(m_outgoingSocket, &Protocol::OutboundConnector::ready, this,
+ [this]() {
+ assignConnection(m_outgoingSocket->takeConnection(this));
+ }
+ );
+
+ /* As an ugly hack, because Ricochet 1.0.x versions have no way to notify about
+ * protocol issues, and it's not feasible to support both protocols for this
+ * tiny upgrade period:
+ *
+ * The first time we make an outgoing connection to an existing contact, if they
+ * are using the old version, send a chat message that lets them know about the
+ * new version, then disconnect. This message is only sent once per contact.
+ *
+ * XXX: This logic should be removed an appropriate amount of time after the new
+ * protocol has been released.
+ */
+ connect(m_outgoingSocket, &Protocol::OutboundConnector::oldVersionNegotiated, this,
+ [this](QTcpSocket *socket) {
+ if (m_settings->read("sentUpgradeNotification").toBool())
+ return;
+ QByteArray secret = m_settings->read<Base64Encode>("remoteSecret");
+ if (secret.size() != 16)
+ return;
+
+ static const char upgradeMessage[] =
+ "[automatic message] I'm using a newer version of Ricochet that is not "
+ "compatible with yours. This is a one-time change to help improve Ricochet. "
+ "See https://ricochet.im/upgrade for instructions on getting the latest "
+ "version. Once you have upgraded, I will be able to see your messages again.";
+ uchar command[] = {
+ 0x00, 0x00, 0x10, 0x00, 0x00, 0x01, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+ };
+
+ qToBigEndian(quint16(sizeof(upgradeMessage) + 7), command);
+ qToBigEndian(quint16(sizeof(upgradeMessage) - 1), command + sizeof(command) - sizeof(quint16));
+
+ QByteArray data;
+ data.append((char)0x00);
+ data.append(secret);
+ data.append(reinterpret_cast<const char*>(command), sizeof(command));
+ data.append(upgradeMessage);
+ socket->write(data);
+
+ m_settings->write("sentUpgradeNotification", true);
+ updateStatus();
+ }
+ );
+ }
+
+ m_outgoingSocket->connectToHost(hostname(), port());
+}
+
+void ContactUser::onConnected()
+{
+ if (!m_connection || !m_connection->isConnected()) {
+ /* This case can happen if disconnected very quickly after connecting,
+ * before the (queued) slot has been called. Ignore the signal.
+ */
+ return;
+ }
+
+ m_settings->write("lastConnected", QDateTime::currentDateTime());
+
+ if (m_contactRequest && m_connection->purpose() == Protocol::Connection::Purpose::OutboundRequest) {
+ qDebug() << "Sending contact request for" << uniqueID << nickname();
+ m_contactRequest->sendRequest(m_connection);
+ }
+
+ if (!m_settings->read("sentUpgradeNotification").isNull())
+ m_settings->unset("sentUpgradeNotification");
+
+ /* The 'rejected' mark comes from failed authentication to someone who we thought was a known
+ * contact. Normally, it would mean that you were removed from that person's contacts. It's
+ * possible for this to be undone; for example, if that person sends you a new contact request,
+ * it will be automatically accepted. If this happens, unset the 'rejected' flag for correct UI.
+ */
+ if (m_settings->read("rejected").toBool()) {
+ qDebug() << "Contact had marked us as rejected, but now they've connected again. Re-enabling.";
+ m_settings->unset("rejected");
+ }
+
+ updateStatus();
+ if (isConnected()) {
+ emit connected();
+ emit connectionChanged(m_connection);
+ }
+
+ if (m_status != Online && m_status != RequestPending) {
+ BUG() << "Contact has a connection while in status" << m_status << "which is not expected.";
+ m_connection->close();
+ }
+}
+
+void ContactUser::onDisconnected()
+{
+ qDebug() << "Contact" << uniqueID << "disconnected";
+ m_settings->write("lastConnected", QDateTime::currentDateTime());
+
+ if (m_connection) {
+ if (m_connection->isConnected()) {
+ BUG() << "onDisconnected called, but connection is still connected";
+ return;
+ }
+
+ m_connection->deleteLater();
+ m_connection = 0;
+ } else {
+ BUG() << "onDisconnected called without a connection";
+ }
+
+ updateStatus();
+ emit disconnected();
+ emit connectionChanged(0);
+}
+
+SettingsObject *ContactUser::settings()
+{
+ return m_settings;
+}
+
+QString ContactUser::nickname() const
+{
+ return m_settings->read("nickname").toString();
+}
+
+void ContactUser::setNickname(const QString &nickname)
+{
+ m_settings->write("nickname", nickname);
+}
+
+QString ContactUser::hostname() const
+{
+ return m_settings->read("hostname").toString();
+}
+
+quint16 ContactUser::port() const
+{
+ return m_settings->read("port", 9878).toInt();
+}
+
+QString ContactUser::contactID() const
+{
+ return ContactIDValidator::idFromHostname(hostname());
+}
+
+void ContactUser::setHostname(const QString &hostname)
+{
+ QString fh = hostname;
+
+ if (!hostname.endsWith(QLatin1String(".onion")))
+ fh.append(QLatin1String(".onion"));
+
+ m_settings->write("hostname", fh);
+ updateOutgoingSocket();
+}
+
+void ContactUser::deleteContact()
+{
+ /* Anything that uses ContactUser is required to either respond to the contactDeleted signal
+ * synchronously, or make use of QWeakPointer. */
+
+ qDebug() << "Deleting contact" << uniqueID;
+
+ if (m_contactRequest) {
+ qDebug() << "Cancelling request associated with contact to be deleted";
+ m_contactRequest->cancel();
+ m_contactRequest->deleteLater();
+ }
+
+ emit contactDeleted(this);
+
+ m_settings->undefine();
+ deleteLater();
+}
+
+void ContactUser::requestAccepted()
+{
+ if (!m_contactRequest) {
+ BUG() << "Request accepted but ContactUser doesn't know an active request";
+ return;
+ }
+
+ if (m_connection) {
+ m_connection->setPurpose(Protocol::Connection::Purpose::KnownContact);
+ emit connected();
+ }
+
+ requestRemoved();
+}
+
+void ContactUser::requestRemoved()
+{
+ if (m_contactRequest) {
+ m_contactRequest->deleteLater();
+ m_contactRequest = 0;
+ updateStatus();
+ }
+}
+
+void ContactUser::assignConnection(Protocol::Connection *connection)
+{
+ if (connection == m_connection) {
+ BUG() << "Connection is already assigned to this ContactUser";
+ return;
+ }
+
+ if (qobject_cast<ContactUser*>(connection->parent()) && connection->parent() != this) {
+ BUG() << "Connection is already owned by another ContactUser";
+ connection->close();
+ return;
+ }
+
+ connection->setParent(this);
+ bool isOutbound = connection->direction() == Protocol::Connection::ClientSide;
+
+ if (!connection->isConnected()) {
+ BUG() << "Connection assigned to contact but isn't connected; discarding";
+ connection->close();
+ connection->deleteLater();
+ return;
+ }
+
+ if (!connection->hasAuthenticatedAs(Protocol::Connection::HiddenServiceAuth, hostname())) {
+ BUG() << "Connection assigned to contact without matching authentication";
+ connection->close();
+ connection->deleteLater();
+ return;
+ }
+
+ /* KnownToPeer is set for an outbound connection when the remote end indicates
+ * that it knows us as a contact. If this is set, we can assume that the
+ * connection is fully built and will be kept open.
+ *
+ * If this isn't a request and KnownToPeer is not set, the connection has
+ * effectively failed: it will be timed out and closed without a purpose.
+ * This probably means that peer removed us a contact.
+ */
+ if (isOutbound) {
+ bool knownToPeer = connection->hasAuthenticated(Protocol::Connection::KnownToPeer);
+ if (m_contactRequest && knownToPeer) {
+ m_contactRequest->accept();
+ if (m_contactRequest)
+ BUG() << "Outgoing contact request not unset after implicit accept during connection";
+ } else if (!m_contactRequest && !knownToPeer) {
+ qDebug() << "Contact says we're unknown; marking as rejected";
+ settings()->write("rejected", true);
+ connection->close();
+ connection->deleteLater();
+ updateStatus();
+ updateOutgoingSocket();
+ return;
+ }
+ }
+
+ if (m_connection && !m_connection->isConnected()) {
+ qDebug() << "Replacing dead connection with new connection";
+ clearConnection();
+ }
+
+ /* To resolve a race if two contacts try to connect at the same time:
+ *
+ * If the existing connection is in the same direction as the new one,
+ * always use the new one.
+ */
+ if (m_connection && connection->direction() == m_connection->direction()) {
+ qDebug() << "Replacing existing connection with contact because the new one goes the same direction";
+ clearConnection();
+ }
+
+ /* If the existing connection is more than 30 seconds old, measured from
+ * when it was successfully established, it's replaced with the new one.
+ */
+ if (m_connection && m_connection->age() > 30) {
+ qDebug() << "Replacing existing connection with contact because it's more than 30 seconds old";
+ clearConnection();
+ }
+
+ /* Otherwise, close the connection for which the server's onion-formatted
+ * hostname compares less with a strcmp function
+ */
+ bool preferOutbound = QString::compare(hostname(), identity->hostname()) < 0;
+ if (m_connection) {
+ if (isOutbound == preferOutbound) {
+ // New connection wins
+ clearConnection();
+ } else {
+ // Old connection wins
+ qDebug() << "Closing new connection with contact because the old connection won comparison";
+ connection->close();
+ connection->deleteLater();
+ return;
+ }
+ }
+
+ /* If this connection is inbound and we have an outgoing connection attempt,
+ * use the inbound connection if we haven't sent authentication yet, or if
+ * we would lose the strcmp comparison above.
+ */
+ if (!isOutbound && m_outgoingSocket) {
+ if (m_outgoingSocket->status() != Protocol::OutboundConnector::Authenticating || !preferOutbound) {
+ // Inbound connection wins; outbound connection attempt will abort when status changes
+ qDebug() << "Aborting outbound connection attempt because we got an inbound connection instead";
+ } else {
+ // Outbound attempt wins
+ qDebug() << "Closing inbound connection with contact because the pending outbound connection won comparison";
+ connection->close();
+ connection->deleteLater();
+ return;
+ }
+ }
+
+ if (m_connection) {
+ BUG() << "After resolving connection races, ContactUser still has two connections";
+ connection->close();
+ connection->deleteLater();
+ return;
+ }
+
+ qDebug() << "Assigned" << (isOutbound ? "outbound" : "inbound") << "connection to contact" << uniqueID;
+
+ if (m_contactRequest && isOutbound) {
+ if (!connection->setPurpose(Protocol::Connection::Purpose::OutboundRequest)) {
+ qWarning() << "BUG: Failed setting connection purpose for request";
+ connection->close();
+ connection->deleteLater();
+ return;
+ }
+ } else {
+ if (m_contactRequest && !isOutbound) {
+ qDebug() << "Implicitly accepting outgoing contact request for" << uniqueID << "due to incoming connection";
+ m_contactRequest->accept();
+ }
+
+ if (!connection->setPurpose(Protocol::Connection::Purpose::KnownContact)) {
+ qWarning() << "BUG: Failed setting connection purpose";
+ connection->close();
+ connection->deleteLater();
+ return;
+ }
+ }
+
+ m_connection = connection;
+
+ /* Use a queued connection to onDisconnected, because it clears m_connection.
+ * If we cleared that immediately, it would be possible for the value to change
+ * effectively any time we call into protocol code, which would be dangerous.
+ */
+ connect(m_connection.data(), &Protocol::Connection::closed, this, &ContactUser::onDisconnected, Qt::QueuedConnection);
+
+ /* Delay the call to onConnected to allow protocol code to finish before everything
+ * kicks in. In particular, this is important to allow AuthHiddenServiceChannel to
+ * respond before other channels are created. */
+ if (!metaObject()->invokeMethod(this, "onConnected", Qt::QueuedConnection))
+ BUG() << "Failed queuing invocation of onConnected method";
+}
+
+void ContactUser::clearConnection()
+{
+ if (!m_connection)
+ return;
+
+ disconnect(m_connection.data(), 0, this, 0);
+ if (m_connection->isConnected()) {
+ connect(m_connection.data(), &Protocol::Connection::closed, m_connection.data(), &QObject::deleteLater);
+ m_connection->close();
+ } else {
+ m_connection->deleteLater();
+ }
+
+ m_connection = 0;
+}
+
diff --git a/src/core/ContactUser.h b/src/core/ContactUser.h
new file mode 100644
index 0000000..68a39b6
--- /dev/null
+++ b/src/core/ContactUser.h
@@ -0,0 +1,173 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef CONTACTUSER_H
+#define CONTACTUSER_H
+
+#include <QObject>
+#include <QHash>
+#include <QMetaType>
+#include <QVariant>
+#include <QPointer>
+#include "utils/Settings.h"
+#include "protocol/Connection.h"
+
+class UserIdentity;
+class OutgoingContactRequest;
+class ConversationModel;
+
+namespace Protocol
+{
+ class OutboundConnector;
+}
+
+/* Represents a user on the contact list.
+ * All persistent uses of a ContactUser instance must either connect to the
+ * contactDeleted() signal, or use a QWeakPointer to track deletion. A ContactUser
+ * can be removed at essentially any time. */
+
+class ContactUser : public QObject
+{
+ Q_OBJECT
+ Q_DISABLE_COPY(ContactUser)
+ Q_ENUMS(Status)
+
+ Q_PROPERTY(int uniqueID READ getUniqueID CONSTANT)
+ Q_PROPERTY(UserIdentity* identity READ getIdentity CONSTANT)
+ Q_PROPERTY(QString nickname READ nickname WRITE setNickname NOTIFY nicknameChanged)
+ Q_PROPERTY(QString contactID READ contactID CONSTANT)
+ Q_PROPERTY(Status status READ status NOTIFY statusChanged)
+ Q_PROPERTY(OutgoingContactRequest *contactRequest READ contactRequest NOTIFY statusChanged)
+ Q_PROPERTY(SettingsObject *settings READ settings CONSTANT)
+ Q_PROPERTY(ConversationModel *conversation READ conversation CONSTANT)
+
+ friend class ContactsManager;
+ friend class OutgoingContactRequest;
+
+public:
+ enum Status
+ {
+ Online,
+ Offline,
+ RequestPending,
+ RequestRejected,
+ Outdated
+ };
+
+ UserIdentity * const identity;
+ const int uniqueID;
+
+ explicit ContactUser(UserIdentity *identity, int uniqueID, QObject *parent = 0);
+ virtual ~ContactUser();
+
+ Protocol::Connection *connection() { return m_connection.data(); }
+ bool isConnected() const { return status() == Online; }
+
+ OutgoingContactRequest *contactRequest() { return m_contactRequest; }
+ ConversationModel *conversation() { return m_conversation; }
+
+ UserIdentity *getIdentity() const { return identity; }
+ int getUniqueID() const { return uniqueID; }
+
+ QString nickname() const;
+ /* Hostname is in the onion hostname format, i.e. it ends with .onion */
+ QString hostname() const;
+ quint16 port() const;
+ /* Contact ID in the ricochet: format */
+ QString contactID() const;
+
+ Status status() const { return m_status; }
+
+ SettingsObject *settings();
+
+ Q_INVOKABLE void deleteContact();
+
+public slots:
+ /* Assign a connection to this user
+ *
+ * The connection must be connected, and the peer must be authenticated and
+ * must match this user. ContactUser will assume ownership of the connection,
+ * and it will be closed and deleted when it's no longer used.
+ *
+ * It is valid to pass an incoming or outgoing connection. If there is already
+ * a connection, protocol-specific rules are applied and the new connection
+ * may be closed to favor the older one.
+ *
+ * If the existing connection is replaced, that is equivalent to disconnecting
+ * and reconnectng immediately - any ongoing operations will fail and need to
+ * be retried at a higher level.
+ */
+ void assignConnection(Protocol::Connection *connection);
+
+ void setNickname(const QString &nickname);
+ void setHostname(const QString &hostname);
+
+ void updateStatus();
+
+signals:
+ void statusChanged();
+ void connected();
+ void disconnected();
+ void connectionChanged(Protocol::Connection *connection);
+
+ void nicknameChanged();
+ void contactDeleted(ContactUser *user);
+
+private slots:
+ void onConnected();
+ void onDisconnected();
+ void requestRemoved();
+ void requestAccepted();
+ void onSettingsModified(const QString &key, const QJsonValue &value);
+
+private:
+ QPointer<Protocol::Connection> m_connection;
+ Protocol::OutboundConnector *m_outgoingSocket;
+
+ Status m_status;
+ quint16 m_lastReceivedChatID;
+ OutgoingContactRequest *m_contactRequest;
+ SettingsObject *m_settings;
+ ConversationModel *m_conversation;
+
+ /* See ContactsManager::addContact */
+ static ContactUser *addNewContact(UserIdentity *identity, int id);
+
+ void loadContactRequest();
+ void updateOutgoingSocket();
+
+ void clearConnection();
+};
+
+Q_DECLARE_METATYPE(ContactUser*)
+
+#endif // CONTACTUSER_H
diff --git a/src/core/ContactsManager.cpp b/src/core/ContactsManager.cpp
new file mode 100644
index 0000000..05ffd76
--- /dev/null
+++ b/src/core/ContactsManager.cpp
@@ -0,0 +1,208 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ContactsManager.h"
+#include "IncomingRequestManager.h"
+#include "OutgoingContactRequest.h"
+#include "ContactIDValidator.h"
+#include "ConversationModel.h"
+#include <QStringList>
+#include <QDebug>
+
+#ifdef Q_OS_MAC
+#include <QtMac>
+#endif
+
+ContactsManager *contactsManager = 0;
+
+ContactsManager::ContactsManager(UserIdentity *id)
+ : identity(id), incomingRequests(this), highestID(-1)
+{
+ contactsManager = this;
+}
+
+void ContactsManager::loadFromSettings()
+{
+ SettingsObject settings(QStringLiteral("contacts"));
+ foreach (const QString &key, settings.data().keys())
+ {
+ bool ok = false;
+ int id = key.toInt(&ok);
+ if (!ok)
+ {
+ qWarning() << "Ignoring contact" << key << " with a non-integer ID";
+ continue;
+ }
+
+ ContactUser *user = new ContactUser(identity, id, this);
+ connectSignals(user);
+ pContacts.append(user);
+ emit contactAdded(user);
+ highestID = qMax(id, highestID);
+ }
+
+ incomingRequests.loadRequests();
+}
+
+ContactUser *ContactsManager::addContact(const QString &nickname)
+{
+ Q_ASSERT(!nickname.isEmpty());
+
+ highestID++;
+ ContactUser *user = ContactUser::addNewContact(identity, highestID);
+ user->setParent(this);
+ user->setNickname(nickname);
+ connectSignals(user);
+
+ qDebug() << "Added new contact" << nickname << "with ID" << user->uniqueID;
+
+ pContacts.append(user);
+ emit contactAdded(user);
+
+ return user;
+}
+
+void ContactsManager::connectSignals(ContactUser *user)
+{
+ connect(user, SIGNAL(contactDeleted(ContactUser*)), SLOT(contactDeleted(ContactUser*)));
+ connect(user->conversation(), &ConversationModel::unreadCountChanged, this, &ContactsManager::onUnreadCountChanged);
+ connect(user, &ContactUser::statusChanged, [this,user]() { emit contactStatusChanged(user, user->status()); });
+}
+
+ContactUser *ContactsManager::createContactRequest(const QString &contactid, const QString &nickname,
+ const QString &myNickname, const QString &message)
+{
+ QString hostname = ContactIDValidator::hostnameFromID(contactid);
+ if (hostname.isEmpty() || lookupHostname(contactid) || lookupNickname(nickname))
+ {
+ return 0;
+ }
+
+ bool b = blockSignals(true);
+ ContactUser *user = addContact(nickname);
+ blockSignals(b);
+ if (!user)
+ return user;
+ user->setHostname(ContactIDValidator::hostnameFromID(contactid));
+
+ OutgoingContactRequest::createNewRequest(user, myNickname, message);
+
+ /* Signal deferred from addContact to avoid changing the status immediately */
+ Q_ASSERT(user->status() == ContactUser::RequestPending);
+ emit contactAdded(user);
+ return user;
+}
+
+void ContactsManager::contactDeleted(ContactUser *user)
+{
+ pContacts.removeOne(user);
+}
+
+ContactUser *ContactsManager::lookupSecret(const QByteArray &secret) const
+{
+ Q_ASSERT(secret.size() == 16);
+
+ for (QList<ContactUser*>::ConstIterator it = pContacts.begin(); it != pContacts.end(); ++it)
+ {
+ if (secret == (*it)->settings()->read<Base64Encode>("localSecret"))
+ return *it;
+ }
+
+ return 0;
+}
+
+ContactUser *ContactsManager::lookupHostname(const QString &hostname) const
+{
+ QString ohost = ContactIDValidator::hostnameFromID(hostname);
+ if (ohost.isNull())
+ ohost = hostname;
+
+ if (!ohost.endsWith(QLatin1String(".onion")))
+ ohost.append(QLatin1String(".onion"));
+
+ for (QList<ContactUser*>::ConstIterator it = pContacts.begin(); it != pContacts.end(); ++it)
+ {
+ if (ohost.compare((*it)->hostname(), Qt::CaseInsensitive) == 0)
+ return *it;
+ }
+
+ return 0;
+}
+
+ContactUser *ContactsManager::lookupNickname(const QString &nickname) const
+{
+ for (QList<ContactUser*>::ConstIterator it = pContacts.begin(); it != pContacts.end(); ++it)
+ {
+ if (QString::compare(nickname, (*it)->nickname(), Qt::CaseInsensitive) == 0)
+ return *it;
+ }
+
+ return 0;
+}
+
+ContactUser *ContactsManager::lookupUniqueID(int uniqueID) const
+{
+ for (QList<ContactUser*>::ConstIterator it = pContacts.begin(); it != pContacts.end(); ++it)
+ {
+ if ((*it)->uniqueID == uniqueID)
+ return *it;
+ }
+
+ return 0;
+}
+
+void ContactsManager::onUnreadCountChanged()
+{
+ ConversationModel *model = qobject_cast<ConversationModel*>(sender());
+ Q_ASSERT(model);
+ if (!model)
+ return;
+ ContactUser *user = model->contact();
+
+ emit unreadCountChanged(user, model->unreadCount());
+
+#ifdef Q_OS_MAC
+ int unread = globalUnreadCount();
+ QtMac::setBadgeLabelText(unread == 0 ? QString() : QString::number(unread));
+#endif
+}
+
+int ContactsManager::globalUnreadCount() const
+{
+ int re = 0;
+ foreach (ContactUser *u, pContacts) {
+ if (u->conversation())
+ re += u->conversation()->unreadCount();
+ }
+ return re;
+}
+
diff --git a/src/core/ContactsManager.h b/src/core/ContactsManager.h
new file mode 100644
index 0000000..1c06277
--- /dev/null
+++ b/src/core/ContactsManager.h
@@ -0,0 +1,102 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef CONTACTSMANAGER_H
+#define CONTACTSMANAGER_H
+
+#include <QObject>
+#include <QList>
+#include "ContactUser.h"
+#include "IncomingRequestManager.h"
+
+class OutgoingContactRequest;
+class UserIdentity;
+class IncomingRequestManager;
+
+class ContactsManager : public QObject
+{
+ Q_OBJECT
+ Q_DISABLE_COPY(ContactsManager)
+
+ Q_PROPERTY(IncomingRequestManager* incomingRequests READ incomingRequestManager CONSTANT)
+ Q_PROPERTY(int globalUnreadCount READ globalUnreadCount NOTIFY unreadCountChanged)
+
+ friend class OutgoingContactRequest;
+
+public:
+ UserIdentity * const identity;
+ IncomingRequestManager incomingRequests;
+
+ explicit ContactsManager(UserIdentity *identity);
+
+ IncomingRequestManager *incomingRequestManager() { return &incomingRequests; }
+
+ const QList<ContactUser*> &contacts() const { return pContacts; }
+ ContactUser *lookupSecret(const QByteArray &secret) const;
+ ContactUser *lookupHostname(const QString &hostname) const;
+ ContactUser *lookupNickname(const QString &nickname) const;
+ ContactUser *lookupUniqueID(int uniqueID) const;
+
+ /* Create a new user and a contact request for that user. Use this instead of addContact.
+ * Note that contactID should be an ricochet: ID. */
+ Q_INVOKABLE ContactUser *createContactRequest(const QString &contactID, const QString &nickname,
+ const QString &myNickname, const QString &message);
+
+ /* addContact will add the contact, but does not create a request. Use createContactRequest */
+ ContactUser *addContact(const QString &nickname);
+
+ static QString hostnameFromID(const QString &ID);
+
+ void loadFromSettings();
+
+ int globalUnreadCount() const;
+
+signals:
+ void contactAdded(ContactUser *user);
+ void outgoingRequestAdded(OutgoingContactRequest *request);
+
+ void unreadCountChanged(ContactUser *user, int unreadCount);
+
+ void contactStatusChanged(ContactUser* user, int status);
+
+private slots:
+ void contactDeleted(ContactUser *user);
+ void onUnreadCountChanged();
+
+private:
+ QList<ContactUser*> pContacts;
+ int highestID;
+
+ void connectSignals(ContactUser *user);
+};
+
+#endif // CONTACTSMANAGER_H
diff --git a/src/core/ConversationModel.cpp b/src/core/ConversationModel.cpp
new file mode 100644
index 0000000..6fcd96a
--- /dev/null
+++ b/src/core/ConversationModel.cpp
@@ -0,0 +1,318 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ConversationModel.h"
+#include "protocol/Connection.h"
+#include "protocol/ChatChannel.h"
+#include <QDebug>
+
+ConversationModel::ConversationModel(QObject *parent)
+ : QAbstractListModel(parent)
+ , m_contact(0)
+ , m_unreadCount(0)
+{
+}
+
+void ConversationModel::setContact(ContactUser *contact)
+{
+ if (contact == m_contact)
+ return;
+
+ beginResetModel();
+ messages.clear();
+
+ if (m_contact)
+ disconnect(m_contact, 0, this, 0);
+ m_contact = contact;
+ if (m_contact) {
+ auto connectChannel = [this](Protocol::Channel *channel) {
+ if (Protocol::ChatChannel *chat = qobject_cast<Protocol::ChatChannel*>(channel)) {
+ connect(chat, &Protocol::ChatChannel::messageReceived, this, &ConversationModel::messageReceived);
+ connect(chat, &Protocol::ChatChannel::messageAcknowledged, this, &ConversationModel::messageAcknowledged);
+
+ if (chat->direction() == Protocol::Channel::Outbound) {
+ connect(chat, &Protocol::Channel::invalidated, this, &ConversationModel::outboundChannelClosed);
+ sendQueuedMessages();
+ }
+ }
+ };
+
+ auto connectConnection = [this,connectChannel]() {
+ if (m_contact->connection()) {
+ connect(m_contact->connection(), &Protocol::Connection::channelOpened, this, connectChannel);
+ foreach (auto channel, m_contact->connection()->findChannels<Protocol::ChatChannel>())
+ connectChannel(channel);
+ sendQueuedMessages();
+ }
+ };
+
+ connect(m_contact, &ContactUser::connected, this, connectConnection);
+ connectConnection();
+ connect(m_contact, &ContactUser::statusChanged,
+ this, &ConversationModel::onContactStatusChanged);
+ }
+
+ endResetModel();
+ emit contactChanged();
+}
+
+void ConversationModel::sendMessage(const QString &text)
+{
+ if (text.isEmpty())
+ return;
+
+ MessageData message(text, QDateTime::currentDateTime(), 0, Queued);
+
+ if (m_contact->connection()) {
+ auto channel = m_contact->connection()->findChannel<Protocol::ChatChannel>(Protocol::Channel::Outbound);
+ if (!channel) {
+ channel = new Protocol::ChatChannel(Protocol::Channel::Outbound, m_contact->connection());
+ if (!channel->openChannel()) {
+ message.status = Error;
+ delete channel;
+ channel = 0;
+ }
+ }
+
+ if (channel && channel->isOpened()) {
+ MessageId id = 0;
+ if (channel->sendChatMessage(text, QDateTime(), id))
+ message.status = Sending;
+ else
+ message.status = Error;
+ message.identifier = id;
+ message.attemptCount++;
+ }
+ }
+
+ beginInsertRows(QModelIndex(), 0, 0);
+ messages.prepend(message);
+ endInsertRows();
+}
+
+void ConversationModel::sendQueuedMessages()
+{
+ if (!m_contact->connection())
+ return;
+
+ // Quickly scan to see if we have any queued messages
+ bool haveQueued = false;
+ foreach (const MessageData &data, messages) {
+ if (data.status == Queued) {
+ haveQueued = true;
+ break;
+ }
+ }
+
+ if (!haveQueued)
+ return;
+
+ auto channel = m_contact->connection()->findChannel<Protocol::ChatChannel>(Protocol::Channel::Outbound);
+ if (!channel) {
+ channel = new Protocol::ChatChannel(Protocol::Channel::Outbound, m_contact->connection());
+ if (!channel->openChannel()) {
+ delete channel;
+ return;
+ }
+ }
+
+ // sendQueuedMessages is called at channelOpened
+ if (!channel->isOpened())
+ return;
+
+ // Iterate backwards, from oldest to newest messages
+ for (int i = messages.size() - 1; i >= 0; i--) {
+ if (messages[i].status == Queued) {
+ qDebug() << "Sending queued chat message";
+ bool ok = false;
+ if (messages[i].identifier)
+ ok = channel->sendChatMessageWithId(messages[i].text, messages[i].time, messages[i].identifier);
+ else
+ ok = channel->sendChatMessage(messages[i].text, messages[i].time, messages[i].identifier);
+ if (ok)
+ messages[i].status = Sending;
+ else
+ messages[i].status = Error;
+ messages[i].attemptCount++;
+ emit dataChanged(index(i, 0), index(i, 0));
+ }
+ }
+}
+
+void ConversationModel::messageReceived(const QString &text, const QDateTime &time, MessageId id)
+{
+ // To preserve conversation flow despite potentially high latency, incoming messages
+ // are positioned above the last unacknowledged messages to the peer. We assume that
+ // the peer hadn't seen any unacknowledged message when this message was sent.
+ int row = 0;
+ for (int i = 0; i < messages.size() && i < 5; i++) {
+ if (messages[i].status != Sending && messages[i].status != Queued) {
+ row = i;
+ break;
+ }
+ }
+
+ beginInsertRows(QModelIndex(), row, row);
+ MessageData message(text, time, id, Received);
+ messages.insert(row, message);
+ endInsertRows();
+
+ m_unreadCount++;
+ emit unreadCountChanged();
+}
+
+void ConversationModel::messageAcknowledged(MessageId id, bool accepted)
+{
+ int row = indexOfIdentifier(id, true);
+ if (row < 0)
+ return;
+
+ MessageData &data = messages[row];
+ data.status = accepted ? Delivered : Error;
+ emit dataChanged(index(row, 0), index(row, 0));
+}
+
+void ConversationModel::outboundChannelClosed()
+{
+ // Any messages that are Sending are moved back to Queued, so they
+ // will be re-sent when we reconnect.
+ for (int i = 0; i < messages.size(); i++) {
+ if (messages[i].status != Sending)
+ continue;
+ if (messages[i].attemptCount >= 2) {
+ qDebug() << "Outbound chat channel closed, and unacknowledged message has been tried twice already. Marking as error.";
+ messages[i].status = Error;
+ } else {
+ qDebug() << "Outbound chat channel closed, putting unacknowledged chat message back in queue";
+ messages[i].status = Queued;
+ }
+ emit dataChanged(index(i, 0), index(i, 0));
+ }
+
+ // Try to reopen the channel if we're still connected
+ if (m_contact && m_contact->connection() && m_contact->connection()->isConnected()) {
+ metaObject()->invokeMethod(this, "sendQueuedMessages", Qt::QueuedConnection);
+ }
+}
+
+void ConversationModel::clear()
+{
+ if (messages.isEmpty())
+ return;
+
+ beginRemoveRows(QModelIndex(), 0, messages.size()-1);
+ messages.clear();
+ endRemoveRows();
+
+ resetUnreadCount();
+}
+
+void ConversationModel::resetUnreadCount()
+{
+ if (m_unreadCount == 0)
+ return;
+ m_unreadCount = 0;
+ emit unreadCountChanged();
+}
+
+void ConversationModel::onContactStatusChanged()
+{
+ // Update in case section has changed
+ emit dataChanged(index(0, 0), index(rowCount()-1, 0), QVector<int>() << SectionRole);
+}
+
+QHash<int,QByteArray> ConversationModel::roleNames() const
+{
+ QHash<int, QByteArray> roles;
+ roles[Qt::DisplayRole] = "text";
+ roles[TimestampRole] = "timestamp";
+ roles[IsOutgoingRole] = "isOutgoing";
+ roles[StatusRole] = "status";
+ roles[SectionRole] = "section";
+ roles[TimespanRole] = "timespan";
+ return roles;
+}
+
+int ConversationModel::rowCount(const QModelIndex &parent) const
+{
+ if (parent.isValid())
+ return 0;
+ return messages.size();
+}
+
+QVariant ConversationModel::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid() || index.row() >= messages.size())
+ return QVariant();
+
+ const MessageData &message = messages[index.row()];
+
+ switch (role) {
+ case Qt::DisplayRole: return message.text;
+ case TimestampRole: return message.time;
+ case IsOutgoingRole: return message.status != Received;
+ case StatusRole: return message.status;
+
+ case SectionRole: {
+ if (m_contact->status() == ContactUser::Online)
+ return QString();
+ if (index.row() < messages.size() - 1) {
+ const MessageData &next = messages[index.row()+1];
+ if (next.status != Received && next.status != Delivered)
+ return QString();
+ }
+ for (int i = 0; i <= index.row(); i++) {
+ if (messages[i].status == Received || messages[i].status == Delivered)
+ return QString();
+ }
+ return QStringLiteral("offline");
+ }
+ case TimespanRole: {
+ if (index.row() < messages.size() - 1)
+ return messages[index.row() + 1].time.secsTo(messages[index.row()].time);
+ else
+ return -1;
+ }
+ }
+
+ return QVariant();
+}
+
+int ConversationModel::indexOfIdentifier(MessageId identifier, bool isOutgoing) const
+{
+ for (int i = 0; i < messages.size(); i++) {
+ if (messages[i].identifier == identifier && (messages[i].status != Received) == isOutgoing)
+ return i;
+ }
+ return -1;
+}
+
diff --git a/src/core/ConversationModel.h b/src/core/ConversationModel.h
new file mode 100644
index 0000000..5d1d498
--- /dev/null
+++ b/src/core/ConversationModel.h
@@ -0,0 +1,117 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef CONVERSATIONMODEL_H
+#define CONVERSATIONMODEL_H
+
+#include <QAbstractListModel>
+#include <QDateTime>
+#include "core/ContactUser.h"
+#include "protocol/ChatChannel.h"
+
+class ConversationModel : public QAbstractListModel
+{
+ Q_OBJECT
+ Q_ENUMS(MessageStatus)
+
+ Q_PROPERTY(ContactUser* contact READ contact WRITE setContact NOTIFY contactChanged)
+ Q_PROPERTY(int unreadCount READ unreadCount RESET resetUnreadCount NOTIFY unreadCountChanged)
+
+public:
+ typedef Protocol::ChatChannel::MessageId MessageId;
+
+ enum {
+ TimestampRole = Qt::UserRole,
+ IsOutgoingRole,
+ StatusRole,
+ SectionRole,
+ TimespanRole
+ };
+
+ enum MessageStatus {
+ Received,
+ Queued,
+ Sending,
+ Delivered,
+ Error
+ };
+
+ ConversationModel(QObject *parent = 0);
+
+ ContactUser *contact() const { return m_contact; }
+ void setContact(ContactUser *contact);
+
+ int unreadCount() const { return m_unreadCount; }
+ Q_INVOKABLE void resetUnreadCount();
+
+ virtual QHash<int,QByteArray> roleNames() const;
+ virtual int rowCount(const QModelIndex &parent = QModelIndex()) const;
+ virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
+
+public slots:
+ void sendMessage(const QString &text);
+ void clear();
+
+signals:
+ void contactChanged();
+ void unreadCountChanged();
+
+private slots:
+ void messageReceived(const QString &text, const QDateTime &time, MessageId id);
+ void messageAcknowledged(MessageId id, bool accepted);
+ void outboundChannelClosed();
+ void sendQueuedMessages();
+ void onContactStatusChanged();
+
+private:
+ struct MessageData {
+ QString text;
+ QDateTime time;
+ MessageId identifier;
+ MessageStatus status;
+ quint8 attemptCount;
+
+ MessageData(const QString &text, const QDateTime &time, MessageId id, MessageStatus status)
+ : text(text), time(time), identifier(id), status(status), attemptCount(0)
+ {
+ }
+ };
+
+ ContactUser *m_contact;
+ QList<MessageData> messages;
+ int m_unreadCount;
+
+ int indexOfIdentifier(MessageId identifier, bool isOutgoing) const;
+};
+
+#endif
+
diff --git a/src/core/IdentityManager.cpp b/src/core/IdentityManager.cpp
new file mode 100644
index 0000000..4e8fd23
--- /dev/null
+++ b/src/core/IdentityManager.cpp
@@ -0,0 +1,155 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "IdentityManager.h"
+#include "ContactIDValidator.h"
+#include "core/OutgoingContactRequest.h"
+#include <QDebug>
+
+IdentityManager *identityManager = 0;
+
+IdentityManager::IdentityManager(QObject *parent)
+ : QObject(parent), highestID(-1)
+{
+ identityManager = this;
+
+ loadFromSettings();
+}
+
+IdentityManager::~IdentityManager()
+{
+ identityManager = 0;
+}
+
+void IdentityManager::addIdentity(UserIdentity *identity)
+{
+ m_identities.append(identity);
+ highestID = qMax(identity->uniqueID, highestID);
+
+ connect(&identity->contacts, SIGNAL(contactAdded(ContactUser*)), SLOT(onContactAdded(ContactUser*)));
+ connect(&identity->contacts, SIGNAL(outgoingRequestAdded(OutgoingContactRequest*)),
+ SLOT(onOutgoingRequest(OutgoingContactRequest*)));
+ connect(&identity->contacts.incomingRequests, SIGNAL(requestAdded(IncomingContactRequest*)),
+ SLOT(onIncomingRequest(IncomingContactRequest*)));
+ connect(&identity->contacts.incomingRequests, SIGNAL(requestRemoved(IncomingContactRequest*)),
+ SLOT(onIncomingRequestRemoved(IncomingContactRequest*)));
+
+ emit identityAdded(identity);
+}
+
+void IdentityManager::loadFromSettings()
+{
+ SettingsObject settings;
+ if (settings.read("identity") != QJsonValue::Undefined)
+ {
+ addIdentity(new UserIdentity(0, this));
+ }
+ else
+ {
+ /* No identities exist (probably inital run); create one */
+ createIdentity();
+ }
+}
+
+UserIdentity *IdentityManager::createIdentity(const QString &serviceDirectory, const QString &nickname)
+{
+ UserIdentity *identity = UserIdentity::createIdentity(++highestID, serviceDirectory);
+ if (!identity)
+ return identity;
+
+ if (!nickname.isEmpty())
+ identity->setNickname(nickname);
+
+ addIdentity(identity);
+
+ return identity;
+}
+
+UserIdentity *IdentityManager::lookupHostname(const QString &hostname) const
+{
+ QString ohost = ContactIDValidator::hostnameFromID(hostname);
+ if (ohost.isNull())
+ ohost = hostname;
+
+ if (!ohost.endsWith(QLatin1String(".onion")))
+ ohost.append(QLatin1String(".onion"));
+
+ for (QList<UserIdentity*>::ConstIterator it = m_identities.begin(); it != m_identities.end(); ++it)
+ {
+ if (ohost.compare((*it)->hostname(), Qt::CaseInsensitive) == 0)
+ return *it;
+ }
+
+ return 0;
+}
+
+UserIdentity *IdentityManager::lookupNickname(const QString &nickname) const
+{
+ for (QList<UserIdentity*>::ConstIterator it = m_identities.begin(); it != m_identities.end(); ++it)
+ {
+ if (QString::compare(nickname, (*it)->nickname(), Qt::CaseInsensitive) == 0)
+ return *it;
+ }
+
+ return 0;
+}
+
+UserIdentity *IdentityManager::lookupUniqueID(int uniqueID) const
+{
+ for (QList<UserIdentity*>::ConstIterator it = m_identities.begin(); it != m_identities.end(); ++it)
+ {
+ if ((*it)->uniqueID == uniqueID)
+ return *it;
+ }
+
+ return 0;
+}
+
+void IdentityManager::onContactAdded(ContactUser *user)
+{
+ emit contactAdded(user, user->identity);
+}
+
+void IdentityManager::onOutgoingRequest(OutgoingContactRequest *request)
+{
+ emit outgoingRequestAdded(request, request->user->identity);
+}
+
+void IdentityManager::onIncomingRequest(IncomingContactRequest *request)
+{
+ emit incomingRequestAdded(request, request->manager->contacts->identity);
+}
+
+void IdentityManager::onIncomingRequestRemoved(IncomingContactRequest *request)
+{
+ emit incomingRequestRemoved(request, request->manager->contacts->identity);
+}
diff --git a/src/core/IdentityManager.h b/src/core/IdentityManager.h
new file mode 100644
index 0000000..ccf9fdf
--- /dev/null
+++ b/src/core/IdentityManager.h
@@ -0,0 +1,79 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef IDENTITYMANAGER_H
+#define IDENTITYMANAGER_H
+
+#include <QObject>
+#include "UserIdentity.h"
+
+class IdentityManager : public QObject
+{
+ Q_OBJECT
+ Q_DISABLE_COPY(IdentityManager)
+
+public:
+ explicit IdentityManager(QObject *parent = 0);
+ ~IdentityManager();
+
+ const QList<UserIdentity*> &identities() const { return m_identities; }
+ UserIdentity *lookupNickname(const QString &nickname) const;
+ UserIdentity *lookupHostname(const QString &hostname) const;
+ UserIdentity *lookupUniqueID(int uniqueID) const;
+
+ UserIdentity *createIdentity(const QString &serviceDirectory = QString(), const QString &nickname = QString());
+
+signals:
+ void identityAdded(UserIdentity *identity);
+ void contactAdded(ContactUser *user, UserIdentity *identity);
+ void contactDeleted(ContactUser *user, UserIdentity *identity);
+ void outgoingRequestAdded(OutgoingContactRequest *request, UserIdentity *identity);
+ void incomingRequestAdded(IncomingContactRequest *request, UserIdentity *identity);
+ void incomingRequestRemoved(IncomingContactRequest *request, UserIdentity *identity);
+
+private slots:
+ void onContactAdded(ContactUser *user);
+ void onOutgoingRequest(OutgoingContactRequest *request);
+ void onIncomingRequest(IncomingContactRequest *request);
+ void onIncomingRequestRemoved(IncomingContactRequest *request);
+
+private:
+ QList<UserIdentity*> m_identities;
+ int highestID;
+
+ void loadFromSettings();
+ void addIdentity(UserIdentity *identity);
+};
+
+extern IdentityManager *identityManager;
+
+#endif // IDENTITYMANAGER_H
diff --git a/src/core/IncomingRequestManager.cpp b/src/core/IncomingRequestManager.cpp
new file mode 100644
index 0000000..443b4d1
--- /dev/null
+++ b/src/core/IncomingRequestManager.cpp
@@ -0,0 +1,372 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "IdentityManager.h"
+#include "IncomingRequestManager.h"
+#include "ContactsManager.h"
+#include "OutgoingContactRequest.h"
+#include "ContactIDValidator.h"
+#include "utils/Useful.h"
+#include "protocol/Connection.h"
+#include "protocol/ContactRequestChannel.h"
+
+IncomingRequestManager::IncomingRequestManager(ContactsManager *c)
+ : QObject(c), contacts(c)
+{
+ connect(this, SIGNAL(requestAdded(IncomingContactRequest*)), this, SIGNAL(requestsChanged()));
+ connect(this, SIGNAL(requestRemoved(IncomingContactRequest*)), this, SIGNAL(requestsChanged()));
+
+ auto attachChannel = [this](Protocol::Channel *channel) {
+ if (Protocol::ContactRequestChannel *req = qobject_cast<Protocol::ContactRequestChannel*>(channel)) {
+ connect(req, &Protocol::ContactRequestChannel::requestReceived, this, &IncomingRequestManager::requestReceived);
+ }
+ };
+
+ // Attach to any ContactRequestChannel on an incoming connection for this identity
+ connect(contacts->identity, &UserIdentity::incomingConnection, this,
+ [this,attachChannel](Protocol::Connection *connection) {
+ connect(connection, &Protocol::Connection::channelCreated, this, attachChannel);
+ }
+ );
+}
+
+void IncomingRequestManager::loadRequests()
+{
+ SettingsObject settings(QStringLiteral("contactRequests"));
+
+ foreach (const QString &hostStr, settings.data().keys()) {
+ QByteArray host = hostStr.toLatin1();
+ if (!host.endsWith(".onion"))
+ host.append(".onion");
+
+ IncomingContactRequest *request = new IncomingContactRequest(this, host);
+ request->load();
+
+ m_requests.append(request);
+ emit requestAdded(request);
+ }
+}
+
+QList<QObject*> IncomingRequestManager::requestObjects() const
+{
+ QList<QObject*> re;
+ re.reserve(m_requests.size());
+ foreach (IncomingContactRequest *o, m_requests)
+ re.append(o);
+ return re;
+}
+
+IncomingContactRequest *IncomingRequestManager::requestFromHostname(const QByteArray &hostname)
+{
+ Q_ASSERT(hostname.endsWith(".onion"));
+
+ Q_ASSERT(hostname == hostname.toLower());
+
+ for (QList<IncomingContactRequest*>::ConstIterator it = m_requests.begin(); it != m_requests.end(); ++it)
+ if ((*it)->hostname() == hostname)
+ return *it;
+
+ return 0;
+}
+
+void IncomingRequestManager::requestReceived()
+{
+ Protocol::ContactRequestChannel *channel = qobject_cast<Protocol::ContactRequestChannel*>(sender());
+ if (!channel) {
+ BUG() << "Called without a valid sender";
+ return;
+ }
+
+ using namespace Protocol::Data::ContactRequest;
+
+ QString hostname = channel->connection()->authenticatedIdentity(Protocol::Connection::HiddenServiceAuth);
+ if (hostname.isEmpty() || !hostname.endsWith(QStringLiteral(".onion"))) {
+ BUG() << "Incoming contact request received but connection isn't authenticated";
+ channel->setResponseStatus(Response::Error);
+ return;
+ }
+
+ if (isHostnameRejected(hostname.toLatin1())) {
+ qDebug() << "Rejecting contact request due to a blacklist match for" << hostname;
+ channel->setResponseStatus(Response::Rejected);
+ return;
+ }
+
+ if (identityManager->lookupHostname(hostname)) {
+ qDebug() << "Rejecting contact request from a local identity (which shouldn't have been allowed)";
+ channel->setResponseStatus(Response::Error);
+ return;
+ }
+
+ IncomingContactRequest *request = requestFromHostname(hostname.toLatin1());
+ bool newRequest = false;
+
+ if (request) {
+ // Update the existing request
+ request->setChannel(channel);
+ request->renew();
+ } else {
+ newRequest = true;
+ request = new IncomingContactRequest(this, hostname.toLatin1());
+ request->setChannel(channel);
+ }
+
+ /* It shouldn't be possible to get an incoming contact request for a known
+ * contact, including an outgoing request. Those are implicitly accepted at
+ * a different level. */
+ if (contacts->lookupHostname(hostname)) {
+ BUG() << "Created an inbound contact request matching a known contact; this shouldn't be allowed";
+ return;
+ }
+
+ qDebug() << "Recording" << (newRequest ? "new" : "existing") << "incoming contact request from" << hostname;
+ channel->setResponseStatus(Response::Pending);
+
+ request->save();
+ if (newRequest) {
+ m_requests.append(request);
+ emit requestAdded(request);
+ }
+}
+
+void IncomingRequestManager::removeRequest(IncomingContactRequest *request)
+{
+ if (m_requests.removeOne(request))
+ emit requestRemoved(request);
+
+ request->deleteLater();
+}
+
+void IncomingRequestManager::addRejectedHost(const QByteArray &hostname)
+{
+ SettingsObject *settings = contacts->identity->settings();
+ QJsonArray blacklist = settings->read<QJsonArray>("hostnameBlacklist");
+ if (!blacklist.contains(QString::fromLatin1(hostname))) {
+ blacklist.append(QString::fromLatin1(hostname));
+ settings->write("hostnameBlacklist", blacklist);
+ }
+}
+
+bool IncomingRequestManager::isHostnameRejected(const QByteArray &hostname) const
+{
+ QJsonArray blacklist = contacts->identity->settings()->read<QJsonArray>("hostnameBlacklist");
+ return blacklist.contains(QString::fromLatin1(hostname));
+}
+
+IncomingContactRequest::IncomingContactRequest(IncomingRequestManager *m, const QByteArray &h
+ )
+ : QObject(m)
+ , manager(m)
+ , m_hostname(h)
+{
+ Q_ASSERT(manager);
+ Q_ASSERT(m_hostname.endsWith(".onion"));
+
+ qDebug() << "Created contact request from" << m_hostname << (connection ? "with" : "without") << "connection";
+}
+
+QString IncomingContactRequest::settingsKey() const
+{
+ QString key = QString(QLatin1String(m_hostname));
+ key.chop(QStringLiteral(".onion").size());
+ return QStringLiteral("contactRequests.%1").arg(key);
+}
+
+void IncomingContactRequest::load()
+{
+ SettingsObject settings(settingsKey());
+
+ setNickname(settings.read("nickname").toString());
+ setMessage(settings.read("message").toString());
+
+ m_requestDate = settings.read<QDateTime>("requestDate");
+ m_lastRequestDate = settings.read<QDateTime>("lastRequestDate");
+}
+
+void IncomingContactRequest::save()
+{
+ SettingsObject settings(settingsKey());
+
+ settings.write("nickname", nickname());
+ settings.write("message", message());
+
+ if (m_requestDate.isNull())
+ m_requestDate = m_lastRequestDate = QDateTime::currentDateTime();
+
+ settings.write("requestDate", m_requestDate);
+ settings.write("lastRequestDate", m_lastRequestDate);
+}
+
+void IncomingContactRequest::renew()
+{
+ m_lastRequestDate = QDateTime::currentDateTime();
+}
+
+void IncomingContactRequest::removeRequest()
+{
+ SettingsObject(settingsKey()).undefine();
+}
+
+QString IncomingContactRequest::contactId() const
+{
+ return ContactIDValidator::idFromHostname(hostname());
+}
+
+void IncomingContactRequest::setRemoteSecret(const QByteArray &remoteSecret)
+{
+ Q_ASSERT(remoteSecret.size() == 16);
+ m_remoteSecret = remoteSecret;
+}
+
+void IncomingContactRequest::setMessage(const QString &message)
+{
+ m_message = message;
+}
+
+void IncomingContactRequest::setNickname(const QString &nickname)
+{
+ m_nickname = nickname;
+ emit nicknameChanged();
+}
+
+void IncomingContactRequest::setChannel(Protocol::ContactRequestChannel *channel)
+{
+ if (connection) {
+ qDebug() << "Replacing connection on an IncomingContactRequest. Old connection is" << connection->age() << "seconds old.";
+ connection->close();
+ }
+
+ // When the channel is closed, also close the connection
+ connect(channel, &Protocol::Channel::invalidated, this,
+ [this,channel]() {
+ if (connection == channel->connection() &&
+ connection->purpose() == Protocol::Connection::Purpose::InboundRequest)
+ {
+ qDebug() << "Closing connection attached to an IncomingContactRequest because ContactRequestChannel was closed";
+ connection->close();
+ }
+ }
+ );
+
+ /* Inbound requests are only valid on connections with an Unknown purpose, meaning
+ * they also haven't been claimed by any parent object other than the default. We're
+ * attaching this channel to the request, so we take ownership of the connection here
+ * and set its purpose to InboundRequest. That implicitly means that the channel is
+ * ours too - channels are always owned by the connection.
+ */
+ qDebug() << "Assigning connection to IncomingContactRequest from" << m_hostname;
+ Protocol::Connection *newConnection = channel->connection();
+ if (!newConnection->setPurpose(Protocol::Connection::Purpose::InboundRequest)) {
+ qWarning() << "Setting purpose on incoming contact request connection failed; killing connection";
+ newConnection->close();
+ return;
+ }
+
+ newConnection->setParent(this);
+ connect(newConnection, &Protocol::Connection::closed, this,
+ [this,newConnection]() {
+ if (newConnection && !newConnection->isConnected()) {
+ newConnection->deleteLater();
+ if (newConnection == connection)
+ connection.clear();
+ }
+ }
+ );
+
+ connection = newConnection;
+
+ setNickname(channel->nickname());
+ setMessage(channel->message());
+ emit hasActiveConnectionChanged();
+}
+
+void IncomingContactRequest::accept(ContactUser *user)
+{
+ qDebug() << "Accepting contact request from" << m_hostname;
+
+ // Create the contact if necessary
+ if (!user) {
+ Q_ASSERT(!nickname().isEmpty());
+ user = manager->contacts->addContact(nickname());
+ user->setHostname(QString::fromLatin1(m_hostname));
+ }
+
+ using namespace Protocol::Data::ContactRequest;
+
+ // If we have a connection, send the response and pass it to ContactUser
+ if (connection) {
+ auto channel = connection->findChannel<Protocol::ContactRequestChannel>();
+ if (channel) {
+ // Channel will close after sending a final response
+ user->assignConnection(connection.data());
+ channel->setResponseStatus(Response::Accepted);
+
+ if (connection->parent() != user) {
+ BUG() << "ContactUser didn't claim connection from incoming contact request";
+ connection->close();
+ }
+ } else {
+ connection->close();
+ }
+ connection.clear();
+ }
+
+ // Remove the request
+ removeRequest();
+ manager->removeRequest(this);
+
+ user->updateStatus();
+}
+
+void IncomingContactRequest::reject()
+{
+ qDebug() << "Rejecting contact request from" << m_hostname;
+
+ using namespace Protocol::Data::ContactRequest;
+
+ if (connection) {
+ auto channel = connection->findChannel<Protocol::ContactRequestChannel>();
+ if (channel)
+ channel->setResponseStatus(Response::Rejected);
+ connection->close();
+ connection.clear();
+ }
+
+ // Remove the request from the config
+ removeRequest();
+ // Blacklist the host to prevent repeat requests
+ manager->addRejectedHost(m_hostname);
+ // Remove the request from the manager
+ manager->removeRequest(this);
+
+ // Object is now scheduled for deletion by the manager
+}
diff --git a/src/core/IncomingRequestManager.h b/src/core/IncomingRequestManager.h
new file mode 100644
index 0000000..3ea3d83
--- /dev/null
+++ b/src/core/IncomingRequestManager.h
@@ -0,0 +1,161 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef INCOMINGREQUESTMANAGER_H
+#define INCOMINGREQUESTMANAGER_H
+
+#include <QObject>
+#include <QPointer>
+#include <QDateTime>
+#include "protocol/Connection.h"
+
+class IncomingRequestManager;
+class ContactsManager;
+class ContactUser;
+
+namespace Protocol {
+ class ContactRequestChannel;
+}
+
+class IncomingContactRequest : public QObject
+{
+ Q_OBJECT
+ Q_DISABLE_COPY(IncomingContactRequest)
+
+ Q_PROPERTY(QByteArray hostname READ hostname CONSTANT)
+ Q_PROPERTY(QString contactId READ contactId CONSTANT)
+ Q_PROPERTY(QString message READ message CONSTANT)
+ Q_PROPERTY(QString nickname READ nickname WRITE setNickname NOTIFY nicknameChanged)
+ Q_PROPERTY(bool hasActiveConnection READ hasActiveConnection NOTIFY hasActiveConnectionChanged)
+ Q_PROPERTY(QDateTime requestDate READ requestDate CONSTANT)
+ Q_PROPERTY(QDateTime lastRequestDate READ lastRequestDate CONSTANT)
+
+public:
+ IncomingRequestManager * const manager;
+
+ IncomingContactRequest(IncomingRequestManager *manager, const QByteArray &hostname);
+
+ QByteArray hostname() const { return m_hostname; }
+ QString contactId() const;
+
+ QByteArray remoteSecret() const { return m_remoteSecret; }
+ void setRemoteSecret(const QByteArray &remoteSecret);
+
+ QString message() const { return m_message; }
+ void setMessage(const QString &message);
+
+ QString nickname() const { return m_nickname; }
+ void setNickname(const QString &nickname);
+
+ bool hasActiveConnection() const { return connection != 0; }
+ void setChannel(Protocol::ContactRequestChannel *channel);
+
+ QDateTime requestDate() const { return m_requestDate; }
+ QDateTime lastRequestDate() const { return m_lastRequestDate; }
+
+ void renew();
+
+ QString settingsKey() const;
+ void load();
+ void save();
+
+public slots:
+ void accept(ContactUser *user = 0);
+ void reject();
+
+signals:
+ void nicknameChanged();
+ void hasActiveConnectionChanged();
+
+private:
+ QPointer<Protocol::Connection> connection;
+ QByteArray m_hostname;
+ QByteArray m_remoteSecret;
+ QString m_message, m_nickname;
+ QDateTime m_requestDate, m_lastRequestDate;
+
+ void removeRequest();
+};
+
+/* IncomingRequestManager handles all incoming contact requests under a
+ * UserIdentity. It receives incoming requests from connections, stores them,
+ * interacts with the UI, and handles approval or rejection.
+ *
+ * Existing requests are loaded at initialization from the configuration file,
+ * and new requests are added via inbound ContactRequestChannel instances.
+ *
+ * Each request has an IncomingContactRequest instance. This manager handles
+ * those instances.
+ */
+class IncomingRequestManager : public QObject
+{
+ Q_OBJECT
+ Q_DISABLE_COPY(IncomingRequestManager)
+
+ Q_PROPERTY(QList<QObject*> requests READ requestObjects NOTIFY requestsChanged)
+
+ friend class IncomingContactRequest;
+
+public:
+ ContactsManager * const contacts;
+
+ explicit IncomingRequestManager(ContactsManager *contactsManager);
+
+ QList<QObject*> requestObjects() const;
+ QList<IncomingContactRequest*> requests() const { return m_requests; }
+
+ /* Hostname is an onion address, including the '.onion' suffix */
+ IncomingContactRequest *requestFromHostname(const QByteArray &hostname);
+
+ /* Called by ContactsManager to trigger loading past requests from the
+ * configuration. */
+ void loadRequests();
+
+ /* Blacklist a host for immediate rejection in the future */
+ void addRejectedHost(const QByteArray &hostname);
+ bool isHostnameRejected(const QByteArray &hostname) const;
+
+signals:
+ void requestAdded(IncomingContactRequest *request);
+ void requestRemoved(IncomingContactRequest *request);
+ void requestsChanged();
+
+private slots:
+ void requestReceived();
+
+private:
+ QList<IncomingContactRequest*> m_requests;
+
+ void removeRequest(IncomingContactRequest *request);
+};
+
+#endif // INCOMINGREQUESTMANAGER_H
diff --git a/src/core/OutgoingContactRequest.cpp b/src/core/OutgoingContactRequest.cpp
new file mode 100644
index 0000000..755278a
--- /dev/null
+++ b/src/core/OutgoingContactRequest.cpp
@@ -0,0 +1,218 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "OutgoingContactRequest.h"
+#include "ContactsManager.h"
+#include "ContactUser.h"
+#include "UserIdentity.h"
+#include "IncomingRequestManager.h"
+#include "utils/Useful.h"
+#include "protocol/ContactRequestChannel.h"
+#include <QDebug>
+
+OutgoingContactRequest *OutgoingContactRequest::createNewRequest(ContactUser *user, const QString &myNickname,
+ const QString &message)
+{
+ Q_ASSERT(!user->contactRequest());
+
+ SettingsObject *settings = user->settings();
+ settings->write("request.status", static_cast<int>(Pending));
+ settings->write("request.myNickname", myNickname);
+ settings->write("request.message", message);
+
+ user->loadContactRequest();
+ Q_ASSERT(user->contactRequest());
+ return user->contactRequest();
+}
+
+OutgoingContactRequest::OutgoingContactRequest(ContactUser *u)
+ : QObject(u), user(u)
+ , m_settings(new SettingsObject(u->settings(), QStringLiteral("request"), this))
+{
+ emit user->identity->contacts.outgoingRequestAdded(this);
+
+ attemptAutoAccept();
+}
+
+OutgoingContactRequest::~OutgoingContactRequest()
+{
+ user->setProperty("contactRequest", QVariant());
+}
+
+QString OutgoingContactRequest::myNickname() const
+{
+ return m_settings->read("myNickname").toString();
+}
+
+QString OutgoingContactRequest::message() const
+{
+ return m_settings->read("message").toString();
+}
+
+OutgoingContactRequest::Status OutgoingContactRequest::status() const
+{
+ return static_cast<Status>(m_settings->read("status").toInt());
+}
+
+QString OutgoingContactRequest::rejectMessage() const
+{
+ return m_settings->read("rejectMessage").toString();
+}
+
+void OutgoingContactRequest::setStatus(Status newStatus)
+{
+ Status oldStatus = status();
+ if (newStatus == oldStatus)
+ return;
+
+ m_settings->write("status", static_cast<int>(newStatus));
+ emit statusChanged(newStatus, oldStatus);
+}
+
+void OutgoingContactRequest::attemptAutoAccept()
+{
+ /* Check if there is an existing incoming request that matches this one; if so, treat this as accepted
+ * automatically and accept that incoming request for this user */
+ QByteArray hostname = user->hostname().toLatin1();
+
+ IncomingContactRequest *incomingReq = user->identity->contacts.incomingRequests.requestFromHostname(hostname);
+ if (incomingReq)
+ {
+ qDebug() << "Automatically accepting an incoming contact request matching a newly created outgoing request";
+
+ accept();
+ incomingReq->accept(user);
+ }
+}
+
+void OutgoingContactRequest::sendRequest(Protocol::Connection *connection)
+{
+ if (connection != user->connection()) {
+ BUG() << "OutgoingContactRequest connection doesn't match the assigned user";
+ return;
+ }
+
+ if (connection->purpose() != Protocol::Connection::Purpose::OutboundRequest) {
+ BUG() << "OutgoingContactRequest told to use a connection of invalid purpose" << int(connection->purpose());
+ return;
+ }
+
+ // XXX timeouts
+ Protocol::ContactRequestChannel *channel = new Protocol::ContactRequestChannel(Protocol::Channel::Outbound, connection);
+ connect(channel, &Protocol::ContactRequestChannel::requestStatusChanged,
+ this, &OutgoingContactRequest::requestStatusChanged);
+
+ // On any final response, the channel will be closed. Unless the purpose has been
+ // changed (to KnownContact, on accept), close the connection at that time. That
+ // will eventually trigger a retry via ContactUser if the request is still valid.
+ connect(channel, &Protocol::Channel::invalidated, this,
+ [this,connection]() {
+ if (connection->isConnected() &&
+ connection->purpose() == Protocol::Connection::Purpose::OutboundRequest)
+ {
+ qDebug() << "Closing connection attached to an OutgoingContactRequest because ContactRequestChannel was closed";
+ connection->close();
+ }
+ }
+ );
+
+ if (!message().isEmpty())
+ channel->setMessage(message());
+ if (!myNickname().isEmpty())
+ channel->setNickname(myNickname());
+
+ if (!channel->openChannel()) {
+ BUG() << "Channel for outgoing contact request failed";
+ return;
+ }
+}
+
+void OutgoingContactRequest::removeRequest()
+{
+ if (user->connection()) {
+ Protocol::Channel *channel = user->connection()->findChannel<Protocol::ContactRequestChannel>();
+ if (channel)
+ channel->closeChannel();
+ }
+
+ /* Clear the request settings */
+ m_settings->undefine();
+ emit removed();
+}
+
+void OutgoingContactRequest::accept()
+{
+ setStatus(Accepted);
+ emit accepted();
+ removeRequest();
+}
+
+void OutgoingContactRequest::reject(bool error, const QString &reason)
+{
+ m_settings->write("rejectMessage", reason);
+ setStatus(error ? Error : Rejected);
+
+ if (user->connection()) {
+ Protocol::Channel *channel = user->connection()->findChannel<Protocol::ContactRequestChannel>();
+ if (channel)
+ channel->closeChannel();
+ }
+
+ emit rejected(reason);
+}
+
+void OutgoingContactRequest::cancel()
+{
+ removeRequest();
+}
+
+void OutgoingContactRequest::requestStatusChanged(int status)
+{
+ using namespace Protocol::Data::ContactRequest;
+ switch (status) {
+ case Response::Pending:
+ setStatus(Acknowledged);
+ break;
+ case Response::Accepted:
+ accept();
+ break;
+ case Response::Rejected:
+ reject();
+ break;
+ case Response::Error:
+ reject(true);
+ break;
+ default:
+ BUG() << "Unknown ContactRequest response status";
+ break;
+ }
+}
diff --git a/src/core/OutgoingContactRequest.h b/src/core/OutgoingContactRequest.h
new file mode 100644
index 0000000..c91bdd1
--- /dev/null
+++ b/src/core/OutgoingContactRequest.h
@@ -0,0 +1,104 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef OUTGOINGCONTACTREQUEST_H
+#define OUTGOINGCONTACTREQUEST_H
+
+#include <QObject>
+#include "utils/Settings.h"
+
+class ContactUser;
+class ContactRequestClient;
+
+namespace Protocol {
+ class Connection;
+}
+
+class OutgoingContactRequest : public QObject
+{
+ Q_OBJECT
+ Q_DISABLE_COPY(OutgoingContactRequest)
+ Q_ENUMS(Status)
+
+ Q_PROPERTY(Status status READ status NOTIFY statusChanged)
+ Q_PROPERTY(QString myNickname READ myNickname CONSTANT)
+ Q_PROPERTY(QString message READ message CONSTANT)
+ Q_PROPERTY(QString rejectMessage READ rejectMessage NOTIFY rejected)
+
+public:
+ enum Status
+ {
+ Pending,
+ Acknowledged,
+ Accepted,
+ Error,
+ Rejected,
+ FirstResult = Accepted
+ };
+
+ static OutgoingContactRequest *createNewRequest(ContactUser *user, const QString &myNickname, const QString &message);
+
+ ContactUser * const user;
+
+ OutgoingContactRequest(ContactUser *user);
+ virtual ~OutgoingContactRequest();
+
+ QString myNickname() const;
+ QString message() const;
+ Status status() const;
+ QString rejectMessage() const;
+
+public slots:
+ void accept();
+ void reject(bool error = false, const QString &reason = QString());
+ void cancel();
+
+ void sendRequest(Protocol::Connection *connection);
+
+signals:
+ void statusChanged(int newStatus, int oldStatus);
+ void accepted();
+ void rejected(const QString &reason);
+ void removed();
+
+private slots:
+ void requestStatusChanged(int status);
+
+private:
+ SettingsObject *m_settings;
+
+ void setStatus(Status newStatus);
+ void removeRequest();
+ void attemptAutoAccept();
+};
+
+#endif // OUTGOINGCONTACTREQUEST_H
diff --git a/src/core/UserIdentity.cpp b/src/core/UserIdentity.cpp
new file mode 100644
index 0000000..81c8d66
--- /dev/null
+++ b/src/core/UserIdentity.cpp
@@ -0,0 +1,230 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "UserIdentity.h"
+#include "tor/TorControl.h"
+#include "tor/HiddenService.h"
+#include "core/ContactIDValidator.h"
+#include "protocol/Connection.h"
+#include "utils/Useful.h"
+#include <QTcpServer>
+#include <QTcpSocket>
+#include <QBuffer>
+#include <QDir>
+
+using namespace Protocol;
+
+UserIdentity::UserIdentity(int id, QObject *parent)
+ : QObject(parent)
+ , uniqueID(id)
+ , contacts(this)
+ , m_settings(0)
+ , m_hiddenService(0)
+ , m_incomingServer(0)
+{
+ m_settings = new SettingsObject(QStringLiteral("identity"), this);
+ connect(m_settings, &SettingsObject::modified, this, &UserIdentity::onSettingsModified);
+
+ QString dir = m_settings->read("dataDirectory", QString::fromLatin1("data-%1").arg(uniqueID)).toString();
+
+ m_hiddenService = new Tor::HiddenService(dir, this);
+ connect(m_hiddenService, SIGNAL(statusChanged(int,int)), SLOT(onStatusChanged(int,int)));
+
+ // Generally, these are not used, and we bind to localhost and port 0
+ // for an automatic (and portable) selection.
+ QHostAddress address(m_settings->read("localListenAddress").toString());
+ if (address.isNull())
+ address = QHostAddress::LocalHost;
+ quint16 port = (quint16)m_settings->read("localListenPort").toInt();
+
+ if (!m_settings->read("initializing").toBool() && m_hiddenService->status() == Tor::HiddenService::NotCreated)
+ {
+ qWarning("Hidden service data for identity %d in %s does not exist", uniqueID, qPrintable(dir));
+ delete m_hiddenService;
+ m_hiddenService = 0;
+ }
+ else
+ {
+ m_incomingServer = new QTcpServer(this);
+ if (!m_incomingServer->listen(address, port)) {
+ qWarning() << "Failed to open incoming socket:" << m_incomingServer->errorString();
+ return;
+ }
+
+ connect(m_incomingServer, &QTcpServer::newConnection, this, &UserIdentity::onIncomingConnection);
+
+ m_hiddenService->addTarget(9878, m_incomingServer->serverAddress(), m_incomingServer->serverPort());
+ torControl->addHiddenService(m_hiddenService);
+ }
+
+ contacts.loadFromSettings();
+}
+
+UserIdentity *UserIdentity::createIdentity(int uniqueID, const QString &dataDirectory)
+{
+ // There is actually no support for multiple identities currently.
+ Q_ASSERT(uniqueID == 0);
+ if (uniqueID != 0)
+ return 0;
+
+ SettingsObject settings(QStringLiteral("identity"));
+ settings.write("initializing", true);
+ if (dataDirectory.isEmpty())
+ settings.write("dataDirectory", QString::fromLatin1("data-%1").arg(uniqueID));
+ else
+ settings.write("dataDirectory", dataDirectory);
+
+ return new UserIdentity(uniqueID);
+}
+
+SettingsObject *UserIdentity::settings()
+{
+ return m_settings;
+}
+
+QString UserIdentity::hostname() const
+{
+ return m_hiddenService ? m_hiddenService->hostname() : QString();
+}
+
+QString UserIdentity::contactID() const
+{
+ return ContactIDValidator::idFromHostname(hostname());
+}
+
+QString UserIdentity::nickname() const
+{
+ return m_settings->read("nickname").toString();
+}
+
+void UserIdentity::setNickname(const QString &nick)
+{
+ m_settings->write("nickname", nick);
+}
+
+void UserIdentity::onSettingsModified(const QString &key, const QJsonValue &value)
+{
+ Q_UNUSED(value);
+ if (key == QLatin1String("nickname"))
+ emit nicknameChanged();
+}
+
+void UserIdentity::onStatusChanged(int newStatus, int oldStatus)
+{
+ if (oldStatus == Tor::HiddenService::NotCreated && newStatus > oldStatus)
+ {
+ m_settings->write("initializing", QJsonValue::Undefined);
+ emit contactIDChanged();
+ }
+ emit statusChanged();
+}
+
+bool UserIdentity::isServiceOnline() const
+{
+ return m_hiddenService && m_hiddenService->status() == Tor::HiddenService::Online;
+}
+
+/* Handle an incoming connection to this service
+ *
+ * A Protocol::Connection is created to handle this socket. The
+ * connection initially has a purpose of Unknown. It times out
+ * and automatically closes after ConnectionPrivate::UnknownPurposeTimeout
+ * seconds, unless the purpose is changed.
+ *
+ * If the connection successfully completes authentication,
+ * handleIncomingAuthedConnection is called to link it to a ContactUser
+ * (if applicable) and set the purpose.
+ */
+void UserIdentity::onIncomingConnection()
+{
+ while (m_incomingServer->hasPendingConnections()) {
+ QTcpSocket *socket = m_incomingServer->nextPendingConnection();
+
+ /* The localHostname property is used by Connection to determine the
+ * server onion hostname that this socket is connected to, which is
+ * used by the serverHostname() method.
+ */
+ socket->setProperty("localHostname", m_hiddenService->hostname());
+
+ qDebug() << "Accepted new incoming connection";
+ Connection *conn = new Connection(socket, Connection::ServerSide, this);
+ Q_ASSERT(socket->parent());
+
+ // Delete connection when closed, if it's still owned by this object
+ connect(conn, &Connection::closed, this,
+ [this,conn]() {
+ if (conn->parent() == this) {
+ qDebug() << "Deleting closed incoming connection that was never claimed by an owner";
+ conn->deleteLater();
+ }
+ }
+ );
+
+ connect(conn, &Connection::authenticated, this,
+ [this,conn](Connection::AuthenticationType type) {
+ if (type == Connection::HiddenServiceAuth)
+ handleIncomingAuthedConnection(conn);
+ }
+ );
+
+ emit incomingConnection(conn);
+ }
+}
+
+void UserIdentity::handleIncomingAuthedConnection(Connection *conn)
+{
+ if (conn->purpose() != Connection::Purpose::Unknown)
+ return;
+
+ QString clientName = conn->authenticatedIdentity(Connection::HiddenServiceAuth);
+ if (clientName.isEmpty()) {
+ BUG() << "Called to handle incoming authed connection without any authed name";
+ return;
+ }
+
+ ContactUser *user = contacts.lookupHostname(clientName);
+ if (!user) {
+ // This client can start a contact request, for example. The purpose stays unknown, and the
+ // connection will be killed if the purpose isn't changed before the timeout.
+ qDebug() << "Have an incoming connection authenticated as unknown client" << clientName;
+ return;
+ }
+
+ qDebug() << "Incoming connection authenticated as contact" << user->uniqueID << "with hostname" << clientName;
+ user->assignConnection(conn);
+
+ if (conn->parent() != user) {
+ BUG() << "Connection wasn't claimed after authentication";
+ conn->close();
+ }
+}
+
diff --git a/src/core/UserIdentity.h b/src/core/UserIdentity.h
new file mode 100644
index 0000000..d065a98
--- /dev/null
+++ b/src/core/UserIdentity.h
@@ -0,0 +1,122 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef USERIDENTITY_H
+#define USERIDENTITY_H
+
+#include "ContactsManager.h"
+#include <QObject>
+#include <QMetaType>
+
+namespace Tor
+{
+ class HiddenService;
+}
+
+namespace Protocol
+{
+ class Connection;
+}
+
+class QTcpServer;
+
+/* UserIdentity represents the local identity offered by the user.
+ *
+ * In particular, it represents the published hidden service, and
+ * theoretically holds the list of contacts.
+ *
+ * At present, implementation (and settings) assumes that there is
+ * only one identity, but some code is confusingly written to allow
+ * for several.
+ */
+class UserIdentity : public QObject
+{
+ Q_OBJECT
+ Q_DISABLE_COPY(UserIdentity)
+
+ friend class IdentityManager;
+
+ Q_PROPERTY(int uniqueID READ getUniqueID CONSTANT)
+ Q_PROPERTY(QString nickname READ nickname WRITE setNickname NOTIFY nicknameChanged)
+ Q_PROPERTY(QString contactID READ contactID NOTIFY contactIDChanged)
+ Q_PROPERTY(bool isOnline READ isServiceOnline NOTIFY statusChanged)
+ Q_PROPERTY(ContactsManager *contacts READ getContacts CONSTANT)
+ Q_PROPERTY(SettingsObject *settings READ settings CONSTANT)
+
+public:
+ const int uniqueID;
+ ContactsManager contacts;
+
+ explicit UserIdentity(int uniqueID, QObject *parent = 0);
+
+ /* Properties */
+ int getUniqueID() const { return uniqueID; }
+ QString nickname() const;
+ /* Hostname is .onion format, like ContactUser */
+ QString hostname() const;
+ QString contactID() const;
+
+ ContactsManager *getContacts() { return &contacts; }
+
+ void setNickname(const QString &nickname);
+
+ /* State */
+ bool isServiceOnline() const;
+ Tor::HiddenService *hiddenService() const { return m_hiddenService; }
+
+ SettingsObject *settings();
+
+signals:
+ void statusChanged();
+ void contactIDChanged(); // only possible during creation
+ void nicknameChanged();
+ void settingsChanged(const QString &key);
+ void incomingConnection(Protocol::Connection *connection);
+
+private slots:
+ void onStatusChanged(int newStatus, int oldStatus);
+ void onSettingsModified(const QString &key, const QJsonValue &value);
+ void onIncomingConnection();
+
+private:
+ SettingsObject *m_settings;
+ Tor::HiddenService *m_hiddenService;
+ QTcpServer *m_incomingServer;
+
+ static UserIdentity *createIdentity(int uniqueID, const QString &dataDirectory = QString());
+
+ void handleIncomingAuthedConnection(Protocol::Connection *connection);
+};
+
+Q_DECLARE_METATYPE(UserIdentity*)
+
+#endif // USERIDENTITY_H
diff --git a/src/main.cpp b/src/main.cpp
new file mode 100644
index 0000000..0bc4e0b
--- /dev/null
+++ b/src/main.cpp
@@ -0,0 +1,341 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ui/MainWindow.h"
+#include "core/IdentityManager.h"
+#include "tor/TorManager.h"
+#include "tor/TorControl.h"
+#include "utils/CryptoKey.h"
+#include "utils/SecureRNG.h"
+#include "utils/Settings.h"
+#include <QApplication>
+#include <QIcon>
+#include <QLibraryInfo>
+#include <QSettings>
+#include <QTime>
+#include <QDir>
+#include <QTranslator>
+#include <QMessageBox>
+#include <QLocale>
+#include <QLockFile>
+#include <QStandardPaths>
+#include <openssl/crypto.h>
+
+static bool initSettings(SettingsFile *settings, QLockFile **lockFile, QString &errorMessage);
+static bool importLegacySettings(SettingsFile *settings, const QString &oldPath);
+static void initTranslation();
+
+int main(int argc, char *argv[])
+{
+ QApplication a(argc, argv);
+ a.setApplicationVersion(QLatin1String("1.1.1"));
+ a.setOrganizationName(QStringLiteral("Ricochet"));
+
+#if !defined(Q_OS_WIN) && !defined(Q_OS_MAC)
+ a.setWindowIcon(QIcon(QStringLiteral(":/icons/ricochet.svg")));
+#endif
+
+ QScopedPointer<SettingsFile> settings(new SettingsFile);
+ SettingsObject::setDefaultFile(settings.data());
+
+ QString error;
+ QLockFile *lock = 0;
+ if (!initSettings(settings.data(), &lock, error)) {
+ QMessageBox::critical(0, qApp->translate("Main", "Ricochet Error"), error);
+ return 1;
+ }
+ QScopedPointer<QLockFile> lockFile(lock);
+
+ initTranslation();
+
+ /* Initialize OpenSSL's allocator */
+ CRYPTO_malloc_init();
+
+ /* Seed the OpenSSL RNG */
+ if (!SecureRNG::seed())
+ qFatal("Failed to initialize RNG");
+ qsrand(SecureRNG::randomInt(UINT_MAX));
+
+ /* Tor control manager */
+ Tor::TorManager *torManager = Tor::TorManager::instance();
+ torManager->setDataDirectory(QFileInfo(settings->filePath()).path() + QStringLiteral("/tor/"));
+ torControl = torManager->control();
+ torManager->start();
+
+ /* Identities */
+ identityManager = new IdentityManager;
+ QScopedPointer<IdentityManager> scopedIdentityManager(identityManager);
+
+ /* Window */
+ QScopedPointer<MainWindow> w(new MainWindow);
+ if (!w->showUI())
+ return 1;
+
+ return a.exec();
+}
+
+static QString userConfigPath()
+{
+ QString path = QStandardPaths::writableLocation(QStandardPaths::DataLocation);
+ QString oldPath = path;
+ oldPath.replace(QStringLiteral("Ricochet"), QStringLiteral("Torsion"), Qt::CaseInsensitive);
+ if (QFile::exists(oldPath))
+ return oldPath;
+ return path;
+}
+
+#ifdef Q_OS_MAC
+static QString appBundlePath()
+{
+ QString path = QApplication::applicationDirPath();
+ int p = path.lastIndexOf(QLatin1String(".app/"));
+ if (p >= 0)
+ {
+ p = path.lastIndexOf(QLatin1Char('/'), p);
+ path = path.left(p+1);
+ }
+
+ return path;
+}
+#endif
+
+static bool initSettings(SettingsFile *settings, QLockFile **lockFile, QString &errorMessage)
+{
+ /* If built in portable mode (default), configuration is stored in the 'config'
+ * directory next to the binary. If not writable, launching fails.
+ *
+ * Portable OS X is an exception. In that case, configuration is stored in a
+ * 'config.ricochet' folder next to the application bundle, unless the application
+ * path contains "/Applications", in which case non-portable mode is used.
+ *
+ * When not in portable mode, a platform-specific per-user config location is used.
+ *
+ * This behavior may be overriden by passing a folder path as the first argument.
+ */
+
+ QString configPath;
+ QStringList args = qApp->arguments();
+ if (args.size() > 1) {
+ configPath = args[1];
+ } else {
+#ifndef RICOCHET_NO_PORTABLE
+# ifdef Q_OS_MAC
+ if (!qApp->applicationDirPath().contains(QStringLiteral("/Applications"))) {
+ // Try old configuration path first
+ configPath = appBundlePath() + QStringLiteral("config.torsion");
+ if (!QFile::exists(configPath))
+ configPath = appBundlePath() + QStringLiteral("config.ricochet");
+ }
+# else
+ configPath = qApp->applicationDirPath() + QStringLiteral("/config");
+# endif
+#endif
+ if (configPath.isEmpty())
+ configPath = userConfigPath();
+ }
+
+ QDir dir(configPath);
+ if (!dir.exists() && !dir.mkpath(QStringLiteral("."))) {
+ errorMessage = QStringLiteral("Cannot create directory: %1").arg(dir.path());
+ return false;
+ }
+
+ // Reset to config directory for consistency; avoid depending on this behavior for paths
+ if (QDir::setCurrent(dir.absolutePath()) && dir.isRelative())
+ dir.setPath(QStringLiteral("."));
+
+ QLockFile *lock = new QLockFile(dir.filePath(QStringLiteral("ricochet.json.lock")));
+ *lockFile = lock;
+ lock->setStaleLockTime(0);
+ if (!lock->tryLock()) {
+ if (lock->error() == QLockFile::LockFailedError) {
+ // This happens if a stale lock file exists and another process uses that PID.
+ // Try removing the stale file, which will fail if a real process is holding a
+ // file-level lock. A false error is more problematic than not locking properly
+ // on corner-case systems.
+ if (!lock->removeStaleLockFile() || !lock->tryLock()) {
+ errorMessage = QStringLiteral("Configuration file is already in use");
+ return false;
+ } else
+ qDebug() << "Removed stale lock file";
+ } else {
+ errorMessage = QStringLiteral("Cannot write configuration file (failed to acquire lock)");
+ return false;
+ }
+ }
+
+ settings->setFilePath(dir.filePath(QStringLiteral("ricochet.json")));
+ if (settings->hasError()) {
+ errorMessage = settings->errorMessage();
+ return false;
+ }
+
+ if (settings->root()->data().isEmpty()) {
+ QString filePath = dir.filePath(QStringLiteral("Torsion.ini"));
+ if (!QFile::exists(filePath))
+ filePath = dir.filePath(QStringLiteral("ricochet.ini"));
+ if (QFile::exists(filePath))
+ importLegacySettings(settings, filePath);
+ }
+
+ return true;
+}
+
+static void copyKeys(QSettings &old, SettingsObject *object)
+{
+ foreach (const QString &key, old.childKeys()) {
+ QVariant value = old.value(key);
+ if ((QMetaType::Type)value.type() == QMetaType::QDateTime)
+ object->write(key, value.toDateTime());
+ else if ((QMetaType::Type)value.type() == QMetaType::QByteArray)
+ object->write(key, Base64Encode(value.toByteArray()));
+ else
+ object->write(key, value.toString());
+ }
+}
+
+static bool importLegacySettings(SettingsFile *settings, const QString &oldPath)
+{
+ QSettings old(oldPath, QSettings::IniFormat);
+ SettingsObject *root = settings->root();
+ QVariant value;
+
+ qDebug() << "Importing legacy format settings from" << oldPath;
+
+ if (!(value = old.value(QStringLiteral("tor/controlIp"))).isNull())
+ root->write("tor.controlAddress", value.toString());
+ if (!(value = old.value(QStringLiteral("tor/controlPort"))).isNull())
+ root->write("tor.controlPort", value.toInt());
+ if (!(value = old.value(QStringLiteral("tor/authPassword"))).isNull())
+ root->write("tor.controlPassword", value.toString());
+ if (!(value = old.value(QStringLiteral("tor/socksIp"))).isNull())
+ root->write("tor.socksAddress", value.toString());
+ if (!(value = old.value(QStringLiteral("tor/socksPort"))).isNull())
+ root->write("tor.socksPort", value.toInt());
+ if (!(value = old.value(QStringLiteral("tor/executablePath"))).isNull())
+ root->write("tor.executablePath", value.toString());
+ if (!(value = old.value(QStringLiteral("core/neverPublishService"))).isNull())
+ root->write("tor.neverPublishServices", value.toBool());
+ if (!(value = old.value(QStringLiteral("identity/0/dataDirectory"))).isNull())
+ root->write("identity.dataDirectory", value.toString());
+ if (!(value = old.value(QStringLiteral("identity/0/createNewService"))).isNull())
+ root->write("identity.initializing", value.toBool());
+ if (!(value = old.value(QStringLiteral("core/listenIp"))).isNull())
+ root->write("identity.localListenAddress", value.toString());
+ if (!(value = old.value(QStringLiteral("core/listenPort"))).isNull())
+ root->write("identity.localListenPort", value.toInt());
+
+ {
+ old.beginGroup(QStringLiteral("contacts"));
+ QStringList ids = old.childGroups();
+ foreach (const QString &id, ids) {
+ old.beginGroup(id);
+ SettingsObject userObject(root, QStringLiteral("contacts.%1").arg(id));
+
+ copyKeys(old, &userObject);
+
+ if (old.childGroups().contains(QStringLiteral("request"))) {
+ old.beginGroup(QStringLiteral("request"));
+ QStringList requestKeys = old.childKeys();
+ foreach (const QString &key, requestKeys)
+ userObject.write(QStringLiteral("request.") + key, old.value(key).toString());
+ old.endGroup();
+ }
+
+ old.endGroup();
+ }
+ old.endGroup();
+ }
+
+ {
+ old.beginGroup(QStringLiteral("contactRequests"));
+ QStringList contacts = old.childGroups();
+
+ foreach (const QString &hostname, contacts) {
+ old.beginGroup(hostname);
+ SettingsObject requestObject(root, QStringLiteral("contactRequests.%1").arg(hostname));
+ copyKeys(old, &requestObject);
+ old.endGroup();
+ }
+
+ old.endGroup();
+ }
+
+ if (!(value = old.value(QStringLiteral("core/hostnameBlacklist"))).isNull()) {
+ QStringList blacklist = value.toStringList();
+ root->write("identity.hostnameBlacklist", QJsonArray::fromStringList(blacklist));
+ }
+
+ return true;
+}
+
+static void initTranslation()
+{
+ QTranslator *translator = new QTranslator;
+
+ bool ok = false;
+ QString appPath = qApp->applicationDirPath();
+ QString resPath = QLatin1String(":/lang/");
+
+ QLocale locale = QLocale::system();
+ if (!qgetenv("RICOCHET_LOCALE").isEmpty()) {
+ locale = QLocale(QString::fromLatin1(qgetenv("RICOCHET_LOCALE")));
+ qDebug() << "Forcing locale" << locale << "from environment" << locale.uiLanguages();
+ }
+
+ SettingsObject settings;
+ QString settingsLanguage(settings.read("ui.language").toString());
+
+ if (!settingsLanguage.isEmpty()) {
+ locale = settingsLanguage;
+ } else {
+ //write an empty string to get "System default" language selected automatically in preferences
+ settings.write(QStringLiteral("ui.language"), QString());
+ }
+
+ ok = translator->load(locale, QStringLiteral("ricochet"), QStringLiteral("_"), appPath);
+ if (!ok)
+ ok = translator->load(locale, QStringLiteral("ricochet"), QStringLiteral("_"), resPath);
+
+ if (ok) {
+ qApp->installTranslator(translator);
+
+ QTranslator *qtTranslator = new QTranslator;
+ ok = qtTranslator->load(QStringLiteral("qt_") + locale.name(), QLibraryInfo::location(QLibraryInfo::TranslationsPath));
+ if (ok)
+ qApp->installTranslator(qtTranslator);
+ else
+ delete qtTranslator;
+ } else
+ delete translator;
+}
+
diff --git a/src/protocol/AuthHiddenService.proto b/src/protocol/AuthHiddenService.proto
new file mode 100644
index 0000000..afbe209
--- /dev/null
+++ b/src/protocol/AuthHiddenService.proto
@@ -0,0 +1,25 @@
+package Protocol.Data.AuthHiddenService;
+import "ControlChannel.proto";
+
+extend Control.OpenChannel {
+ optional bytes client_cookie = 7200; // 16 random bytes
+}
+
+extend Control.ChannelResult {
+ optional bytes server_cookie = 7200; // 16 random bytes
+}
+
+message Packet {
+ optional Proof proof = 1;
+ optional Result result = 2;
+}
+
+message Proof {
+ optional bytes public_key = 1; // DER encoded public key
+ optional bytes signature = 2; // RSA signature
+}
+
+message Result {
+ required bool accepted = 1;
+ optional bool is_known_contact = 2;
+}
diff --git a/src/protocol/AuthHiddenServiceChannel.cpp b/src/protocol/AuthHiddenServiceChannel.cpp
new file mode 100644
index 0000000..350c1ad
--- /dev/null
+++ b/src/protocol/AuthHiddenServiceChannel.cpp
@@ -0,0 +1,351 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "AuthHiddenServiceChannel.h"
+#include "AuthHiddenService.pb.h"
+#include "Connection.h"
+#include "Channel_p.h"
+#include "utils/SecureRNG.h"
+#include "utils/CryptoKey.h"
+#include "utils/Useful.h"
+#include <QMessageAuthenticationCode>
+
+using namespace Protocol;
+
+namespace Protocol {
+
+class AuthHiddenServiceChannelPrivate : public ChannelPrivate
+{
+public:
+ CryptoKey privateKey;
+ QByteArray clientCookie, serverCookie;
+ bool accepted;
+
+ AuthHiddenServiceChannelPrivate(Channel *q, Channel::Direction direction, Connection *conn)
+ : ChannelPrivate(q, QStringLiteral("im.ricochet.auth.hidden-service"), direction, conn)
+ , accepted(false)
+ {
+ }
+
+ QByteArray getProofData(const QString &clientHostname);
+};
+
+}
+
+AuthHiddenServiceChannel::AuthHiddenServiceChannel(Direction dir, Connection *conn)
+ : Channel(new AuthHiddenServiceChannelPrivate(this, dir, conn))
+{
+ if (direction() == Outbound)
+ connect(this, &Channel::channelOpened, this, &AuthHiddenServiceChannel::sendAuthMessage);
+
+ connect(this, &Channel::invalidated, this,
+ [this]() {
+ Q_D(AuthHiddenServiceChannel);
+ if (d->accepted)
+ emit authSuccessful();
+ else
+ emit authFailed();
+ }
+ );
+}
+
+void AuthHiddenServiceChannel::setPrivateKey(const CryptoKey &key)
+{
+ Q_D(AuthHiddenServiceChannel);
+ if (isOpened()) {
+ BUG() << "Channel is already open";
+ return;
+ }
+
+ if (!key.isLoaded() || !key.isPrivate()) {
+ BUG() << "AuthHiddenServiceChannel cannot authenticate without a valid private key";
+ return;
+ }
+
+ d->privateKey = key;
+}
+
+bool AuthHiddenServiceChannel::allowInboundChannelRequest(const Data::Control::OpenChannel *request, Data::Control::ChannelResult *result)
+{
+ Q_D(AuthHiddenServiceChannel);
+
+ using namespace Data::Control;
+
+ if (connection()->direction() != Connection::ServerSide) {
+ // Hidden service authentication is only allowed from the client-side connection
+ qDebug() << "Rejecting AuthHiddenServiceChannel from server side";
+ result->set_common_error(ChannelResult::BadUsageError);
+ return false;
+ }
+
+ if (connection()->hasAuthenticated(Connection::HiddenServiceAuth)) {
+ // You can only authenticate a connection once
+ qDebug() << "Rejecting AuthHiddenServiceChannel on authenticated connection";
+ result->set_common_error(ChannelResult::BadUsageError);
+ return false;
+ }
+
+ if (connection()->findChannel<AuthHiddenServiceChannel>()) {
+ // Refuse if another channel already exists
+ qDebug() << "Rejecting instance of AuthHiddenServiceChannel on a connection that already has one";
+ result->set_common_error(ChannelResult::BadUsageError);
+ return false;
+ }
+
+ // Store client cookie
+ std::string clientCookie = request->GetExtension(Data::AuthHiddenService::client_cookie);
+ if (clientCookie.size() != 16) {
+ qDebug() << "Received OpenChannel for" << type() << "with no valid client_cookie";
+ result->set_common_error(ChannelResult::BadUsageError);
+ return false;
+ }
+ d->clientCookie = QByteArray(clientCookie.c_str(), clientCookie.size());
+
+ // Generate a random cookie and return result
+ d->serverCookie = SecureRNG::random(16);
+ if (d->serverCookie.isEmpty())
+ return false;
+
+ qDebug() << "Accepted inbound AuthHiddenServiceChannel";
+
+ result->SetExtension(Data::AuthHiddenService::server_cookie, std::string(d->serverCookie.constData(), d->serverCookie.size()));
+ return true;
+}
+
+bool AuthHiddenServiceChannel::allowOutboundChannelRequest(Data::Control::OpenChannel *request)
+{
+ Q_D(AuthHiddenServiceChannel);
+
+ if (!d->privateKey.isLoaded()) {
+ BUG() << "AuthHiddenServiceChannel can't be opened without a private key";
+ return false;
+ }
+
+ d->clientCookie = SecureRNG::random(16);
+ if (d->clientCookie.isEmpty())
+ return false;
+ request->SetExtension(Data::AuthHiddenService::client_cookie, std::string(d->clientCookie.constData(), d->clientCookie.size()));
+ return true;
+}
+
+bool AuthHiddenServiceChannel::processChannelOpenResult(const Data::Control::ChannelResult *result)
+{
+ Q_D(AuthHiddenServiceChannel);
+
+ if (result->opened()) {
+ std::string cookie = result->GetExtension(Data::AuthHiddenService::server_cookie);
+ if (cookie.size() != 16) {
+ qDebug() << "Received ChannelResult for" << type() << "with no valid server_cookie";
+ return false;
+ }
+
+ d->serverCookie = QByteArray(cookie.c_str(), cookie.size());
+ return true;
+ }
+
+ return false;
+}
+
+void AuthHiddenServiceChannel::sendAuthMessage()
+{
+ Q_D(AuthHiddenServiceChannel);
+
+ if (direction() != Outbound) {
+ BUG() << "Proof message is only sent from outbound channels";
+ return;
+ }
+
+ if (!isOpened())
+ return;
+
+ if (d->clientCookie.size() != 16 || d->serverCookie.size() != 16) {
+ BUG() << "AuthHiddenServiceChannel can't create a proof without valid cookies";
+ closeChannel();
+ return;
+ }
+
+ QByteArray publicKey = d->privateKey.encodedPublicKey(CryptoKey::DER);
+ if (publicKey.size() > 150) {
+ BUG() << "Unexpected size for encoded public key";
+ closeChannel();
+ return;
+ }
+
+ QByteArray signature;
+ QByteArray proofData = d->getProofData(d->privateKey.torServiceID());
+ if (!proofData.isEmpty()) {
+ QByteArray proofHMAC = QMessageAuthenticationCode::hash(proofData, d->clientCookie + d->serverCookie,
+ QCryptographicHash::Sha256);
+ signature = d->privateKey.signSHA256(proofHMAC);
+ }
+
+ if (signature.isEmpty()) {
+ BUG() << "Creating proof on AuthHiddenServiceChannel failed";
+ closeChannel();
+ return;
+ }
+
+ QScopedPointer<Data::AuthHiddenService::Proof> proof(new Data::AuthHiddenService::Proof);
+ proof->set_public_key(std::string(publicKey.constData(), publicKey.size()));
+ proof->set_signature(std::string(signature.constData(), signature.size()));
+
+ Data::AuthHiddenService::Packet message;
+ message.set_allocated_proof(proof.take());
+ sendMessage(message);
+
+ qDebug() << "AuthHiddenServiceChannel sent outbound authentication packet";
+}
+
+QByteArray AuthHiddenServiceChannelPrivate::getProofData(const QString &client)
+{
+ QByteArray serverHostname = connection->serverHostname().toLatin1().mid(0, 16);
+ QByteArray clientHostname = client.toLatin1();
+
+ if (clientHostname.size() != 16 || serverHostname.size() != 16) {
+ BUG() << "AuthHiddenServiceChannel can't figure out the client and server hostnames";
+ return QByteArray();
+ }
+
+ return clientHostname + serverHostname;
+}
+
+void AuthHiddenServiceChannel::receivePacket(const QByteArray &packet)
+{
+ Data::AuthHiddenService::Packet message;
+ if (!message.ParseFromArray(packet.constData(), packet.size())) {
+ closeChannel();
+ return;
+ }
+
+ if (message.has_proof()) {
+ handleProof(message.proof());
+ } else if (message.has_result()) {
+ handleResult(message.result());
+ } else {
+ qWarning() << "Unrecognized message on" << type();
+ closeChannel();
+ }
+}
+
+void AuthHiddenServiceChannel::handleProof(const Data::AuthHiddenService::Proof &message)
+{
+ Q_D(AuthHiddenServiceChannel);
+
+ if (direction() != Inbound) {
+ qWarning() << "Received unexpected proof on outbound" << type();
+ closeChannel();
+ return;
+ }
+
+ if (d->clientCookie.size() != 16 || d->serverCookie.size() != 16) {
+ BUG() << "AuthHiddenServiceChannel can't create a proof without valid cookies";
+ closeChannel();
+ return;
+ }
+
+ QByteArray publicKeyData(message.public_key().c_str(), message.public_key().size());
+ QByteArray signature(message.signature().c_str(), message.signature().size());
+
+ QScopedPointer<Data::AuthHiddenService::Result> result(new Data::AuthHiddenService::Result);
+ result->set_accepted(false);
+
+ // Hidden services always use a 1024bit key. A valid signature will always be exactly 128 bytes.
+ CryptoKey publicKey;
+ if (signature.size() != 128) {
+ qWarning() << "Received invalid signature (size" << signature.size() << ") on" << type();
+ } else if (publicKeyData.size() > 150) {
+ qWarning() << "Received invalid public key (size" << publicKeyData.size() << ") on" << type();
+ } else if (!publicKey.loadFromData(publicKeyData, CryptoKey::PublicKey, CryptoKey::DER)) {
+ qWarning() << "Unable to parse public key from" << type();
+ } else if (publicKey.bits() != 1024) {
+ qWarning() << "Received invalid public key (" << publicKey.bits() << "bits) on" << type();
+ } else {
+ bool ok = false;
+ QByteArray proofData = d->getProofData(publicKey.torServiceID());
+ if (!proofData.isEmpty()) {
+ QByteArray proofHMAC = QMessageAuthenticationCode::hash(proofData, d->clientCookie + d->serverCookie,
+ QCryptographicHash::Sha256);
+ ok = publicKey.verifySHA256(proofHMAC, signature);
+ }
+
+ if (!ok) {
+ qWarning() << "Signature verification failed on" << type();
+ result->set_accepted(false);
+ } else {
+ result->set_accepted(true);
+ qDebug() << type() << "accepted inbound authentication for" << publicKey.torServiceID();
+ }
+ }
+
+ if (result->accepted()) {
+ connection()->grantAuthentication(Connection::HiddenServiceAuth, publicKey.torServiceID() + QStringLiteral(".onion"));
+ d->accepted = true;
+ result->set_is_known_contact(connection()->purpose() == Connection::Purpose::KnownContact);
+ } else {
+ d->accepted = false;
+ }
+
+ Data::AuthHiddenService::Packet resultMessage;
+ resultMessage.set_allocated_result(result.data());
+ sendMessage(resultMessage);
+
+ // Clear QScopedPointer, value is now owned by the Packet
+ result.take();
+
+ // In all cases, close the channel afterwards. This also emits the
+ // authSucceeded or authFailed signals.
+ closeChannel();
+}
+
+void AuthHiddenServiceChannel::handleResult(const Data::AuthHiddenService::Result &message)
+{
+ Q_D(AuthHiddenServiceChannel);
+
+ if (direction() != Outbound) {
+ qWarning() << "Received invalid message on AuthHiddenServiceChannel";
+ closeChannel();
+ return;
+ }
+
+ if (message.accepted()) {
+ qDebug() << "AuthHiddenServiceChannel succeeded as" << (message.is_known_contact() ? "known" : "unknown") << "contact";
+ d->accepted = true;
+ if (message.is_known_contact())
+ connection()->grantAuthentication(Connection::KnownToPeer);
+ } else {
+ qWarning() << "AuthHiddenServiceChannel rejected";
+ d->accepted = false;
+ }
+
+ closeChannel();
+}
+
diff --git a/src/protocol/AuthHiddenServiceChannel.h b/src/protocol/AuthHiddenServiceChannel.h
new file mode 100644
index 0000000..fc0a238
--- /dev/null
+++ b/src/protocol/AuthHiddenServiceChannel.h
@@ -0,0 +1,76 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef PROTOCOL_AUTHHIDDENSERVICECHANNEL_H
+#define PROTOCOL_AUTHHIDDENSERVICECHANNEL_H
+
+#include "Channel.h"
+#include "utils/CryptoKey.h"
+#include "AuthHiddenService.pb.h"
+
+namespace Protocol
+{
+
+class AuthHiddenServiceChannelPrivate;
+
+class AuthHiddenServiceChannel : public Channel
+{
+ Q_OBJECT
+ Q_DISABLE_COPY(AuthHiddenServiceChannel)
+ Q_DECLARE_PRIVATE(AuthHiddenServiceChannel)
+
+public:
+ explicit AuthHiddenServiceChannel(Direction direction, Connection *connection);
+
+ void setPrivateKey(const CryptoKey &key);
+
+signals:
+ void authSuccessful();
+ void authFailed();
+
+private slots:
+ void sendAuthMessage();
+
+protected:
+ virtual bool allowInboundChannelRequest(const Data::Control::OpenChannel *request, Data::Control::ChannelResult *result);
+ virtual bool allowOutboundChannelRequest(Data::Control::OpenChannel *request);
+ virtual bool processChannelOpenResult(const Data::Control::ChannelResult *result);
+ virtual void receivePacket(const QByteArray &packet);
+
+private:
+ void handleProof(const Data::AuthHiddenService::Proof &message);
+ void handleResult(const Data::AuthHiddenService::Result &message);
+};
+
+}
+
+#endif
diff --git a/src/protocol/Channel.cpp b/src/protocol/Channel.cpp
new file mode 100644
index 0000000..891c73a
--- /dev/null
+++ b/src/protocol/Channel.cpp
@@ -0,0 +1,328 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Channel_p.h"
+#include "Connection_p.h"
+#include "ControlChannel.h"
+#include "utils/Useful.h"
+#include <QDebug>
+
+#include "AuthHiddenServiceChannel.h"
+#include "ChatChannel.h"
+#include "ContactRequestChannel.h"
+
+using namespace Protocol;
+
+Channel *Channel::create(const QString &type, Direction direction, Connection *connection)
+{
+ if (!connection)
+ return 0;
+
+ if (type == QStringLiteral("im.ricochet.auth.hidden-service")) {
+ return new AuthHiddenServiceChannel(direction, connection);
+ } else if (type == QStringLiteral("im.ricochet.chat")) {
+ return new ChatChannel(direction, connection);
+ } else if (type == QStringLiteral("im.ricochet.contact.request")) {
+ return new ContactRequestChannel(direction, connection);
+ } else {
+ return 0;
+ }
+}
+
+Channel::Channel(const QString &type, Direction direction, Connection *connection)
+ : QObject(connection)
+ , d_ptr(new ChannelPrivate(this, type, direction, connection))
+{
+}
+
+Channel::Channel(ChannelPrivate *d_ptr)
+ : QObject(d_ptr->connection)
+ , d_ptr(d_ptr)
+{
+}
+
+Channel::~Channel()
+{
+ Q_D(Channel);
+ if (d->identifier >= 0 && !d->isInvalidated)
+ d->connection->d->removeChannel(this);
+}
+
+QString Channel::type() const
+{
+ Q_D(const Channel);
+ return d->type;
+}
+
+// May return -1 for unassigned channels
+int Channel::identifier() const
+{
+ Q_D(const Channel);
+ if (d->identifier > UINT16_MAX)
+ return -1;
+ return d->identifier;
+}
+
+Channel::Direction Channel::direction() const
+{
+ Q_D(const Channel);
+ return d->direction;
+}
+
+Connection *Channel::connection()
+{
+ Q_D(Channel);
+ return d->connection;
+}
+
+bool Channel::isOpened() const
+{
+ Q_D(const Channel);
+ if (d->isOpened && d->identifier < 0)
+ BUG() << "Channel is marked as open, but has no identifier";
+ return d->isOpened;
+}
+
+bool Channel::openChannel()
+{
+ Q_D(Channel);
+ if (direction() != Channel::Outbound || isOpened() || identifier() >= 0) {
+ BUG() << "Cannot send request to open" << type() << "channel in an incorrect state";
+ if (isOpened())
+ closeChannel();
+ d->invalidate();
+ return false;
+ } else if (!connection()->findChannel<ControlChannel>()->sendOpenChannel(this)) {
+ if (isOpened()) {
+ BUG() << "Channel somehow opened instantly in an impossible situation";
+ closeChannel();
+ }
+ d->invalidate();
+ return false;
+ }
+
+ return true;
+}
+
+void Channel::closeChannel()
+{
+ Q_D(Channel);
+
+ if (!d->hasSentClose && d->identifier >= 0 && connection()->isConnected()) {
+ d->hasSentClose = true;
+ bool ok = connection()->d->writePacket(this, QByteArray());
+ if (!ok)
+ qDebug() << "Failed sending channel close message";
+ }
+
+ // Invalidate will remove and eventually destroy the Channel
+ d->isOpened = false;
+ d->invalidate();
+}
+
+/* Called by ControlChannel to handle an inbound OpenChannel message.
+ * This Channel must be in a clean, inbound state. The Channel subclass
+ * decides whether to accept the request, and can add data to the result.
+ */
+bool ChannelPrivate::openChannelInbound(const Data::Control::OpenChannel *request, Data::Control::ChannelResult *result)
+{
+ Q_Q(Channel);
+ result->set_opened(false);
+ if (direction != Channel::Inbound || isOpened || identifier >= 0 || hasSentClose || isInvalidated) {
+ BUG() << "Handling inbound open channel request on a channel in an unexpected state; rejecting";
+ return false;
+ }
+
+ if (request->channel_identifier() <= 0) {
+ BUG() << "Invalid channel identifier in inboundOpenChannel handler";
+ return false;
+ }
+
+ // The Connection::channelCreated signal must emit once, after the Channel
+ // is fully constructed, but before it's used. This is that moment: just
+ // before we check whether to allow an inbound/outbound request.
+ emit connection->channelCreated(q);
+
+ if (!q->allowInboundChannelRequest(request, result))
+ return false;
+
+ if (isInvalidated) {
+ // This can happen as a result of something calling closeChannel under the
+ // allowInboundChannelRequest handler, which can't be easily ruled out due
+ // to signals. Treat is as a generic error and fail the channel.
+ result->set_opened(false);
+ return false;
+ }
+
+ if (result->has_common_error()) {
+ BUG() << "Accepted inbound OpenChannel request, but result has error details set. Assuming it's actually an error.";
+ result->set_opened(false);
+ return false;
+ }
+
+ result->set_opened(true);
+ identifier = request->channel_identifier();
+ isOpened = true;
+ emit q->channelOpened();
+ return true;
+}
+
+bool ChannelPrivate::openChannelOutbound(Data::Control::OpenChannel *request)
+{
+ Q_Q(Channel);
+ if (direction != Channel::Outbound || isOpened || identifier >= 0) {
+ BUG() << "Handling outbound open channel request on a channel in an unexpected state; rejecting";
+ return false;
+ }
+
+ // The Connection::channelCreated signal must emit once, after the Channel
+ // is fully constructed, but before it's used. This is that moment: just
+ // before we check whether to allow an inbound/outbound request.
+ emit connection->channelCreated(q);
+
+ if (!q->allowOutboundChannelRequest(request))
+ return false;
+
+ request->set_channel_type(type.toStdString());
+ identifier = request->channel_identifier();
+ return true;
+}
+
+bool ChannelPrivate::openChannelResult(const Data::Control::ChannelResult *result)
+{
+ Q_Q(Channel);
+ // ControlChannel should weed out clearly invalid messages, so assert here if it didn't
+ if (direction != Channel::Outbound || isOpened || identifier < 0) {
+ BUG() << "Handling response for outbound open channel on a channel in an unexpected state; ignoring";
+ return false;
+ }
+
+ bool ok = result->opened();
+ if (!q->processChannelOpenResult(result)) {
+ // If the peer thinks the channel was opened successfully, send a close
+ if (result->opened())
+ q->closeChannel();
+ ok = false;
+ }
+
+ if (ok) {
+ isOpened = true;
+ emit q->channelOpened();
+ } else {
+ Data::Control::ChannelResult::CommonError error = Data::Control::ChannelResult::GenericError;
+ if (result->has_common_error())
+ error = result->common_error();
+ emit q->channelRejected(error);
+ invalidate();
+ }
+
+ return ok;
+}
+
+bool Channel::processChannelOpenResult(const Data::Control::ChannelResult *result)
+{
+ Q_UNUSED(result);
+ return true;
+}
+
+bool Channel::sendPacket(const QByteArray &packet)
+{
+ Q_D(Channel);
+ if (d->identifier < 0) {
+ BUG() << "Cannot send packet to channel" << type() << "without an assigned identifier";
+ return false;
+ }
+
+ if (packet.size() == 0) {
+ BUG() << "Cannot send empty packet to channel" << type();
+ return false;
+ }
+
+ if (packet.size() > ConnectionPrivate::PacketMaxDataSize) {
+ BUG() << "Packet is too big on channel" << type();
+ return false;
+ }
+
+ return connection()->d->writePacket(this, packet);
+}
+
+void Channel::requestInboundApproval()
+{
+ if (direction() != Channel::Inbound || isOpened()) {
+ BUG() << "Called in an unexpected channel state";
+ return;
+ }
+
+ emit connection()->channelRequestingInboundApproval(this);
+}
+
+ChannelPrivate::ChannelPrivate(Channel *q, const QString &type, Channel::Direction direction, Connection *conn)
+ : q_ptr(q)
+ , connection(conn)
+ , type(type)
+ , identifier(-1)
+ , direction(direction)
+ , isOpened(false)
+ , hasSentClose(false)
+ , isInvalidated(false)
+{
+}
+
+ChannelPrivate::~ChannelPrivate()
+{
+ Q_Q(Channel);
+ if (identifier >= 0 && !isInvalidated) {
+ BUG() << "Channel of type" << type << "was deleted without being invalidated";
+ connection->d->removeChannel(q);
+ }
+}
+
+void ChannelPrivate::invalidate()
+{
+ Q_Q(Channel);
+ if (isInvalidated)
+ return;
+
+ Q_ASSERT(!isOpened);
+
+ qDebug() << "Invalidating channel" << q << "type" << type << "id" << identifier;
+
+ isInvalidated = true;
+ emit q->invalidated();
+
+ if (identifier >= 0) {
+ connection->d->removeChannel(q);
+ Q_ASSERT(!connection->channel(identifier));
+ }
+
+ q->deleteLater();
+}
diff --git a/src/protocol/Channel.h b/src/protocol/Channel.h
new file mode 100644
index 0000000..e6b13f8
--- /dev/null
+++ b/src/protocol/Channel.h
@@ -0,0 +1,234 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef PROTOCOL_CHANNEL_H
+#define PROTOCOL_CHANNEL_H
+
+#include <QObject>
+#include <QScopedPointer>
+#include "ControlChannel.pb.h"
+
+namespace Protocol
+{
+
+class Connection;
+class ChannelPrivate;
+
+/* Base representation of a channel inside of a connection
+ *
+ * Channel is subclassed by channel type implementations to handle channel
+ * requests and inbound or outbound packets. Generally, the channel subclass
+ * implements low-level communication, and a higher level class will wrap
+ * the channel with real functionality.
+ *
+ * Outbound channels are opened by creating an instance of the channel type,
+ * setting up any necessary properties, and sending it to
+ * ControlChannel::openChannel. The result is reported through the
+ * outboundOpenResult callback, which will emit channelOpened or channelRejected.
+ *
+ * Incoming channel requests create an instance by name via the create method
+ * and call the inboundOpenChannel method. If that method indicates success, the
+ * channel is inserted. If failed, the channel will be closed and destroyed.
+ *
+ * When a channel is closed, the instance is invalidated and will be deleted
+ * automatically. Pointers to channel should be stored using QPointer, or should
+ * be reset immediately when the invalidated() signal is emitted.
+ */
+class Channel : public QObject
+{
+ Q_OBJECT
+ Q_DISABLE_COPY(Channel)
+ Q_DECLARE_PRIVATE(Channel)
+
+ friend class ControlChannel;
+ friend class Connection;
+ friend class ConnectionPrivate;
+
+public:
+ enum Direction {
+ Invalid = -1,
+ Inbound,
+ Outbound
+ };
+
+ /* Create a Channel instance of the specified type
+ *
+ * Returns null if 'type' is unrecognized.
+ */
+ static Channel *create(const QString &type, Direction direction, Connection *connection);
+
+ QString type() const;
+ int identifier() const;
+ Direction direction() const;
+ Connection *connection();
+ bool isOpened() const;
+
+ /* Send the OpenChannel request for this channel
+ *
+ * Only valid when the channel hasn't been opened yet. If successful,
+ * identifier() will be set and this function returns true. The channel
+ * isn't open until the response arrives, signalled by the channelOpened
+ * or channelRejected signals.
+ *
+ * If the channel is rejected, it will asynchronously emit the channelRejected
+ * signal, and will be invalidated and deleted.
+ *
+ * If this function returns false, the request wasn't sent due to a local
+ * error. In this case, the channel is also invalidated and will be deleted.
+ */
+ bool openChannel();
+
+signals:
+ void channelOpened();
+ void channelRejected(Data::Control::ChannelResult::CommonError error);
+
+ /* Emitted when the channel has become invalid and will be destroyed
+ *
+ * This signal is emitted when a channel is closed, an outbound channel request is
+ * rejected, or the connection is lost. It indicates that the channel is no longer
+ * valid and will be deleted once control reaches the event loop (i.e.
+ * QObject::deleteLater).
+ *
+ * Any object using the channel must clear all references to it when this signal
+ * is emitted.
+ */
+ void invalidated();
+
+public slots:
+ void closeChannel();
+
+protected:
+ explicit Channel(const QString &type, Direction direction, Connection *connection);
+ explicit Channel(ChannelPrivate *d);
+ virtual ~Channel();
+
+ /* Determine the response to an inbound OpenChannel request
+ *
+ * Subclasses must implement this method to accept inbound OpenChannel requests.
+ * The subclass implements any type-specific rules, and may update the result
+ * object with error messages or other data.
+ *
+ * Basic sanity checking is performed before this method is called; it may
+ * assume that the channel is in a sane state to receive an inbound request.
+ *
+ * The channel will be opened if this method returns true.
+ */
+ virtual bool allowInboundChannelRequest(const Data::Control::OpenChannel *request, Data::Control::ChannelResult *result) = 0;
+
+ /* Determine whether to send an outbound OpenChannel request
+ *
+ * Subclasses may implement this method to approve outbound OpenChannel requests,
+ * and attach type-specific data to the request.
+ *
+ * Basic sanity checking is performed before this method is called; it may
+ * assume that the channel is in a sane state to send an outbound request.
+ *
+ * Return true to send the OpenChannel request, false to cancel.
+ */
+ virtual bool allowOutboundChannelRequest(Data::Control::OpenChannel *request) = 0;
+
+ /* Process data from the response to an outbound OpenChannel request
+ *
+ * Subclasses may implement this method to handle data attached to a
+ * ChannelResult message, received in response to an outbound OpenChannel
+ * request. This method is called for all valid responses, including failure.
+ *
+ * Basic sanity checking is performed before this method is called; it may
+ * assume that the channel was waiting for a response to an outbound request.
+ *
+ * Regardless of the result, the channel is not yet open when this method is
+ * called. The channel will be opened (or failed and invalidated) afterwards.
+ *
+ * Return true to continue handling the response, false to cancel and close the
+ * channel. The default implementation always returns true.
+ */
+ virtual bool processChannelOpenResult(const Data::Control::ChannelResult *result);
+
+ /* Process data from an inbound packet for this channel
+ *
+ * Subclasses must implement this method to handle inbound packets for this
+ * channel. 'packet' is raw data from the packet, and will not be empty.
+ *
+ * Generally, a channel will parse packets using the protobuf ParseFromArray
+ * method of their packet message type, and call appropriate handlers for
+ * the messages it contains.
+ */
+ virtual void receivePacket(const QByteArray &packet) = 0;
+
+ /* Send raw data as a packet on this channel
+ *
+ * Sends the contents of 'packet' as a packet for this channel. Often, you
+ * will not use this method directly, in favor of a method like sendMessage
+ * that handles data serialization as well. 'packet' must not be empty.
+ *
+ * If this method returns false, the packet was not sent due to an error
+ * with the state or contents of the packet. The caller is responsible for
+ * handling any response to that failure.
+ *
+ * If this method returns true, but the packet later fails to send due to
+ * a network issue, the channel will be closed.
+ */
+ bool sendPacket(const QByteArray &packet);
+
+ /* Serialize a protobuf message and send it as a packet on this channel
+ *
+ * This function behaves like sendPacket, except that it accepts a
+ * templated subclass of google::protobuf::Message, and serializes that
+ * message into the packet. In addition to the cases where sendPacket
+ * returns false, this function will return false if serialization fails.
+ */
+ template<typename T> bool sendMessage(const T &message);
+
+ /* Get approval for an inbound channel from the Connection's handlers
+ *
+ * Channels that require approval from higher-layer functionality before
+ * opening can use this method to emit the
+ * Connection::channelRequestingInboundApproval signal. For example, this
+ * can be used to look up whether an identifier for a channel is recognized,
+ * and allow higher layers to attach signals and update data before the
+ * channel is fully open.
+ *
+ * Approval should be signaled to the channel by means of some
+ * channel-specific API; in many cases, that might involve setting certain
+ * properties that the channel requires.
+ *
+ * This method may only be called from within the
+ * allowInboundChannelRequest handler.
+ */
+ void requestInboundApproval();
+
+ QScopedPointer<ChannelPrivate> d_ptr;
+};
+
+}
+
+#endif
diff --git a/src/protocol/Channel_p.h b/src/protocol/Channel_p.h
new file mode 100644
index 0000000..5178384
--- /dev/null
+++ b/src/protocol/Channel_p.h
@@ -0,0 +1,99 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef PROTOCOL_CHANNEL_P_H
+#define PROTOCOL_CHANNEL_P_H
+
+#include "Channel.h"
+#include "Connection_p.h"
+#include "utils/Useful.h"
+#include <QDebug>
+
+namespace Protocol
+{
+
+class ChannelPrivate : public QObject
+{
+ Q_OBJECT
+ Q_DISABLE_COPY(ChannelPrivate)
+ Q_DECLARE_PUBLIC(Channel)
+
+public:
+ explicit ChannelPrivate(Channel *q, const QString &type, Channel::Direction direction, Connection *conn);
+ virtual ~ChannelPrivate();
+
+ Channel *q_ptr;
+ Connection *connection;
+ QString type;
+ int identifier;
+ Channel::Direction direction;
+ bool isOpened;
+ bool hasSentClose;
+ bool isInvalidated;
+
+ void invalidate();
+
+ // Called by ControlChannel to act on valid channel request/result messages
+ bool openChannelInbound(const Data::Control::OpenChannel *request, Data::Control::ChannelResult *result);
+ bool openChannelOutbound(Data::Control::OpenChannel *request);
+ bool openChannelResult(const Data::Control::ChannelResult *result);
+};
+
+template<typename T> bool Channel::sendMessage(const T &message)
+{
+ int size = message.ByteSize();
+ if (size > ConnectionPrivate::PacketMaxDataSize) {
+ BUG() << "Message on" << type() << "channel is too big -" << size << "bytes:"
+ << QString::fromStdString(message.DebugString());
+ return false;
+ }
+
+ if (size < 1) {
+ BUG() << "Message on" << type() << "channel encoded as invalid length; this isn't possible to send:"
+ << QString::fromStdString(message.DebugString());
+ return false;
+ }
+
+ QByteArray packet(size, 0);
+ quint8 *end = message.SerializeWithCachedSizesToArray(reinterpret_cast<quint8*>(packet.data()));
+ quint8 *expected_end = reinterpret_cast<quint8*>(packet.data() + size);
+ if (end != expected_end) {
+ BUG() << "Unexpected packet size after message serialization. Expected" << size << "but got" << qptrdiff(end - expected_end);
+ return false;
+ }
+
+ return sendPacket(packet);
+}
+
+}
+
+#endif
diff --git a/src/protocol/ChatChannel.cpp b/src/protocol/ChatChannel.cpp
new file mode 100644
index 0000000..0e62b00
--- /dev/null
+++ b/src/protocol/ChatChannel.cpp
@@ -0,0 +1,196 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ChatChannel.h"
+#include "Channel_p.h"
+#include "Connection.h"
+#include "utils/SecureRNG.h"
+#include "utils/Useful.h"
+
+using namespace Protocol;
+
+ChatChannel::ChatChannel(Direction direction, Connection *connection)
+ : Channel(QStringLiteral("im.ricochet.chat"), direction, connection)
+{
+ // The peer might use recent message IDs between connections to handle
+ // re-send. Start at a random ID to reduce chance of collisions, then increment
+ lastMessageId = SecureRNG::randomInt(UINT32_MAX);
+}
+
+bool ChatChannel::allowInboundChannelRequest(const Data::Control::OpenChannel *request, Data::Control::ChannelResult *result)
+{
+ Q_UNUSED(request);
+
+ if (connection()->purpose() != Connection::Purpose::KnownContact) {
+ qDebug() << "Rejecting request for" << type() << "channel from connection with purpose" << int(connection()->purpose());
+ result->set_common_error(Data::Control::ChannelResult::UnauthorizedError);
+ return false;
+ }
+
+ if (connection()->findChannel<ChatChannel>(Channel::Inbound)) {
+ qDebug() << "Rejecting request for" << type() << "channel because one is already open";
+ return false;
+ }
+
+ return true;
+}
+
+bool ChatChannel::allowOutboundChannelRequest(Data::Control::OpenChannel *request)
+{
+ Q_UNUSED(request);
+
+ if (connection()->findChannel<ChatChannel>(Channel::Outbound)) {
+ BUG() << "Rejecting outbound request for" << type() << "channel because one is already open on this connection";
+ return false;
+ }
+
+ if (connection()->purpose() != Connection::Purpose::KnownContact) {
+ BUG() << "Rejecting outbound request for" << type() << "channel for connection with unexpected purpose" << int(connection()->purpose());
+ return false;
+ }
+
+ return true;
+}
+
+void ChatChannel::receivePacket(const QByteArray &packet)
+{
+ Data::Chat::Packet message;
+ if (!message.ParseFromArray(packet.constData(), packet.size())) {
+ closeChannel();
+ return;
+ }
+
+ if (message.has_chat_message()) {
+ handleChatMessage(message.chat_message());
+ } else if (message.has_chat_acknowledge()) {
+ handleChatAcknowledge(message.chat_acknowledge());
+ } else {
+ qWarning() << "Unrecognized message on" << type();
+ closeChannel();
+ }
+}
+
+bool ChatChannel::sendChatMessage(QString text, QDateTime time, MessageId &id)
+{
+ id = ++lastMessageId;
+ return sendChatMessageWithId(text, time, id);
+}
+
+bool ChatChannel::sendChatMessageWithId(QString text, QDateTime time, MessageId id)
+{
+ if (direction() != Outbound) {
+ BUG() << "Chat channels are unidirectional, and this is not an outbound channel";
+ return false;
+ }
+
+ QScopedPointer<Data::Chat::ChatMessage> message(new Data::Chat::ChatMessage);
+ message->set_message_id(id);
+
+ if (text.isEmpty()) {
+ BUG() << "Chat message is empty, and it should've been discarded";
+ return false;
+ } else if (text.size() > MessageMaxCharacters) {
+ BUG() << "Chat message is too long (" << text.size() << "characters), and it should've been limited already. Truncated.";
+ text.truncate(MessageMaxCharacters);
+ }
+
+ // Also converts to UTF-8
+ message->set_message_text(text.toStdString());
+
+ if (!time.isNull())
+ message->set_time_delta(qMin(QDateTime::currentDateTime().secsTo(time), qint64(0)));
+
+ Data::Chat::Packet packet;
+ packet.set_allocated_chat_message(message.take());
+ if (!Channel::sendMessage(packet))
+ return false;
+
+ pendingMessages.insert(id);
+ return true;
+}
+
+void ChatChannel::handleChatMessage(const Data::Chat::ChatMessage &message)
+{
+ QScopedPointer<Data::Chat::ChatAcknowledge> response(new Data::Chat::ChatAcknowledge);
+
+ // QString::fromStdString decodes the string as UTF-8, replacing all invalid sequences and
+ // codepoints with the unicode replacement character.
+ QString text = QString::fromStdString(message.message_text());
+
+ if (direction() != Inbound) {
+ qWarning() << "Rejected inbound message on an outbound chat channel";
+ response->set_accepted(false);
+ } else if (text.isEmpty()) {
+ qWarning() << "Rejected empty chat message";
+ response->set_accepted(false);
+ } else if (text.size() > MessageMaxCharacters) {
+ qWarning() << "Rejected oversize chat message of" << text.size() << "characters";
+ response->set_accepted(false);
+ } else {
+ QDateTime time = QDateTime::currentDateTime();
+ if (message.has_time_delta() && message.time_delta() <= 0)
+ time = time.addSecs(message.time_delta());
+
+ emit messageReceived(text, time, message.message_id());
+ response->set_accepted(true);
+ }
+
+ if (message.has_message_id()) {
+ response->set_message_id(message.message_id());
+ Data::Chat::Packet packet;
+ packet.set_allocated_chat_acknowledge(response.take());
+ Channel::sendMessage(packet);
+ }
+}
+
+void ChatChannel::handleChatAcknowledge(const Data::Chat::ChatAcknowledge &message)
+{
+ if (direction() != Outbound) {
+ qWarning() << "Rejected inbound acknowledgement on an inbound chat channel";
+ closeChannel();
+ return;
+ }
+
+ if (!message.has_message_id()) {
+ qDebug() << "Chat acknowledgement doesn't have a message ID we understand";
+ closeChannel();
+ return;
+ }
+
+ MessageId id = message.message_id();
+ if (pendingMessages.remove(id)) {
+ emit messageAcknowledged(id, message.accepted());
+ } else {
+ qDebug() << "Received chat acknowledgement for unknown message" << id;
+ }
+}
+
diff --git a/src/protocol/ChatChannel.h b/src/protocol/ChatChannel.h
new file mode 100644
index 0000000..d45cf66
--- /dev/null
+++ b/src/protocol/ChatChannel.h
@@ -0,0 +1,77 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef PROTOCOL_CHATCHANNEL_H
+#define PROTOCOL_CHATCHANNEL_H
+
+#include "Channel.h"
+#include "ChatChannel.pb.h"
+#include <QDateTime>
+#include <QSet>
+
+namespace Protocol
+{
+
+class ChatChannel : public Channel
+{
+ Q_OBJECT
+ Q_DISABLE_COPY(ChatChannel)
+
+public:
+ typedef quint32 MessageId;
+ static const int MessageMaxCharacters = 2000;
+
+ explicit ChatChannel(Direction direction, Connection *connection);
+
+ bool sendChatMessage(QString text, QDateTime time, MessageId &id);
+ bool sendChatMessageWithId(QString text, QDateTime time, MessageId id);
+
+signals:
+ void messageAcknowledged(MessageId id, bool accepted);
+ void messageReceived(const QString &text, const QDateTime &time, MessageId id);
+
+protected:
+ virtual bool allowInboundChannelRequest(const Data::Control::OpenChannel *request, Data::Control::ChannelResult *result);
+ virtual bool allowOutboundChannelRequest(Data::Control::OpenChannel *request);
+ virtual void receivePacket(const QByteArray &packet);
+
+private:
+ QSet<MessageId> pendingMessages;
+ MessageId lastMessageId;
+
+ void handleChatMessage(const Data::Chat::ChatMessage &message);
+ void handleChatAcknowledge(const Data::Chat::ChatAcknowledge &message);
+};
+
+}
+
+#endif
diff --git a/src/protocol/ChatChannel.proto b/src/protocol/ChatChannel.proto
new file mode 100644
index 0000000..30adac4
--- /dev/null
+++ b/src/protocol/ChatChannel.proto
@@ -0,0 +1,18 @@
+package Protocol.Data.Chat;
+
+message Packet {
+ optional ChatMessage chat_message = 1;
+ optional ChatAcknowledge chat_acknowledge = 2;
+}
+
+message ChatMessage {
+ required string message_text = 1;
+ optional uint32 message_id = 2; // Random ID for ack
+ optional int64 time_delta = 3; // Delta in seconds between now and when message was written
+}
+
+message ChatAcknowledge {
+ optional uint32 message_id = 1;
+ optional bool accepted = 2 [default = true];
+}
+
diff --git a/src/protocol/Connection.cpp b/src/protocol/Connection.cpp
new file mode 100644
index 0000000..4e6d986
--- /dev/null
+++ b/src/protocol/Connection.cpp
@@ -0,0 +1,661 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Connection_p.h"
+#include "ControlChannel.h"
+#include "utils/Useful.h"
+#include <QTcpSocket>
+#include <QTimer>
+#include <QtEndian>
+#include <QDebug>
+
+using namespace Protocol;
+
+Connection::Connection(QTcpSocket *socket, Direction direction, QObject *parent)
+ : QObject(parent)
+ , d(new ConnectionPrivate(this))
+{
+ d->setSocket(socket, direction);
+}
+
+ConnectionPrivate::ConnectionPrivate(Connection *qq)
+ : QObject(qq)
+ , q(qq)
+ , socket(0)
+ , direction(Connection::ClientSide)
+ , purpose(Connection::Purpose::Unknown)
+ , wasClosed(false)
+ , handshakeDone(false)
+ , nextOutboundChannelId(-1)
+{
+ ageTimer.start();
+
+ QTimer *timeout = new QTimer(this);
+ timeout->setSingleShot(true);
+ timeout->setInterval(UnknownPurposeTimeout * 1000);
+ connect(timeout, &QTimer::timeout, this,
+ [this,timeout]() {
+ if (purpose == Connection::Purpose::Unknown) {
+ qDebug() << "Closing connection" << q << "with unknown purpose after timeout";
+ q->close();
+ }
+ timeout->deleteLater();
+ }
+ );
+ timeout->start();
+}
+
+Connection::~Connection()
+{
+ // When we call closeImemdiately, the list of channels will be cleared.
+ // In the normal case, they will all use deleteLater to be freed at the
+ // next event loop. Since the connection is being destructed immediately,
+ // and we want to be certain that channels don't outlive it, copy the
+ // list before it's cleared and delete them immediately afterwards.
+ auto channels = d->channels;
+ d->closeImmediately();
+
+ // These would be deleted by QObject ownership as well, but we want to
+ // give them a chance to destruct before the connection d pointer is reset.
+ foreach (Channel *c, channels)
+ delete c;
+
+ // Reset d pointer, so we'll crash nicely if anything tries to call
+ // into Connection after this.
+ d = 0;
+}
+
+ConnectionPrivate::~ConnectionPrivate()
+{
+ // Reset q pointer, for the same reason as above
+ q = 0;
+}
+
+Connection::Direction Connection::direction() const
+{
+ return d->direction;
+}
+
+bool Connection::isConnected() const
+{
+ bool re = d->socket && d->socket->state() == QAbstractSocket::ConnectedState;
+ if (d->wasClosed) {
+ Q_ASSERT(!re);
+ }
+ return re;
+}
+
+QString Connection::serverHostname() const
+{
+ QString hostname;
+ if (direction() == ClientSide)
+ hostname = d->socket->peerName();
+ else if (direction() == ServerSide)
+ hostname = d->socket->property("localHostname").toString();
+
+ if (!hostname.endsWith(QStringLiteral(".onion"))) {
+ BUG() << "Connection does not have a valid server hostname:" << hostname;
+ return QString();
+ }
+
+ return hostname;
+}
+
+int Connection::age() const
+{
+ return qRound(d->ageTimer.elapsed() / 1000.0);
+}
+
+void ConnectionPrivate::setSocket(QTcpSocket *s, Connection::Direction d)
+{
+ if (socket) {
+ BUG() << "Connection already has a socket";
+ return;
+ }
+
+ socket = s;
+ direction = d;
+ connect(socket, &QAbstractSocket::disconnected, this, &ConnectionPrivate::socketDisconnected);
+ connect(socket, &QIODevice::readyRead, this, &ConnectionPrivate::socketReadable);
+
+ socket->setParent(q);
+
+ if (socket->state() != QAbstractSocket::ConnectedState) {
+ BUG() << "Connection created with socket in a non-connected state" << socket->state();
+ }
+
+ Channel *control = new ControlChannel(direction == Connection::ClientSide ? Channel::Outbound : Channel::Inbound, q);
+ // Closing the control channel must also close the connection
+ connect(control, &Channel::invalidated, q, &Connection::close);
+ insertChannel(control);
+
+ if (!control->isOpened() || control->identifier() != 0 || q->channel(0) != control) {
+ BUG() << "Control channel on new connection is not set up properly";
+ q->close();
+ return;
+ }
+
+ if (direction == Connection::ClientSide) {
+ // The server side is implicitly authenticated (by the transport) as the correct service, so grant that
+ QString serverName = q->serverHostname();
+ if (serverName.isEmpty()) {
+ BUG() << "Server side of connection doesn't have an authenticated name, aborting";
+ q->close();
+ return;
+ }
+
+ q->grantAuthentication(Connection::HiddenServiceAuth, serverName);
+
+ // Send the introduction version handshake message
+ char intro[] = { 0x49, 0x4D, 0x02, ProtocolVersion, 0 };
+ if (socket->write(intro, sizeof(intro)) < (int)sizeof(intro)) {
+ qDebug() << "Failed writing introduction message to socket";
+ q->close();
+ return;
+ }
+ }
+}
+
+void Connection::close()
+{
+ if (isConnected()) {
+ Q_ASSERT(!d->wasClosed);
+ qDebug() << "Disconnecting socket for connection" << this;
+ d->socket->disconnectFromHost();
+
+ // If not fully closed in 5 seconds, abort
+ QTimer *timeout = new QTimer(this);
+ timeout->setSingleShot(true);
+ connect(timeout, &QTimer::timeout, d, &ConnectionPrivate::closeImmediately);
+ timeout->start(5000);
+ }
+}
+
+void ConnectionPrivate::closeImmediately()
+{
+ if (socket)
+ socket->abort();
+
+ if (!wasClosed) {
+ BUG() << "Socket was forcefully closed but never emitted closed signal";
+ wasClosed = true;
+ emit q->closed();
+ }
+
+ if (!channels.isEmpty()) {
+ foreach (Channel *c, channels)
+ qDebug() << "Open channel:" << c << c->type() << c->connection();
+ BUG() << "Channels remain open after forcefully closing connection socket";
+ }
+}
+
+void ConnectionPrivate::socketDisconnected()
+{
+ qDebug() << "Connection" << this << "disconnected";
+ closeAllChannels();
+
+ if (!wasClosed) {
+ wasClosed = true;
+ emit q->closed();
+ }
+
+ // Ensure that we never leak Connection objects by scheduling deletion here.
+ // Everything should be using QPointer on a stored Connection for safety.
+ q->deleteLater();
+}
+
+void ConnectionPrivate::socketReadable()
+{
+ if (!handshakeDone) {
+ qint64 available = socket->bytesAvailable();
+
+ if (direction == Connection::ClientSide && available >= 1) {
+ // Expecting a single byte in response with the chosen version
+ uchar version = ProtocolVersionFailed;
+ if (socket->read(reinterpret_cast<char*>(&version), 1) < 1) {
+ qDebug() << "Connection socket error" << socket->error() << "during read:" << socket->errorString();
+ socket->abort();
+ return;
+ }
+
+ handshakeDone = true;
+ if (version == 0) {
+ qDebug() << "Server in outbound connection is using the version 1.0 protocol";
+ emit q->oldVersionNegotiated(socket);
+ q->close();
+ return;
+ } else if (version != ProtocolVersion) {
+ qDebug() << "Version negotiation failed on outbound connection";
+ emit q->versionNegotiationFailed();
+ socket->abort();
+ return;
+ } else
+ emit q->ready();
+ } else if (direction == Connection::ServerSide && available >= 3) {
+ // Expecting at least 3 bytes
+ uchar intro[3] = { 0 };
+ qint64 re = socket->peek(reinterpret_cast<char*>(intro), sizeof(intro));
+ if (re < (int)sizeof(intro)) {
+ qDebug() << "Connection socket error" << socket->error() << "during read:" << socket->errorString();
+ socket->abort();
+ return;
+ }
+
+ quint8 nVersions = intro[2];
+ if (intro[0] != 0x49 || intro[1] != 0x4D || nVersions == 0) {
+ qDebug() << "Invalid introduction sequence on inbound connection";
+ socket->abort();
+ return;
+ }
+
+ if (available < (qint64)sizeof(intro) + nVersions)
+ return;
+
+ // Discard intro header
+ re = socket->read(reinterpret_cast<char*>(intro), sizeof(intro));
+
+ QByteArray versions(nVersions, 0);
+ re = socket->read(versions.data(), versions.size());
+ if (re != versions.size()) {
+ qDebug() << "Connection socket error" << socket->error() << "during read:" << socket->errorString();
+ socket->abort();
+ return;
+ }
+
+ quint8 selectedVersion = ProtocolVersionFailed;
+ foreach (quint8 v, versions) {
+ if (v == ProtocolVersion) {
+ selectedVersion = v;
+ break;
+ }
+ }
+
+ re = socket->write(reinterpret_cast<char*>(&selectedVersion), 1);
+ if (re != 1) {
+ qDebug() << "Connection socket error" << socket->error() << "during write:" << socket->errorString();
+ socket->abort();
+ return;
+ }
+
+ handshakeDone = true;
+ if (selectedVersion != ProtocolVersion) {
+ qDebug() << "Version negotiation failed on inbound connection";
+ emit q->versionNegotiationFailed();
+ // Close gracefully to allow the response to write
+ q->close();
+ return;
+ } else
+ emit q->ready();
+ } else {
+ return;
+ }
+ }
+
+ qint64 available;
+ while ((available = socket->bytesAvailable()) >= PacketHeaderSize) {
+ uchar header[PacketHeaderSize];
+ // Peek at the header first, to read the size of the packet and make sure
+ // the entire thing is available within the buffer.
+ qint64 re = socket->peek(reinterpret_cast<char*>(header), PacketHeaderSize);
+ if (re < 0) {
+ qDebug() << "Connection socket error" << socket->error() << "during read:" << socket->errorString();
+ socket->abort();
+ return;
+ } else if (re < PacketHeaderSize) {
+ BUG() << "Socket had" << available << "bytes available but peek only returned" << re;
+ return;
+ }
+
+ Q_STATIC_ASSERT(PacketHeaderSize == 4);
+ quint16 packetSize = qFromBigEndian<quint16>(header);
+ quint16 channelId = qFromBigEndian<quint16>(&header[2]);
+
+ if (packetSize < PacketHeaderSize) {
+ qWarning() << "Corrupted data from connection (packet size is too small); disconnecting";
+ socket->abort();
+ return;
+ }
+
+ if (packetSize > available)
+ break;
+
+ // Read header out of the buffer and discard
+ re = socket->read(reinterpret_cast<char*>(header), PacketHeaderSize);
+ if (re != PacketHeaderSize) {
+ if (re < 0) {
+ qDebug() << "Connection socket error" << socket->error() << "during read:" << socket->errorString();
+ } else {
+ // Because of QTcpSocket buffering, we can expect that up to 'available' bytes
+ // will read. Treat anything less as an error condition.
+ BUG() << "Socket read was unexpectedly small;" << available << "bytes should've been available but we read" << re;
+ }
+ socket->abort();
+ return;
+ }
+
+ // Read data
+ QByteArray data(packetSize - PacketHeaderSize, 0);
+ re = (data.size() == 0) ? 0 : socket->read(data.data(), data.size());
+ if (re != data.size()) {
+ if (re < 0) {
+ qDebug() << "Connection socket error" << socket->error() << "during read:" << socket->errorString();
+ } else {
+ // As above
+ BUG() << "Socket read was unexpectedly small;" << available << "bytes should've been available but we read" << re;
+ }
+ socket->abort();
+ return;
+ }
+
+ Channel *channel = q->channel(channelId);
+ if (!channel) {
+ // XXX We should sanity-check and rate limit these responses better
+ if (data.isEmpty()) {
+ qDebug() << "Ignoring channel close message for non-existent channel" << channelId;
+ } else {
+ qDebug() << "Ignoring" << data.size() << "byte packet for non-existent channel" << channelId;
+ // Send channel close message
+ writePacket(channelId, QByteArray());
+ }
+ continue;
+ }
+
+ if (channel->connection() != q) {
+ // If this fails, something is extremely broken. It may be dangerous to continue
+ // processing any data at all. Crash gracefully.
+ BUG() << "Channel" << channelId << "found on connection" << this << "but its connection is"
+ << channel->connection();
+ qFatal("Connection mismatch while handling packet");
+ return;
+ }
+
+ if (data.isEmpty()) {
+ channel->closeChannel();
+ } else {
+ channel->receivePacket(data);
+ }
+ }
+}
+
+bool ConnectionPrivate::writePacket(Channel *channel, const QByteArray &data)
+{
+ if (channel->connection() != q) {
+ // As above, dangerously broken, crash the process to avoid damage
+ BUG() << "Writing packet for channel" << channel->identifier() << "on connection" << this
+ << "but its connection is" << channel->connection();
+ qFatal("Connection mismatch while writing packet");
+ return false;
+ }
+
+ return writePacket(channel->identifier(), data);
+}
+
+bool ConnectionPrivate::writePacket(int channelId, const QByteArray &data)
+{
+ if (channelId < 0 || channelId > UINT16_MAX) {
+ BUG() << "Cannot write packet for channel with invalid identifier" << channelId;
+ return false;
+ }
+
+ if (data.size() > PacketMaxDataSize) {
+ BUG() << "Cannot write oversized packet of" << data.size() << "bytes to channel" << channelId;
+ return false;
+ }
+
+ if (!q->isConnected()) {
+ qDebug() << "Cannot write packet to closed connection";
+ return false;
+ }
+
+ Q_STATIC_ASSERT(PacketHeaderSize + PacketMaxDataSize <= UINT16_MAX);
+ Q_STATIC_ASSERT(PacketHeaderSize == 4);
+ uchar header[PacketHeaderSize] = { 0 };
+ qToBigEndian(static_cast<quint16>(PacketHeaderSize + data.size()), header);
+ qToBigEndian(static_cast<quint16>(channelId), &header[2]);
+
+ qint64 re = socket->write(reinterpret_cast<char*>(header), PacketHeaderSize);
+ if (re != PacketHeaderSize) {
+ qDebug() << "Connection socket error" << socket->error() << "during write:" << socket->errorString();
+ socket->abort();
+ return false;
+ }
+
+ re = socket->write(data);
+ if (re != data.size()) {
+ qDebug() << "Connection socket error" << socket->error() << "during write:" << socket->errorString();
+ socket->abort();
+ return false;
+ }
+
+ return true;
+}
+
+int ConnectionPrivate::availableOutboundChannelId()
+{
+ // Server opens even-nubmered channels, client opens odd-numbered
+ bool evenNumbered = (direction == Connection::ServerSide);
+ const int minId = evenNumbered ? 2 : 1;
+ const int maxId = evenNumbered ? (UINT16_MAX-1) : UINT16_MAX;
+
+ if (nextOutboundChannelId < minId || nextOutboundChannelId > maxId)
+ nextOutboundChannelId = minId;
+
+ // Find an unused id, trying a maximum of 100 times, using a random step to avoid collision
+ for (int i = 0; i < 100 && channels.contains(nextOutboundChannelId); i++) {
+ nextOutboundChannelId += 1 + (qrand() % 200);
+ if (evenNumbered)
+ nextOutboundChannelId += nextOutboundChannelId % 2;
+ if (nextOutboundChannelId > maxId)
+ nextOutboundChannelId = minId;
+ }
+
+ if (channels.contains(nextOutboundChannelId)) {
+ // Abort the connection if we still couldn't find an id, because it's probably a nasty bug
+ BUG() << "Can't find an available outbound channel ID for connection; aborting connection";
+ socket->abort();
+ return -1;
+ }
+
+ if (nextOutboundChannelId < minId || nextOutboundChannelId > maxId) {
+ BUG() << "Selected a channel id that isn't within range";
+ return -1;
+ }
+
+ if (evenNumbered == bool(nextOutboundChannelId % 2)) {
+ BUG() << "Selected a channel id that isn't valid for this side of the connection";
+ return -1;
+ }
+
+ int re = nextOutboundChannelId;
+ nextOutboundChannelId += 2;
+ return re;
+}
+
+bool ConnectionPrivate::isValidAvailableChannelId(int id, Connection::Direction side)
+{
+ if (id < 1 || id > UINT16_MAX)
+ return false;
+
+ bool evenNumbered = bool(id % 2);
+ if (evenNumbered == (side == Connection::ServerSide))
+ return false;
+
+ if (channels.contains(id))
+ return false;
+
+ return true;
+}
+
+bool ConnectionPrivate::insertChannel(Channel *channel)
+{
+ if (channel->connection() != q) {
+ BUG() << "Connection tried to insert a channel assigned to a different connection";
+ return false;
+ }
+
+ if (channel->identifier() < 0) {
+ BUG() << "Connection tried to insert a channel without a valid identifier";
+ return false;
+ }
+
+ if (channels.contains(channel->identifier())) {
+ BUG() << "Connection tried to insert a channel with a duplicate id" << channel->identifier()
+ << "- we have" << channels.value(channel->identifier()) << "and inserted" << channel;
+ return false;
+ }
+
+ if (channel->parent() != q) {
+ BUG() << "Connection inserted a channel without expected parent object. Fixing.";
+ channel->setParent(q);
+ }
+
+ channels.insert(channel->identifier(), channel);
+ return true;
+}
+
+void ConnectionPrivate::removeChannel(Channel *channel)
+{
+ if (channel->connection() != q) {
+ BUG() << "Connection tried to remove a channel assigned to a different connection";
+ return;
+ }
+
+ // Out of caution, find the channel by pointer instead of identifier. This will make sure
+ // it's always removed from the list, even if the identifier was somehow reset or lost.
+ for (auto it = channels.begin(); it != channels.end(); ) {
+ if (*it == channel)
+ it = channels.erase(it);
+ else
+ it++;
+ }
+}
+
+void ConnectionPrivate::closeAllChannels()
+{
+ // Takes a copy, won't be broken by removeChannel calls
+ foreach (Channel *channel, channels)
+ channel->closeChannel();
+
+ if (!channels.isEmpty())
+ BUG() << "Channels remain open on connection after calling closeAllChannels";
+}
+
+QHash<int,Channel*> Connection::channels()
+{
+ return d->channels;
+}
+
+Channel *Connection::channel(int identifier)
+{
+ return d->channels.value(identifier);
+}
+
+Connection::Purpose Connection::purpose() const
+{
+ return d->purpose;
+}
+
+bool Connection::setPurpose(Purpose value)
+{
+ if (d->purpose == value)
+ return true;
+
+ switch (value) {
+ case Purpose::Unknown:
+ BUG() << "A connection can't reset to unknown purpose";
+ return false;
+ case Purpose::KnownContact:
+ if (!hasAuthenticated(HiddenServiceAuth)) {
+ BUG() << "Connection purpose cannot be KnownContact without authenticating a service";
+ return false;
+ }
+ break;
+ case Purpose::OutboundRequest:
+ if (d->direction != ClientSide) {
+ BUG() << "Connection purpose cannot be OutboundRequest on an inbound connection";
+ return false;
+ } else if (d->purpose != Purpose::Unknown) {
+ BUG() << "Connection purpose cannot change from" << int(d->purpose) << "to OutboundRequest";
+ return false;
+ }
+ break;
+ case Purpose::InboundRequest:
+ if (d->direction != ServerSide) {
+ BUG() << "Connection purpose cannot be InboundRequest on an outbound connection";
+ return false;
+ } else if (d->purpose != Purpose::Unknown) {
+ BUG() << "Connection purpose cannot change from" << int(d->purpose) << "to InboundRequest";
+ return false;
+ }
+ break;
+ default:
+ BUG() << "Purpose type" << int(value) << "is not defined";
+ return false;
+ }
+
+ Purpose old = d->purpose;
+ d->purpose = value;
+ emit purposeChanged(d->purpose, old);
+ return true;
+}
+
+bool Connection::hasAuthenticated(AuthenticationType type) const
+{
+ return d->authentication.contains(type);
+}
+
+bool Connection::hasAuthenticatedAs(AuthenticationType type, const QString &identity) const
+{
+ auto it = d->authentication.find(type);
+ if (!identity.isEmpty() && it != d->authentication.end())
+ return *it == identity;
+ return false;
+}
+
+QString Connection::authenticatedIdentity(AuthenticationType type) const
+{
+ return d->authentication.value(type);
+}
+
+void Connection::grantAuthentication(AuthenticationType type, const QString &identity)
+{
+ if (hasAuthenticated(type)) {
+ BUG() << "Tried to redundantly grant" << type << "authentication to connection";
+ return;
+ }
+
+ qDebug() << "Granting" << type << "authentication as" << identity << "to connection";
+
+ d->authentication.insert(type, identity);
+ emit authenticated(type, identity);
+}
+
diff --git a/src/protocol/Connection.h b/src/protocol/Connection.h
new file mode 100644
index 0000000..b817349
--- /dev/null
+++ b/src/protocol/Connection.h
@@ -0,0 +1,243 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef PROTOCOL_CONNECTION_H
+#define PROTOCOL_CONNECTION_H
+
+#include <QObject>
+#include <QHash>
+#include "Channel.h"
+
+class QTcpSocket;
+
+namespace Protocol
+{
+
+class ConnectionPrivate;
+
+/* Represents a protocol connection associated with a socket
+ *
+ * Connection is created to handle protocol communication over a socket. It
+ * handles reading, writing, creating channels, and all protocol behavior. A
+ * connection instance is created for a specific socket and cannot be changed.
+ * When the socket is closed, the Connection closes all channels and cannot be
+ * used again.
+ *
+ * All protocol behavior takes place by creating and using channels, represented
+ * by subclasses of Channel. The channelCreated and channelOpened signals can be
+ * used to attach to new channels. A new channel can be created by instantiating
+ * it and calling its openChannel method.
+ *
+ * The socket and all channels are owned by the Connection instance. In
+ * particular, channel instances will be deleted automatically after being
+ * closed. Avoid storing pointers to channels, or use a safe pointer to do so.
+ *
+ * The channel's functionality is controlled by authentication grants and by its
+ * assigned purpose. The purpose declares the current use of the channel (e.g.
+ * for a known contact or an incoming contact request). Higher level classes
+ * assign and change the connection's purpose. Connections with an Unknown
+ * purpose are closed automatically after a short timeout.
+ */
+class Connection : public QObject
+{
+ Q_OBJECT
+ Q_DISABLE_COPY(Connection)
+
+ friend class Channel;
+ friend class ChannelPrivate;
+ friend class ControlChannel;
+
+public:
+ /* Direction of the underlying socket connection
+ *
+ * The protocol is peer-to-peer and doesn't differentiate between
+ * server and client except for small behavior details.
+ */
+ enum Direction {
+ ClientSide,
+ ServerSide
+ };
+
+ /* Construct a connection handler for a socket
+ *
+ * This connection will take ownership of the socket, and
+ * becomes invalid (but is not automatically deleted) once
+ * the socket has disconnected.
+ */
+ explicit Connection(QTcpSocket *socket, Direction direction, QObject *parent);
+ virtual ~Connection();
+
+ Direction direction() const;
+ bool isConnected() const;
+
+ /* Hostname of the server side of the connection
+ *
+ * For a ClientSide connection, this returns the hostname that
+ * the socket has connected to. For a ServerSide connection,
+ * the local hostname which accepted the socket is returned.
+ *
+ * In all cases, the returned hostname will end with ".onion"
+ */
+ QString serverHostname() const;
+
+ /* Age of the connection in seconds */
+ int age() const;
+
+ /* Assigned purpose of this connection
+ *
+ * A purpose is assigned to the connection after the peer has
+ * authenticated or otherwise indicated what the connection will
+ * be used for.
+ *
+ * Purposes may be used to limit the features available on a
+ * connection, change behavior, and impose restrictions.
+ *
+ * Connections with an unknown purpose are killed after a timeout.
+ */
+ enum class Purpose {
+ Unknown,
+ KnownContact,
+ OutboundRequest,
+ InboundRequest
+ };
+
+ Purpose purpose() const;
+ bool setPurpose(Purpose purpose);
+
+ QHash<int,Channel*> channels();
+ Channel *channel(int identifier);
+ template<typename T> T *findChannel(Channel::Direction direction = Channel::Invalid);
+ template<typename T> QList<T*> findChannels(Channel::Direction direction = Channel::Invalid);
+
+ enum AuthenticationType {
+ HiddenServiceAuth,
+ KnownToPeer // For outbound connections, set when the peer indicates we are a known contact
+ };
+
+ bool hasAuthenticated(AuthenticationType type) const;
+ bool hasAuthenticatedAs(AuthenticationType type, const QString &identity) const;
+ QString authenticatedIdentity(AuthenticationType type) const;
+ void grantAuthentication(AuthenticationType type, const QString &identity = QString());
+
+public slots:
+ /* Close this connection and the underlying socket
+ *
+ * All pending data to write will be sent, and the socket will be
+ * asynchronously closed. If data hasn't been written after 5 seconds, the
+ * socket will timeout and close anyway.
+ *
+ * isConnected will return false immediately after this function is called.
+ * The closed signal is emitted when the socket and all channels have closed.
+ */
+ void close();
+
+signals:
+ /* Emitted when the socket is closed. All channels will be closed
+ * automatically. It is not possible to re-use the same Connection instance,
+ * or to reconnect the socket.
+ */
+ void closed();
+ /* Emitted once, after version negotiation has finished and the connection
+ * is ready to use. If negotiation fails, the versionNegotiationFailed
+ * signal is emitted instead, and the socket is closed.
+ */
+ void ready();
+ /* Emitted once when version negotiation has failed; meaning, there is no
+ * protocol version that both peers will accept. The socket will be closed.
+ */
+ void versionNegotiationFailed();
+ /* Hack to allow delivering an upgrade message to old clients
+ * XXX: Remove this once enough time has passed for most clients to be upgraded.
+ */
+ void oldVersionNegotiated(QTcpSocket *socket);
+
+ void authenticated(AuthenticationType type, const QString &identity);
+ void purposeChanged(Purpose after, Purpose before);
+ /* Emitted when a new Channel instance is created, before it has opened
+ *
+ * This signal can be used to attach to signals on a channel before it's
+ * opened. This signal is emitted for both inbound and outbound channels,
+ * before the request is approved. If a request is rejected or fails, the
+ * channel may be deleted shortly afterwards, without emitting its
+ * channelClosed signal.
+ */
+ void channelCreated(Channel *channel);
+ /* Emitted when an inbound channel needs approval to open
+ *
+ * This signal is emitted for channel types that require approval by
+ * higher-layer functionality before opening, based on the information
+ * in the OpenChannel message. Handlers should use channel-specific methods
+ * to approve the inbound channel.
+ *
+ * This signal is only emitted for channels that specifically invoke the
+ * Channel::requestInboundApproval() method.
+ */
+ void channelRequestingInboundApproval(Channel *channel);
+ /* Emitted when a channel is opened
+ *
+ * This signal is emitted after an inbound or outbound channel has been
+ * opened. At this point, the channel can be used or closed normally.
+ */
+ void channelOpened(Channel *channel);
+
+private:
+ ConnectionPrivate *d;
+};
+
+template<typename T> T *Connection::findChannel(Channel::Direction direction)
+{
+ T *re = 0;
+ foreach (Channel *c, channels()) {
+ if (direction != Channel::Invalid && c->direction() != direction)
+ continue;
+ if ((re = qobject_cast<T*>(c)))
+ return re;
+ }
+ return 0;
+}
+
+template<typename T> QList<T*> Connection::findChannels(Channel::Direction direction)
+{
+ QList<T*> re;
+ T *tmp = 0;
+ foreach (Channel *c, channels()) {
+ if (direction != Channel::Invalid && c->direction() != direction)
+ continue;
+ if ((tmp = qobject_cast<T*>(c)))
+ re.append(tmp);
+ }
+ return re;
+}
+
+}
+
+#endif
diff --git a/src/protocol/Connection_p.h b/src/protocol/Connection_p.h
new file mode 100644
index 0000000..cdaad9c
--- /dev/null
+++ b/src/protocol/Connection_p.h
@@ -0,0 +1,96 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef PROTOCOL_CONNECTION_P_H
+#define PROTOCOL_CONNECTION_P_H
+
+#include "Connection.h"
+#include <QMap>
+#include <QElapsedTimer>
+#include <cstdint>
+
+namespace Protocol
+{
+
+class ConnectionPrivate : public QObject
+{
+ Q_OBJECT
+ Q_DISABLE_COPY(ConnectionPrivate)
+
+public:
+ static const quint8 ProtocolVersion = 1;
+ static const quint8 ProtocolVersionFailed = 0xff;
+ static const int PacketHeaderSize = 4;
+ static const int PacketMaxDataSize = UINT16_MAX - PacketHeaderSize;
+ // Time in seconds before a connection with a purpose of Unknown is killed
+ static const int UnknownPurposeTimeout = 15;
+
+ explicit ConnectionPrivate(Connection *q);
+ virtual ~ConnectionPrivate();
+
+ Connection *q;
+ QTcpSocket *socket;
+ QHash<int,Channel*> channels;
+ QMap<Connection::AuthenticationType,QString> authentication;
+ QElapsedTimer ageTimer;
+ Connection::Direction direction;
+ Connection::Purpose purpose;
+ bool wasClosed;
+ bool handshakeDone;
+
+ void setSocket(QTcpSocket *socket, Connection::Direction direction);
+
+ int availableOutboundChannelId();
+ bool isValidAvailableChannelId(int channelId, Connection::Direction idDirection);
+
+ bool insertChannel(Channel *channel);
+ void removeChannel(Channel *channel);
+
+ void closeAllChannels();
+
+ bool writePacket(Channel *channel, const QByteArray &data);
+ bool writePacket(int channelId, const QByteArray &data);
+
+public slots:
+ void closeImmediately();
+
+private slots:
+ void socketReadable();
+ void socketDisconnected();
+
+private:
+ int nextOutboundChannelId;
+};
+
+}
+
+#endif
diff --git a/src/protocol/ContactRequestChannel.cpp b/src/protocol/ContactRequestChannel.cpp
new file mode 100644
index 0000000..fbb37d2
--- /dev/null
+++ b/src/protocol/ContactRequestChannel.cpp
@@ -0,0 +1,292 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ContactRequestChannel.h"
+#include "Channel_p.h"
+
+using namespace Protocol;
+
+/* Regarding message and nickname limitations:
+ *
+ * For messages, we should use limits the same as those of chat, including limits on the
+ * length and content.
+ *
+ * For nicknames, we should be quite restrictive, and these should be applied consistently
+ * with protocol and UI. It should be similar to the restrictions on filenames, particularly
+ * by excluding control characters. Length limit is short.
+ *
+ * If the nickname duplicates an existing contact, it must be changed for the request, but
+ * the peer must not be aware of this in any way.
+ */
+
+ContactRequestChannel::ContactRequestChannel(Direction direction, Connection *connection)
+ : Channel(QStringLiteral("im.ricochet.contact.request"), direction, connection)
+ , m_responseStatus(Data::ContactRequest::Response::Undefined)
+{
+}
+
+QString ContactRequestChannel::message() const
+{
+ return m_message;
+}
+
+void ContactRequestChannel::setMessage(const QString &message)
+{
+ if (direction() != Outbound) {
+ BUG() << "Request messages can only be set on outbound messages";
+ return;
+ }
+
+ // Only valid before channel opened
+ if (isOpened() || identifier() >= 0) {
+ BUG() << "Request data must be set before opening channel";
+ return;
+ }
+
+ if (message.size() > Data::ContactRequest::MessageMaxCharacters) {
+ BUG() << "Outbound contact request message is too long (" << message.size() << ")";
+ return;
+ }
+
+ m_message = message;
+}
+
+static bool isAcceptableNickname(const QString &input)
+{
+ if (input.size() > Data::ContactRequest::NicknameMaxCharacters)
+ return false;
+
+ QVector<uint> chars = input.toUcs4();
+ foreach (uint value, chars) {
+ QChar c(value);
+ if (c.category() == QChar::Other_Format ||
+ c.category() == QChar::Other_Control ||
+ c.isNonCharacter())
+ return false;
+ }
+
+ return true;
+}
+
+QString ContactRequestChannel::nickname() const
+{
+ return m_nickname;
+}
+
+void ContactRequestChannel::setNickname(const QString &nickname)
+{
+ if (direction() != Outbound) {
+ BUG() << "Request messages can only be set on outbound messages";
+ return;
+ }
+
+ if (isOpened() || identifier() >= 0) {
+ BUG() << "Request data must be set before opening channel";
+ return;
+ }
+
+ if (!isAcceptableNickname(nickname)) {
+ BUG() << "Outbound contact request nickname isn't acceptable:" << nickname;
+ return;
+ }
+
+ m_nickname = nickname;
+}
+
+bool ContactRequestChannel::allowInboundChannelRequest(const Data::Control::OpenChannel *request, Data::Control::ChannelResult *result)
+{
+ using namespace Data::ContactRequest;
+ using namespace Data::Control;
+
+ // If this connection is already KnownContact, report that the request is accepted
+ if (connection()->purpose() == Connection::Purpose::KnownContact) {
+ QScopedPointer<Response> response(new Response);
+ response->set_status(Response::Accepted);
+ result->SetAllocatedExtension(Data::ContactRequest::response, response.take());
+ return false;
+ }
+
+ // We'll only accept requests on inbound connections with an unknown purpose
+ if (connection()->direction() != Connection::ServerSide ||
+ connection()->purpose() != Connection::Purpose::Unknown)
+ {
+ result->set_common_error(ChannelResult::BadUsageError);
+ return false;
+ }
+
+ // Only allow one ContactRequestChannel
+ if (connection()->findChannel<ContactRequestChannel>()) {
+ result->set_common_error(ChannelResult::BadUsageError);
+ return false;
+ }
+
+ // Require HiddenServiceAuth
+ if (!connection()->hasAuthenticated(Connection::HiddenServiceAuth)) {
+ result->set_common_error(ChannelResult::UnauthorizedError);
+ return false;
+ }
+
+ if (!request->HasExtension(Data::ContactRequest::contact_request)) {
+ result->set_common_error(ChannelResult::BadUsageError);
+ return false;
+ }
+
+ ContactRequest contactData = request->GetExtension(Data::ContactRequest::contact_request);
+ QString nickname = QString::fromStdString(contactData.nickname());
+ QString message = QString::fromStdString(contactData.message_text());
+
+ m_responseStatus = Response::Undefined;
+ if (message.size() > Data::ContactRequest::MessageMaxCharacters ||
+ !isAcceptableNickname(nickname))
+ {
+ qWarning() << "Rejecting incoming contact request with invalid nickname/message";
+ setResponseStatus(Response::Error);
+ } else {
+ m_nickname = nickname;
+ m_message = message;
+ emit requestReceived();
+
+ if (m_responseStatus == Response::Undefined) {
+ BUG() << "No response to incoming contact request after requestReceived signal";
+ setResponseStatus(Response::Error);
+ }
+ }
+
+ QScopedPointer<Response> response(new Response);
+ response->set_status(m_responseStatus);
+ result->SetAllocatedExtension(Data::ContactRequest::response, response.take());
+
+ // If the response is final, close the channel immediately once it's fully open
+ if (m_responseStatus > Response::Pending)
+ connect(this, &Channel::channelOpened, this, &Channel::closeChannel, Qt::QueuedConnection);
+ return true;
+}
+
+void ContactRequestChannel::setResponseStatus(Status status)
+{
+ if (m_responseStatus == status)
+ return;
+
+ if (direction() != Inbound) {
+ BUG() << "Can't set the response on an outbound contact request";
+ return;
+ }
+
+ using namespace Data::ContactRequest;
+ if (m_responseStatus > Response::Pending)
+ BUG() << "Response status is already a final state" << m_responseStatus << "but was changed to" << status;
+
+ m_responseStatus = status;
+
+ // If the channel is already open, the response is sent as a separate packet
+ if (isOpened()) {
+ Response response;
+ response.set_status(m_responseStatus);
+ sendMessage(response);
+
+ if (m_responseStatus > Response::Pending)
+ closeChannel();
+ }
+}
+
+bool ContactRequestChannel::allowOutboundChannelRequest(Data::Control::OpenChannel *request)
+{
+ if (connection()->direction() != Connection::ClientSide ||
+ connection()->purpose() != Connection::Purpose::OutboundRequest)
+ {
+ BUG() << "ContactRequestChannel can only be used on OutboundRequest connections. Has purpose"
+ << int(connection()->purpose());
+ return false;
+ }
+
+ if (connection()->findChannel<ContactRequestChannel>()) {
+ BUG() << "ContactRequestChannel can only be used once per connection";
+ return false;
+ }
+
+ QScopedPointer<Data::ContactRequest::ContactRequest> contactData(new Data::ContactRequest::ContactRequest);
+ if (!m_nickname.isEmpty())
+ contactData->set_nickname(m_nickname.toStdString());
+ if (!m_message.isEmpty())
+ contactData->set_message_text(m_message.toStdString());
+
+ request->SetAllocatedExtension(Data::ContactRequest::contact_request, contactData.take());
+ return true;
+}
+
+bool ContactRequestChannel::processChannelOpenResult(const Data::Control::ChannelResult *result)
+{
+ if (!result->HasExtension(Data::ContactRequest::response)) {
+ qDebug() << "Expected a response for the contact request";
+ return false;
+ }
+
+ Data::ContactRequest::Response response = result->GetExtension(Data::ContactRequest::response);
+ return handleResponse(&response);
+}
+
+void ContactRequestChannel::receivePacket(const QByteArray &packet)
+{
+ Data::ContactRequest::Response response;
+ if (!response.ParseFromArray(packet.constData(), packet.size())) {
+ qDebug() << "Invalid message received on contact request channel";
+ closeChannel();
+ return;
+ }
+
+ if (!handleResponse(&response))
+ closeChannel();
+}
+
+bool ContactRequestChannel::handleResponse(const Data::ContactRequest::Response *response)
+{
+ using namespace Data::ContactRequest;
+ if (response->status() == Response::Undefined) {
+ qDebug() << "Got an invalid response (undefined status) to a contact request";
+ return false;
+ }
+
+ if (m_responseStatus > Response::Pending) {
+ qDebug() << "Received a response" << response->status() << "to a contact request which already had a final response" << m_responseStatus;
+ return false;
+ }
+
+ m_responseStatus = response->status();
+ emit requestStatusChanged(m_responseStatus);
+ // If the response is final, close the channel. Use a queued invoke to avoid any potential
+ // issue when called from processChannelOpenResult
+ if (m_responseStatus > Response::Pending)
+ metaObject()->invokeMethod(this, "closeChannel", Qt::QueuedConnection);
+
+ return true;
+}
+
diff --git a/src/protocol/ContactRequestChannel.h b/src/protocol/ContactRequestChannel.h
new file mode 100644
index 0000000..5117138
--- /dev/null
+++ b/src/protocol/ContactRequestChannel.h
@@ -0,0 +1,86 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef PROTOCOL_CONTACTREQUESTCHANNEL_H
+#define PROTOCOL_CONTACTREQUESTCHANNEL_H
+
+#include "Channel.h"
+#include "ContactRequestChannel.pb.h"
+
+namespace Protocol
+{
+
+class ContactRequestChannel : public Channel
+{
+ Q_OBJECT
+ Q_DISABLE_COPY(ContactRequestChannel)
+
+public:
+ typedef Data::ContactRequest::Response::Status Status;
+
+ explicit ContactRequestChannel(Direction direction, Connection *connection);
+
+ QString message() const;
+ QString nickname() const;
+
+ // Outbound
+ void setMessage(const QString &message);
+ void setNickname(const QString &nickname);
+
+ // Inbound
+ void setResponseStatus(Status status);
+
+signals:
+ /* Emitted during the inbound channel request handler, when a new request
+ * arrives. A handler is expected to synchronously call setResponseStatus
+ * if it claims this request; otherwise, it will be closed.
+ */
+ void requestReceived();
+ void requestStatusChanged(Status status);
+
+protected:
+ virtual bool allowInboundChannelRequest(const Data::Control::OpenChannel *request, Data::Control::ChannelResult *result);
+ virtual bool allowOutboundChannelRequest(Data::Control::OpenChannel *request);
+ virtual bool processChannelOpenResult(const Data::Control::ChannelResult *result);
+ virtual void receivePacket(const QByteArray &packet);
+
+private:
+ QString m_nickname;
+ QString m_message;
+ Status m_responseStatus;
+
+ bool handleResponse(const Data::ContactRequest::Response *response);
+};
+
+}
+
+#endif
diff --git a/src/protocol/ContactRequestChannel.proto b/src/protocol/ContactRequestChannel.proto
new file mode 100644
index 0000000..75591d4
--- /dev/null
+++ b/src/protocol/ContactRequestChannel.proto
@@ -0,0 +1,35 @@
+package Protocol.Data.ContactRequest;
+import "ControlChannel.proto";
+
+enum Limits {
+ MessageMaxCharacters = 2000;
+ NicknameMaxCharacters = 30;
+}
+
+extend Control.OpenChannel {
+ optional ContactRequest contact_request = 200;
+}
+
+extend Control.ChannelResult {
+ optional Response response = 201;
+}
+
+// Sent only as an attachment to OpenChannel
+message ContactRequest {
+ optional string nickname = 1;
+ optional string message_text = 2;
+}
+
+// Response is the only valid message to send on the channel
+message Response {
+ enum Status {
+ Undefined = 0; // Not valid on the wire
+ Pending = 1;
+ Accepted = 2;
+ Rejected = 3;
+ Error = 4;
+ }
+
+ required Status status = 1;
+}
+
diff --git a/src/protocol/ControlChannel.cpp b/src/protocol/ControlChannel.cpp
new file mode 100644
index 0000000..877ffdf
--- /dev/null
+++ b/src/protocol/ControlChannel.cpp
@@ -0,0 +1,286 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ControlChannel.h"
+#include "Channel_p.h"
+#include "Connection_p.h"
+#include "utils/Useful.h"
+#include <QScopedPointer>
+#include <QDebug>
+
+using namespace Protocol;
+
+ControlChannel::ControlChannel(Direction direction, Connection *connection)
+ : Channel(QStringLiteral("control"), direction, connection)
+{
+ if (connection->channel(0))
+ BUG() << "Created ControlChannel for connection which already has a channel 0";
+
+ Q_D(Channel);
+ d->isOpened = true;
+ d->identifier = 0;
+}
+
+bool ControlChannel::sendOpenChannel(Channel *channel)
+{
+ if (channel->isOpened() || channel->direction() != Outbound || channel->identifier() >= 0) {
+ BUG() << "openChannel called for a" << channel->type() << "channel in an unexpected state";
+ return false;
+ }
+
+ if (channel->connection() != connection()) {
+ BUG() << "openChannel called for" << channel->type() << "channel on a different connection";
+ return false;
+ }
+
+ QScopedPointer<Data::Control::OpenChannel> request(new Data::Control::OpenChannel);
+ int channelId = connection()->d->availableOutboundChannelId();
+ if (channelId <= 0)
+ return false;
+ request->set_channel_identifier(channelId);
+
+ if (!channel->d_ptr->openChannelOutbound(request.data())) {
+ qDebug() << "Outbound OpenChannel request of type" << channel->type() << "refused locally";
+ return false;
+ }
+
+ if (!request->has_channel_type() || !request->has_channel_identifier() ||
+ request->channel_identifier() < 0 || request->channel_identifier() > UINT16_MAX)
+ {
+ BUG() << "Outbound OpenChannel request isn't valid:" << QString::fromStdString(request->DebugString());
+ return false;
+ }
+
+ if (request->channel_identifier() != channel->identifier()) {
+ BUG() << "Channel identifier doesn't match in OpenChannel request of type" << channel->type();
+ return false;
+ }
+
+ if (!connection()->d->insertChannel(channel)) {
+ BUG() << "Valid channel refused by connection";
+ return false;
+ }
+
+ Data::Control::Packet packet;
+ packet.set_allocated_open_channel(request.take());
+ return sendMessage(packet);
+}
+
+void ControlChannel::keepAlive()
+{
+ Data::Control::KeepAlive *request = new Data::Control::KeepAlive;
+ request->set_response_requested(true);
+
+ Data::Control::Packet packet;
+ packet.set_allocated_keep_alive(request);
+ sendMessage(packet);
+}
+
+bool ControlChannel::allowInboundChannelRequest(const Data::Control::OpenChannel *request, Data::Control::ChannelResult *result)
+{
+ Q_UNUSED(request);
+ Q_UNUSED(result);
+ BUG() << "ControlChannel should never receive channel requests";
+ return false;
+}
+
+bool ControlChannel::allowOutboundChannelRequest(Data::Control::OpenChannel *request)
+{
+ Q_UNUSED(request);
+ BUG() << "ControlChannel should never send channel requests";
+ return false;
+}
+
+bool ControlChannel::processChannelOpenResult(const Data::Control::ChannelResult *result)
+{
+ Q_UNUSED(result);
+ BUG() << "ControlChannel should never receive a channel request response";
+ return false;
+}
+
+void ControlChannel::receivePacket(const QByteArray &packet)
+{
+ Data::Control::Packet message;
+ if (!message.ParseFromArray(packet.constData(), packet.size())) {
+ qWarning() << "Control channel failed parsing packet; connection will be killed";
+ closeChannel();
+ return;
+ }
+
+ if (message.has_open_channel()) {
+ handleOpenChannel(message.open_channel());
+ } else if (message.has_channel_result()) {
+ handleChannelResult(message.channel_result());
+ } else if (message.has_keep_alive()) {
+ handleKeepAlive(message.keep_alive());
+ } else if (message.has_enable_features()) {
+ handleEnableFeatures(message.enable_features());
+ } else if (message.has_features_enabled()) {
+ handleFeaturesEnabled(message.features_enabled());
+ } else {
+ qWarning() << "Unrecognized message on control channel; connection will be killed";
+ closeChannel();
+ return;
+ }
+}
+
+void ControlChannel::handleOpenChannel(const Data::Control::OpenChannel &message)
+{
+ // Validate channel_identifier
+ int id = message.channel_identifier();
+ Connection::Direction peerSide = (connection()->direction() == Connection::ClientSide) ? Connection::ServerSide : Connection::ClientSide;
+ if (!connection()->d->isValidAvailableChannelId(id, peerSide)) {
+ qWarning() << "Received OpenChannel with invalid channel_identifier:" << QString::fromStdString(message.DebugString());
+ // Deliberately invalid behavior; kill the connection
+ closeChannel();
+ return;
+ }
+
+ Data::Control::ChannelResult *response = new Data::Control::ChannelResult;
+ response->set_channel_identifier(id);
+
+ Channel *channel = Channel::create(QString::fromStdString(message.channel_type()), Inbound, connection());
+ if (!channel) {
+ qDebug() << "Received OpenChannel for unknown channel type:" << QString::fromStdString(message.channel_type());
+ response->set_opened(false);
+ response->set_common_error(Data::Control::ChannelResult::UnknownTypeError);
+ } else {
+ if (!channel->d_ptr->openChannelInbound(&message, response)) {
+ if (response->opened())
+ BUG() << "openChannelInbound handler failed but response said successful. Assuming failure.";
+ response->set_opened(false);
+ }
+
+ if (!response->has_opened()) {
+ BUG() << "inboundOpenChannel handler for" << channel->type() << "did not update response message";
+ response->set_opened(false);
+ response->set_common_error(Data::Control::ChannelResult::GenericError);
+ }
+ }
+
+ if (response->opened()) {
+ if (!channel || !channel->isOpened() || channel->direction() != Inbound ||
+ channel->identifier() != id)
+ {
+ BUG() << "Channel" << channel->type() << "in unexpected state after inbound open";
+ response->set_opened(false);
+ // The channel may think it's open, so force it to close
+ channel->closeChannel();
+ } else if (!connection()->d->insertChannel(channel)) {
+ Q_ASSERT_X(false, "handleOpenChannel", "Valid channel refused by connection");
+ qWarning() << "BUG: Valid channel refused by connection";
+ response->set_opened(false);
+ channel->closeChannel();
+ }
+ }
+
+ if (!response->opened()) {
+ qDebug() << "Rejected OpenChannel request:" << QString::fromStdString(message.DebugString()) << "response:" << QString::fromStdString(response->DebugString());
+ // Clean up channel instance
+ delete channel;
+ channel = 0;
+ }
+
+ Data::Control::Packet responseMessage;
+ responseMessage.set_allocated_channel_result(response);
+ sendMessage(responseMessage);
+
+ if (response->opened())
+ emit connection()->channelOpened(channel);
+}
+
+void ControlChannel::handleChannelResult(const Data::Control::ChannelResult &message)
+{
+ int id = message.channel_identifier();
+ Channel *channel = connection()->channel(id);
+ if (!channel) {
+ qWarning() << "Received ChannelResult for unknown identifier, ignoring:" << QString::fromStdString(message.DebugString());
+ return;
+ }
+
+ if (channel->direction() != Outbound || channel->isOpened()) {
+ qWarning() << "Received (duplicate?) ChannelResult for existing channel in an unexpected state:" << QString::fromStdString(message.DebugString());
+ return;
+ }
+
+ bool opened = channel->d_ptr->openChannelResult(&message);
+
+ if (opened && !channel->isOpened()) {
+ BUG() << "Outbound channel isn't open after successful ChannelResult";
+ channel->closeChannel();
+ } else if (!opened && channel->isOpened()) {
+ BUG() << "Outbound channel is open after failed ChannelResult";
+ channel->closeChannel();
+ }
+
+ // Channel::outboundOpenResult will invalidate on failure, causing the
+ // instance to be deleted once it's safe to do so
+ if (!opened || !channel->isOpened()) {
+ if (connection()->channel(channel->identifier())) {
+ BUG() << "Channel not invalidated after failed outbound OpenChannel request";
+ channel->closeChannel();
+ }
+ } else {
+ emit connection()->channelOpened(channel);
+ }
+}
+
+void ControlChannel::handleKeepAlive(const Data::Control::KeepAlive &message)
+{
+ if (message.response_requested()) {
+ Data::Control::KeepAlive *pong = new Data::Control::KeepAlive;
+ pong->set_response_requested(false);
+ Data::Control::Packet response;
+ response.set_allocated_keep_alive(pong);
+ sendMessage(response);
+ } else {
+ emit keepAliveResponse();
+ }
+}
+
+void ControlChannel::handleEnableFeatures(const Data::Control::EnableFeatures &message)
+{
+ Q_UNUSED(message);
+ // This version does not support any features.
+ Data::Control::Packet responseMessage;
+ responseMessage.mutable_features_enabled();
+ sendMessage(responseMessage);
+}
+
+void ControlChannel::handleFeaturesEnabled(const Data::Control::FeaturesEnabled &message)
+{
+ Q_UNUSED(message);
+ // This version does not generate EnableFeatures messages, so receiving this is an error.
+ qDebug() << "Unexpectedly received FeaturesEnabled message from peer, but we never send EnableFeatures";
+ closeChannel();
+}
+
diff --git a/src/protocol/ControlChannel.h b/src/protocol/ControlChannel.h
new file mode 100644
index 0000000..5c5f355
--- /dev/null
+++ b/src/protocol/ControlChannel.h
@@ -0,0 +1,75 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef PROTOCOL_CONTROLCHANNEL_H
+#define PROTOCOL_CONTROLCHANNEL_H
+
+#include "Channel.h"
+#include "ControlChannel.pb.h"
+
+namespace Protocol
+{
+
+class ControlChannel : public Channel
+{
+ Q_OBJECT
+ Q_DISABLE_COPY(ControlChannel)
+
+ friend class ConnectionPrivate;
+
+public:
+ bool sendOpenChannel(Channel *channel);
+ void keepAlive();
+
+signals:
+ void keepAliveResponse();
+
+protected:
+ explicit ControlChannel(Direction direction, Connection *connection);
+
+ virtual bool allowInboundChannelRequest(const Data::Control::OpenChannel *request, Data::Control::ChannelResult *result);
+ virtual bool allowOutboundChannelRequest(Data::Control::OpenChannel *request);
+ virtual bool processChannelOpenResult(const Data::Control::ChannelResult *result);
+
+ virtual void receivePacket(const QByteArray &packet);
+
+private:
+ void handleOpenChannel(const Data::Control::OpenChannel &message);
+ void handleChannelResult(const Data::Control::ChannelResult &message);
+ void handleKeepAlive(const Data::Control::KeepAlive &message);
+ void handleEnableFeatures(const Data::Control::EnableFeatures &message);
+ void handleFeaturesEnabled(const Data::Control::FeaturesEnabled &message);
+};
+
+}
+
+#endif
diff --git a/src/protocol/ControlChannel.proto b/src/protocol/ControlChannel.proto
new file mode 100644
index 0000000..7d66516
--- /dev/null
+++ b/src/protocol/ControlChannel.proto
@@ -0,0 +1,52 @@
+package Protocol.Data.Control;
+
+message Packet {
+ // Must contain exactly one field
+ optional OpenChannel open_channel = 1;
+ optional ChannelResult channel_result = 2;
+ optional KeepAlive keep_alive = 3;
+ optional EnableFeatures enable_features = 4;
+ optional FeaturesEnabled features_enabled = 5;
+}
+
+message OpenChannel {
+ required int32 channel_identifier = 1; // Arbitrary unique identifier for this channel instance
+ required string channel_type = 2; // String identifying channel type; e.g. im.ricochet.chat
+
+ // It is valid to extend the OpenChannel message to add fields specific
+ // to the requested channel_type.
+ extensions 100 to max;
+}
+
+message ChannelResult {
+ required int32 channel_identifier = 1; // Matching the value from OpenChannel
+ required bool opened = 2; // If the channel is now open
+
+ enum CommonError {
+ GenericError = 0;
+ UnknownTypeError = 1;
+ UnauthorizedError = 2;
+ BadUsageError = 3;
+ FailedError = 4;
+ }
+
+ optional CommonError common_error = 3;
+
+ // As with OpenChannel, it is valid to extend this message with fields specific
+ // to the channel type.
+ extensions 100 to max;
+}
+
+message KeepAlive {
+ required bool response_requested = 1;
+}
+
+message EnableFeatures {
+ repeated string feature = 1;
+ extensions 100 to max;
+}
+
+message FeaturesEnabled {
+ repeated string feature = 1;
+ extensions 100 to max;
+}
diff --git a/src/protocol/OutboundConnector.cpp b/src/protocol/OutboundConnector.cpp
new file mode 100644
index 0000000..feca491
--- /dev/null
+++ b/src/protocol/OutboundConnector.cpp
@@ -0,0 +1,314 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "OutboundConnector.h"
+#include "utils/Useful.h"
+#include "tor/TorSocket.h"
+#include "ControlChannel.h"
+#include "AuthHiddenServiceChannel.h"
+
+using namespace Protocol;
+
+namespace Protocol
+{
+
+class OutboundConnectorPrivate : public QObject
+{
+ Q_OBJECT
+
+public:
+ OutboundConnector *q;
+ Tor::TorSocket *socket;
+ Connection *connection;
+ QString hostname;
+ quint16 port;
+ OutboundConnector::Status status;
+ CryptoKey authPrivateKey;
+ QString errorMessage;
+ QTimer errorRetryTimer;
+ int errorRetryCount;
+
+ OutboundConnectorPrivate(OutboundConnector *q)
+ : QObject(q)
+ , q(q)
+ , socket(0)
+ , connection(0)
+ , port(0)
+ , status(OutboundConnector::Inactive)
+ , errorRetryCount(0)
+ {
+ connect(&errorRetryTimer, &QTimer::timeout, this, &OutboundConnectorPrivate::retryAfterError);
+ }
+
+ void setStatus(OutboundConnector::Status status);
+ void setError(const QString &errorMessage);
+
+public slots:
+ void onConnected();
+ void startAuthentication();
+ void abort();
+ void retryAfterError();
+};
+
+}
+
+OutboundConnector::OutboundConnector(QObject *parent)
+ : QObject(parent), d(new OutboundConnectorPrivate(this))
+{
+}
+
+OutboundConnector::~OutboundConnector()
+{
+}
+
+void OutboundConnector::setAuthPrivateKey(const CryptoKey &key)
+{
+ if (!key.isLoaded() || !key.isPrivate()) {
+ BUG() << "Cannot make outbound connection without a valid private key";
+ return;
+ }
+
+ d->authPrivateKey = key;
+}
+
+bool OutboundConnector::connectToHost(const QString &hostname, quint16 port)
+{
+ if (port <= 0 || hostname.isEmpty()) {
+ d->errorMessage = QStringLiteral("Invalid hostname or port");
+ d->setStatus(Error);
+ return false;
+ }
+
+ if (d->status == Ready) {
+ BUG() << "Reusing an OutboundConnector object";
+ d->errorMessage = QStringLiteral("Outbound connection handler was already used");
+ d->setStatus(Error);
+ return false;
+ }
+
+ if (isActive() && hostname == d->hostname && port == d->port)
+ return true;
+
+ // There is no reason to be connecting to anything but onions for now, so add a safety net here
+ if (!hostname.endsWith(QLatin1String(".onion"))) {
+ d->errorMessage = QStringLiteral("Invalid (non-onion) hostname");
+ d->setStatus(Error);
+ return false;
+ }
+
+ abort();
+
+ d->hostname = hostname;
+ d->port = port;
+
+ d->socket = new Tor::TorSocket(this);
+ connect(d->socket, &Tor::TorSocket::connected, d, &OutboundConnectorPrivate::onConnected);
+ d->setStatus(Connecting);
+ d->socket->connectToHost(d->hostname, d->port);
+ return true;
+}
+
+void OutboundConnector::abort()
+{
+ d->abort();
+ d->hostname.clear();
+ d->port = 0;
+ d->errorRetryCount = 0;
+ d->errorRetryTimer.stop();
+ d->errorMessage.clear();
+ d->setStatus(Inactive);
+}
+
+void OutboundConnectorPrivate::abort()
+{
+ if (connection) {
+ connection->close();
+ connection->deleteLater();
+ connection = 0;
+ }
+
+ if (socket) {
+ socket->disconnect(this);
+ delete socket;
+ socket = 0;
+ }
+}
+
+OutboundConnector::Status OutboundConnector::status() const
+{
+ return d->status;
+}
+
+bool OutboundConnector::isActive() const
+{
+ return d->status > Inactive && d->status < Ready;
+}
+
+QString OutboundConnector::errorMessage() const
+{
+ return d->errorMessage;
+}
+
+Connection *OutboundConnector::takeConnection(QObject *newParent)
+{
+ if (status() != Ready || !d->connection) {
+ BUG() << "Cannot take connection when not in the Ready state";
+ return 0;
+ }
+
+ Q_ASSERT(newParent);
+ Connection *c = d->connection;
+ c->setParent(newParent);
+
+ Q_ASSERT(!d->socket);
+ d->connection = 0;
+ d->setStatus(Inactive);
+
+ return c;
+}
+
+void OutboundConnectorPrivate::setStatus(OutboundConnector::Status value)
+{
+ if (status == value)
+ return;
+
+ bool wasActive = q->isActive();
+ status = value;
+ emit q->statusChanged();
+ if (wasActive != q->isActive())
+ emit q->isActiveChanged();
+}
+
+void OutboundConnectorPrivate::setError(const QString &message)
+{
+ abort();
+ errorMessage = message;
+ setStatus(OutboundConnector::Error);
+
+ // XXX This is a bad solution, but it will hold until we can revisit the
+ // reconnecting and connection error behavior as a whole.
+ if (++errorRetryCount > 5) {
+ qDebug() << "Outbound connection attempt has had five errors in a row, stopping attempts";
+ return;
+ }
+
+ errorRetryTimer.setSingleShot(true);
+ errorRetryTimer.start(60 * 1000);
+ qDebug() << "Retrying outbound connection attempt in 60 seconds after an error";
+}
+
+void OutboundConnectorPrivate::retryAfterError()
+{
+ if (status != OutboundConnector::Error) {
+ qDebug() << "Error retry timer triggered, but not in an error state anymore. Ignoring.";
+ return;
+ }
+
+ if (hostname.isEmpty() || port <= 0) {
+ qDebug() << "Connection info cleared during error retry period, stopping OutboundConnector";
+ q->abort();
+ return;
+ }
+
+ q->connectToHost(hostname, port);
+}
+
+void OutboundConnectorPrivate::onConnected()
+{
+ if (!socket || status != OutboundConnector::Connecting) {
+ BUG() << "OutboundConnector connected in an unexpected state";
+ setError(QStringLiteral("Connected in an unexpected state"));
+ return;
+ }
+
+ connection = new Connection(socket, Connection::ClientSide, q);
+
+ // Socket is now owned by connection
+ Q_ASSERT(socket->parent() == connection);
+ socket->setReconnectEnabled(false);
+ socket = 0;
+
+ connect(connection, &Connection::ready, this, &OutboundConnectorPrivate::startAuthentication);
+ // XXX Needs special treatment in UI (along with some other error types here)
+ connect(connection, &Connection::versionNegotiationFailed, this,
+ [this]() {
+ setError(QStringLiteral("Protocol version negotiation failed with peer"));
+ }
+ );
+ connect(connection, &Connection::oldVersionNegotiated, q, &OutboundConnector::oldVersionNegotiated);
+ setStatus(OutboundConnector::Initializing);
+}
+
+void OutboundConnectorPrivate::startAuthentication()
+{
+ if (!connection || status != OutboundConnector::Initializing) {
+ BUG() << "OutboundConnector startAuthentication in an unexpected state";
+ setError(QStringLiteral("Connected in an unexpected state"));
+ return;
+ }
+
+ if (!authPrivateKey.isLoaded() || !authPrivateKey.isPrivate()) {
+ qDebug() << "Skipping authentication for OutboundConnector without a private key";
+ setStatus(OutboundConnector::Ready);
+ emit q->ready();
+ return;
+ }
+
+ // XXX Timeouts and errors and all of that
+ AuthHiddenServiceChannel *authChannel = new AuthHiddenServiceChannel(Channel::Outbound, connection);
+ connect(authChannel, &AuthHiddenServiceChannel::authSuccessful, this,
+ [this]() {
+ setStatus(OutboundConnector::Ready);
+ emit q->ready();
+ }
+ );
+ connect(authChannel, &AuthHiddenServiceChannel::authFailed, this,
+ [this]() {
+ qDebug() << "Authentication failed for outbound connection to" << hostname;
+ setError(QStringLiteral("Authentication failed"));
+ }
+ );
+
+ // Set the Authenticating state when we send the actual authentication message
+ connect(authChannel, &Channel::channelOpened, this,
+ [this]() {
+ setStatus(OutboundConnector::Authenticating);
+ }
+ );
+
+ authChannel->setPrivateKey(authPrivateKey);
+ if (!authChannel->openChannel()) {
+ setError(QStringLiteral("Unable to open authentication channel"));
+ }
+}
+
+#include "OutboundConnector.moc"
diff --git a/src/protocol/OutboundConnector.h b/src/protocol/OutboundConnector.h
new file mode 100644
index 0000000..6ab35be
--- /dev/null
+++ b/src/protocol/OutboundConnector.h
@@ -0,0 +1,110 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef PROTOCOL_OUTBOUNDCONNECTOR_H
+#define PROTOCOL_OUTBOUNDCONNECTOR_H
+
+#include <QObject>
+#include "Connection.h"
+#include "utils/CryptoKey.h"
+
+namespace Protocol
+{
+
+class OutboundConnectorPrivate;
+
+/* Manages making and authenticating an outbound connection to peers
+ *
+ * OutboundConnector handles the process of establishing a connection
+ * to a remote hidden service host (with appropriate timeout and retry
+ * behavior) and authenticating itself. Once the connection is
+ * established and authenticated, the ready() signal is emitted.
+ */
+class OutboundConnector : public QObject
+{
+ Q_OBJECT
+ Q_DISABLE_COPY(OutboundConnector)
+ Q_ENUMS(Status)
+
+ Q_PROPERTY(Status status READ status NOTIFY statusChanged)
+ Q_PROPERTY(bool isActive READ isActive NOTIFY isActiveChanged)
+
+public:
+ enum Status {
+ Inactive,
+ Connecting,
+ Initializing,
+ Authenticating,
+ Ready,
+ Error
+ };
+
+ explicit OutboundConnector(QObject *parent);
+ virtual ~OutboundConnector();
+
+ Status status() const;
+ bool isActive() const;
+ QString errorMessage() const;
+
+ bool connectToHost(const QString &hostname, quint16 port);
+ void setAuthPrivateKey(const CryptoKey &key);
+
+ /* Take ownership of the Connection object when Ready
+ *
+ * This function is only valid in the Ready state. QObject
+ * ownership of the connection is passed to newParent, and
+ * the OutboundConnector is cleared and reset to the inactive
+ * state.
+ */
+ Connection *takeConnection(QObject *newParent);
+
+public slots:
+ void abort();
+
+signals:
+ void ready();
+ void statusChanged();
+ void isActiveChanged();
+
+ /* Hack to allow sending an upgrade message to peers with old
+ * software versions that don't have a good way to handle this
+ * sort of situation.
+ */
+ void oldVersionNegotiated(QTcpSocket *socket);
+
+private:
+ OutboundConnectorPrivate *d;
+};
+
+}
+
+#endif
diff --git a/src/ricochet.desktop b/src/ricochet.desktop
new file mode 100644
index 0000000..357afde
--- /dev/null
+++ b/src/ricochet.desktop
@@ -0,0 +1,9 @@
+[Desktop Entry]
+Encoding=UTF-8
+Type=Application
+Name=Ricochet IM
+Categories=Network;InstantMessaging;Chat;FileTransfer;Qt
+Comment=Anonymous instant messaging
+Icon=ricochet
+Exec=ricochet
+
diff --git a/src/tor/AuthenticateCommand.cpp b/src/tor/AuthenticateCommand.cpp
new file mode 100644
index 0000000..497c28f
--- /dev/null
+++ b/src/tor/AuthenticateCommand.cpp
@@ -0,0 +1,64 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "AuthenticateCommand.h"
+
+using namespace Tor;
+
+AuthenticateCommand::AuthenticateCommand()
+{
+}
+
+QByteArray AuthenticateCommand::build(const QByteArray &data)
+{
+ if (data.isNull())
+ return QByteArray("AUTHENTICATE\r\n");
+
+ return QByteArray("AUTHENTICATE ") + data.toHex() + "\r\n";
+}
+
+void AuthenticateCommand::onReply(int statusCode, const QByteArray &data)
+{
+ TorControlCommand::onReply(statusCode, data);
+ m_statusMessage = QString::fromLatin1(data);
+}
+
+void AuthenticateCommand::onFinished(int statusCode)
+{
+ if (statusCode == 515) {
+ m_statusMessage = QStringLiteral("Authentication failed - incorrect password");
+ } else if (statusCode != 250) {
+ if (m_statusMessage.isEmpty())
+ m_statusMessage = QStringLiteral("Authentication failed (error %1").arg(statusCode);
+ }
+ TorControlCommand::onFinished(statusCode);
+}
diff --git a/src/tor/AuthenticateCommand.h b/src/tor/AuthenticateCommand.h
new file mode 100644
index 0000000..79c901d
--- /dev/null
+++ b/src/tor/AuthenticateCommand.h
@@ -0,0 +1,63 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef AUTHENTICATECOMMAND_H
+#define AUTHENTICATECOMMAND_H
+
+#include "TorControlCommand.h"
+
+namespace Tor
+{
+
+class AuthenticateCommand : public TorControlCommand
+{
+ Q_OBJECT
+
+public:
+ AuthenticateCommand();
+
+ QByteArray build(const QByteArray &data = QByteArray());
+
+ bool isSuccessful() const { return statusCode() == 250; }
+ QString errorMessage() const { return m_statusMessage; }
+
+protected:
+ virtual void onReply(int statusCode, const QByteArray &data);
+ virtual void onFinished(int statusCode);
+
+private:
+ QString m_statusMessage;
+};
+
+}
+
+#endif // AUTHENTICATECOMMAND_H
diff --git a/src/tor/GetConfCommand.cpp b/src/tor/GetConfCommand.cpp
new file mode 100644
index 0000000..77b511e
--- /dev/null
+++ b/src/tor/GetConfCommand.cpp
@@ -0,0 +1,124 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "GetConfCommand.h"
+#include "utils/StringUtil.h"
+#include <QDebug>
+
+using namespace Tor;
+
+GetConfCommand::GetConfCommand(Type t)
+ : type(t)
+{
+}
+
+QByteArray GetConfCommand::build(const QByteArray &key)
+{
+ return build(QList<QByteArray>() << key);
+}
+
+QByteArray GetConfCommand::build(const QList<QByteArray> &keys)
+{
+ QByteArray out;
+ if (type == GetConf) {
+ out = "GETCONF";
+ } else if (type == GetInfo) {
+ out = "GETINFO";
+ } else {
+ Q_ASSERT(false);
+ return out;
+ }
+
+ foreach (const QByteArray &key, keys) {
+ out.append(' ');
+ out.append(key);
+ }
+
+ out.append("\r\n");
+ return out;
+}
+
+void GetConfCommand::onReply(int statusCode, const QByteArray &data)
+{
+ TorControlCommand::onReply(statusCode, data);
+ if (statusCode != 250)
+ return;
+
+ int kep = data.indexOf('=');
+ QString key = QString::fromLatin1(data.mid(0, kep));
+ QVariant value;
+ if (kep >= 0)
+ value = QString::fromLatin1(unquotedString(data.mid(kep + 1)));
+
+ m_lastKey = key;
+ QVariantMap::iterator it = m_results.find(key);
+ if (it != m_results.end()) {
+ // Make a list of values
+ QVariantList results = it->toList();
+ if (results.isEmpty())
+ results.append(*it);
+ results.append(value);
+ *it = QVariant(results);
+ } else {
+ m_results.insert(key, value);
+ }
+}
+
+void GetConfCommand::onDataLine(const QByteArray &data)
+{
+ if (m_lastKey.isEmpty()) {
+ qWarning() << "torctrl: Unexpected data line in GetConf command";
+ return;
+ }
+
+ QVariantMap::iterator it = m_results.find(m_lastKey);
+ if (it != m_results.end()) {
+ QVariantList results = it->toList();
+ if (results.isEmpty() && !it->toByteArray().isEmpty())
+ results.append(*it);
+ results.append(data);
+ *it = QVariant(results);
+ } else {
+ m_results.insert(m_lastKey, QVariantList() << data);
+ }
+}
+
+void GetConfCommand::onDataFinished()
+{
+ m_lastKey.clear();
+}
+
+QVariant GetConfCommand::get(const QByteArray &key) const
+{
+ return m_results.value(QString::fromLatin1(key));
+}
+
diff --git a/src/tor/GetConfCommand.h b/src/tor/GetConfCommand.h
new file mode 100644
index 0000000..0de97d1
--- /dev/null
+++ b/src/tor/GetConfCommand.h
@@ -0,0 +1,77 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef GETCONFCOMMAND_H
+#define GETCONFCOMMAND_H
+
+#include "TorControlCommand.h"
+#include <QList>
+#include <QVariantMap>
+
+namespace Tor
+{
+
+class GetConfCommand : public TorControlCommand
+{
+ Q_OBJECT
+ Q_DISABLE_COPY(GetConfCommand)
+
+ Q_PROPERTY(QVariantMap results READ results CONSTANT)
+
+public:
+ enum Type {
+ GetConf,
+ GetInfo
+ };
+ const Type type;
+
+ GetConfCommand(Type type);
+
+ QByteArray build(const QByteArray &key);
+ QByteArray build(const QList<QByteArray> &keys);
+
+ const QVariantMap &results() const { return m_results; }
+ QVariant get(const QByteArray &key) const;
+
+protected:
+ virtual void onReply(int statusCode, const QByteArray &data);
+ virtual void onDataLine(const QByteArray &data);
+ virtual void onDataFinished();
+
+private:
+ QVariantMap m_results;
+ QString m_lastKey;
+};
+
+}
+
+#endif // GETCONFCOMMAND_H
diff --git a/src/tor/HiddenService.cpp b/src/tor/HiddenService.cpp
new file mode 100644
index 0000000..ad9abc9
--- /dev/null
+++ b/src/tor/HiddenService.cpp
@@ -0,0 +1,140 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "HiddenService.h"
+#include "TorControl.h"
+#include "TorSocket.h"
+#include "utils/CryptoKey.h"
+#include <QDir>
+#include <QFile>
+#include <QTimer>
+#include <QDebug>
+
+using namespace Tor;
+
+HiddenService::HiddenService(const QString &p, QObject *parent)
+ : QObject(parent), dataPath(p), pStatus(NotCreated)
+{
+ /* Set the initial status and, if possible, load the hostname */
+ QDir dir(dataPath);
+ if (dir.exists(QLatin1String("hostname")) && dir.exists(QLatin1String("private_key")))
+ {
+ readHostname();
+ if (!pHostname.isEmpty())
+ pStatus = Offline;
+ }
+}
+
+void HiddenService::setStatus(Status newStatus)
+{
+ if (pStatus == newStatus)
+ return;
+
+ Status old = pStatus;
+ pStatus = newStatus;
+
+ emit statusChanged(pStatus, old);
+
+ if (pStatus == Online)
+ emit serviceOnline();
+}
+
+void HiddenService::addTarget(const Target &target)
+{
+ pTargets.append(target);
+}
+
+void HiddenService::addTarget(quint16 servicePort, QHostAddress targetAddress, quint16 targetPort)
+{
+ Target t = { targetAddress, servicePort, targetPort };
+ pTargets.append(t);
+}
+
+void HiddenService::readHostname()
+{
+ pHostname.clear();
+
+ QFile file(dataPath + QLatin1String("/hostname"));
+ if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
+ {
+ qDebug() << "Failed to open hostname file for hidden service" << dataPath << "-" << file.errorString();
+ return;
+ }
+
+ QByteArray data;
+ data.resize(32);
+
+ int rd = file.readLine(data.data(), data.size());
+ if (rd < 0)
+ {
+ qDebug() << "Failed to read hostname file for hidden service" << dataPath << "-" << file.errorString();
+ return;
+ }
+
+ data.resize(rd);
+
+ int sep = data.lastIndexOf('.');
+ if (sep != 16 || data.mid(sep) != ".onion\n")
+ {
+ qDebug() << "Failed to read hostname file for hidden service" << dataPath << "- invalid contents";
+ return;
+ }
+
+ pHostname = QString::fromLatin1(data.constData(), sep) + QLatin1String(".onion");
+ qDebug() << "Hidden service hostname is" << pHostname;
+}
+
+CryptoKey HiddenService::cryptoKey()
+{
+ if (!pCryptoKey.isLoaded()) {
+ bool ok = pCryptoKey.loadFromFile(dataPath + QLatin1String("/private_key"), CryptoKey::PrivateKey);
+ if (!ok)
+ qWarning() << "Failed to load hidden service key";
+ }
+
+ return pCryptoKey;
+}
+
+void HiddenService::servicePublished()
+{
+ readHostname();
+
+ if (pHostname.isEmpty())
+ {
+ qDebug() << "Failed to read hidden service hostname";
+ return;
+ }
+
+ qDebug() << "Hidden service published successfully";
+ setStatus(Online);
+}
+
diff --git a/src/tor/HiddenService.h b/src/tor/HiddenService.h
new file mode 100644
index 0000000..5aa9f71
--- /dev/null
+++ b/src/tor/HiddenService.h
@@ -0,0 +1,99 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef HIDDENSERVICE_H
+#define HIDDENSERVICE_H
+
+#include <QObject>
+#include <QHostAddress>
+#include <QList>
+#include "utils/CryptoKey.h"
+
+namespace Tor
+{
+
+class TorSocket;
+
+class HiddenService : public QObject
+{
+ Q_OBJECT
+ Q_DISABLE_COPY(HiddenService)
+
+ friend class TorControlPrivate;
+
+public:
+ struct Target
+ {
+ QHostAddress targetAddress;
+ quint16 servicePort, targetPort;
+ };
+
+ enum Status
+ {
+ NotCreated = -1, /* Service has not been created yet */
+ Offline = 0, /* Data exists, but service is not published */
+ Online /* Published */
+ };
+
+ const QString dataPath;
+
+ HiddenService(const QString &dataPath, QObject *parent = 0);
+
+ Status status() const { return pStatus; }
+
+ const QString &hostname() const { return pHostname; }
+ CryptoKey cryptoKey();
+
+ const QList<Target> &targets() const { return pTargets; }
+ void addTarget(const Target &target);
+ void addTarget(quint16 servicePort, QHostAddress targetAddress, quint16 targetPort);
+
+signals:
+ void statusChanged(int newStatus, int oldStatus);
+ void serviceOnline();
+
+private slots:
+ void servicePublished();
+
+private:
+ QList<Target> pTargets;
+ QString pHostname;
+ Status pStatus;
+ CryptoKey pCryptoKey;
+
+ void setStatus(Status newStatus);
+ void readHostname();
+};
+
+}
+
+#endif // HIDDENSERVICE_H
diff --git a/src/tor/ProtocolInfoCommand.cpp b/src/tor/ProtocolInfoCommand.cpp
new file mode 100644
index 0000000..8222d80
--- /dev/null
+++ b/src/tor/ProtocolInfoCommand.cpp
@@ -0,0 +1,85 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ProtocolInfoCommand.h"
+#include "TorControl.h"
+#include "utils/StringUtil.h"
+#include <QList>
+
+using namespace Tor;
+
+ProtocolInfoCommand::ProtocolInfoCommand(TorControl *m)
+ : manager(m)
+{
+}
+
+QByteArray ProtocolInfoCommand::build()
+{
+ return QByteArray("PROTOCOLINFO 1\r\n");
+}
+
+void ProtocolInfoCommand::onReply(int statusCode, const QByteArray &data)
+{
+ TorControlCommand::onReply(statusCode, data);
+ if (statusCode != 250)
+ return;
+
+ if (data.startsWith("AUTH "))
+ {
+ QList<QByteArray> tokens = splitQuotedStrings(data.mid(5), ' ');
+
+ foreach (QByteArray token, tokens)
+ {
+ if (token.startsWith("METHODS="))
+ {
+ QList<QByteArray> textMethods = unquotedString(token.mid(8)).split(',');
+ for (QList<QByteArray>::Iterator it = textMethods.begin(); it != textMethods.end(); ++it)
+ {
+ if (*it == "NULL")
+ m_authMethods |= AuthNull;
+ else if (*it == "HASHEDPASSWORD")
+ m_authMethods |= AuthHashedPassword;
+ else if (*it == "COOKIE")
+ m_authMethods |= AuthCookie;
+ }
+ }
+ else if (token.startsWith("COOKIEFILE="))
+ {
+ m_cookieFile = QString::fromLatin1(unquotedString(token.mid(11)));
+ }
+ }
+ }
+ else if (data.startsWith("VERSION Tor="))
+ {
+ m_torVersion = QString::fromLatin1(unquotedString(data.mid(12, data.indexOf(' ', 12))));
+ }
+}
diff --git a/src/tor/ProtocolInfoCommand.h b/src/tor/ProtocolInfoCommand.h
new file mode 100644
index 0000000..7789cfe
--- /dev/null
+++ b/src/tor/ProtocolInfoCommand.h
@@ -0,0 +1,78 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef PROTOCOLINFOCOMMAND_H
+#define PROTOCOLINFOCOMMAND_H
+
+#include "TorControlCommand.h"
+#include <QFlags>
+
+namespace Tor
+{
+
+class TorControl;
+
+class ProtocolInfoCommand : public TorControlCommand
+{
+ Q_OBJECT
+ Q_DISABLE_COPY(ProtocolInfoCommand)
+
+public:
+ enum AuthMethod
+ {
+ AuthUnknown = 0,
+ AuthNull = 0x1,
+ AuthHashedPassword = 0x2,
+ AuthCookie = 0x4
+ };
+ Q_DECLARE_FLAGS(AuthMethods, AuthMethod)
+
+ ProtocolInfoCommand(TorControl *manager);
+ QByteArray build();
+
+ AuthMethods authMethods() const { return m_authMethods; }
+ QString torVersion() const { return m_torVersion; }
+ QString cookieFile() const { return m_cookieFile; }
+
+protected:
+ virtual void onReply(int statusCode, const QByteArray &data);
+
+private:
+ TorControl *manager;
+ AuthMethods m_authMethods;
+ QString m_torVersion;
+ QString m_cookieFile;
+};
+
+}
+
+#endif // PROTOCOLINFOCOMMAND_H
diff --git a/src/tor/SetConfCommand.cpp b/src/tor/SetConfCommand.cpp
new file mode 100644
index 0000000..eb3a13b
--- /dev/null
+++ b/src/tor/SetConfCommand.cpp
@@ -0,0 +1,106 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "SetConfCommand.h"
+#include "utils/StringUtil.h"
+
+using namespace Tor;
+
+SetConfCommand::SetConfCommand()
+ : m_resetMode(false)
+{
+}
+
+void SetConfCommand::setResetMode(bool enabled)
+{
+ m_resetMode = enabled;
+}
+
+bool SetConfCommand::isSuccessful() const
+{
+ return statusCode() == 250;
+}
+
+QByteArray SetConfCommand::build(const QByteArray &key, const QByteArray &value)
+{
+ return build(QList<QPair<QByteArray, QByteArray> >() << qMakePair(key, value));
+}
+
+QByteArray SetConfCommand::build(const QVariantMap &data)
+{
+ QList<QPair<QByteArray, QByteArray> > out;
+
+ for (QVariantMap::ConstIterator it = data.begin(); it != data.end(); it++) {
+ QByteArray key = it.key().toLatin1();
+
+ if (static_cast<QMetaType::Type>(it.value().type()) == QMetaType::QVariantList) {
+ QVariantList values = it.value().value<QVariantList>();
+ foreach (const QVariant &value, values)
+ out.append(qMakePair(key, value.toString().toLatin1()));
+ } else {
+ out.append(qMakePair(key, it.value().toString().toLatin1()));
+ }
+ }
+
+ return build(out);
+}
+
+QByteArray SetConfCommand::build(const QList<QPair<QByteArray, QByteArray> > &data)
+{
+ QByteArray out(m_resetMode ? "RESETCONF" : "SETCONF");
+
+ for (int i = 0; i < data.size(); i++) {
+ out += " " + data[i].first;
+ if (!data[i].second.isEmpty())
+ out += "=" + quotedString(data[i].second);
+ }
+
+ out.append("\r\n");
+ return out;
+}
+
+void SetConfCommand::onReply(int statusCode, const QByteArray &data)
+{
+ TorControlCommand::onReply(statusCode, data);
+ if (statusCode != 250)
+ m_errorMessage = QString::fromLatin1(data);
+}
+
+void SetConfCommand::onFinished(int statusCode)
+{
+ TorControlCommand::onFinished(statusCode);
+ if (isSuccessful())
+ emit setConfSucceeded();
+ else
+ emit setConfFailed(statusCode);
+}
+
diff --git a/src/tor/SetConfCommand.h b/src/tor/SetConfCommand.h
new file mode 100644
index 0000000..5bdcb93
--- /dev/null
+++ b/src/tor/SetConfCommand.h
@@ -0,0 +1,78 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef SETCONFCOMMAND_H
+#define SETCONFCOMMAND_H
+
+#include "TorControlCommand.h"
+#include <QList>
+#include <QPair>
+#include <QVariant>
+
+namespace Tor
+{
+
+class SetConfCommand : public TorControlCommand
+{
+ Q_OBJECT
+ Q_DISABLE_COPY(SetConfCommand)
+
+ Q_PROPERTY(QString errorMessage READ errorMessage CONSTANT)
+ Q_PROPERTY(bool successful READ isSuccessful CONSTANT)
+
+public:
+ SetConfCommand();
+
+ void setResetMode(bool resetMode);
+
+ QByteArray build(const QByteArray &key, const QByteArray &value);
+ QByteArray build(const QVariantMap &data);
+ QByteArray build(const QList<QPair<QByteArray, QByteArray> > &data);
+
+ QString errorMessage() const { return m_errorMessage; }
+ bool isSuccessful() const;
+
+signals:
+ void setConfSucceeded();
+ void setConfFailed(int code);
+
+protected:
+ QString m_errorMessage;
+ bool m_resetMode;
+
+ virtual void onReply(int statusCode, const QByteArray &data);
+ virtual void onFinished(int statusCode);
+};
+
+}
+
+#endif // SETCONFCOMMAND_H
diff --git a/src/tor/TorControl.cpp b/src/tor/TorControl.cpp
new file mode 100644
index 0000000..a5c5b0f
--- /dev/null
+++ b/src/tor/TorControl.cpp
@@ -0,0 +1,690 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "TorControl.h"
+#include "TorControlSocket.h"
+#include "HiddenService.h"
+#include "ProtocolInfoCommand.h"
+#include "AuthenticateCommand.h"
+#include "SetConfCommand.h"
+#include "GetConfCommand.h"
+#include "utils/StringUtil.h"
+#include "utils/Settings.h"
+#include "utils/PendingOperation.h"
+#include <QHostAddress>
+#include <QDir>
+#include <QNetworkProxy>
+#include <QQmlEngine>
+#include <QTimer>
+#include <QSaveFile>
+#include <QDebug>
+
+Tor::TorControl *torControl = 0;
+
+using namespace Tor;
+
+namespace Tor {
+
+class TorControlPrivate : public QObject
+{
+ Q_OBJECT
+
+public:
+ TorControl *q;
+
+ TorControlSocket *socket;
+ QHostAddress torAddress;
+ QString errorMessage;
+ QString torVersion;
+ QByteArray authPassword;
+ QHostAddress socksAddress;
+ QList<HiddenService*> services;
+ quint16 controlPort, socksPort;
+ TorControl::Status status;
+ TorControl::TorStatus torStatus;
+ QVariantMap bootstrapStatus;
+
+ TorControlPrivate(TorControl *parent);
+
+ void setStatus(TorControl::Status status);
+ void setTorStatus(TorControl::TorStatus status);
+
+ void getTorInfo();
+ void publishServices();
+
+public slots:
+ void socketConnected();
+ void socketDisconnected();
+ void socketError();
+
+ void authenticateReply();
+ void protocolInfoReply();
+ void getTorInfoReply();
+ void setError(const QString &message);
+
+ void statusEvent(int code, const QByteArray &data);
+ void updateBootstrap(const QList<QByteArray> &data);
+};
+
+}
+
+TorControl::TorControl(QObject *parent)
+ : QObject(parent), d(new TorControlPrivate(this))
+{
+}
+
+TorControlPrivate::TorControlPrivate(TorControl *parent)
+ : QObject(parent), q(parent), controlPort(0), socksPort(0),
+ status(TorControl::NotConnected), torStatus(TorControl::TorUnknown)
+{
+ socket = new TorControlSocket(this);
+ QObject::connect(socket, SIGNAL(connected()), this, SLOT(socketConnected()));
+ QObject::connect(socket, SIGNAL(disconnected()), this, SLOT(socketDisconnected()));
+ QObject::connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(socketError()));
+ QObject::connect(socket, SIGNAL(error(QString)), this, SLOT(setError(QString)));
+}
+
+QNetworkProxy TorControl::connectionProxy()
+{
+ return QNetworkProxy(QNetworkProxy::Socks5Proxy, d->socksAddress.toString(), d->socksPort);
+}
+
+void TorControlPrivate::setStatus(TorControl::Status n)
+{
+ if (n == status)
+ return;
+
+ TorControl::Status old = status;
+ status = n;
+
+ if (old == TorControl::Error)
+ errorMessage.clear();
+
+ emit q->statusChanged(status, old);
+
+ if (status == TorControl::Connected && old < TorControl::Connected)
+ emit q->connected();
+ else if (status < TorControl::Connected && old >= TorControl::Connected)
+ emit q->disconnected();
+}
+
+void TorControlPrivate::setTorStatus(TorControl::TorStatus n)
+{
+ if (n == torStatus)
+ return;
+
+ TorControl::TorStatus old = torStatus;
+ torStatus = n;
+ emit q->torStatusChanged(torStatus, old);
+ emit q->connectivityChanged();
+
+ if (torStatus == TorControl::TorReady && socksAddress.isNull()) {
+ // Request info again to read the SOCKS port
+ getTorInfo();
+ }
+}
+
+void TorControlPrivate::setError(const QString &message)
+{
+ errorMessage = message;
+ setStatus(TorControl::Error);
+
+ qWarning() << "torctrl: Error:" << errorMessage;
+
+ socket->abort();
+
+ QTimer::singleShot(15000, q, SLOT(reconnect()));
+}
+
+TorControl::Status TorControl::status() const
+{
+ return d->status;
+}
+
+TorControl::TorStatus TorControl::torStatus() const
+{
+ return d->torStatus;
+}
+
+QString TorControl::torVersion() const
+{
+ return d->torVersion;
+}
+
+QString TorControl::errorMessage() const
+{
+ return d->errorMessage;
+}
+
+bool TorControl::hasConnectivity() const
+{
+ return torStatus() == TorReady && !d->socksAddress.isNull();
+}
+
+QHostAddress TorControl::socksAddress() const
+{
+ return d->socksAddress;
+}
+
+quint16 TorControl::socksPort() const
+{
+ return d->socksPort;
+}
+
+QList<HiddenService*> TorControl::hiddenServices() const
+{
+ return d->services;
+}
+
+QVariantMap TorControl::bootstrapStatus() const
+{
+ return d->bootstrapStatus;
+}
+
+void TorControl::setAuthPassword(const QByteArray &password)
+{
+ d->authPassword = password;
+}
+
+void TorControl::connect(const QHostAddress &address, quint16 port)
+{
+ if (status() > Connecting)
+ {
+ qDebug() << "Ignoring TorControl::connect due to existing connection";
+ return;
+ }
+
+ d->torAddress = address;
+ d->controlPort = port;
+ d->setTorStatus(TorUnknown);
+
+ bool b = d->socket->blockSignals(true);
+ d->socket->abort();
+ d->socket->blockSignals(b);
+
+ d->setStatus(Connecting);
+ d->socket->connectToHost(address, port);
+}
+
+void TorControl::reconnect()
+{
+ Q_ASSERT(!d->torAddress.isNull() && d->controlPort);
+ if (d->torAddress.isNull() || !d->controlPort || status() >= Connecting)
+ return;
+
+ d->setStatus(Connecting);
+ d->socket->connectToHost(d->torAddress, d->controlPort);
+}
+
+void TorControlPrivate::authenticateReply()
+{
+ AuthenticateCommand *command = qobject_cast<AuthenticateCommand*>(sender());
+ Q_ASSERT(command);
+ Q_ASSERT(status == TorControl::Authenticating);
+ if (!command)
+ return;
+
+ if (!command->isSuccessful()) {
+ setError(command->errorMessage());
+ return;
+ }
+
+ qDebug() << "torctrl: Authentication successful";
+ setStatus(TorControl::Connected);
+
+ setTorStatus(TorControl::TorUnknown);
+
+ TorControlCommand *clientEvents = new TorControlCommand;
+ connect(clientEvents, &TorControlCommand::replyLine, this, &TorControlPrivate::statusEvent);
+ socket->registerEvent("STATUS_CLIENT", clientEvents);
+
+ getTorInfo();
+ publishServices();
+
+ // XXX Fix old configurations that would store unwanted options in torrc.
+ // This can be removed some suitable amount of time after 1.0.4.
+ q->saveConfiguration();
+}
+
+void TorControlPrivate::socketConnected()
+{
+ Q_ASSERT(status == TorControl::Connecting);
+
+ qDebug() << "torctrl: Connected socket; querying information";
+ setStatus(TorControl::Authenticating);
+
+ ProtocolInfoCommand *command = new ProtocolInfoCommand(q);
+ connect(command, &TorControlCommand::finished, this, &TorControlPrivate::protocolInfoReply);
+ socket->sendCommand(command, command->build());
+}
+
+void TorControlPrivate::socketDisconnected()
+{
+ /* Clear some internal state */
+ torVersion.clear();
+ socksAddress.clear();
+ socksPort = 0;
+ setTorStatus(TorControl::TorUnknown);
+
+ /* This emits the disconnected() signal as well */
+ setStatus(TorControl::NotConnected);
+}
+
+void TorControlPrivate::socketError()
+{
+ setError(QStringLiteral("Connection failed: %1").arg(socket->errorString()));
+}
+
+void TorControlPrivate::protocolInfoReply()
+{
+ ProtocolInfoCommand *info = qobject_cast<ProtocolInfoCommand*>(sender());
+ if (!info)
+ return;
+
+ torVersion = info->torVersion();
+
+ if (status == TorControl::Authenticating)
+ {
+ AuthenticateCommand *auth = new AuthenticateCommand;
+ connect(auth, &TorControlCommand::finished, this, &TorControlPrivate::authenticateReply);
+
+ QByteArray data;
+ ProtocolInfoCommand::AuthMethods methods = info->authMethods();
+
+ if (methods.testFlag(ProtocolInfoCommand::AuthNull))
+ {
+ qDebug() << "torctrl: Using null authentication";
+ data = auth->build();
+ }
+ else if (methods.testFlag(ProtocolInfoCommand::AuthCookie) && !info->cookieFile().isEmpty())
+ {
+ QString cookieFile = info->cookieFile();
+ QString cookieError;
+ qDebug() << "torctrl: Using cookie authentication with file" << cookieFile;
+
+ QFile file(cookieFile);
+ if (file.open(QIODevice::ReadOnly))
+ {
+ QByteArray cookie = file.readAll();
+ file.close();
+
+ /* Simple test to avoid a vulnerability where any process listening on what we think is
+ * the control port could trick us into sending the contents of an arbitrary file */
+ if (cookie.size() == 32)
+ data = auth->build(cookie);
+ else
+ cookieError = QStringLiteral("Unexpected file size");
+ }
+ else
+ cookieError = file.errorString();
+
+ if (!cookieError.isNull() || data.isNull())
+ {
+ /* If we know a password and password authentication is allowed, try using that instead.
+ * This is a strange corner case that will likely never happen in a normal configuration,
+ * but it has happened. */
+ if (methods.testFlag(ProtocolInfoCommand::AuthHashedPassword) && !authPassword.isEmpty())
+ {
+ qDebug() << "torctrl: Unable to read authentication cookie file:" << cookieError;
+ goto usePasswordAuth;
+ }
+
+ setError(QStringLiteral("Unable to read authentication cookie file: %1").arg(cookieError));
+ delete auth;
+ return;
+ }
+ }
+ else if (methods.testFlag(ProtocolInfoCommand::AuthHashedPassword) && !authPassword.isEmpty())
+ {
+ usePasswordAuth:
+ qDebug() << "torctrl: Using hashed password authentication";
+ data = auth->build(authPassword);
+ }
+ else
+ {
+ if (methods.testFlag(ProtocolInfoCommand::AuthHashedPassword))
+ setError(QStringLiteral("Tor requires a control password to connect, but no password is configured."));
+ else
+ setError(QStringLiteral("Tor is not configured to accept any supported authentication methods."));
+ delete auth;
+ return;
+ }
+
+ socket->sendCommand(auth, data);
+ }
+}
+
+void TorControlPrivate::getTorInfo()
+{
+ Q_ASSERT(q->isConnected());
+
+ GetConfCommand *command = new GetConfCommand(GetConfCommand::GetInfo);
+ connect(command, &TorControlCommand::finished, this, &TorControlPrivate::getTorInfoReply);
+
+ QList<QByteArray> keys;
+ keys << QByteArray("status/circuit-established") << QByteArray("status/bootstrap-phase");
+
+ /* If these are set in the config, they override the automatic behavior. */
+ SettingsObject settings(QStringLiteral("tor"));
+ QHostAddress forceAddress(settings.read("socksAddress").toString());
+ quint16 port = (quint16)settings.read("socksPort").toInt();
+
+ if (!forceAddress.isNull() && port) {
+ qDebug() << "torctrl: Using manually specified SOCKS connection settings";
+ socksAddress = forceAddress;
+ socksPort = port;
+ emit q->connectivityChanged();
+ } else
+ keys << QByteArray("net/listeners/socks");
+
+ socket->sendCommand(command, command->build(keys));
+}
+
+void TorControlPrivate::getTorInfoReply()
+{
+ GetConfCommand *command = qobject_cast<GetConfCommand*>(sender());
+ if (!command || !q->isConnected())
+ return;
+
+ QList<QByteArray> listenAddresses = splitQuotedStrings(command->get(QByteArray("net/listeners/socks")).toString().toLatin1(), ' ');
+ for (QList<QByteArray>::Iterator it = listenAddresses.begin(); it != listenAddresses.end(); ++it) {
+ QByteArray value = unquotedString(*it);
+ int sepp = value.indexOf(':');
+ QHostAddress address(QString::fromLatin1(value.mid(0, sepp)));
+ quint16 port = (quint16)value.mid(sepp+1).toUInt();
+
+ /* Use the first address that matches the one used for this control connection. If none do,
+ * just use the first address and rely on the user to reconfigure if necessary (not a problem;
+ * their setup is already very customized) */
+ if (socksAddress.isNull() || address == socket->peerAddress()) {
+ socksAddress = address;
+ socksPort = port;
+ if (address == socket->peerAddress())
+ break;
+ }
+ }
+
+ /* It is not immediately an error to have no SOCKS address; when DisableNetwork is set there won't be a
+ * listener yet. To handle that situation, we'll try to read the socks address again when TorReady state
+ * is reached. */
+ if (!socksAddress.isNull()) {
+ qDebug().nospace() << "torctrl: SOCKS address is " << socksAddress.toString() << ":" << socksPort;
+ emit q->connectivityChanged();
+ }
+
+ if (command->get(QByteArray("status/circuit-established")).toInt() == 1) {
+ qDebug() << "torctrl: Tor indicates that circuits have been established; state is TorReady";
+ setTorStatus(TorControl::TorReady);
+ } else {
+ setTorStatus(TorControl::TorOffline);
+ }
+
+ QByteArray bootstrap = command->get(QByteArray("status/bootstrap-phase")).toString().toLatin1();
+ if (!bootstrap.isEmpty())
+ updateBootstrap(splitQuotedStrings(bootstrap, ' '));
+}
+
+void TorControl::addHiddenService(HiddenService *service)
+{
+ if (d->services.contains(service))
+ return;
+
+ d->services.append(service);
+}
+
+void TorControlPrivate::publishServices()
+{
+ Q_ASSERT(q->isConnected());
+ if (services.isEmpty())
+ return;
+
+ SettingsObject settings(QStringLiteral("tor"));
+ if (settings.read("neverPublishServices").toBool())
+ {
+ qDebug() << "torctrl: Skipping service publication because neverPublishService is enabled";
+
+ /* Call servicePublished under the assumption that they're published externally. */
+ for (QList<HiddenService*>::Iterator it = services.begin(); it != services.end(); ++it)
+ (*it)->servicePublished();
+
+ return;
+ }
+
+ SetConfCommand *command = new SetConfCommand;
+ QList<QPair<QByteArray,QByteArray> > torConfig;
+
+ for (QList<HiddenService*>::Iterator it = services.begin(); it != services.end(); ++it)
+ {
+ HiddenService *service = *it;
+ QDir dir(service->dataPath);
+
+ qDebug() << "torctrl: Configuring hidden service at" << service->dataPath;
+
+ torConfig.append(qMakePair(QByteArray("HiddenServiceDir"), dir.absolutePath().toLocal8Bit()));
+
+ const QList<HiddenService::Target> &targets = service->targets();
+ for (QList<HiddenService::Target>::ConstIterator tit = targets.begin(); tit != targets.end(); ++tit)
+ {
+ QString target = QString::fromLatin1("%1 %2:%3").arg(tit->servicePort)
+ .arg(tit->targetAddress.toString())
+ .arg(tit->targetPort);
+ torConfig.append(qMakePair(QByteArray("HiddenServicePort"), target.toLatin1()));
+ }
+
+ QObject::connect(command, &SetConfCommand::setConfSucceeded, service, &HiddenService::servicePublished);
+ }
+
+ socket->sendCommand(command, command->build(torConfig));
+}
+
+void TorControl::shutdown()
+{
+ d->socket->sendCommand("SIGNAL SHUTDOWN\r\n");
+}
+
+void TorControl::shutdownSync()
+{
+ shutdown();
+ while (d->socket->bytesToWrite())
+ {
+ if (!d->socket->waitForBytesWritten(5000))
+ return;
+ }
+}
+
+void TorControlPrivate::statusEvent(int code, const QByteArray &data)
+{
+ Q_UNUSED(code);
+
+ QList<QByteArray> tokens = splitQuotedStrings(data.trimmed(), ' ');
+ if (tokens.size() < 3)
+ return;
+
+ qDebug() << "torctrl: status event:" << data.trimmed();
+
+ if (tokens[2] == "CIRCUIT_ESTABLISHED") {
+ setTorStatus(TorControl::TorReady);
+ } else if (tokens[2] == "CIRCUIT_NOT_ESTABLISHED") {
+ setTorStatus(TorControl::TorOffline);
+ } else if (tokens[2] == "BOOTSTRAP") {
+ tokens.takeFirst();
+ updateBootstrap(tokens);
+ }
+}
+
+void TorControlPrivate::updateBootstrap(const QList<QByteArray> &data)
+{
+ bootstrapStatus.clear();
+ // WARN or NOTICE
+ bootstrapStatus[QStringLiteral("severity")] = data.value(0);
+ for (int i = 1; i < data.size(); i++) {
+ int equals = data[i].indexOf('=');
+ QString key = QString::fromLatin1(data[i].mid(0, equals));
+ QString value;
+ if (equals >= 0)
+ value = QString::fromLatin1(unquotedString(data[i].mid(equals + 1)));
+ bootstrapStatus[key.toLower()] = value;
+ }
+
+ qDebug() << bootstrapStatus;
+ emit q->bootstrapStatusChanged();
+}
+
+QObject *TorControl::getConfiguration(const QString &options)
+{
+ GetConfCommand *command = new GetConfCommand(GetConfCommand::GetConf);
+ d->socket->sendCommand(command, command->build(options.toLatin1()));
+
+ QQmlEngine::setObjectOwnership(command, QQmlEngine::CppOwnership);
+ return command;
+}
+
+QObject *TorControl::setConfiguration(const QVariantMap &options)
+{
+ SetConfCommand *command = new SetConfCommand;
+ command->setResetMode(true);
+ d->socket->sendCommand(command, command->build(options));
+
+ QQmlEngine::setObjectOwnership(command, QQmlEngine::CppOwnership);
+ return command;
+}
+
+namespace Tor {
+
+class SaveConfigOperation : public PendingOperation
+{
+ Q_OBJECT
+
+public:
+ SaveConfigOperation(QObject *parent)
+ : PendingOperation(parent), command(0)
+ {
+ }
+
+ void start(TorControlSocket *socket)
+ {
+ Q_ASSERT(!command);
+ command = new GetConfCommand(GetConfCommand::GetInfo);
+ QObject::connect(command, &TorControlCommand::finished, this, &SaveConfigOperation::configTextReply);
+ socket->sendCommand(command, command->build(QList<QByteArray>() << "config-text" << "config-file"));
+ }
+
+private slots:
+ void configTextReply()
+ {
+ Q_ASSERT(command);
+ if (!command)
+ return;
+
+ QString path = QFile::decodeName(command->get("config-file").toByteArray());
+ if (path.isEmpty()) {
+ finishWithError(QStringLiteral("Cannot write torrc without knowing its path"));
+ return;
+ }
+
+ // Out of paranoia, refuse to write any file not named 'torrc', or if the
+ // file doesn't exist
+ QFileInfo fileInfo(path);
+ if (fileInfo.fileName() != QStringLiteral("torrc") || !fileInfo.exists()) {
+ finishWithError(QStringLiteral("Refusing to write torrc to unacceptable path %1").arg(path));
+ return;
+ }
+
+ QSaveFile file(path);
+ if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
+ finishWithError(QStringLiteral("Failed opening torrc file for writing: %1").arg(file.errorString()));
+ return;
+ }
+
+ // Remove these keys when writing torrc; they are set at runtime and contain
+ // absolute paths or port numbers
+ static const char *bannedKeys[] = {
+ "ControlPortWriteToFile",
+ "DataDirectory",
+ "HiddenServiceDir",
+ "HiddenServicePort",
+ 0
+ };
+
+ QVariantList configText = command->get("config-text").toList();
+ foreach (const QVariant &value, configText) {
+ QByteArray line = value.toByteArray();
+
+ bool skip = false;
+ for (const char **key = bannedKeys; *key; key++) {
+ if (line.startsWith(*key)) {
+ skip = true;
+ break;
+ }
+ }
+ if (skip)
+ continue;
+
+ file.write(line);
+ file.write("\n");
+ }
+
+ if (!file.commit()) {
+ finishWithError(QStringLiteral("Failed writing torrc: %1").arg(file.errorString()));
+ return;
+ }
+
+ qDebug() << "torctrl: Wrote torrc file";
+ finishWithSuccess();
+ }
+
+private:
+ GetConfCommand *command;
+};
+
+}
+
+PendingOperation *TorControl::saveConfiguration()
+{
+ SaveConfigOperation *operation = new SaveConfigOperation(this);
+ QObject::connect(operation, &PendingOperation::finished, operation, &QObject::deleteLater);
+ operation->start(d->socket);
+ return operation;
+}
+
+void TorControl::takeOwnership()
+{
+ d->socket->sendCommand("TAKEOWNERSHIP\r\n");
+
+ // Reset PID-based polling
+ QVariantMap options;
+ options[QStringLiteral("__OwningControllerProcess")] = QVariant();
+ setConfiguration(options);
+}
+
+#include "TorControl.moc"
+
diff --git a/src/tor/TorControl.h b/src/tor/TorControl.h
new file mode 100644
index 0000000..15c1ccf
--- /dev/null
+++ b/src/tor/TorControl.h
@@ -0,0 +1,134 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef TORCONTROL_H
+#define TORCONTROL_H
+
+#include <QObject>
+#include <QHostAddress>
+#include "utils/PendingOperation.h"
+
+class QNetworkProxy;
+
+namespace Tor
+{
+
+class HiddenService;
+class TorControlPrivate;
+
+class TorControl : public QObject
+{
+ Q_OBJECT
+ Q_ENUMS(Status TorStatus)
+
+ // Status of the control connection
+ Q_PROPERTY(Status status READ status NOTIFY statusChanged)
+ // Status of Tor (and whether it believes it can connect)
+ Q_PROPERTY(TorStatus torStatus READ torStatus NOTIFY torStatusChanged)
+ // Whether it's possible to make a SOCKS connection and connect
+ Q_PROPERTY(bool hasConnectivity READ hasConnectivity NOTIFY connectivityChanged)
+ Q_PROPERTY(QString torVersion READ torVersion NOTIFY connected)
+ Q_PROPERTY(QString errorMessage READ errorMessage NOTIFY statusChanged)
+ Q_PROPERTY(QVariantMap bootstrapStatus READ bootstrapStatus NOTIFY bootstrapStatusChanged)
+
+public:
+ enum Status
+ {
+ Error = -1,
+ NotConnected,
+ Connecting,
+ Authenticating,
+ Connected
+ };
+
+ enum TorStatus
+ {
+ TorUnknown,
+ TorOffline,
+ TorReady
+ };
+
+ explicit TorControl(QObject *parent = 0);
+
+ /* Information */
+ Status status() const;
+ TorStatus torStatus() const;
+ QString torVersion() const;
+ QString errorMessage() const;
+
+ bool hasConnectivity() const;
+ QHostAddress socksAddress() const;
+ quint16 socksPort() const;
+ QNetworkProxy connectionProxy();
+
+ /* Authentication */
+ void setAuthPassword(const QByteArray &password);
+
+ /* Connection */
+ bool isConnected() const { return status() == Connected; }
+ void connect(const QHostAddress &address, quint16 port);
+ void takeOwnership();
+
+ /* Hidden Services */
+ QList<HiddenService*> hiddenServices() const;
+ void addHiddenService(HiddenService *service);
+
+ QVariantMap bootstrapStatus() const;
+ Q_INVOKABLE QObject *getConfiguration(const QString &options);
+ Q_INVOKABLE QObject *setConfiguration(const QVariantMap &options);
+ Q_INVOKABLE PendingOperation *saveConfiguration();
+
+signals:
+ void statusChanged(int newStatus, int oldStatus);
+ void torStatusChanged(int newStatus, int oldStatus);
+ void connected();
+ void disconnected();
+ void connectivityChanged();
+ void bootstrapStatusChanged();
+
+public slots:
+ /* Instruct Tor to shutdown */
+ void shutdown();
+ /* Call shutdown(), and wait synchronously for the command to be written */
+ void shutdownSync();
+
+ void reconnect();
+
+private:
+ TorControlPrivate *d;
+};
+
+}
+
+extern Tor::TorControl *torControl;
+
+#endif // TORCONTROLMANAGER_H
diff --git a/src/tor/TorControlCommand.cpp b/src/tor/TorControlCommand.cpp
new file mode 100644
index 0000000..48d4aab
--- /dev/null
+++ b/src/tor/TorControlCommand.cpp
@@ -0,0 +1,63 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "TorControlCommand.h"
+#include <QDebug>
+
+using namespace Tor;
+
+TorControlCommand::TorControlCommand()
+ : m_finalStatus(0)
+{
+}
+
+void TorControlCommand::onReply(int statusCode, const QByteArray &data)
+{
+ emit replyLine(statusCode, data);
+}
+
+void TorControlCommand::onFinished(int statusCode)
+{
+ m_finalStatus = statusCode;
+ emit finished();
+}
+
+void TorControlCommand::onDataLine(const QByteArray &data)
+{
+ Q_UNUSED(data);
+}
+
+void TorControlCommand::onDataFinished()
+{
+ qWarning() << "torctrl: Unexpected data response for command";
+}
+
diff --git a/src/tor/TorControlCommand.h b/src/tor/TorControlCommand.h
new file mode 100644
index 0000000..8943810
--- /dev/null
+++ b/src/tor/TorControlCommand.h
@@ -0,0 +1,70 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef TORCONTROLCOMMAND_H
+#define TORCONTROLCOMMAND_H
+
+#include <QObject>
+#include <QByteArray>
+
+namespace Tor
+{
+
+class TorControlCommand : public QObject
+{
+ Q_OBJECT
+ Q_DISABLE_COPY(TorControlCommand)
+
+ friend class TorControlSocket;
+
+public:
+ TorControlCommand();
+
+ int statusCode() const { return m_finalStatus; }
+
+signals:
+ void replyLine(int statusCode, const QByteArray &data);
+ void finished();
+
+protected:
+ virtual void onReply(int statusCode, const QByteArray &data);
+ virtual void onFinished(int statusCode);
+ virtual void onDataLine(const QByteArray &data);
+ virtual void onDataFinished();
+
+private:
+ int m_finalStatus;
+};
+
+}
+
+#endif // TORCONTROLCOMMAND_H
diff --git a/src/tor/TorControlSocket.cpp b/src/tor/TorControlSocket.cpp
new file mode 100644
index 0000000..33b411c
--- /dev/null
+++ b/src/tor/TorControlSocket.cpp
@@ -0,0 +1,176 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "TorControlSocket.h"
+#include "TorControlCommand.h"
+#include <QDebug>
+
+using namespace Tor;
+
+TorControlSocket::TorControlSocket(QObject *parent)
+ : QTcpSocket(parent), currentCommand(0), inDataReply(false)
+{
+ connect(this, SIGNAL(readyRead()), this, SLOT(process()));
+ connect(this, SIGNAL(disconnected()), this, SLOT(clear()));
+}
+
+TorControlSocket::~TorControlSocket()
+{
+ clear();
+}
+
+void TorControlSocket::sendCommand(TorControlCommand *command, const QByteArray &data)
+{
+ Q_ASSERT(data.endsWith("\r\n"));
+
+ commandQueue.append(command);
+ write(data);
+
+ qDebug() << "torctrl: Sent" << data.trimmed();
+}
+
+void TorControlSocket::registerEvent(const QByteArray &event, TorControlCommand *command)
+{
+ eventCommands.insert(event, command);
+
+ QByteArray data("SETEVENTS");
+ foreach (const QByteArray &key, eventCommands.keys()) {
+ data += ' ';
+ data += key;
+ }
+ data += "\r\n";
+
+ sendCommand(data);
+}
+
+void TorControlSocket::clear()
+{
+ qDeleteAll(commandQueue);
+ commandQueue.clear();
+ qDeleteAll(eventCommands);
+ eventCommands.clear();
+ inDataReply = false;
+ currentCommand = 0;
+}
+
+void TorControlSocket::setError(const QString &message)
+{
+ m_errorMessage = message;
+ emit error(message);
+ abort();
+}
+
+void TorControlSocket::process()
+{
+ for (;;) {
+ if (!canReadLine())
+ return;
+
+ QByteArray line = readLine(5120);
+ if (!line.endsWith("\r\n")) {
+ setError(QStringLiteral("Invalid control message syntax"));
+ return;
+ }
+ line.chop(2);
+
+ if (inDataReply) {
+ if (line == ".") {
+ inDataReply = false;
+ if (currentCommand)
+ currentCommand->onDataFinished();
+ currentCommand = 0;
+ } else {
+ if (currentCommand)
+ currentCommand->onDataLine(line);
+ }
+ continue;
+ }
+
+ if (line.size() < 4) {
+ setError(QStringLiteral("Invalid control message syntax"));
+ return;
+ }
+
+ int statusCode = line.left(3).toInt();
+ char type = line[3];
+ bool isFinalReply = (type == ' ');
+ inDataReply = (type == '+');
+
+ // Trim down to just data
+ line = line.mid(4);
+
+ if (!isFinalReply && !inDataReply && type != '-') {
+ setError(QStringLiteral("Invalid control message syntax"));
+ return;
+ }
+
+ // 6xx replies are asynchronous responses
+ if (statusCode >= 600 && statusCode < 700) {
+ if (!currentCommand) {
+ int space = line.indexOf(' ');
+ if (space > 0)
+ currentCommand = eventCommands.value(line.mid(0, space));
+
+ if (!currentCommand) {
+ qWarning() << "torctrl: Ignoring unknown event";
+ continue;
+ }
+ }
+
+ currentCommand->onReply(statusCode, line);
+ if (isFinalReply) {
+ currentCommand->onFinished(statusCode);
+ currentCommand = 0;
+ }
+ continue;
+ }
+
+ if (commandQueue.isEmpty()) {
+ qWarning() << "torctrl: Received unexpected data";
+ continue;
+ }
+
+ TorControlCommand *command = commandQueue.first();
+ if (command)
+ command->onReply(statusCode, line);
+
+ if (inDataReply) {
+ currentCommand = command;
+ } else if (isFinalReply) {
+ commandQueue.takeFirst();
+ if (command) {
+ command->onFinished(statusCode);
+ command->deleteLater();
+ }
+ }
+ }
+}
diff --git a/src/tor/TorControlSocket.h b/src/tor/TorControlSocket.h
new file mode 100644
index 0000000..2db9115
--- /dev/null
+++ b/src/tor/TorControlSocket.h
@@ -0,0 +1,77 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef TORCONTROLSOCKET_H
+#define TORCONTROLSOCKET_H
+
+#include <QTcpSocket>
+#include <QQueue>
+
+namespace Tor
+{
+
+class TorControlCommand;
+
+class TorControlSocket : public QTcpSocket
+{
+Q_OBJECT
+public:
+ explicit TorControlSocket(QObject *parent = 0);
+ virtual ~TorControlSocket();
+
+ QString errorMessage() const { return m_errorMessage; }
+
+ void registerEvent(const QByteArray &event, TorControlCommand *handler);
+
+ void sendCommand(const QByteArray &data) { sendCommand(0, data); }
+ void sendCommand(TorControlCommand *command, const QByteArray &data);
+
+signals:
+ void error(const QString &message);
+
+private slots:
+ void process();
+ void clear();
+
+private:
+ QQueue<TorControlCommand*> commandQueue;
+ QHash<QByteArray,TorControlCommand*> eventCommands;
+ QString m_errorMessage;
+ TorControlCommand *currentCommand;
+ bool inDataReply;
+
+ void setError(const QString &message);
+};
+
+}
+
+#endif // TORCONTROLSOCKET_H
diff --git a/src/tor/TorManager.cpp b/src/tor/TorManager.cpp
new file mode 100644
index 0000000..f63574b
--- /dev/null
+++ b/src/tor/TorManager.cpp
@@ -0,0 +1,306 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "TorManager.h"
+#include "TorProcess.h"
+#include "TorControl.h"
+#include "GetConfCommand.h"
+#include "utils/Settings.h"
+#include <QFile>
+#include <QDir>
+#include <QCoreApplication>
+
+using namespace Tor;
+
+namespace Tor
+{
+
+class TorManagerPrivate : public QObject
+{
+ Q_OBJECT
+
+public:
+ TorManager *q;
+ TorProcess *process;
+ TorControl *control;
+ QString dataDir;
+ QStringList logMessages;
+ QString errorMessage;
+ bool configNeeded;
+
+ explicit TorManagerPrivate(TorManager *parent = 0);
+
+ QString torExecutablePath() const;
+ bool createDataDir(const QString &path);
+ bool createDefaultTorrc(const QString &path);
+
+ void setError(const QString &errorMessage);
+
+public slots:
+ void processStateChanged(int state);
+ void processErrorChanged(const QString &errorMessage);
+ void processLogMessage(const QString &message);
+ void controlStatusChanged(int status);
+ void getConfFinished();
+};
+
+}
+
+TorManager::TorManager(QObject *parent)
+ : QObject(parent), d(new TorManagerPrivate(this))
+{
+}
+
+TorManagerPrivate::TorManagerPrivate(TorManager *parent)
+ : QObject(parent)
+ , q(parent)
+ , process(0)
+ , control(new TorControl(this))
+ , configNeeded(false)
+{
+ connect(control, SIGNAL(statusChanged(int,int)), SLOT(controlStatusChanged(int)));
+}
+
+TorManager *TorManager::instance()
+{
+ static TorManager *p = 0;
+ if (!p)
+ p = new TorManager(qApp);
+ return p;
+}
+
+TorControl *TorManager::control()
+{
+ return d->control;
+}
+
+TorProcess *TorManager::process()
+{
+ return d->process;
+}
+
+QString TorManager::dataDirectory() const
+{
+ return d->dataDir;
+}
+
+void TorManager::setDataDirectory(const QString &path)
+{
+ d->dataDir = QDir::fromNativeSeparators(path);
+ if (!d->dataDir.isEmpty() && !d->dataDir.endsWith(QLatin1Char('/')))
+ d->dataDir.append(QLatin1Char('/'));
+}
+
+bool TorManager::configurationNeeded() const
+{
+ return d->configNeeded;
+}
+
+QStringList TorManager::logMessages() const
+{
+ return d->logMessages;
+}
+
+bool TorManager::hasError() const
+{
+ return !d->errorMessage.isEmpty();
+}
+
+QString TorManager::errorMessage() const
+{
+ return d->errorMessage;
+}
+
+void TorManager::start()
+{
+ if (!d->errorMessage.isEmpty()) {
+ d->errorMessage.clear();
+ emit errorChanged();
+ }
+
+ SettingsObject settings(QStringLiteral("tor"));
+ if (settings.read("controlPort").isUndefined()) {
+ // Launch a bundled Tor instance
+ QString executable = d->torExecutablePath();
+ if (executable.isEmpty()) {
+ d->setError(QStringLiteral("Cannot find tor executable"));
+ return;
+ }
+
+ if (!d->process) {
+ d->process = new TorProcess(this);
+ connect(d->process, SIGNAL(stateChanged(int)), d, SLOT(processStateChanged(int)));
+ connect(d->process, SIGNAL(errorMessageChanged(QString)), d,
+ SLOT(processErrorChanged(QString)));
+ connect(d->process, SIGNAL(logMessage(QString)), d, SLOT(processLogMessage(QString)));
+ }
+
+ if (!QFile::exists(d->dataDir) && !d->createDataDir(d->dataDir)) {
+ d->setError(QStringLiteral("Cannot write data location: %1").arg(d->dataDir));
+ return;
+ }
+
+ QString defaultTorrc = d->dataDir + QStringLiteral("default_torrc");
+ if (!QFile::exists(defaultTorrc) && !d->createDefaultTorrc(defaultTorrc)) {
+ d->setError(QStringLiteral("Cannot write data files: %1").arg(defaultTorrc));
+ return;
+ }
+
+ QFile torrc(d->dataDir + QStringLiteral("torrc"));
+ if (!torrc.exists() || torrc.size() == 0) {
+ d->configNeeded = true;
+ emit configurationNeededChanged();
+ }
+
+ d->process->setExecutable(executable);
+ d->process->setDataDir(d->dataDir);
+ d->process->setDefaultTorrc(defaultTorrc);
+ d->process->start();
+ } else {
+ QHostAddress address(settings.read("controlAddress").toString());
+ quint16 port = (quint16)settings.read("controlPort").toInt();
+ if (address.isNull())
+ address = QHostAddress::LocalHost;
+
+ d->control->setAuthPassword(settings.read("controlPassword").toString().toLatin1());
+ d->control->connect(address, port);
+ }
+}
+
+void TorManagerPrivate::processStateChanged(int state)
+{
+ qDebug() << Q_FUNC_INFO << state << TorProcess::Ready << process->controlPassword() << process->controlHost() << process->controlPort();
+ if (state == TorProcess::Ready) {
+ control->setAuthPassword(process->controlPassword());
+ control->connect(process->controlHost(), process->controlPort());
+ }
+}
+
+void TorManagerPrivate::processErrorChanged(const QString &errorMessage)
+{
+ qDebug() << "tor error:" << errorMessage;
+ setError(errorMessage);
+}
+
+void TorManagerPrivate::processLogMessage(const QString &message)
+{
+ qDebug() << "tor:" << message;
+ if (logMessages.size() >= 50)
+ logMessages.takeFirst();
+ logMessages.append(message);
+}
+
+void TorManagerPrivate::controlStatusChanged(int status)
+{
+ if (status == TorControl::Connected) {
+ if (!configNeeded) {
+ // If DisableNetwork is 1, trigger configurationNeeded
+ connect(control->getConfiguration(QStringLiteral("DisableNetwork")),
+ SIGNAL(finished()), SLOT(getConfFinished()));
+ }
+
+ if (process) {
+ // Take ownership via this control socket
+ control->takeOwnership();
+ }
+ }
+}
+
+void TorManagerPrivate::getConfFinished()
+{
+ GetConfCommand *command = qobject_cast<GetConfCommand*>(sender());
+ if (!command)
+ return;
+
+ if (command->get("DisableNetwork").toInt() == 1 && !configNeeded) {
+ configNeeded = true;
+ emit q->configurationNeededChanged();
+ }
+}
+
+QString TorManagerPrivate::torExecutablePath() const
+{
+ SettingsObject settings(QStringLiteral("tor"));
+ QString path = settings.read("executablePath").toString();
+ if (!path.isEmpty())
+ return path;
+
+#ifdef Q_OS_WIN
+ QString filename(QStringLiteral("/tor.exe"));
+#else
+ QString filename(QStringLiteral("/tor"));
+#endif
+
+ path = qApp->applicationDirPath();
+ if (QFile::exists(path + filename))
+ return path + filename;
+
+#ifdef BUNDLED_TOR_PATH
+ path = QStringLiteral(BUNDLED_TOR_PATH);
+ if (QFile::exists(path + filename))
+ return path + filename;
+#endif
+
+ // Try $PATH
+ return filename.mid(1);
+}
+
+bool TorManagerPrivate::createDataDir(const QString &path)
+{
+ QDir dir(path);
+ return dir.mkpath(QStringLiteral("."));
+}
+
+bool TorManagerPrivate::createDefaultTorrc(const QString &path)
+{
+ static const char defaultTorrcContent[] =
+ "SocksPort auto\n"
+ "AvoidDiskWrites 1\n"
+ "DisableNetwork 1\n"
+ "__ReloadTorrcOnSIGHUP 0\n";
+
+ QFile file(path);
+ if (!file.open(QIODevice::WriteOnly))
+ return false;
+ if (file.write(defaultTorrcContent) < 0)
+ return false;
+ return true;
+}
+
+void TorManagerPrivate::setError(const QString &message)
+{
+ errorMessage = message;
+ emit q->errorChanged();
+}
+
+#include "TorManager.moc"
+
diff --git a/src/tor/TorManager.h b/src/tor/TorManager.h
new file mode 100644
index 0000000..1de5133
--- /dev/null
+++ b/src/tor/TorManager.h
@@ -0,0 +1,92 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef TORMANAGER_H
+#define TORMANAGER_H
+
+#include <QObject>
+#include <QStringList>
+
+namespace Tor
+{
+
+class TorProcess;
+class TorControl;
+class TorManagerPrivate;
+
+/* Run/connect to an instance of Tor according to configuration, and manage
+ * UI interaction, first time configuration, etc. */
+class TorManager : public QObject
+{
+ Q_OBJECT
+
+ Q_PROPERTY(bool configurationNeeded READ configurationNeeded NOTIFY configurationNeededChanged)
+ Q_PROPERTY(QStringList logMessages READ logMessages CONSTANT)
+ Q_PROPERTY(Tor::TorProcess* process READ process CONSTANT)
+ Q_PROPERTY(Tor::TorControl* control READ control CONSTANT)
+ Q_PROPERTY(bool hasError READ hasError NOTIFY errorChanged)
+ Q_PROPERTY(QString errorMessage READ errorMessage NOTIFY errorChanged)
+ Q_PROPERTY(QString dataDirectory READ dataDirectory WRITE setDataDirectory)
+
+public:
+ explicit TorManager(QObject *parent = 0);
+ static TorManager *instance();
+
+ TorProcess *process();
+ TorControl *control();
+
+ QString dataDirectory() const;
+ void setDataDirectory(const QString &path);
+
+ // True on first run or when the Tor configuration wizard needs to be shown
+ bool configurationNeeded() const;
+
+ QStringList logMessages() const;
+
+ bool hasError() const;
+ QString errorMessage() const;
+
+public slots:
+ void start();
+
+signals:
+ void configurationNeededChanged();
+ void errorChanged();
+
+private:
+ TorManagerPrivate *d;
+};
+
+}
+
+#endif
+#
diff --git a/src/tor/TorProcess.cpp b/src/tor/TorProcess.cpp
new file mode 100644
index 0000000..0601578
--- /dev/null
+++ b/src/tor/TorProcess.cpp
@@ -0,0 +1,311 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "TorProcess_p.h"
+#include "utils/CryptoKey.h"
+#include "utils/SecureRNG.h"
+#include <QDir>
+#include <QDebug>
+#include <QCoreApplication>
+
+using namespace Tor;
+
+TorProcess::TorProcess(QObject *parent)
+ : QObject(parent), d(new TorProcessPrivate(this))
+{
+}
+
+TorProcess::~TorProcess()
+{
+ if (state() > NotStarted)
+ stop();
+}
+
+TorProcessPrivate::TorProcessPrivate(TorProcess *q)
+ : QObject(q), q(q), state(TorProcess::NotStarted), controlPort(0), controlPortAttempts(0)
+{
+ connect(&process, &QProcess::started, this, &TorProcessPrivate::processStarted);
+ connect(&process, (void (QProcess::*)(int, QProcess::ExitStatus))&QProcess::finished,
+ this, &TorProcessPrivate::processFinished);
+ connect(&process, (void (QProcess::*)(QProcess::ProcessError))&QProcess::error,
+ this, &TorProcessPrivate::processError);
+ connect(&process, &QProcess::readyRead, this, &TorProcessPrivate::processReadable);
+
+ controlPortTimer.setInterval(500);
+ connect(&controlPortTimer, &QTimer::timeout, this, &TorProcessPrivate::tryReadControlPort);
+}
+
+QString TorProcess::executable() const
+{
+ return d->executable;
+}
+
+void TorProcess::setExecutable(const QString &path)
+{
+ d->executable = path;
+}
+
+QString TorProcess::dataDir() const
+{
+ return d->dataDir;
+}
+
+void TorProcess::setDataDir(const QString &path)
+{
+ d->dataDir = path;
+}
+
+QString TorProcess::defaultTorrc() const
+{
+ return d->defaultTorrc;
+}
+
+void TorProcess::setDefaultTorrc(const QString &path)
+{
+ d->defaultTorrc = path;
+}
+
+QStringList TorProcess::extraSettings() const
+{
+ return d->extraSettings;
+}
+
+void TorProcess::setExtraSettings(const QStringList &settings)
+{
+ d->extraSettings = settings;
+}
+
+TorProcess::State TorProcess::state() const
+{
+ return d->state;
+}
+
+QString TorProcess::errorMessage() const
+{
+ return d->errorMessage;
+}
+
+void TorProcess::start()
+{
+ if (state() > NotStarted)
+ return;
+
+ d->errorMessage.clear();
+
+ if (d->executable.isEmpty() || d->dataDir.isEmpty()) {
+ d->errorMessage = QStringLiteral("Tor executable and data directory not specified");
+ d->state = Failed;
+ emit errorMessageChanged(d->errorMessage);
+ emit stateChanged(d->state);
+ return;
+ }
+
+ if (!d->ensureFilesExist()) {
+ d->state = Failed;
+ emit errorMessageChanged(d->errorMessage);
+ emit stateChanged(d->state);
+ return;
+ }
+
+ QByteArray password = controlPassword();
+ QByteArray hashedPassword = torControlHashedPassword(password);
+ if (password.isEmpty() || hashedPassword.isEmpty()) {
+ d->errorMessage = QStringLiteral("Random password generation failed");
+ d->state = Failed;
+ emit errorMessageChanged(d->errorMessage);
+ emit stateChanged(d->state);
+ }
+
+ QStringList args;
+ if (!d->defaultTorrc.isEmpty())
+ args << QStringLiteral("--defaults-torrc") << d->defaultTorrc;
+ args << QStringLiteral("-f") << d->torrcPath();
+ args << QStringLiteral("DataDirectory") << d->dataDir;
+ args << QStringLiteral("HashedControlPassword") << QString::fromLatin1(hashedPassword);
+ args << QStringLiteral("ControlPort") << QStringLiteral("auto");
+ args << QStringLiteral("ControlPortWriteToFile") << d->controlPortFilePath();
+ args << QStringLiteral("__OwningControllerProcess") << QString::number(qApp->applicationPid());
+ args << d->extraSettings;
+
+ d->state = Starting;
+ emit stateChanged(d->state);
+
+ if (QFile::exists(d->controlPortFilePath()))
+ QFile::remove(d->controlPortFilePath());
+ d->controlPort = 0;
+ d->controlHost.clear();
+
+ d->process.setProcessChannelMode(QProcess::MergedChannels);
+ d->process.start(d->executable, args, QIODevice::ReadOnly);
+}
+
+void TorProcess::stop()
+{
+ if (state() < Starting)
+ return;
+
+ d->controlPortTimer.stop();
+
+ if (d->process.state() == QProcess::Starting)
+ d->process.waitForStarted(2000);
+
+ d->state = NotStarted;
+
+ // Windows can't terminate the process well, but Tor will clean itself up
+#ifndef Q_OS_WIN
+ if (d->process.state() == QProcess::Running) {
+ d->process.terminate();
+ if (!d->process.waitForFinished(5000)) {
+ qWarning() << "Tor process" << d->process.pid() << "did not respond to terminate, killing...";
+ d->process.kill();
+ if (!d->process.waitForFinished(2000)) {
+ qCritical() << "Tor process" << d->process.pid() << "did not respond to kill!";
+ }
+ }
+ }
+#endif
+
+ emit stateChanged(d->state);
+}
+
+QByteArray TorProcess::controlPassword()
+{
+ if (d->controlPassword.isEmpty())
+ d->controlPassword = SecureRNG::randomPrintable(16);
+ return d->controlPassword;
+}
+
+QHostAddress TorProcess::controlHost()
+{
+ return d->controlHost;
+}
+
+quint16 TorProcess::controlPort()
+{
+ return d->controlPort;
+}
+
+bool TorProcessPrivate::ensureFilesExist()
+{
+ QFile torrc(torrcPath());
+ if (!torrc.exists()) {
+ QDir dir(dataDir);
+ if (!dir.exists() && !dir.mkpath(QStringLiteral("."))) {
+ errorMessage = QStringLiteral("Cannot create Tor data directory: %1").arg(dataDir);
+ return false;
+ }
+
+ if (!torrc.open(QIODevice::ReadWrite)) {
+ errorMessage = QStringLiteral("Cannot create Tor configuration file: %1").arg(torrcPath());
+ return false;
+ }
+ }
+
+ return true;
+}
+
+QString TorProcessPrivate::torrcPath() const
+{
+ return QDir::toNativeSeparators(dataDir) + QDir::separator() + QStringLiteral("torrc");
+}
+
+QString TorProcessPrivate::controlPortFilePath() const
+{
+ return QDir::toNativeSeparators(dataDir) + QDir::separator() + QStringLiteral("control-port");
+}
+
+void TorProcessPrivate::processStarted()
+{
+ state = TorProcess::Connecting;
+ emit q->stateChanged(state);
+
+ controlPortAttempts = 0;
+ controlPortTimer.start();
+}
+
+void TorProcessPrivate::processFinished()
+{
+ if (state < TorProcess::Starting)
+ return;
+
+ controlPortTimer.stop();
+ errorMessage = process.errorString();
+ if (errorMessage.isEmpty())
+ errorMessage = QStringLiteral("Process exited unexpectedly (code %1)").arg(process.exitCode());
+ state = TorProcess::Failed;
+ emit q->errorMessageChanged(errorMessage);
+ emit q->stateChanged(state);
+}
+
+void TorProcessPrivate::processError(QProcess::ProcessError error)
+{
+ if (error == QProcess::FailedToStart || error == QProcess::Crashed)
+ processFinished();
+}
+
+void TorProcessPrivate::processReadable()
+{
+ while (process.bytesAvailable() > 0) {
+ QByteArray line = process.readLine(2048).trimmed();
+ if (!line.isEmpty())
+ emit q->logMessage(QString::fromLatin1(line));
+ }
+}
+
+void TorProcessPrivate::tryReadControlPort()
+{
+ QFile file(controlPortFilePath());
+ if (file.open(QIODevice::ReadOnly)) {
+ QByteArray data = file.readLine().trimmed();
+
+ int p;
+ if (data.startsWith("PORT=") && (p = data.lastIndexOf(':')) > 0) {
+ controlHost = QHostAddress(QString::fromLatin1(data.mid(5, p - 5)));
+ controlPort = data.mid(p+1).toUShort();
+
+ if (!controlHost.isNull() && controlPort > 0) {
+ controlPortTimer.stop();
+ state = TorProcess::Ready;
+ emit q->stateChanged(state);
+ return;
+ }
+ }
+ }
+
+ if (++controlPortAttempts * controlPortTimer.interval() > 10000) {
+ errorMessage = QStringLiteral("No control port available after launching process");
+ state = TorProcess::Failed;
+ emit q->errorMessageChanged(errorMessage);
+ emit q->stateChanged(state);
+ }
+}
+
diff --git a/src/tor/TorProcess.h b/src/tor/TorProcess.h
new file mode 100644
index 0000000..ad489dc
--- /dev/null
+++ b/src/tor/TorProcess.h
@@ -0,0 +1,100 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef TORPROCESS_H
+#define TORPROCESS_H
+
+#include <QObject>
+#include <QHostAddress>
+
+namespace Tor
+{
+
+class TorProcessPrivate;
+
+/* Launches and controls a Tor instance with behavior suitable for bundling
+ * an instance with the application. */
+class TorProcess : public QObject
+{
+ Q_OBJECT
+ Q_ENUMS(State)
+
+ Q_PROPERTY(State state READ state NOTIFY stateChanged)
+ Q_PROPERTY(QString errorMessage READ errorMessage NOTIFY errorMessageChanged)
+
+public:
+ enum State {
+ Failed = -1,
+ NotStarted,
+ Starting,
+ Connecting,
+ Ready
+ };
+
+ explicit TorProcess(QObject *parent = 0);
+ virtual ~TorProcess();
+
+ QString executable() const;
+ void setExecutable(const QString &path);
+
+ QString dataDir() const;
+ void setDataDir(const QString &path);
+
+ QString defaultTorrc() const;
+ void setDefaultTorrc(const QString &path);
+
+ QStringList extraSettings() const;
+ void setExtraSettings(const QStringList &settings);
+
+ State state() const;
+ QString errorMessage() const;
+ QHostAddress controlHost();
+ quint16 controlPort();
+ QByteArray controlPassword();
+
+public slots:
+ void start();
+ void stop();
+
+signals:
+ void stateChanged(int newState);
+ void errorMessageChanged(const QString &errorMessage);
+ void logMessage(const QString &message);
+
+private:
+ TorProcessPrivate *d;
+};
+
+}
+
+#endif
+
diff --git a/src/tor/TorProcess_p.h b/src/tor/TorProcess_p.h
new file mode 100644
index 0000000..9aa2585
--- /dev/null
+++ b/src/tor/TorProcess_p.h
@@ -0,0 +1,79 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef TORPROCESS_P_H
+#define TORPROCESS_P_H
+
+#include "TorProcess.h"
+#include <QProcess>
+#include <QTimer>
+
+namespace Tor {
+
+class TorProcessPrivate : public QObject
+{
+ Q_OBJECT
+
+public:
+ TorProcess *q;
+ QProcess process;
+ QString executable;
+ QString dataDir;
+ QString defaultTorrc;
+ QStringList extraSettings;
+ TorProcess::State state;
+ QString errorMessage;
+ QHostAddress controlHost;
+ quint16 controlPort;
+ QByteArray controlPassword;
+
+ QTimer controlPortTimer;
+ int controlPortAttempts;
+
+ TorProcessPrivate(TorProcess *q);
+
+ QString torrcPath() const;
+ QString controlPortFilePath() const;
+ bool ensureFilesExist();
+
+public slots:
+ void processStarted();
+ void processFinished();
+ void processError(QProcess::ProcessError error);
+ void processReadable();
+ void tryReadControlPort();
+};
+
+}
+
+#endif
+
diff --git a/src/tor/TorSocket.cpp b/src/tor/TorSocket.cpp
new file mode 100644
index 0000000..c093dcc
--- /dev/null
+++ b/src/tor/TorSocket.cpp
@@ -0,0 +1,151 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "TorSocket.h"
+#include "TorControl.h"
+#include <QNetworkProxy>
+
+using namespace Tor;
+
+TorSocket::TorSocket(QObject *parent)
+ : QTcpSocket(parent)
+ , m_port(0)
+ , m_reconnectEnabled(true)
+ , m_maxInterval(900)
+ , m_connectAttempts(0)
+{
+ connect(torControl, SIGNAL(connectivityChanged()), SLOT(connectivityChanged()));
+ connect(&m_connectTimer, SIGNAL(timeout()), SLOT(reconnect()));
+ connect(this, SIGNAL(disconnected()), SLOT(onFailed()));
+ connect(this, SIGNAL(error(QAbstractSocket::SocketError)), SLOT(onFailed()));
+
+ m_connectTimer.setSingleShot(true);
+ connectivityChanged();
+}
+
+TorSocket::~TorSocket()
+{
+}
+
+void TorSocket::setReconnectEnabled(bool enabled)
+{
+ if (enabled == m_reconnectEnabled)
+ return;
+
+ m_reconnectEnabled = enabled;
+ if (m_reconnectEnabled) {
+ m_connectAttempts = 0;
+ reconnect();
+ } else {
+ m_connectTimer.stop();
+ }
+}
+
+void TorSocket::setMaxAttemptInterval(int interval)
+{
+ m_maxInterval = interval;
+}
+
+void TorSocket::resetAttempts()
+{
+ m_connectAttempts = 0;
+ if (m_connectTimer.isActive()) {
+ m_connectTimer.stop();
+ m_connectTimer.start(reconnectInterval() * 1000);
+ }
+}
+
+int TorSocket::reconnectInterval()
+{
+ int delay = 0;
+ if (m_connectAttempts <= 4)
+ delay = 30;
+ else if (m_connectAttempts <= 6)
+ delay = 120;
+ else
+ delay = m_maxInterval;
+
+ return qMin(delay, m_maxInterval);
+}
+
+void TorSocket::reconnect()
+{
+ if (!torControl->hasConnectivity() || !reconnectEnabled())
+ return;
+
+ m_connectTimer.stop();
+ if (!m_host.isEmpty() && m_port) {
+ qDebug() << "Attempting reconnection of socket to" << m_host << m_port;
+ connectToHost(m_host, m_port);
+ }
+}
+
+void TorSocket::connectivityChanged()
+{
+ if (torControl->hasConnectivity()) {
+ setProxy(torControl->connectionProxy());
+ if (state() == QAbstractSocket::UnconnectedState)
+ reconnect();
+ } else {
+ m_connectTimer.stop();
+ m_connectAttempts = 0;
+ }
+}
+
+void TorSocket::connectToHost(const QString &hostName, quint16 port, OpenMode openMode,
+ NetworkLayerProtocol protocol)
+{
+ m_host = hostName;
+ m_port = port;
+
+ if (!torControl->hasConnectivity())
+ return;
+
+ if (proxy() != torControl->connectionProxy())
+ setProxy(torControl->connectionProxy());
+
+ QAbstractSocket::connectToHost(hostName, port, openMode, protocol);
+}
+
+void TorSocket::connectToHost(const QHostAddress &address, quint16 port, OpenMode openMode)
+{
+ TorSocket::connectToHost(address.toString(), port, openMode);
+}
+
+void TorSocket::onFailed()
+{
+ if (reconnectEnabled() && !m_connectTimer.isActive()) {
+ m_connectAttempts++;
+ m_connectTimer.start(reconnectInterval() * 1000);
+ qDebug() << "Reconnecting socket to" << m_host << m_port << "in" << m_connectTimer.interval() / 1000 << "seconds";
+ }
+}
diff --git a/src/tor/TorSocket.h b/src/tor/TorSocket.h
new file mode 100644
index 0000000..0c68f78
--- /dev/null
+++ b/src/tor/TorSocket.h
@@ -0,0 +1,97 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef TORSOCKET_H
+#define TORSOCKET_H
+
+#include <QTcpSocket>
+#include <QTimer>
+
+namespace Tor {
+
+/* Specialized QTcpSocket which makes connections over the SOCKS proxy
+ * from a TorControl instance, automatically attempts reconnections, and
+ * reacts to Tor's connectivity state.
+ *
+ * Use normal QTcpSocket/QAbstractSocket API. When a connection fails, it
+ * will be retried automatically after the correct interval and when
+ * connectivity is available.
+ *
+ * To fully disconnect, destroy the object, or call
+ * setReconnectEnabled(false) and disconnect the socket with
+ * disconnectFromHost or abort.
+ *
+ * The caller is responsible for resetting the attempt counter if a
+ * connection was successful and reconnection will be used again.
+ */
+class TorSocket : public QTcpSocket
+{
+ Q_OBJECT
+
+public:
+ explicit TorSocket(QObject *parent = 0);
+ virtual ~TorSocket();
+
+ bool reconnectEnabled() const { return m_reconnectEnabled; }
+ void setReconnectEnabled(bool enabled);
+ int maxAttemptInterval() { return m_maxInterval; }
+ void setMaxAttemptInterval(int interval);
+ void resetAttempts();
+
+ virtual void connectToHost(const QString &hostName, quint16 port, OpenMode openMode = ReadWrite, NetworkLayerProtocol protocol = AnyIPProtocol);
+ virtual void connectToHost(const QHostAddress &address, quint16 port, OpenMode openMode = ReadWrite);
+
+ QString hostName() const { return m_host; }
+ quint16 port() const { return m_port; }
+
+protected:
+ virtual int reconnectInterval();
+
+private slots:
+ void reconnect();
+ void connectivityChanged();
+ void onFailed();
+
+private:
+ QString m_host;
+ quint16 m_port;
+ QTimer m_connectTimer;
+ bool m_reconnectEnabled;
+ int m_maxInterval;
+ int m_connectAttempts;
+
+ using QAbstractSocket::connectToHost;
+};
+
+}
+
+#endif
diff --git a/src/ui/ContactsModel.cpp b/src/ui/ContactsModel.cpp
new file mode 100644
index 0000000..6a9a878
--- /dev/null
+++ b/src/ui/ContactsModel.cpp
@@ -0,0 +1,194 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ContactsModel.h"
+#include "core/IdentityManager.h"
+#include "core/ContactsManager.h"
+#include <QDebug>
+
+inline bool contactSort(const ContactUser *c1, const ContactUser *c2)
+{
+ if (c1->status() != c2->status())
+ return c1->status() < c2->status();
+ return c1->nickname().localeAwareCompare(c2->nickname()) < 0;
+}
+
+ContactsModel::ContactsModel(QObject *parent)
+ : QAbstractListModel(parent), m_identity(0)
+{
+}
+
+void ContactsModel::setIdentity(UserIdentity *identity)
+{
+ if (identity == m_identity)
+ return;
+
+ beginResetModel();
+
+ foreach (ContactUser *user, contacts)
+ user->disconnect(this);
+ contacts.clear();
+
+ if (m_identity) {
+ disconnect(m_identity, 0, this, 0);
+ disconnect(&m_identity->contacts, 0, this, 0);
+ }
+
+ m_identity = identity;
+
+ if (m_identity) {
+ connect(&identity->contacts, SIGNAL(contactAdded(ContactUser*)), SLOT(contactAdded(ContactUser*)));
+
+ contacts = identity->contacts.contacts();
+ std::sort(contacts.begin(), contacts.end(), contactSort);
+
+ foreach (ContactUser *user, contacts)
+ connectSignals(user);
+ }
+
+ endResetModel();
+ emit identityChanged();
+}
+
+QModelIndex ContactsModel::indexOfContact(ContactUser *user) const
+{
+ int row = contacts.indexOf(user);
+ if (row < 0)
+ return QModelIndex();
+ return index(row, 0);
+}
+
+ContactUser *ContactsModel::contact(int row) const
+{
+ return contacts.value(row);
+}
+
+void ContactsModel::updateUser(ContactUser *user)
+{
+ if (!user)
+ {
+ user = qobject_cast<ContactUser*>(sender());
+ if (!user)
+ return;
+ }
+
+ int row = contacts.indexOf(user);
+ if (row < 0)
+ {
+ user->disconnect(this);
+ return;
+ }
+
+ QList<ContactUser*> sorted = contacts;
+ std::sort(sorted.begin(), sorted.end(), contactSort);
+ int newRow = sorted.indexOf(user);
+
+ if (row != newRow)
+ {
+ beginMoveRows(QModelIndex(), row, row, QModelIndex(), (newRow > row) ? (newRow+1) : newRow);
+ contacts = sorted;
+ endMoveRows();
+ }
+ emit dataChanged(index(newRow, 0), index(newRow, 0));
+}
+
+void ContactsModel::connectSignals(ContactUser *user)
+{
+ connect(user, SIGNAL(statusChanged()), SLOT(updateUser()));
+ connect(user, SIGNAL(nicknameChanged()), SLOT(updateUser()));
+ connect(user, SIGNAL(contactDeleted(ContactUser*)), SLOT(contactRemoved(ContactUser*)));
+}
+
+void ContactsModel::contactAdded(ContactUser *user)
+{
+ Q_ASSERT(!indexOfContact(user).isValid());
+
+ connectSignals(user);
+
+ QList<ContactUser*>::Iterator lp = qLowerBound(contacts.begin(), contacts.end(), user, contactSort);
+ int row = lp - contacts.begin();
+
+ beginInsertRows(QModelIndex(), row, row);
+ contacts.insert(lp, user);
+ endInsertRows();
+}
+
+void ContactsModel::contactRemoved(ContactUser *user)
+{
+ if (!user && !(user = qobject_cast<ContactUser*>(sender())))
+ return;
+
+ int row = contacts.indexOf(user);
+ beginRemoveRows(QModelIndex(), row, row);
+ contacts.removeAt(row);
+ endRemoveRows();
+
+ disconnect(user, 0, this, 0);
+}
+
+QHash<int,QByteArray> ContactsModel::roleNames() const
+{
+ QHash<int, QByteArray> roles;
+ roles[Qt::DisplayRole] = "name";
+ roles[PointerRole] = "contact";
+ roles[StatusRole] = "status";
+ return roles;
+}
+
+int ContactsModel::rowCount(const QModelIndex &parent) const
+{
+ if (parent.isValid())
+ return 0;
+ return contacts.size();
+}
+
+QVariant ContactsModel::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid() || index.row() >= contacts.size())
+ return QVariant();
+
+ ContactUser *user = contacts[index.row()];
+
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ case Qt::EditRole:
+ return user->nickname();
+ case PointerRole:
+ return QVariant::fromValue(user);
+ case StatusRole:
+ return user->status();
+ }
+
+ return QVariant();
+}
+
diff --git a/src/ui/ContactsModel.h b/src/ui/ContactsModel.h
new file mode 100644
index 0000000..3d60a0d
--- /dev/null
+++ b/src/ui/ContactsModel.h
@@ -0,0 +1,85 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef CONTACTSMODEL_H
+#define CONTACTSMODEL_H
+
+#include <QAbstractListModel>
+#include <QList>
+
+class UserIdentity;
+class ContactUser;
+
+class ContactsModel : public QAbstractListModel
+{
+ Q_OBJECT
+ Q_DISABLE_COPY(ContactsModel)
+
+ Q_PROPERTY(UserIdentity* identity READ identity WRITE setIdentity NOTIFY identityChanged)
+
+public:
+ enum
+ {
+ PointerRole = Qt::UserRole,
+ StatusRole,
+ AlertRole /* bool */
+ };
+
+ explicit ContactsModel(QObject *parent = 0);
+
+ UserIdentity *identity() const { return m_identity; }
+ void setIdentity(UserIdentity *identity);
+
+ Q_INVOKABLE QModelIndex indexOfContact(ContactUser *user) const;
+ Q_INVOKABLE int rowOfContact(ContactUser *user) const { return indexOfContact(user).row(); }
+ Q_INVOKABLE ContactUser *contact(int row) const;
+
+ virtual int rowCount(const QModelIndex &parent = QModelIndex()) const;
+ virtual QHash<int,QByteArray> roleNames() const;
+ virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
+
+signals:
+ void identityChanged();
+
+private slots:
+ void updateUser(ContactUser *user = 0);
+ void contactAdded(ContactUser *user);
+ void contactRemoved(ContactUser *user);
+
+private:
+ UserIdentity *m_identity;
+ QList<ContactUser*> contacts;
+
+ void connectSignals(ContactUser *user);
+};
+
+#endif // CONTACTSMODEL_H
diff --git a/src/ui/LanguagesModel.cpp b/src/ui/LanguagesModel.cpp
new file mode 100644
index 0000000..c7fa2c9
--- /dev/null
+++ b/src/ui/LanguagesModel.cpp
@@ -0,0 +1,80 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "LanguagesModel.h"
+#include <QDir>
+#include <QLocale>
+#include <QVariant>
+
+LanguagesModel::LanguagesModel(QObject* parent)
+ : QAbstractListModel(parent)
+{
+ // create the list of languages based on present translation files in ":/lang" folder
+ QDir languagesFolder(QStringLiteral(":/lang"));
+ languages.append(LanguageEntry(tr("System default"), QString()));
+ foreach (const QString& translationFile, languagesFolder.entryList()) {
+ QString localeID = translationFile;
+ localeID.remove(QLatin1String("ricochet_")).remove(QLatin1String(".qm"));
+ QString nativeName = QLocale(localeID).nativeLanguageName();
+ languages.append(LanguageEntry(nativeName, localeID));
+ }
+}
+
+int LanguagesModel::rowCount(const QModelIndex &) const
+{
+ return languages.length();
+}
+
+QVariant LanguagesModel::data(const QModelIndex &index, int role) const
+{
+ if (index.row() >= 0 && index.row() < languages.length()) {
+ switch( role ) {
+ case NameRole:
+ return languages[index.row()].nativeName;
+ case LocaleIDRole:
+ return languages[index.row()].localeID;
+ default:
+ return QVariant();
+ }
+ } else {
+ return QVariant();
+ }
+}
+
+QHash<int, QByteArray> LanguagesModel::roleNames() const
+{
+ QHash<int, QByteArray> roles;
+ roles[NameRole] = "nativeName";
+ roles[LocaleIDRole] = "localeID";
+ return roles;
+}
+
diff --git a/src/ui/LanguagesModel.h b/src/ui/LanguagesModel.h
new file mode 100644
index 0000000..5e9a3f7
--- /dev/null
+++ b/src/ui/LanguagesModel.h
@@ -0,0 +1,65 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef LANGUAGESMODEL_H
+#define LANGUAGESMODEL_H
+
+#include <QAbstractListModel>
+
+class LanguagesModel : public QAbstractListModel
+{
+public:
+ enum {
+ NameRole = Qt::UserRole,
+ LocaleIDRole
+ };
+
+ LanguagesModel(QObject* parent = 0);
+
+ virtual int rowCount(const QModelIndex &parent) const;
+ virtual QVariant data(const QModelIndex &index, int role) const;
+ virtual QHash<int,QByteArray> roleNames() const;
+
+private:
+ struct LanguageEntry
+ {
+ QString nativeName;
+ QString localeID;
+
+ LanguageEntry(const QString& name, const QString& localeID)
+ : nativeName(name), localeID(localeID) {}
+ };
+
+ QList<LanguageEntry> languages;
+};
+
+#endif // LANGUAGESMODEL_H
diff --git a/src/ui/LinkedText.cpp b/src/ui/LinkedText.cpp
new file mode 100644
index 0000000..ecb2f15
--- /dev/null
+++ b/src/ui/LinkedText.cpp
@@ -0,0 +1,85 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "LinkedText.h"
+#include <QRegularExpression>
+#include <QUrl>
+#include <QClipboard>
+#include <QGuiApplication>
+#include <QDebug>
+
+LinkedText::LinkedText(QObject *parent)
+ : QObject(parent)
+{
+ // Select things that look like URLs of some kind and allow QUrl::fromUserInput to validate them
+ linkRegex = QRegularExpression(QStringLiteral("([a-z]{3,9}:|www\\.)([^\\s,.);!>]|[,.);!>](?!\\s|$))+"), QRegularExpression::CaseInsensitiveOption);
+
+ allowedSchemes << QStringLiteral("http")
+ << QStringLiteral("https")
+ << QStringLiteral("torsion")
+ << QStringLiteral("ricochet");
+}
+
+QString LinkedText::parsed(const QString &input)
+{
+ QString re;
+ int p = 0;
+ QRegularExpressionMatchIterator it = linkRegex.globalMatch(input);
+ while (it.hasNext()) {
+ QRegularExpressionMatch match = it.next();
+ int start = match.capturedStart();
+
+ QUrl url = QUrl::fromUserInput(match.capturedRef().toString());
+ if (!allowedSchemes.contains(url.scheme().toLower()))
+ continue;
+
+ if (start > p)
+ re.append(input.mid(p, start - p).toHtmlEscaped().replace(QLatin1Char('\n'), QStringLiteral("<br/>")));
+ re.append(QStringLiteral("<a href=\"%1\">%2</a>").arg(QString::fromLatin1(url.toEncoded()).toHtmlEscaped()).arg(match.capturedRef().toString().toHtmlEscaped()));
+ p = match.capturedEnd();
+ }
+
+ if (p < input.size())
+ re.append(input.mid(p).toHtmlEscaped().replace(QLatin1Char('\n'), QStringLiteral("<br/>")));
+
+ return re;
+}
+
+void LinkedText::copyToClipboard(const QString &text)
+{
+ QClipboard *clipboard = qApp->clipboard();
+ clipboard->setText(text);
+ // For X11, also copy to the selection clipboard (middle-click)
+ if (clipboard->supportsSelection())
+ clipboard->setText(text, QClipboard::Selection);
+}
+
diff --git a/src/ui/LinkedText.h b/src/ui/LinkedText.h
new file mode 100644
index 0000000..deb7475
--- /dev/null
+++ b/src/ui/LinkedText.h
@@ -0,0 +1,56 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef LINKEDTEXT_H
+#define LINKEDTEXT_H
+
+#include <QObject>
+#include <QRegularExpression>
+
+class LinkedText : public QObject
+{
+ Q_OBJECT
+ Q_DISABLE_COPY(LinkedText)
+
+public:
+ explicit LinkedText(QObject *parent = 0);
+
+ Q_INVOKABLE QString parsed(const QString &input);
+ Q_INVOKABLE void copyToClipboard(const QString &text);
+
+private:
+ QRegularExpression linkRegex;
+ QStringList allowedSchemes;
+};
+
+#endif
+
diff --git a/src/ui/MainWindow.cpp b/src/ui/MainWindow.cpp
new file mode 100644
index 0000000..c76f811
--- /dev/null
+++ b/src/ui/MainWindow.cpp
@@ -0,0 +1,158 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "MainWindow.h"
+#include "core/UserIdentity.h"
+#include "core/IncomingRequestManager.h"
+#include "core/OutgoingContactRequest.h"
+#include "core/IdentityManager.h"
+#include "core/ContactIDValidator.h"
+#include "core/ConversationModel.h"
+#include "tor/TorControl.h"
+#include "tor/TorManager.h"
+#include "tor/TorProcess.h"
+#include "ContactsModel.h"
+#include "ui/LinkedText.h"
+#include "utils/Settings.h"
+#include "utils/PendingOperation.h"
+#include "ui/LanguagesModel.h"
+#include <QtQml>
+#include <QQmlApplicationEngine>
+#include <QQmlContext>
+#include <QMessageBox>
+#include <QPushButton>
+#include <QQuickItem>
+#include <QGuiApplication>
+#include <QScreen>
+
+MainWindow *uiMain = 0;
+
+static QObject *linkedtext_singleton(QQmlEngine *, QJSEngine *)
+{
+ return new LinkedText;
+}
+
+MainWindow::MainWindow(QObject *parent)
+ : QObject(parent)
+{
+ Q_ASSERT(!uiMain);
+ uiMain = this;
+
+ qml = new QQmlApplicationEngine(this);
+
+ qmlRegisterUncreatableType<ContactUser>("im.ricochet", 1, 0, "ContactUser", QString());
+ qmlRegisterUncreatableType<UserIdentity>("im.ricochet", 1, 0, "UserIdentity", QString());
+ qmlRegisterUncreatableType<ContactsManager>("im.ricochet", 1, 0, "ContactsManager", QString());
+ qmlRegisterUncreatableType<IncomingRequestManager>("im.ricochet", 1, 0, "IncomingRequestManager", QString());
+ qmlRegisterUncreatableType<IncomingContactRequest>("im.ricochet", 1, 0, "IncomingContactRequest", QString());
+ qmlRegisterUncreatableType<OutgoingContactRequest>("im.ricochet", 1, 0, "OutgoingContactRequest", QString());
+ qmlRegisterUncreatableType<Tor::TorControl>("im.ricochet", 1, 0, "TorControl", QString());
+ qmlRegisterUncreatableType<Tor::TorProcess>("im.ricochet", 1, 0, "TorProcess", QString());
+ qmlRegisterType<ConversationModel>("im.ricochet", 1, 0, "ConversationModel");
+ qmlRegisterType<ContactsModel>("im.ricochet", 1, 0, "ContactsModel");
+ qmlRegisterType<ContactIDValidator>("im.ricochet", 1, 0, "ContactIDValidator");
+ qmlRegisterType<SettingsObject>("im.ricochet", 1, 0, "Settings");
+ qmlRegisterSingletonType<LinkedText>("im.ricochet", 1, 0, "LinkedText", linkedtext_singleton);
+ qmlRegisterType<LanguagesModel>("im.ricochet", 1, 0, "LanguagesModel");
+
+ qRegisterMetaType<PendingOperation*>();
+}
+
+MainWindow::~MainWindow()
+{
+}
+
+bool MainWindow::showUI()
+{
+ Q_ASSERT(!identityManager->identities().isEmpty());
+ qml->rootContext()->setContextProperty(QLatin1String("userIdentity"), identityManager->identities()[0]);
+ qml->rootContext()->setContextProperty(QLatin1String("torControl"), torControl);
+ qml->rootContext()->setContextProperty(QLatin1String("torInstance"), Tor::TorManager::instance());
+ qml->rootContext()->setContextProperty(QLatin1String("uiMain"), this);
+
+ qml->load(QUrl(QLatin1String("qrc:/ui/main.qml")));
+
+ if (qml->rootObjects().isEmpty()) {
+ // Assume this is only applicable to technical users; not worth translating or simplifying.
+ QMessageBox::critical(0, QStringLiteral("Ricochet"),
+ QStringLiteral("An error occurred while loading the Ricochet UI.\n\n"
+ "You might be missing plugins or dependency packages."));
+ qCritical() << "Failed to load UI. Exiting.";
+ return false;
+ }
+
+ return true;
+}
+
+QString MainWindow::version() const
+{
+ return qApp->applicationVersion();
+}
+
+QString MainWindow::aboutText() const
+{
+ QFile file(QStringLiteral(":/text/LICENSE"));
+ file.open(QIODevice::ReadOnly);
+ QString text = QString::fromUtf8(file.readAll());
+ return text;
+}
+
+QVariantMap MainWindow::screens() const
+{
+ QVariantMap mapScreenSizes;
+ foreach (QScreen *screen, QGuiApplication::screens()) {
+ QVariantMap screenObj;
+ screenObj.insert(QString::fromUtf8("width"), screen->availableSize().width());
+ screenObj.insert(QString::fromUtf8("height"), screen->availableSize().height());
+ screenObj.insert(QString::fromUtf8("left"), screen->geometry().left());
+ screenObj.insert(QString::fromUtf8("top"), screen->geometry().top());
+ mapScreenSizes.insert(screen->name(), screenObj);
+ }
+ return mapScreenSizes;
+}
+
+/* QMessageBox implementation for Qt <5.2 */
+bool MainWindow::showRemoveContactDialog(ContactUser *user)
+{
+ if (!user)
+ return false;
+ QMessageBox::StandardButton btn = QMessageBox::question(0,
+ tr("Remove %1").arg(user->nickname()),
+ tr("Do you want to permanently remove %1?").arg(user->nickname()));
+ return btn == QMessageBox::Yes;
+}
+
+QQuickWindow *MainWindow::findParentWindow(QQuickItem *item)
+{
+ Q_ASSERT(item);
+ return item ? item->window() : 0;
+}
diff --git a/src/ui/MainWindow.h b/src/ui/MainWindow.h
new file mode 100644
index 0000000..575789c
--- /dev/null
+++ b/src/ui/MainWindow.h
@@ -0,0 +1,77 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QObject>
+#include <QVariantMap>
+
+class ContactUser;
+class UserIdentity;
+class IncomingContactRequest;
+class OutgoingContactRequest;
+class QQmlApplicationEngine;
+class QQuickItem;
+class QQuickWindow;
+
+class MainWindow : public QObject
+{
+ Q_OBJECT
+ Q_DISABLE_COPY(MainWindow)
+
+ Q_PROPERTY(QString version READ version CONSTANT)
+ Q_PROPERTY(QString aboutText READ aboutText CONSTANT)
+ Q_PROPERTY(QVariantMap screens READ screens CONSTANT)
+
+public:
+ explicit MainWindow(QObject *parent = 0);
+ ~MainWindow();
+
+ bool showUI();
+
+ QString aboutText() const;
+ QString version() const;
+ QVariantMap screens() const;
+
+ Q_INVOKABLE bool showRemoveContactDialog(ContactUser *user);
+
+ // Find parent window of a QQuickItem; exposed as property after Qt 5.4
+ Q_INVOKABLE QQuickWindow *findParentWindow(QQuickItem *item);
+
+private:
+ QQmlApplicationEngine *qml;
+};
+
+extern MainWindow *uiMain;
+
+#endif // MAINWINDOW_H
diff --git a/src/ui/qml/AboutPreferences.qml b/src/ui/qml/AboutPreferences.qml
new file mode 100644
index 0000000..088880d
--- /dev/null
+++ b/src/ui/qml/AboutPreferences.qml
@@ -0,0 +1,36 @@
+import QtQuick 2.0
+import QtQuick.Controls 1.0
+import QtQuick.Layouts 1.0
+
+ColumnLayout {
+ anchors {
+ fill: parent
+ margins: 8
+ }
+
+ Label {
+ Layout.fillWidth: true
+ //: %1 version, e.g. 1.0.0
+ text: qsTr("Ricochet %1").arg(uiMain.version)
+ horizontalAlignment: Qt.AlignHCenter
+ }
+
+ Label {
+ Layout.fillWidth: true
+ text: "<a href='https://ricochet.im/'>ricochet.im</a>"
+ horizontalAlignment: Qt.AlignHCenter
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: Qt.openUrlExternally("https://ricochet.im/")
+ }
+ }
+
+ TextArea {
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ readOnly: true
+ text: uiMain.aboutText
+ }
+}
+
diff --git a/src/ui/qml/AddContactDialog.qml b/src/ui/qml/AddContactDialog.qml
new file mode 100644
index 0000000..7295c78
--- /dev/null
+++ b/src/ui/qml/AddContactDialog.qml
@@ -0,0 +1,119 @@
+import QtQuick 2.0
+import QtQuick.Controls 1.0
+import QtQuick.Layouts 1.0
+
+ApplicationWindow {
+ id: addContactWindow
+ width: 400
+ height: 300
+ minimumWidth: width
+ maximumWidth: width
+ minimumHeight: height
+ maximumHeight: height
+ flags: styleHelper.dialogWindowFlags
+ modality: Qt.WindowModal
+ title: mainWindow.title
+
+ signal closed
+ onVisibleChanged: if (!visible) closed()
+
+ property string staticContactId: fields.contactId.text
+
+ function close() {
+ visible = false
+ }
+
+ function accept() {
+ if (!fields.hasValidRequest)
+ return
+
+ userIdentity.contacts.createContactRequest(fields.contactId.text, fields.name.text, "", fields.message.text)
+ close()
+ }
+
+ ColumnLayout {
+ id: infoArea
+ z: 2
+ anchors {
+ left: parent.left
+ right: parent.right
+ top: parent.top
+ topMargin: 8
+ leftMargin: 16
+ rightMargin: 16
+ }
+
+ Label {
+ Layout.columnSpan: 2
+ Layout.fillWidth: true
+ horizontalAlignment: Qt.AlignHCenter
+ wrapMode: Text.Wrap
+ text: qsTr("Share your Ricochet ID to allow connection requests")
+ }
+
+ ContactIDField {
+ id: localId
+ Layout.fillWidth: true
+ readOnly: true
+ text: userIdentity.contactID
+ horizontalAlignment: Qt.AlignHCenter
+ }
+
+ Item { height: 1 }
+
+ Rectangle {
+ color: palette.mid
+ height: 1
+ Layout.fillWidth: true
+ Layout.columnSpan: 2
+ }
+
+ Item { height: 1 }
+ }
+
+ ContactRequestFields {
+ id: fields
+ anchors {
+ left: parent.left
+ right: parent.right
+ top: infoArea.bottom
+ bottom: buttonRow.top
+ margins: 8
+ leftMargin: 16
+ rightMargin: 16
+ }
+
+ Component.onCompleted: {
+ if (staticContactId.length > 0) {
+ fields.contactId.text = staticContactId
+ fields.contactId.readOnly = true
+ fields.name.focus = true
+ } else {
+ fields.contactId.focus = true
+ }
+ }
+ }
+
+ RowLayout {
+ id: buttonRow
+ anchors {
+ right: parent.right
+ bottom: parent.bottom
+ rightMargin: 16
+ bottomMargin: 8
+ }
+
+ Button {
+ text: qsTr("Cancel")
+ onClicked: addContactWindow.close()
+ }
+
+ Button {
+ text: qsTr("Add")
+ isDefault: true
+ enabled: fields.hasValidRequest
+ onClicked: addContactWindow.accept()
+ }
+ }
+}
+
diff --git a/src/ui/qml/AudioNotifications.qml b/src/ui/qml/AudioNotifications.qml
new file mode 100644
index 0000000..71615cd
--- /dev/null
+++ b/src/ui/qml/AudioNotifications.qml
@@ -0,0 +1,18 @@
+import QtQuick 2.0
+import QtMultimedia 5.0
+
+QtObject {
+ id: audioNotifications
+
+ property real volume: uiSettings.data.notificationVolume
+
+ property SoundEffect message: SoundEffect {
+ source: "qrc:/sounds/message.wav"
+ volume: audioNotifications.volume
+ }
+
+ property SoundEffect contactOnline: SoundEffect {
+ source: "qrc:/sounds/online.wav"
+ volume: audioNotifications.volume
+ }
+}
diff --git a/src/ui/qml/Bubble.qml b/src/ui/qml/Bubble.qml
new file mode 100644
index 0000000..cacb443
--- /dev/null
+++ b/src/ui/qml/Bubble.qml
@@ -0,0 +1,57 @@
+import QtQuick 2.0
+import QtQuick.Controls 1.0
+
+Rectangle {
+ id: bubble
+ x: {
+ switch (horizontalAlignment) {
+ case Qt.AlignHCenter: return -width + (target.width / 2) + 15
+ case Qt.AlignLeft: return 20
+ case Qt.AlignRight: return (target.width - width - 20)
+ }
+ }
+ y: parent.height + 14
+ width: label.contentWidth + 12
+ height: label.contentHeight + 12
+ color: "#c4e7ff"
+ border.color: Qt.darker(color, 1.2)
+ border.width: 1
+ opacity: displayed ? 1 : 0
+ visible: opacity
+
+ property Item target
+ property int maximumWidth: target ? target.width : 100
+ property int horizontalAlignment: Qt.AlignHCenter
+ property bool displayed: text.length
+ property alias text: label.text
+
+ Behavior on opacity { NumberAnimation { duration: 200 } }
+
+ Rectangle {
+ id: arrow
+ rotation: 45
+ width: 10
+ height: 10
+ x: (horizontalAlignment == Qt.AlignLeft) ? 20 : parent.width - 20
+ y: -5
+ color: parent.color
+ border.color: parent.border.color
+ border.width: 1
+ }
+
+ Rectangle {
+ x: arrow.x - 1
+ width: arrow.width + 2
+ height: 10
+ color: parent.color
+ }
+
+ Label {
+ id: label
+ wrapMode: Text.Wrap
+ width: maximumWidth - 16
+ x: 6
+ y: 6
+ }
+}
+
diff --git a/src/ui/qml/ChatMessageArea.qml b/src/ui/qml/ChatMessageArea.qml
new file mode 100644
index 0000000..8c3c000
--- /dev/null
+++ b/src/ui/qml/ChatMessageArea.qml
@@ -0,0 +1,53 @@
+import QtQuick 2.0
+import QtQuick.Controls 1.0
+import QtQuick.Layouts 1.0
+
+Rectangle {
+ id: scroll
+ clip: true
+ color: palette.base
+
+ property alias model: messageView.model
+
+ /* As of Qt 5.5.0, ScrollView is too buggy to use. It often fails to keep the
+ * view scrolled to the bottom, and moves erratically on wheel events. */
+ Rectangle {
+ id: scrollBar
+ width: 5
+ height: messageView.visibleArea.heightRatio * (messageView.height - 10)
+ y: 5 + messageView.visibleArea.yPosition * (messageView.height - 10)
+ x: parent.width - width - 3
+ z: 1000
+ visible: messageView.visibleArea.heightRatio < 1
+ color: "#bbbbbb"
+ radius: 14
+ }
+
+ ListView {
+ id: messageView
+ spacing: 12
+ pixelAligned: true
+ boundsBehavior: Flickable.StopAtBounds
+ anchors.fill: parent
+
+ MouseArea {
+ anchors.fill: parent
+ acceptedButtons: Qt.NoButton
+ onWheel: {
+ wheel.accepted = true
+ if (wheel.pixelDelta.y !== 0) {
+ messageView.contentY = Math.max(messageView.originY, Math.min(messageView.originY + messageView.contentHeight - messageView.height, messageView.contentY - wheel.pixelDelta.y))
+ } else if (wheel.angleDelta.y !== 0) {
+ messageView.flick(0, wheel.angleDelta.y * 5)
+ }
+ }
+ }
+
+ header: Item { width: 1; height: messageView.spacing }
+ footer: Item { width: 1; height: messageView.spacing }
+ delegate: MessageDelegate { }
+
+ verticalLayoutDirection: ListView.BottomToTop
+ }
+}
+
diff --git a/src/ui/qml/ChatPage.qml b/src/ui/qml/ChatPage.qml
new file mode 100644
index 0000000..cbe394c
--- /dev/null
+++ b/src/ui/qml/ChatPage.qml
@@ -0,0 +1,161 @@
+import QtQuick 2.0
+import QtQuick.Controls 1.0
+import QtQuick.Layouts 1.0
+import im.ricochet 1.0
+
+FocusScope {
+ id: chatPage
+
+ property var contact
+ property TextArea textField: textInput
+ property var conversationModel: (contact !== null) ? contact.conversation : null
+
+ function forceActiveFocus() {
+ textField.forceActiveFocus()
+ }
+
+ onVisibleChanged: if (visible) forceActiveFocus()
+
+ property bool active: visible && activeFocusItem !== null
+ onActiveChanged: {
+ if (active)
+ conversationModel.resetUnreadCount()
+ }
+
+ Connections {
+ target: conversationModel
+ onUnreadCountChanged: if (active) conversationModel.resetUnreadCount()
+ }
+
+ RowLayout {
+ id: infoBar
+ anchors {
+ top: parent.top
+ left: parent.left
+ leftMargin: 4
+ right: parent.right
+ rightMargin: 4
+ }
+ height: implicitHeight + 8
+ spacing: 8
+
+ PresenceIcon {
+ status: contact.status
+ }
+
+ Label {
+ text: contact.nickname
+ font.pointSize: styleHelper.pointSize
+ }
+
+ Item {
+ Layout.fillWidth: true
+ height: 1
+ }
+ }
+
+ Rectangle {
+ anchors {
+ left: parent.left
+ right: parent.right
+ top: infoBar.top
+ bottom: infoBar.bottom
+ }
+ color: palette.base
+ z: -1
+
+ Column {
+ anchors {
+ top: parent.bottom
+ left: parent.left
+ right: parent.right
+ }
+ Rectangle { width: parent.width; height: 1; color: palette.midlight; }
+ Rectangle { width: parent.width; height: 1; color: palette.window; }
+ }
+ }
+
+ ChatMessageArea {
+ anchors {
+ top: infoBar.bottom
+ topMargin: 2
+ left: parent.left
+ right: parent.right
+ bottom: statusBar.top
+ }
+ model: conversationModel
+ }
+
+ StatusBar {
+ id: statusBar
+ anchors {
+ left: parent.left
+ right: parent.right
+ bottom: parent.bottom
+ }
+ height: statusLayout.height + 8
+
+ RowLayout {
+ id: statusLayout
+ width: statusBar.width - 8
+ y: 2
+
+ TextArea {
+ id: textInput
+ Layout.fillWidth: true
+ y: 2
+ // This ridiculous incantation enables an automatically sized TextArea
+ Layout.preferredHeight: mapFromItem(flickableItem, 0, 0).y * 2 +
+ Math.max(styleHelper.textHeight + 2*edit.textMargin, flickableItem.contentHeight)
+ Layout.maximumHeight: (styleHelper.textHeight * 4) + (2 * edit.textMargin)
+ textMargin: 3
+ wrapMode: TextEdit.Wrap
+ font.pointSize: styleHelper.pointSize
+ focus: true
+
+ property TextEdit edit
+
+ Component.onCompleted: {
+ var objects = contentItem.contentItem.children
+ for (var i = 0; i < objects.length; i++) {
+ if (objects[i].hasOwnProperty('textDocument')) {
+ edit = objects[i]
+ break
+ }
+ }
+
+ edit.Keys.pressed.connect(keyHandler)
+ }
+
+ function keyHandler(event) {
+ switch (event.key) {
+ case Qt.Key_Enter:
+ case Qt.Key_Return:
+ if (event.modifiers & Qt.ShiftModifier || event.modifiers & Qt.AltModifier) {
+ textInput.insert(textInput.cursorPosition, "\n")
+ } else {
+ send()
+ }
+ event.accepted = true
+ break
+ default:
+ event.accepted = false
+ }
+ }
+
+ function send() {
+ if (textInput.length > 2000)
+ textInput.remove(2000, textInput.length)
+ conversationModel.sendMessage(textInput.text)
+ textInput.remove(0, textInput.length)
+ }
+
+ onLengthChanged: {
+ if (textInput.length > 2000)
+ textInput.remove(2000, textInput.length)
+ }
+ }
+ }
+ }
+}
+
diff --git a/src/ui/qml/ChatWindow.qml b/src/ui/qml/ChatWindow.qml
new file mode 100644
index 0000000..5530cc4
--- /dev/null
+++ b/src/ui/qml/ChatWindow.qml
@@ -0,0 +1,48 @@
+import QtQuick 2.0
+import QtQuick.Controls 1.0
+import QtQuick.Layouts 1.0
+import im.ricochet 1.0
+
+ApplicationWindow {
+ id: chatWindow
+ width: 500
+ height: 400
+ title: contact !== null ? contact.nickname : ""
+
+ property alias contact: chatPage.contact
+ signal closed
+
+ onVisibleChanged: {
+ if (!visible)
+ closed()
+ }
+
+ onClosed: {
+ // If not also in combined window mode, clear chat history when closing
+ if (!uiSettings.data.combinedChatWindow)
+ chatPage.conversationModel.clear()
+ }
+
+ property bool inactive: true
+ onActiveFocusItemChanged: {
+ // Focus text input when window regains focus
+ if (activeFocusItem !== null && inactive) {
+ inactive = false
+ retakeFocus.start()
+ } else if (activeFocusItem === null) {
+ inactive = true
+ }
+ }
+
+ Timer {
+ id: retakeFocus
+ onTriggered: chatPage.forceActiveFocus()
+ interval: 1
+ }
+
+ ChatPage {
+ id: chatPage
+ anchors.fill: parent
+ }
+}
+
diff --git a/src/ui/qml/ContactActions.qml b/src/ui/qml/ContactActions.qml
new file mode 100644
index 0000000..3eab6e2
--- /dev/null
+++ b/src/ui/qml/ContactActions.qml
@@ -0,0 +1,64 @@
+import QtQuick 2.0
+import QtQuick.Controls 1.0
+import QtQuick.Dialogs 1.1
+import "ContactWindow.js" as ContactWindow
+
+Item {
+ id: contactMenu
+
+ property QtObject contact
+
+ function openWindow() {
+ var window = ContactWindow.getWindow(contact)
+ window.raise()
+ window.requestActivate()
+ }
+
+ function removeContact() {
+ removeContactDialog.active = true
+ if (removeContactDialog.item !== null) {
+ removeContactDialog.item.open()
+ } else if (uiMain.showRemoveContactDialog(contact)) {
+ contact.deleteContact()
+ }
+ }
+
+ function openContextMenu() {
+ contextMenu.popup()
+ }
+
+ function openPreferences() {
+ root.openPreferences("ContactPreferences.qml", { 'selectedContact': contact })
+ }
+
+ signal renameTriggered
+
+ Menu {
+ id: contextMenu
+
+ MenuItem {
+ text: qsTr("Open Window")
+ onTriggered: openWindow()
+ }
+ MenuItem {
+ text: qsTr("Details...")
+ onTriggered: openPreferences()
+ }
+ MenuItem {
+ text: qsTr("Rename")
+ onTriggered: renameTriggered()
+ }
+ MenuSeparator { }
+ MenuItem {
+ text: qsTr("Remove")
+ onTriggered: removeContact()
+ }
+ }
+
+ Loader {
+ id: removeContactDialog
+ source: "MessageDialogWrapper.qml"
+ active: false
+ }
+}
+
diff --git a/src/ui/qml/ContactIDField.qml b/src/ui/qml/ContactIDField.qml
new file mode 100644
index 0000000..fc22986
--- /dev/null
+++ b/src/ui/qml/ContactIDField.qml
@@ -0,0 +1,97 @@
+import QtQuick 2.0
+import QtQuick.Controls 1.0
+import QtQuick.Controls.Styles 1.0
+import QtQuick.Layouts 1.0
+import im.ricochet 1.0
+
+FocusScope {
+ id: contactId
+ z: 4
+ height: layout.height
+ Layout.fillWidth: true
+
+ property alias text: field.text
+ property alias readOnly: field.readOnly
+ property alias horizontalAlignment: field.horizontalAlignment
+ property alias acceptableInput: field.acceptableInput
+ property bool showCopyButton: true
+
+ RowLayout {
+ id: layout
+ width: parent.width
+
+ TextField {
+ id: field
+ Layout.fillWidth: true
+ font.family: "Courier"
+ validator: readOnly ? null : idValidator
+ placeholderText: "ricochet:"
+ focus: true
+
+ onTextChanged: errorBubble.clear()
+
+ ContactIDValidator {
+ id: idValidator
+ notContactOfIdentity: userIdentity
+
+ onFailed: {
+ var contact
+ if ((contact = matchingContact(field.text)))
+ errorBubble.show(qsTr("<b>%1</b> is already your contact").arg(contact.nickname))
+ else if (matchesIdentity(field.text))
+ errorBubble.show(qsTr("You can't add yourself as a contact"))
+ else
+ errorBubble.show(qsTr("Enter an ID starting with <b>ricochet:</b>"))
+ }
+ }
+
+ Bubble {
+ id: errorBubble
+ target: field
+ horizontalAlignment: Qt.AlignLeft
+
+ function show(value) {
+ text = value
+ opacity = 1
+ }
+
+ function clear() {
+ opacity = 0
+ }
+ }
+
+ function copyLoudly() {
+ // The LinkedText helper also copies to the X11 selection clipboard
+ LinkedText.copyToClipboard(field.text)
+ copyBubble.displayed = true
+ bubbleResetTimer.start()
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ enabled: field.readOnly
+ onClicked: field.copyLoudly()
+ }
+
+ Bubble {
+ id: copyBubble
+ target: field
+ text: qsTr("Copied to clipboard")
+ displayed: false
+ }
+
+ Timer {
+ id: bubbleResetTimer
+ interval: 1000
+ onTriggered: copyBubble.displayed = false
+ }
+ }
+
+ Button {
+ text: qsTr("Copy")
+ visible: contactId.showCopyButton
+ onClicked: field.copyLoudly()
+ }
+ }
+}
+
diff --git a/src/ui/qml/ContactList.qml b/src/ui/qml/ContactList.qml
new file mode 100644
index 0000000..f72e31c
--- /dev/null
+++ b/src/ui/qml/ContactList.qml
@@ -0,0 +1,99 @@
+import QtQuick 2.0
+import QtQuick.Controls 1.0
+import QtQuick.Layouts 1.0
+import im.ricochet 1.0
+
+ScrollView {
+ id: scroll
+
+ data: [
+ Rectangle {
+ anchors.fill: scroll
+ z: -1
+ color: palette.base
+ },
+ ContactsModel {
+ id: contactsModel
+ identity: userIdentity
+ }
+ ]
+
+ property QtObject selectedContact
+ property ListView view: contactListView
+
+ // Emitted for double click on a contact
+ signal contactActivated(ContactUser contact, Item actions)
+
+ onSelectedContactChanged: {
+ if (selectedContact !== contactsModel.contact(contactListView.currentIndex)) {
+ contactListView.currentIndex = contactsModel.rowOfContact(selectedContact)
+ }
+ }
+
+ ListView {
+ id: contactListView
+ model: contactsModel
+ currentIndex: -1
+
+ signal contactActivated(ContactUser contact, Item actions)
+ onContactActivated: scroll.contactActivated(contact, actions)
+
+ onCurrentIndexChanged: {
+ // Not using a binding to allow writes to selectedContact
+ scroll.selectedContact = contactsModel.contact(contactListView.currentIndex)
+ }
+
+ data: [
+ MouseArea {
+ anchors.fill: parent
+ z: -100
+ onClicked: contactListView.currentIndex = -1
+ }
+ ]
+
+ section.property: "status"
+ section.delegate: Row {
+ width: parent.width - x
+ height: label.height + 4
+ x: 8
+ spacing: 6
+
+ Label {
+ id: label
+ y: 2
+
+ font.pointSize: styleHelper.pointSize
+ font.bold: true
+ font.capitalization: Font.SmallCaps
+ color: "#3f454a"
+
+ text: {
+ // Translation strings are uppercase for legacy reasons, and because they
+ // should correctly be capitalized. We go lowercase only because it looks
+ // nicer when using SmallCaps, and that's a display detail.
+ switch (parseInt(section)) {
+ case ContactUser.Online: return qsTr("Online").toLowerCase()
+ case ContactUser.Offline: return qsTr("Offline").toLowerCase()
+ case ContactUser.RequestPending: return qsTr("Requests").toLowerCase()
+ case ContactUser.RequestRejected: return qsTr("Rejected").toLowerCase()
+ case ContactUser.Outdated: return qsTr("Outdated").toLowerCase()
+ }
+ }
+ }
+
+ Rectangle {
+ height: 1
+ width: parent.width - x
+ anchors {
+ top: label.verticalCenter
+ topMargin: 1
+ }
+
+ color: "black"
+ opacity: 0.1
+ }
+ }
+
+ delegate: ContactListDelegate { }
+ }
+}
diff --git a/src/ui/qml/ContactListDelegate.qml b/src/ui/qml/ContactListDelegate.qml
new file mode 100644
index 0000000..0f24728
--- /dev/null
+++ b/src/ui/qml/ContactListDelegate.qml
@@ -0,0 +1,115 @@
+import QtQuick 2.0
+import QtQuick.Controls 1.0
+import im.ricochet 1.0
+
+Rectangle {
+ id: delegate
+ color: highlighted ? "#c4e7ff" : "white"
+ width: parent.width
+ height: nameLabel.height + 8
+
+ property bool highlighted: ListView.isCurrentItem
+ onHighlightedChanged: {
+ if (renameMode)
+ renameMode = false
+ }
+
+ PresenceIcon {
+ id: presenceIcon
+ anchors {
+ left: parent.left
+ leftMargin: 20
+ verticalCenter: nameLabel.verticalCenter
+ }
+ status: model.status
+ }
+
+ Label {
+ id: nameLabel
+ anchors {
+ left: presenceIcon.right
+ leftMargin: 6
+ right: unreadBadge.left
+ rightMargin: 8
+ verticalCenter: parent.verticalCenter
+ }
+ text: model.name
+ elide: Text.ElideRight
+ font.pointSize: styleHelper.pointSize
+ color: "black"
+ opacity: model.status === ContactUser.Online ? 1 : 0.8
+ }
+
+ UnreadCountBadge {
+ id: unreadBadge
+ anchors {
+ verticalCenter: parent.verticalCenter
+ right: parent.right
+ rightMargin: 8
+ }
+
+ value: model.contact.conversation.unreadCount
+ }
+
+ ContactActions {
+ id: contextMenu
+ contact: model.contact
+
+ onRenameTriggered: renameMode = true
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ acceptedButtons: Qt.LeftButton | Qt.RightButton
+
+ onPressed: {
+ if (!delegate.ListView.isCurrentItem)
+ contactListView.currentIndex = model.index
+ }
+
+ onClicked: {
+ if (mouse.button === Qt.RightButton) {
+ contextMenu.openContextMenu()
+ }
+ }
+
+ onDoubleClicked: {
+ if (mouse.button === Qt.LeftButton) {
+ contactListView.contactActivated(model.contact, contextMenu)
+ }
+ }
+ }
+
+ property bool renameMode
+ property Item renameItem
+ onRenameModeChanged: {
+ if (renameMode && renameItem === null) {
+ renameItem = renameComponent.createObject(delegate)
+ renameItem.forceActiveFocus()
+ renameItem.selectAll()
+ } else if (!renameMode && renameItem !== null) {
+ renameItem.visible = false
+ renameItem.destroy()
+ renameItem = null
+ }
+ }
+
+ Component {
+ id: renameComponent
+
+ TextField {
+ id: nameField
+ anchors {
+ left: nameLabel.left
+ right: nameLabel.right
+ verticalCenter: nameLabel.verticalCenter
+ }
+ text: model.contact.nickname
+ onAccepted: {
+ model.contact.nickname = text
+ delegate.renameMode = false
+ }
+ }
+ }
+}
+
diff --git a/src/ui/qml/ContactPreferences.qml b/src/ui/qml/ContactPreferences.qml
new file mode 100644
index 0000000..e1d929a
--- /dev/null
+++ b/src/ui/qml/ContactPreferences.qml
@@ -0,0 +1,176 @@
+import QtQuick 2.0
+import QtQuick.Controls 1.0
+import QtQuick.Layouts 1.0
+import im.ricochet 1.0
+
+Item {
+ property alias selectedContact: contacts.selectedContact
+
+ RowLayout {
+ anchors {
+ fill: parent
+ margins: 8
+ }
+
+ ContactList {
+ id: contacts
+ Layout.preferredWidth: 200
+ Layout.minimumWidth: 150
+ Layout.fillHeight: true
+ frameVisible: true
+ }
+
+ data: [
+ ContactActions {
+ id: contactActions
+ contact: contacts.selectedContact
+ }
+ ]
+
+ ColumnLayout {
+ id: contactInfo
+ visible: contact !== null
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+
+ property QtObject contact: contacts.selectedContact
+ property QtObject request: (contact !== null) ? contact.contactRequest : null
+
+ Item { height: 1; width: 1 }
+ Label {
+ id: nickname
+ Layout.fillWidth: true
+ text: visible ? contactInfo.contact.nickname : ""
+ horizontalAlignment: Qt.AlignHCenter
+ font.pointSize: styleHelper.pointSize + 1
+
+ property bool renameMode
+ property Item renameItem
+ onRenameModeChanged: {
+ if (renameMode && renameItem === null) {
+ renameItem = renameComponent.createObject(nickname)
+ renameItem.forceActiveFocus()
+ renameItem.selectAll()
+ } else if (!renameMode && renameItem !== null) {
+ renameItem.focus = false
+ renameItem.visible = false
+ renameItem.destroy()
+ renameItem = null
+ }
+ }
+
+ MouseArea { anchors.fill: parent; onDoubleClicked: nickname.renameMode = true }
+
+ Component {
+ id: renameComponent
+
+ TextField {
+ id: nameField
+ anchors {
+ left: parent.left
+ right: parent.right
+ verticalCenter: parent.verticalCenter
+ }
+ text: contactInfo.contact.nickname
+ horizontalAlignment: nickname.horizontalAlignment
+ font.pointSize: nickname.font.pointSize
+ onEditingFinished: {
+ contactInfo.contact.nickname = text
+ nickname.renameMode = false
+ }
+ }
+ }
+ }
+ Item { height: 1; width: 1 }
+
+ ContactIDField {
+ Layout.fillWidth: true
+ Layout.minimumWidth: 100
+ readOnly: true
+ text: visible ? contactInfo.contact.contactID : ""
+ }
+
+ GridLayout {
+ Layout.fillWidth: true
+ columns: 2
+
+ Label { text: qsTr("Date added:"); Layout.alignment: Qt.AlignRight }
+ Label {
+ Layout.fillWidth: true
+ elide: Text.ElideRight
+ text: visible ? Qt.formatDate(contactInfo.contact.settings.read("whenCreated"), Qt.DefaultLocaleLongDate) : ""
+ }
+
+ Label { text: qsTr("Last seen:"); visible: lastSeen.visible; Layout.alignment: Qt.AlignRight }
+ Label {
+ id: lastSeen
+ Layout.fillWidth: true
+ elide: Text.ElideRight
+ visible: contactInfo.request === null
+ text: visible ? Qt.formatDate(contactInfo.contact.settings.read("lastConnected"), Qt.DefaultLocaleLongDate) : ""
+ }
+
+ Label { text: qsTr("Request:"); visible: requestStatus.visible; Layout.alignment: Qt.AlignRight }
+ Label {
+ id: requestStatus
+ visible: contactInfo.request !== null
+ text: {
+ var re = ""
+ if (contactInfo.request === null)
+ return re
+ switch (contactInfo.request.status) {
+ case OutgoingContactRequest.Pending: re = qsTr("Pending connection"); break
+ case OutgoingContactRequest.Acknowledged: re = qsTr("Delivered"); break
+ case OutgoingContactRequest.Accepted: re = qsTr("Accepted"); break
+ case OutgoingContactRequest.Error: re = qsTr("Error"); break
+ case OutgoingContactRequest.Rejected: re = qsTr("Rejected"); break
+ }
+ if (contactInfo.request.isConnected) {
+ //: %1 status, e.g. "Accepted"
+ re = qsTr("%1 (Connected)").arg(re)
+ }
+ return re
+ }
+ }
+
+ Label { text: qsTr("Response:"); visible: rejectMessage.visible; Layout.alignment: Qt.AlignRight }
+ Label {
+ id: rejectMessage
+ Layout.fillWidth: true
+ elide: Text.ElideRight
+ text: visible ? contactInfo.request.rejectMessage : ""
+ visible: (contactInfo.request !== null) && (contactInfo.request.rejectMessage !== "")
+ }
+ }
+
+ Item { height: 1; width: 1 }
+ Rectangle {
+ color: palette.mid
+ height: 1
+ Layout.fillWidth: true
+ }
+ Item { height: 1; width: 1 }
+
+ RowLayout {
+ Layout.fillWidth: true
+
+ Button {
+ text: qsTr("Rename")
+ onClicked: nickname.renameMode = !nickname.renameMode
+ }
+
+ Item { Layout.fillWidth: true; height: 1 }
+
+ Button {
+ text: qsTr("Remove")
+ onClicked: contactActions.removeContact()
+ }
+ }
+
+ Item {
+ Layout.fillHeight: true
+ width: 1
+ }
+ }
+ }
+}
diff --git a/src/ui/qml/ContactRequestDialog.qml b/src/ui/qml/ContactRequestDialog.qml
new file mode 100644
index 0000000..d7c21ab
--- /dev/null
+++ b/src/ui/qml/ContactRequestDialog.qml
@@ -0,0 +1,113 @@
+import QtQuick 2.0
+import QtQuick.Controls 1.0
+import QtQuick.Layouts 1.0
+
+ApplicationWindow {
+ id: contactRequestDialog
+ width: 350
+ height: 200
+ minimumWidth: width
+ maximumWidth: width
+ minimumHeight: height
+ maximumHeight: height
+ flags: styleHelper.dialogWindowFlags
+ modality: Qt.WindowModal
+ title: mainWindow.title
+
+ signal closed
+ onVisibleChanged: if (!visible) closed()
+
+ property QtObject request
+ property bool hasValidContact: request.hostname != "" && fields.name.text.length
+
+ function close() {
+ visible = false
+ }
+
+ function accept() {
+ request.nickname = fields.name.text
+ request.accept()
+ close()
+ }
+
+ function reject() {
+ request.reject()
+ close()
+ }
+
+ GridLayout {
+ id: infoArea
+ anchors {
+ left: parent.left
+ right: parent.right
+ top: parent.top
+ topMargin: 8
+ leftMargin: 16
+ rightMargin: 16
+ }
+ columns: 2
+
+ Label {
+ Layout.columnSpan: 2
+ Layout.fillWidth: true
+ horizontalAlignment: Qt.AlignHCenter
+ wrapMode: Text.Wrap
+ text: qsTr("Someone new is asking to connect to you")
+ }
+
+ Item { height: 1 }
+
+ Rectangle {
+ color: palette.mid
+ height: 1
+ Layout.fillWidth: true
+ Layout.columnSpan: 2
+ }
+
+ Item { height: 1 }
+ }
+
+ ContactRequestFields {
+ id: fields
+ anchors {
+ left: parent.left
+ right: parent.right
+ top: infoArea.bottom
+ bottom: buttonRow.top
+ margins: 8
+ leftMargin: 16
+ rightMargin: 16
+ }
+ readOnly: true
+
+ Component.onCompleted: {
+ contactId.text = request.contactId
+ name.text = request.nickname
+ name.readOnly = false
+ name.focus = true
+ message.text = request.message
+ }
+ }
+
+ RowLayout {
+ id: buttonRow
+ anchors {
+ right: parent.right
+ bottom: parent.bottom
+ rightMargin: 16
+ bottomMargin: 8
+ }
+
+ Button {
+ text: qsTr("Reject")
+ onClicked: contactRequestDialog.reject()
+ }
+
+ Button {
+ text: qsTr("Accept")
+ enabled: hasValidContact
+ onClicked: contactRequestDialog.accept()
+ }
+ }
+}
+
diff --git a/src/ui/qml/ContactRequestFields.qml b/src/ui/qml/ContactRequestFields.qml
new file mode 100644
index 0000000..e650fe2
--- /dev/null
+++ b/src/ui/qml/ContactRequestFields.qml
@@ -0,0 +1,50 @@
+import QtQuick 2.0
+import QtQuick.Controls 1.0
+import QtQuick.Layouts 1.0
+import im.ricochet 1.0
+
+GridLayout {
+ id: contactFields
+ columns: 2
+
+ property bool readOnly
+ property ContactIDField contactId: contactIdField
+ property TextField name: nameField
+ property TextArea message: messageField
+ property bool hasValidRequest: contactIdField.acceptableInput && nameField.text.length
+
+ Label {
+ text: qsTr("ID:")
+ Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
+ }
+
+ ContactIDField {
+ id: contactIdField
+ Layout.fillWidth: true
+ readOnly: contactFields.readOnly
+ showCopyButton: false
+ }
+
+ Label {
+ text: qsTr("Name:")
+ Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
+ }
+
+ TextField {
+ id: nameField
+ Layout.fillWidth: true
+ readOnly: contactFields.readOnly
+ }
+
+ Label {
+ text: qsTr("Message:")
+ Layout.alignment: Qt.AlignTop | Qt.AlignRight
+ }
+
+ TextArea {
+ id: messageField
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ readOnly: contactFields.readOnly
+ }
+}
diff --git a/src/ui/qml/ContactWindow.js b/src/ui/qml/ContactWindow.js
new file mode 100644
index 0000000..0bb3a4c
--- /dev/null
+++ b/src/ui/qml/ContactWindow.js
@@ -0,0 +1,20 @@
+.pragma library
+
+var windows = { }
+var createWindow = function() { console.log("BUG!") }
+
+function getWindow(user) {
+ var id = user.uniqueID
+ var window = windows[user.uniqueID]
+
+ if (window === undefined || window === null) {
+ window = createWindow(user)
+ window.closed.connect(function() { windows[id] = undefined })
+ windows[id] = window
+ }
+ return window
+}
+
+function windowExists(user) {
+ return windows[user.uniqueID] !== undefined && windows[user.uniqueID] !== null
+}
diff --git a/src/ui/qml/GeneralPreferences.qml b/src/ui/qml/GeneralPreferences.qml
new file mode 100644
index 0000000..1f057a5
--- /dev/null
+++ b/src/ui/qml/GeneralPreferences.qml
@@ -0,0 +1,54 @@
+import QtQuick 2.0
+import QtQuick.Controls 1.0
+import QtQuick.Layouts 1.0
+
+ColumnLayout {
+ anchors {
+ fill: parent
+ margins: 8
+ }
+
+ CheckBox {
+ text: qsTr("Use a single window for conversations")
+ checked: uiSettings.data.combinedChatWindow || false
+ onCheckedChanged: {
+ uiSettings.write("combinedChatWindow", checked)
+ }
+ }
+
+ CheckBox {
+ text: qsTr("Open links in default browser without prompting")
+ checked: uiSettings.data.alwaysOpenBrowser || false
+ onCheckedChanged: {
+ uiSettings.write("alwaysOpenBrowser", checked)
+ }
+ }
+
+ CheckBox {
+ text: qsTr("Play audio notifications")
+ checked: uiSettings.data.playAudioNotification || false
+ onCheckedChanged: {
+ uiSettings.write("playAudioNotification", checked)
+ }
+ }
+ RowLayout {
+ Item { width: 16 }
+
+ Label { text: qsTr("Volume") }
+
+ Slider {
+ maximumValue: 1.0
+ updateValueWhileDragging: false
+ enabled: uiSettings.data.playAudioNotification || false
+ value: uiSettings.read("notificationVolume", 0.75)
+ onValueChanged: {
+ uiSettings.write("notificationVolume", value)
+ }
+ }
+ }
+
+ Item {
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ }
+}
diff --git a/src/ui/qml/LanguagePreferences.qml b/src/ui/qml/LanguagePreferences.qml
new file mode 100644
index 0000000..000008f
--- /dev/null
+++ b/src/ui/qml/LanguagePreferences.qml
@@ -0,0 +1,62 @@
+import QtQuick 2.0
+import QtQuick.Controls 1.0
+import QtQuick.Layouts 1.0
+import im.ricochet 1.0
+
+ColumnLayout {
+ anchors {
+ fill: parent
+ margins: 8
+ }
+
+ property string previousLanguage: uiSettings.data.language
+
+ ExclusiveGroup {
+ id: languageGroup
+ }
+
+ Item { height: 8 }
+
+ Label {
+ Layout.fillWidth: true
+ text: qsTr("Select Language")
+ }
+
+ Item { height: 10 }
+
+ GridLayout {
+ columns: 2
+
+ Repeater {
+ model: LanguagesModel { }
+ delegate: RadioButton {
+ id: languageSelection
+ Layout.fillWidth: true
+ text: nativeName
+ checked: localeID === uiSettings.data.language
+ exclusiveGroup: languageGroup
+ onCheckedChanged: {
+ if (checked && previousLanguage !== localeID) {
+ restartNotification.visible = true
+ uiSettings.write("language", localeID)
+ }
+ }
+ }
+ }
+ }
+
+ Item { height: 15 }
+
+ Label {
+ id: restartNotification
+ text: qsTr("Restart Ricochet to apply changes")
+ Layout.fillWidth: true
+ horizontalAlignment: Text.AlignHCenter
+ visible: false
+ }
+
+ Item {
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ }
+}
diff --git a/src/ui/qml/MainToolBar.qml b/src/ui/qml/MainToolBar.qml
new file mode 100644
index 0000000..a384a1a
--- /dev/null
+++ b/src/ui/qml/MainToolBar.qml
@@ -0,0 +1,87 @@
+import QtQuick 2.0
+import QtQuick.Controls 1.0
+import QtQuick.Layouts 1.0
+import QtQuick.Controls.Styles 1.0
+import im.ricochet 1.0
+
+ToolBar {
+ Layout.minimumWidth: 200
+ Layout.fillWidth: true
+ // Necessary to avoid oversized toolbars, e.g. OS X with Qt 5.4.1
+ implicitHeight: toolBarLayout.height + __style.padding.top + __style.padding.bottom
+
+ property Action addContact: addContactAction
+ property Action preferences: preferencesAction
+
+ data: [
+ Action {
+ id: addContactAction
+ text: qsTr("Add Contact")
+ onTriggered: {
+ var object = createDialog("AddContactDialog.qml", { }, window)
+ object.visible = true
+ }
+ },
+
+ Action {
+ id: preferencesAction
+ text: qsTr("Preferences")
+ onTriggered: root.openPreferences()
+ }
+ ]
+
+ Component {
+ id: iconButtonStyle
+
+ ButtonStyle {
+ background: Item { }
+ label: Text {
+ text: control.text
+ font.family: iconFont.name
+ font.pixelSize: height
+ horizontalAlignment: Qt.AlignHCenter
+ renderType: Text.QtRendering
+ color: "black"
+ }
+ }
+ }
+
+ RowLayout {
+ id: toolBarLayout
+ width: parent.width
+
+ TorStateWidget {
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Item {
+ Layout.fillWidth: true
+ height: 1
+ }
+
+ ToolButton {
+ id: addContactButton
+ implicitHeight: 24
+ action: addContactAction
+ style: iconButtonStyle
+ text: "\ue810" // iconFont plus symbol
+
+ Loader {
+ id: emptyState
+ active: contactList.view.count == 0
+ sourceComponent: Bubble {
+ target: addContactButton
+ maximumWidth: toolBarLayout.width
+ text: qsTr("Click to add contacts")
+ }
+ }
+ }
+
+ ToolButton {
+ action: preferencesAction
+ implicitHeight: 24
+ style: iconButtonStyle
+ text: "\ue803" // iconFont gear
+ }
+ }
+}
diff --git a/src/ui/qml/MainWindow.qml b/src/ui/qml/MainWindow.qml
new file mode 100644
index 0000000..fc1c1e4
--- /dev/null
+++ b/src/ui/qml/MainWindow.qml
@@ -0,0 +1,143 @@
+import QtQuick 2.0
+import QtQuick.Window 2.0
+import QtQuick.Controls 1.0
+import QtQuick.Layouts 1.0
+import im.ricochet 1.0
+import "ContactWindow.js" as ContactWindow
+
+ApplicationWindow {
+ id: window
+ title: "Ricochet"
+ visibility: Window.AutomaticVisibility
+
+ width: 250
+ height: 400
+ minimumHeight: 400
+ minimumWidth: uiSettings.data.combinedChatWindow ? 650 : 250
+ maximumWidth: uiSettings.data.combinedChatWindow ? (1 << 24) - 1 : 250
+
+ onMinimumWidthChanged: width = Math.max(width, minimumWidth)
+ onMaximumWidthChanged: width = Math.min(width, maximumWidth)
+
+ // OS X Menu
+ Loader {
+ active: Qt.platform.os == 'osx'
+ sourceComponent: MenuBar {
+ Menu {
+ title: "Ricochet"
+ MenuItem {
+ text: qsTranslate("QCocoaMenuItem", "Preference")
+ onTriggered: toolBar.preferences.trigger()
+ }
+ }
+ }
+ }
+
+ Connections {
+ target: userIdentity.contacts
+ onUnreadCountChanged: {
+ if (unreadCount > 0) {
+ if (audioNotifications !== null)
+ audioNotifications.message.play()
+ var w = window
+ if (!uiSettings.data.combinedChatWindow || ContactWindow.windowExists(user))
+ w = ContactWindow.getWindow(user)
+ // On OS X, avoid bouncing the dock icon forever
+ w.alert(Qt.platform.os == "osx" ? 1000 : 0)
+ }
+ }
+ onContactStatusChanged: {
+ if (status === ContactUser.Online && audioNotifications !== null) {
+ audioNotifications.contactOnline.play()
+ }
+ }
+ }
+
+ RowLayout {
+ anchors.fill: parent
+ spacing: 0
+
+ ColumnLayout {
+ spacing: 0
+ Layout.preferredWidth: combinedChatView.visible ? 220 : 0
+ Layout.fillWidth: !combinedChatView.visible
+
+ MainToolBar {
+ id: toolBar
+ // Needed to allow bubble to appear over contact list
+ z: 3
+ }
+
+ Item {
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+
+ ContactList {
+ id: contactList
+ anchors.fill: parent
+ opacity: offlineLoader.item !== null ? (1 - offlineLoader.item.opacity) : 1
+
+ onContactActivated: {
+ if (contact.status === ContactUser.RequestPending || contact.status === ContactUser.RequestRejected) {
+ actions.openPreferences()
+ } else if (!uiSettings.data.combinedChatWindow) {
+ actions.openWindow()
+ }
+ }
+ }
+
+ Loader {
+ id: offlineLoader
+ active: torControl.torStatus !== TorControl.TorReady || (item !== null && item.visible)
+ anchors.fill: parent
+ source: Qt.resolvedUrl("OfflineStateItem.qml")
+ }
+ }
+ }
+
+ Rectangle {
+ visible: combinedChatView.visible
+ width: 1
+ Layout.fillHeight: true
+ color: Qt.darker(palette.window, 1.5)
+ }
+
+ PageView {
+ id: combinedChatView
+ visible: uiSettings.data.combinedChatWindow || false
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ property QtObject currentContact: (visible && width > 0) ? contactList.selectedContact : null
+ onCurrentContactChanged: {
+ if (currentContact !== null) {
+ show(currentContact.uniqueID, Qt.resolvedUrl("ChatPage.qml"),
+ { 'contact': currentContact });
+ } else {
+ currentKey = ""
+ }
+ }
+ }
+ }
+
+ property bool inactive: true
+ onActiveFocusItemChanged: {
+ // Focus current page when window regains focus
+ if (activeFocusItem !== null && inactive) {
+ inactive = false
+ retakeFocus.start()
+ } else if (activeFocusItem === null) {
+ inactive = true
+ }
+ }
+
+ Timer {
+ id: retakeFocus
+ interval: 1
+ onTriggered: {
+ if (combinedChatView.currentPage !== null)
+ combinedChatView.currentPage.forceActiveFocus()
+ }
+ }
+}
+
diff --git a/src/ui/qml/MessageDelegate.qml b/src/ui/qml/MessageDelegate.qml
new file mode 100644
index 0000000..f63382c
--- /dev/null
+++ b/src/ui/qml/MessageDelegate.qml
@@ -0,0 +1,179 @@
+import QtQuick 2.0
+import QtQuick.Controls 1.0
+import im.ricochet 1.0
+
+Column {
+ id: delegate
+ width: parent.width
+
+ Loader {
+ active: {
+ if (model.section === "offline")
+ return true
+
+ // either this is the first message, or the message was a long time ago..
+ if ((model.timespan === -1 ||
+ model.timespan > 3600 /* one hour */))
+ return true
+
+ return false
+ }
+
+ sourceComponent: Label {
+ //: %1 nickname
+ text: {
+ if (model.section === "offline")
+ return qsTr("%1 is offline").arg(contact !== null ? contact.nickname : "")
+ else
+ return Qt.formatDateTime(model.timestamp, Qt.DefaultLocaleShortDate)
+ }
+ width: background.parent.width
+ elide: Text.ElideRight
+ horizontalAlignment: Qt.AlignHCenter
+ color: palette.mid
+
+ Rectangle {
+ id: line
+ width: (parent.width - parent.contentWidth) / 2 - 4
+ height: 1
+ y: (parent.height - 1) / 2
+ color: Qt.lighter(palette.mid, 1.4)
+ }
+
+ Rectangle {
+ width: line.width
+ height: 1
+ y: line.y
+ x: parent.width - width
+ color: line.color
+ }
+ }
+ }
+
+ Rectangle {
+ id: background
+ width: Math.max(30, textField.width + 12)
+ height: textField.height + 12
+ x: model.isOutgoing ? parent.width - width - 11 : 10
+
+ property int __maxWidth: parent.width * 0.8
+
+ color: (model.status === ConversationModel.Error) ? "#ffdcc4" : ( model.isOutgoing ? "#eaeced" : "#c4e7ff" )
+ Behavior on color { ColorAnimation { } }
+
+ Rectangle {
+ rotation: 45
+ width: 10
+ height: 10
+ x: model.isOutgoing ? parent.width - 20 : 10
+ y: model.isOutgoing ? parent.height - 5 : -5
+ color: parent.color
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ anchors.margins: 1
+ opacity: (model.status === ConversationModel.Sending || model.status === ConversationModel.Queued || model.status === ConversationModel.Error) ? 1 : 0
+ visible: opacity > 0
+ color: Qt.lighter(parent.color, 1.15)
+
+ Behavior on opacity { NumberAnimation { } }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ acceptedButtons: Qt.RightButton
+
+ onClicked: delegate.showContextMenu()
+ }
+
+ TextEdit {
+ id: textField
+ width: Math.min(implicitWidth, background.__maxWidth)
+ height: contentHeight
+ x: Math.round((parent.width - width) / 2)
+ y: 6
+
+ renderType: Text.NativeRendering
+ textFormat: TextEdit.RichText
+ selectionColor: palette.highlight
+ selectedTextColor: palette.highlightedText
+ font.pointSize: styleHelper.pointSize
+
+ wrapMode: TextEdit.Wrap
+ readOnly: true
+ selectByMouse: true
+ text: LinkedText.parsed(model.text)
+
+ onLinkActivated: {
+ textField.deselect()
+ delegate.showContextMenu(link)
+ }
+
+ // Workaround an incomplete fix for QTBUG-31646
+ Component.onCompleted: {
+ if (textField.hasOwnProperty('linkHovered'))
+ textField.linkHovered.connect(function() { })
+ }
+ }
+ }
+
+ function showContextMenu(link) {
+ var object = contextMenu.createObject(delegate, (link !== undefined) ? { 'hoveredLink': link } : { })
+ // XXX QtQuickControls private API. The only other option is 'visible', and it is not reliable. See PR#183
+ object.popupVisibleChanged.connect(function() { if (!object.__popupVisible) object.destroy(1000) })
+ object.popup()
+ }
+
+ Component {
+ id: contextMenu
+
+ Menu {
+ property string hoveredLink: textField.hasOwnProperty('hoveredLink') ? textField.hoveredLink : ""
+ MenuItem {
+ text: linkAddContact.visible ? qsTr("Copy ID") : qsTr("Copy Link")
+ visible: hoveredLink.length > 0
+ onTriggered: LinkedText.copyToClipboard(hoveredLink)
+ }
+ MenuItem {
+ text: qsTr("Open with Browser")
+ visible: hoveredLink.length > 0 && hoveredLink.substr(0,4).toLowerCase() == "http"
+ onTriggered: {
+ if (uiSettings.data.alwaysOpenBrowser || contact.settings.data.alwaysOpenBrowser) {
+ Qt.openUrlExternally(hoveredLink)
+ } else {
+ var window = uiMain.findParentWindow(delegate)
+ var object = createDialog("OpenBrowserDialog.qml", { 'link': hoveredLink, 'contact': contact }, window)
+ object.visible = true
+ }
+ }
+ }
+ MenuItem {
+ id: linkAddContact
+ text: qsTr("Add as Contact")
+ visible: hoveredLink.length > 0 && (hoveredLink.substr(0,9).toLowerCase() == "ricochet:"
+ || hoveredLink.substr(0,8).toLowerCase() == "torsion:")
+ onTriggered: {
+ var object = createDialog("AddContactDialog.qml", { 'staticContactId': hoveredLink }, chatWindow)
+ object.visible = true
+ }
+ }
+ MenuSeparator {
+ visible: hoveredLink.length > 0
+ }
+ MenuItem {
+ text: qsTr("Copy Message")
+ visible: textField.selectedText.length == 0
+ onTriggered: {
+ LinkedText.copyToClipboard(textField.getText(0, textField.length))
+ }
+ }
+ MenuItem {
+ text: qsTr("Copy Selection")
+ visible: textField.selectedText.length > 0
+ shortcut: "Ctrl+C"
+ onTriggered: textField.copy()
+ }
+ }
+ }
+}
diff --git a/src/ui/qml/MessageDialogWrapper.qml b/src/ui/qml/MessageDialogWrapper.qml
new file mode 100644
index 0000000..c4b3c9f
--- /dev/null
+++ b/src/ui/qml/MessageDialogWrapper.qml
@@ -0,0 +1,13 @@
+import QtQuick 2.0
+import QtQuick.Dialogs 1.1
+
+MessageDialog {
+ id: removeContactDialog
+
+ title: qsTr("Remove %1").arg(contact.nickname)
+ //: %1 nickname
+ text: qsTr("Do you want to permanently remove %1?").arg(contact.nickname)
+ informativeText: qsTr("This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request.")
+ standardButtons: StandardButton.Yes | StandardButton.No
+ onYes: contact.deleteContact()
+}
diff --git a/src/ui/qml/NetworkSetupWizard.qml b/src/ui/qml/NetworkSetupWizard.qml
new file mode 100644
index 0000000..024e67d
--- /dev/null
+++ b/src/ui/qml/NetworkSetupWizard.qml
@@ -0,0 +1,145 @@
+import QtQuick 2.0
+import QtQuick.Controls 1.0
+import QtQuick.Layouts 1.0
+
+ApplicationWindow {
+ id: window
+ width: minimumWidth
+ height: minimumHeight
+ minimumWidth: 400
+ maximumWidth: minimumWidth
+ minimumHeight: visibleItem.height + 16
+ maximumHeight: minimumHeight
+ title: "Ricochet"
+
+ signal networkReady
+ signal closed
+
+ onVisibleChanged: if (!visible) closed()
+
+ property Item visibleItem: configPage.visible ? configPage : pageLoader.item
+
+ function back() {
+ if (pageLoader.visible) {
+ pageLoader.visible = false
+ configPage.visible = true
+ } else {
+ openBeginning()
+ }
+ }
+
+ function openBeginning() {
+ configPage.visible = false
+ configPage.reset()
+ pageLoader.sourceComponent = firstPage
+ pageLoader.visible = true
+ }
+
+ function openConfig() {
+ pageLoader.visible = false
+ configPage.visible = true
+ }
+
+ function openBootstrap() {
+ configPage.visible = false
+ pageLoader.source = Qt.resolvedUrl("TorBootstrapStatus.qml")
+ pageLoader.visible = true
+ }
+
+ Loader {
+ id: pageLoader
+ anchors {
+ top: parent.top
+ left: parent.left
+ right: parent.right
+ margins: 8
+ }
+ sourceComponent: firstPage
+ }
+
+ TorConfigurationPage {
+ id: configPage
+ anchors {
+ top: parent.top
+ left: parent.left
+ right: parent.right
+ margins: 8
+ }
+ visible: false
+ }
+
+ StartupStatusPage {
+ id: statusPage
+ anchors {
+ top: parent.top
+ left: parent.left
+ right: parent.right
+ margins: 8
+ }
+ visible: false
+
+ onHasErrorChanged: {
+ if (hasError) {
+ if (visibleItem)
+ visibleItem.visible = false
+ pageLoader.visible = false
+ statusPage.visible = true
+ visibleItem = statusPage
+ }
+ }
+ }
+
+ Component {
+ id: firstPage
+
+ Column {
+ spacing: 8
+
+ Label {
+ width: parent.width
+ text: qsTr("This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network.")
+ wrapMode: Text.Wrap
+ horizontalAlignment: Qt.AlignHCenter
+ }
+
+ Button {
+ anchors.horizontalCenter: parent.horizontalCenter
+ text: qsTr("Connect")
+ isDefault: true
+ onClicked: {
+ // Reset to defaults and proceed to bootstrap page
+ configPage.reset()
+ configPage.save()
+ }
+ }
+
+ Rectangle {
+ height: 1
+ width: parent.width
+ color: palette.mid
+ }
+
+ Label {
+ width: parent.width
+ text: qsTr("This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings.")
+ wrapMode: Text.Wrap
+ horizontalAlignment: Qt.AlignHCenter
+ }
+
+ Button {
+ anchors.horizontalCenter: parent.horizontalCenter
+ text: qsTr("Configure")
+ onClicked: window.openConfig()
+ }
+ }
+ }
+
+ Behavior on height {
+ // This window animation causes bad graphical behavior on Windows with 5.4.1
+ enabled: Qt.platform.os !== "windows"
+ SmoothedAnimation {
+ easing.type: Easing.InOutQuad
+ velocity: 1500
+ }
+ }
+}
diff --git a/src/ui/qml/OfflineStateItem.qml b/src/ui/qml/OfflineStateItem.qml
new file mode 100644
index 0000000..7acf8b8
--- /dev/null
+++ b/src/ui/qml/OfflineStateItem.qml
@@ -0,0 +1,235 @@
+import QtQuick 2.0
+import QtQuick.Controls 1.0
+import QtQuick.Layouts 1.0
+import im.ricochet 1.0
+
+MouseArea {
+ id: offlineState
+ acceptedButtons: Qt.LeftButton | Qt.RightButton
+ visible: opacity > 0
+ enabled: visible
+ opacity: 0
+ clip: true
+
+ Behavior on opacity { NumberAnimation { duration: 500 } }
+
+ Rectangle {
+ anchors.fill: parent
+ color: palette.base
+ }
+
+ Label {
+ id: label
+ anchors {
+ horizontalCenter: parent.horizontalCenter
+ verticalCenter: parent.verticalCenter
+ verticalCenterOffset: parent.height / -3
+ }
+ font.pointSize: 14
+ }
+
+ Rectangle {
+ id: indicator
+ width: label.width
+ anchors {
+ top: label.bottom
+ topMargin: 2
+ }
+ height: 2
+ x: label.x
+
+ onWidthChanged: if (indicatorAnimation.running) indicatorAnimation.restart()
+
+ property alias running: indicatorAnimation.running
+
+ SequentialAnimation {
+ id: indicatorAnimation
+
+ function restart() {
+ stop()
+ animation1.to = offlineState.width
+ animation2.from = -indicator.width
+ animation2.to = offlineState.width
+ start()
+ }
+
+ NumberAnimation {
+ id: animation1
+ target: indicator
+ property: "x"
+ to: offlineState.width
+ duration: 500
+ easing.type: Easing.InQuad
+ }
+
+ NumberAnimation {
+ id: animation2
+ loops: Animation.Infinite
+ target: indicator
+ property: "x"
+ from: -indicator.width
+ to: offlineState.width
+ duration: 1500
+ easing.type: Easing.OutInQuad
+ }
+ }
+ }
+
+ onWidthChanged: if (indicatorAnimation.running) indicatorAnimation.restart()
+
+ Label {
+ id: detailedLabel
+ anchors {
+ left: parent.left
+ right: parent.right
+ top: indicator.bottom
+ margins: 16
+ }
+ wrapMode: Text.Wrap
+ horizontalAlignment: Text.AlignHCenter
+ color: Qt.lighter(palette.text, 1.2)
+ font.pointSize: 11
+ text: torControl.errorMessage
+ }
+
+ GridLayout {
+ id: buttonRow
+ visible: false
+ anchors {
+ left: parent.left
+ right: parent.right
+ top: detailedLabel.bottom
+ margins: 16
+ topMargin: 32
+ }
+ Button {
+ Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
+ text: qsTr("Configure")
+ onClicked: {
+ var object = createDialog("NetworkSetupWizard.qml", { }, window)
+ object.visible = true
+ }
+ }
+ Button {
+ Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
+ text: qsTr("Details")
+ onClicked: {
+ openPreferences("TorPreferences.qml")
+ }
+ }
+ }
+
+ states: [
+ State {
+ name: "connected"
+ when: torControl.torStatus === TorControl.TorReady
+
+ PropertyChanges {
+ target: offlineState
+ opacity: 0
+ }
+ },
+ State {
+ name: "failed"
+ when: torControl.status === TorControl.Error
+
+ PropertyChanges {
+ target: offlineState
+ opacity: 1
+ }
+
+ PropertyChanges {
+ target: label
+ text: qsTr("Connection failed")
+ }
+
+ PropertyChanges {
+ target: indicator
+ color: "#ffdcc4"
+ running: false
+ }
+
+ PropertyChanges {
+ target: buttonRow
+ visible: true
+ }
+ },
+ State {
+ name: "connecting"
+ when: torControl.torStatus !== TorControl.TorReady
+
+ PropertyChanges {
+ target: offlineState
+ opacity: 1
+ }
+
+ PropertyChanges {
+ target: label
+ //: \u2026 is ellipsis
+ text: qsTr("Connecting\u2026")
+ }
+
+ PropertyChanges {
+ target: indicator
+ color: "#c4e7ff"
+ running: true
+ x: label.x
+ }
+ }
+ ]
+
+ transitions: [
+ Transition {
+ to: "connecting"
+
+ SequentialAnimation {
+ PropertyAction {
+ target: label
+ property: "text"
+ }
+
+ PropertyAction {
+ target: indicator
+ property: "running"
+ }
+
+ ColorAnimation {
+ target: indicator
+ property: "color"
+ duration: 1000
+ }
+ }
+ },
+ Transition {
+ to: "failed"
+
+ SequentialAnimation {
+ PropertyAction {
+ target: indicator
+ property: "running"
+ }
+
+ PropertyAction {
+ target: label
+ property: "text"
+ }
+
+ ParallelAnimation {
+ NumberAnimation {
+ target: indicator
+ property: "x"
+ duration: 1000
+ easing.type: Easing.OutQuad
+ }
+
+ ColorAnimation {
+ target: indicator
+ property: "color"
+ duration: 1000
+ }
+ }
+ }
+ }
+ ]
+}
+
diff --git a/src/ui/qml/OpenBrowserDialog.qml b/src/ui/qml/OpenBrowserDialog.qml
new file mode 100644
index 0000000..b28fda8
--- /dev/null
+++ b/src/ui/qml/OpenBrowserDialog.qml
@@ -0,0 +1,92 @@
+import QtQuick 2.0
+import QtQuick.Controls 1.0
+import QtQuick.Layouts 1.0
+import im.ricochet 1.0
+
+ApplicationWindow {
+ id: dialog
+ width: 400
+ height: layout.height + 32
+ minimumWidth: width
+ maximumWidth: width
+ minimumHeight: height
+ maximumHeight: height
+ flags: styleHelper.dialogWindowFlags
+ modality: Qt.WindowModal
+ title: mainWindow.title
+
+ signal closed
+ onVisibleChanged: if (!visible) closed()
+
+ function close() { visible = false }
+
+ property string link
+ property QtObject contact
+
+ ColumnLayout {
+ id: layout
+ focus: true
+ spacing: 8
+ anchors {
+ left: parent.left
+ right: parent.right
+ top: parent.top
+ margins: 16
+ }
+
+ Label {
+ Layout.fillWidth: true
+ text: qsTr("<b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead.")
+ wrapMode: Text.Wrap
+ horizontalAlignment: Qt.AlignHCenter
+ onLinkActivated: {
+ LinkedText.copyToClipboard(dialog.link)
+ dialog.close()
+ }
+ }
+
+ Item { width: 1; height: 1 }
+
+ Rectangle {
+ height: 1
+ Layout.fillWidth: true
+ color: Qt.darker(palette.window, 1.5)
+ }
+
+ CheckBox {
+ id: alwaysOpenContact
+ text: qsTr("Don't ask again for links from %1").arg(contact ? contact.nickname : "???")
+ checked: contact.settings.data.alwaysOpenBrowser || false
+ }
+
+ CheckBox {
+ id: alwaysOpenAll
+ text: qsTr("Don't ask again for any links (not recommended!)")
+ checked: uiSettings.data.alwaysOpenBrowser || false
+ }
+
+ RowLayout {
+ width: parent.width
+ Button {
+ text: qsTr("Open Browser")
+ onClicked: {
+ if (alwaysOpenContact.checked)
+ contact.settings.write("alwaysOpenBrowser", true)
+ if (alwaysOpenAll.checked)
+ uiSettings.write("alwaysOpenBrowser", true)
+ Qt.openUrlExternally(link)
+ dialog.close()
+ }
+ }
+ Item { Layout.fillWidth: true; height: 1 }
+ Button {
+ text: qsTr("Cancel")
+ isDefault: true
+ onClicked: dialog.close()
+ }
+ }
+
+ Keys.onEscapePressed: dialog.close()
+ Keys.onReturnPressed: dialog.close()
+ }
+}
diff --git a/src/ui/qml/PageView.qml b/src/ui/qml/PageView.qml
new file mode 100644
index 0000000..048c12b
--- /dev/null
+++ b/src/ui/qml/PageView.qml
@@ -0,0 +1,68 @@
+import QtQuick 2.0
+
+/* Simple QML view that associates a string key with a page, and
+ * displays one page at a time. */
+
+FocusScope {
+ property Item currentPage
+ property string currentKey
+ property var _items: { '': null }
+
+ function add(key, source, properties) {
+ if (key === "")
+ return
+ if (_items[key] !== null)
+ remove(key)
+
+ var component = Qt.createComponent(source, content)
+ if (component.status !== Component.Ready) {
+ console.log("PageView:", source, component.errorString())
+ return
+ }
+
+ if (properties === undefined)
+ properties = [ ]
+ properties['visible'] = false
+ properties['anchors.fill'] = content
+
+ var item = component.createObject(content, properties)
+ _items[key] = item
+ }
+
+ function show(key, source, properties) {
+ if (_items[key] === undefined)
+ add(key, source, properties)
+ currentKey = key
+ }
+
+ function remove(key) {
+ var item = _items[key]
+ if (item !== undefined) {
+ if (item === currentPage)
+ currentKey = null
+ _items[key] = undefined
+ item.destroy()
+ }
+ }
+
+ onCurrentKeyChanged: {
+ var item = _items[currentKey]
+ if (item === currentPage)
+ return
+
+ if (currentPage !== null) {
+ currentPage.visible = false
+ currentPage.focus = false
+ }
+ currentPage = item || null
+ if (currentPage !== null) {
+ currentPage.visible = true
+ currentPage.focus = true
+ }
+ }
+
+ Item {
+ id: content
+ anchors.fill: parent
+ }
+}
diff --git a/src/ui/qml/PreferencesDialog.qml b/src/ui/qml/PreferencesDialog.qml
new file mode 100644
index 0000000..95fc3c7
--- /dev/null
+++ b/src/ui/qml/PreferencesDialog.qml
@@ -0,0 +1,65 @@
+import QtQuick 2.0
+import QtQuick.Controls 1.0
+import QtQuick.Layouts 1.0
+import im.ricochet 1.0
+
+ApplicationWindow {
+ id: preferencesWindow
+ width: 550
+ minimumWidth: 550
+ height: 400
+ minimumHeight: 400
+ title: qsTr("Ricochet Preferences")
+
+ signal closed
+ onVisibleChanged: if (!visible) closed()
+
+ property string initialPage
+ property var initialPageProperties: { }
+
+ Component.onCompleted: {
+ if (initialPage != "") {
+ initialPage = Qt.resolvedUrl(initialPage)
+ for (var i = 0; i < tabs.count; i++) {
+ if (tabs.getTab(i).source == initialPage) {
+ tabs.currentIndex = i
+ var item = tabs.getTab(i).item
+ for (var key in initialPageProperties) {
+ item[key] = initialPageProperties[key]
+ }
+ }
+ }
+ }
+ }
+
+ TabView {
+ id: tabs
+ anchors.fill: parent
+ anchors.margins: 8
+
+ Tab {
+ title: qsTr("General")
+ source: Qt.resolvedUrl("GeneralPreferences.qml")
+ }
+
+ Tab {
+ title: qsTr("Language")
+ source: Qt.resolvedUrl("LanguagePreferences.qml")
+ }
+
+ Tab {
+ title: qsTr("Contacts")
+ source: Qt.resolvedUrl("ContactPreferences.qml")
+ }
+
+ Tab {
+ title: qsTr("Tor")
+ source: Qt.resolvedUrl("TorPreferences.qml")
+ }
+
+ Tab {
+ title: qsTr("About")
+ source: Qt.resolvedUrl("AboutPreferences.qml")
+ }
+ }
+}
diff --git a/src/ui/qml/PresenceIcon.qml b/src/ui/qml/PresenceIcon.qml
new file mode 100644
index 0000000..f42bddb
--- /dev/null
+++ b/src/ui/qml/PresenceIcon.qml
@@ -0,0 +1,23 @@
+import QtQuick 2.0
+import im.ricochet 1.0
+
+Rectangle {
+ id: presenceIcon
+ width: 10
+ height: 10
+ radius: 360
+
+ property int status: -1
+
+ onStatusChanged: {
+ if (status === ContactUser.Online)
+ color = "#3EBB4F"
+ else
+ color = "#999999"
+ }
+
+ Behavior on color {
+ ColorAnimation { }
+ }
+}
+
diff --git a/src/ui/qml/StartupStatusPage.qml b/src/ui/qml/StartupStatusPage.qml
new file mode 100644
index 0000000..5b55ba3
--- /dev/null
+++ b/src/ui/qml/StartupStatusPage.qml
@@ -0,0 +1,54 @@
+import QtQuick 2.0
+import QtQuick.Controls 1.0
+import QtQuick.Layouts 1.0
+
+Column {
+ id: statusPage
+ spacing: 8
+
+ property bool hasError: torInstance.hasError
+
+ Label {
+ anchors {
+ left: parent.left
+ right: parent.right
+ margins: 8
+ }
+
+ text: qsTr("The Tor process was not started successfully. This is most likely an installation or system error.")
+ font.bold: true
+ wrapMode: Text.Wrap
+ }
+
+ Label {
+ anchors {
+ left: parent.left
+ right: parent.right
+ margins: 8
+ }
+
+ text: torInstance.errorMessage
+ wrapMode: Text.Wrap
+ }
+
+ TorLogDisplay {
+ id: logDisplay
+ width: parent.width
+ height: text.length > 0 ? 300 : 0
+ }
+
+ RowLayout {
+ anchors {
+ left: parent.left
+ right: parent.right
+ margins: 8
+ }
+
+ Item { height: 1; Layout.fillWidth: true }
+ Button {
+ text: qsTr("Quit")
+ onClicked: Qt.quit()
+ }
+ }
+}
+
diff --git a/src/ui/qml/TorBootstrapStatus.qml b/src/ui/qml/TorBootstrapStatus.qml
new file mode 100644
index 0000000..d265924
--- /dev/null
+++ b/src/ui/qml/TorBootstrapStatus.qml
@@ -0,0 +1,76 @@
+import QtQuick 2.0
+import QtQuick.Controls 1.0
+import QtQuick.Layouts 1.0
+
+Column {
+ id: page
+ spacing: 8
+
+ property var bootstrap: torControl.bootstrapStatus
+ onBootstrapChanged: {
+ if (bootstrap['tag'] === "done")
+ window.networkReady()
+ }
+
+ Label {
+ //: \u2026 is ellipsis
+ text: qsTr("Connecting to the Tor network\u2026")
+ font.bold: true
+ }
+
+ ProgressBar {
+ width: parent.width
+ maximumValue: 100
+ indeterminate: bootstrap.progress === undefined
+ value: bootstrap.progress === undefined ? 0 : bootstrap.progress
+ }
+
+ Label {
+ text: (bootstrap['warning'] !== undefined ) ? bootstrap['warning'] : bootstrap['summary']
+ }
+
+ TorLogDisplay {
+ id: logDisplay
+ width: parent.width
+ height: 0
+ visible: height > 0
+
+ Behavior on height {
+ SmoothedAnimation {
+ easing.type: Easing.InOutQuad
+ velocity: 1500
+ }
+ }
+ }
+
+ RowLayout {
+ width: parent.width
+
+ Button {
+ text: qsTr("Back")
+ onClicked: window.back()
+ }
+
+ Item { height: 1; Layout.fillWidth: true }
+
+ Button {
+ text: logDisplay.height ? qsTr("Hide details") : qsTr("Show details")
+ onClicked: {
+ if (logDisplay.height)
+ logDisplay.height = 0
+ else
+ logDisplay.height = 300
+ }
+ }
+
+ Item { height: 1; Layout.fillWidth: true }
+
+ Button {
+ text: qsTr("Done")
+ isDefault: true
+ enabled: bootstrap.tag === "done"
+ onClicked: window.visible = false
+ }
+ }
+}
+
diff --git a/src/ui/qml/TorConfigurationPage.qml b/src/ui/qml/TorConfigurationPage.qml
new file mode 100644
index 0000000..cf285b9
--- /dev/null
+++ b/src/ui/qml/TorConfigurationPage.qml
@@ -0,0 +1,226 @@
+import QtQuick 2.0
+import QtQuick.Controls 1.0
+import QtQuick.Layouts 1.0
+
+Column {
+ id: setup
+ spacing: 8
+
+ property alias proxyType: proxyTypeField.selectedType
+ property alias proxyAddress: proxyAddressField.text
+ property alias proxyPort: proxyPortField.text
+ property alias proxyUsername: proxyUsernameField.text
+ property alias proxyPassword: proxyPasswordField.text
+ property alias allowedPorts: allowedPortsField.text
+ property alias bridges: bridgesField.text
+
+ function reset() {
+ proxyTypeField.currentIndex = 0
+ proxyAddress = ''
+ proxyPort = ''
+ proxyUsername = ''
+ proxyPassword = ''
+ allowedPorts = ''
+ bridges = ''
+ }
+
+ function save() {
+ // null value is reset
+ var conf = {
+ 'Socks4Proxy': null, 'Socks5Proxy': null, 'Socks5ProxyUsername': null,
+ 'Socks5ProxyPassword': null, 'HTTPProxy': null, 'HTTPProxyAuthenticator': null,
+ 'FirewallPorts': null, 'FascistFirewall': null, 'Bridge': null, 'UseBridges': null,
+ 'DisableNetwork': '0'
+ }
+
+ if (proxyType === "socks4") {
+ conf['Socks4Proxy'] = proxyAddress + ":" + proxyPort
+ } else if (proxyType === "socks5") {
+ conf['Socks5Proxy'] = proxyAddress + ":" + proxyPort
+ if (proxyUsername.length > 0)
+ conf['Socks5ProxyUsername'] = proxyUsername
+ if (proxyPassword.length > 0)
+ conf['Socks5ProxyPassword'] = proxyPassword
+ } else if (proxyType === "http") {
+ conf['HTTPProxy'] = proxyAddress + ":" + proxyPort
+ if (proxyUsername.length > 0 || proxyPassword.length > 0)
+ conf['HTTPProxyAuthenticator'] = proxyUsername + ":" + proxyPassword
+ }
+
+ if (allowedPorts.length > 0) {
+ conf['FirewallPorts'] = allowedPorts
+ conf['FascistFirewall'] = "1"
+ }
+
+ if (bridges.length > 0) {
+ conf['Bridge'] = bridges.split('\n')
+ conf['UseBridges'] = "1"
+ }
+
+ var command = torControl.setConfiguration(conf)
+ command.finished.connect(function() {
+ if (command.successful) {
+ torControl.saveConfiguration()
+ window.openBootstrap()
+ } else
+ console.log("SETCONF error:", command.errorMessage)
+ })
+ }
+
+ Label {
+ width: parent.width
+ text: qsTr("Does this computer need a proxy to access the internet?")
+ wrapMode: Text.Wrap
+ }
+
+ GroupBox {
+ width: setup.width
+
+ GridLayout {
+ anchors.fill: parent
+ columns: 2
+
+ Label {
+ text: qsTr("Proxy type:")
+ color: proxyPalette.text
+ }
+ ComboBox {
+ id: proxyTypeField
+ property string none: qsTr("None")
+ model: [
+ { "text": qsTr("None"), "type": "" },
+ { "text": "SOCKS 4", "type": "socks4" },
+ { "text": "SOCKS 5", "type": "socks5" },
+ { "text": "HTTP", "type": "http" },
+ ]
+ textRole: "text"
+ property string selectedType: currentIndex >= 0 ? model[currentIndex].type : ""
+
+ SystemPalette {
+ id: proxyPalette
+ colorGroup: setup.proxyType == "" ? SystemPalette.Disabled : SystemPalette.Active
+ }
+ }
+
+ Label {
+ text: qsTr("Address:")
+ color: proxyPalette.text
+ }
+ RowLayout {
+ Layout.fillWidth: true
+ TextField {
+ id: proxyAddressField
+ Layout.fillWidth: true
+ enabled: setup.proxyType
+ placeholderText: qsTr("IP address or hostname")
+ }
+ Label {
+ text: qsTr("Port:")
+ color: proxyPalette.text
+ }
+ TextField {
+ id: proxyPortField
+ Layout.preferredWidth: 50
+ enabled: setup.proxyType
+ }
+ }
+
+ Label {
+ text: qsTr("Username:")
+ color: proxyPalette.text
+ }
+ RowLayout {
+ Layout.fillWidth: true
+
+ TextField {
+ id: proxyUsernameField
+ Layout.fillWidth: true
+ enabled: setup.proxyType
+ placeholderText: qsTr("Optional")
+ }
+ Label {
+ text: qsTr("Password:")
+ color: proxyPalette.text
+ }
+ TextField {
+ id: proxyPasswordField
+ Layout.fillWidth: true
+ enabled: setup.proxyType
+ placeholderText: qsTr("Optional")
+ }
+ }
+ }
+ }
+
+ Item { height: 4; width: 1 }
+
+ Label {
+ width: parent.width
+ text: qsTr("Does this computer's Internet connection go through a firewall that only allows connections to certain ports?")
+ wrapMode: Text.Wrap
+ }
+
+ GroupBox {
+ width: parent.width
+ // Workaround OS X visual bug
+ height: Math.max(implicitHeight, 40)
+ RowLayout {
+ anchors.fill: parent
+ Label {
+ text: qsTr("Allowed ports:")
+ }
+ TextField {
+ id: allowedPortsField
+ Layout.fillWidth: true
+ }
+ Label {
+ text: qsTr("Example: 80,443")
+ SystemPalette { id: disabledPalette; colorGroup: SystemPalette.Disabled }
+ color: disabledPalette.text
+ }
+ }
+ }
+
+ Item { height: 4; width: 1 }
+
+ Label {
+ width: parent.width
+ text: qsTr("If this computer's Internet connection is censored, you will need to obtain and use bridge relays.")
+ wrapMode: Text.Wrap
+ }
+
+ GroupBox {
+ width: parent.width
+ ColumnLayout {
+ anchors.fill: parent
+ Label {
+ text: qsTr("Enter one or more bridge relays (one per line):")
+ }
+ TextArea {
+ id: bridgesField
+ Layout.fillWidth: true
+ Layout.preferredHeight: allowedPortsField.height * 2
+ tabChangesFocus: true
+ }
+ }
+ }
+
+ RowLayout {
+ width: parent.width
+
+ Button {
+ text: qsTr("Back")
+ onClicked: window.back()
+ }
+
+ Item { height: 1; Layout.fillWidth: true }
+
+ Button {
+ text: qsTr("Connect")
+ isDefault: true
+ onClicked: {
+ setup.save()
+ }
+ }
+ }
+}
diff --git a/src/ui/qml/TorLogDisplay.qml b/src/ui/qml/TorLogDisplay.qml
new file mode 100644
index 0000000..b5eb7ea
--- /dev/null
+++ b/src/ui/qml/TorLogDisplay.qml
@@ -0,0 +1,17 @@
+import QtQuick 2.0
+import QtQuick.Controls 1.0
+import QtQuick.Layouts 1.0
+
+TextArea {
+ id: logDisplay
+ readOnly: true
+ text: torInstance.logMessages.join('\n')
+ wrapMode: TextEdit.Wrap
+
+ Connections {
+ target: torInstance.process
+ onLogMessage: {
+ logDisplay.append(message)
+ }
+ }
+}
diff --git a/src/ui/qml/TorPreferences.qml b/src/ui/qml/TorPreferences.qml
new file mode 100644
index 0000000..954ca75
--- /dev/null
+++ b/src/ui/qml/TorPreferences.qml
@@ -0,0 +1,94 @@
+import QtQuick 2.0
+import QtQuick.Controls 1.0
+import QtQuick.Layouts 1.0
+import im.ricochet 1.0
+
+Item {
+ anchors.fill: parent
+
+ property var bootstrap: torInstance.control.bootstrapStatus
+
+ Column {
+ id: info
+ anchors {
+ left: parent.left
+ right: parent.right
+ top: parent.top
+ margins: 8
+ }
+ spacing: 6
+
+ GridLayout {
+ columns: 4
+ width: parent.width
+ Label { text: qsTr("Running:") }
+ Label { font.bold: true; Layout.fillWidth: true; text: (torInstance.process ? (torInstance.process.state == TorProcess.Ready ? qsTr("Yes") : qsTr("No")) : qsTr("External")) }
+ Label { text: qsTr("Control connected:") }
+ Label { font.bold: true; Layout.fillWidth: true; text: ((torInstance.control.status == TorControl.Connected) ? qsTr("Yes") : qsTr("No")) }
+ Label { text: qsTr("Circuits established:") }
+ Label { font.bold: true; text: ((torInstance.control.torStatus == TorControl.TorReady) ? qsTr("Yes") : qsTr("No")) }
+ Label { text: qsTr("Hidden service:") }
+ Label { font.bold: true; text: (userIdentity.isOnline ? qsTr("Online") : qsTr("Offline")) }
+ Label { text: qsTr("Version:") }
+ Label { font.bold: true; text: torControl.torVersion }
+ //Label { text: "Recommended:" }
+ //Label { font.bold: true; text: "Unknown" }
+ }
+
+ Rectangle {
+ width: parent.width
+ height: 1
+ color: palette.mid
+ }
+
+ Label {
+ text: bootstrap.summary
+ visible: bootstrap.tag !== 'done'
+ }
+
+ ProgressBar {
+ width: parent.width
+ maximumValue: 100
+ indeterminate: bootstrap.progress === undefined
+ value: bootstrap.progress === undefined ? 0 : bootstrap.progress
+ visible: bootstrap.tag !== 'done'
+ }
+
+ Label {
+ //: %1 is error message
+ text: qsTr("Error: <b>%1</b>").arg(errorMessage)
+ visible: errorMessage != ""
+
+ property string errorMessage: {
+ if (torInstance.hasError)
+ return torInstance.errorMessage
+ else if (torInstance.control.errorMessage != "")
+ return torInstance.control.errorMessage
+ else if (bootstrap.warning !== undefined)
+ return bootstrap.warning
+ else
+ return ""
+ }
+ }
+
+ Button {
+ text: qsTr("Configure")
+ onClicked: {
+ var object = createDialog("NetworkSetupWizard.qml")
+ object.visible = true
+ }
+ }
+ }
+
+ TorLogDisplay {
+ anchors {
+ left: parent.left
+ right: parent.right
+ top: info.bottom
+ bottom: parent.bottom
+ margins: 8
+ }
+ visible: torInstance.process !== null
+ }
+}
+
diff --git a/src/ui/qml/TorStateWidget.qml b/src/ui/qml/TorStateWidget.qml
new file mode 100644
index 0000000..e8def82
--- /dev/null
+++ b/src/ui/qml/TorStateWidget.qml
@@ -0,0 +1,37 @@
+import QtQuick 2.0
+import QtQuick.Controls 1.0
+import QtQuick.Layouts 1.0
+import im.ricochet 1.0
+
+Label {
+ text: {
+ if (torControl.status === TorControl.Error)
+ return qsTr("Connection failed")
+ if (torControl.status < TorControl.Connected) {
+ //: \u2026 is ellipsis
+ return qsTr("Connecting\u2026")
+ }
+
+ if (torControl.torStatus === TorControl.TorUnknown ||
+ torControl.torStatus === TorControl.TorOffline)
+ {
+ var bootstrap = torControl.bootstrapStatus
+ if (bootstrap['recommendation'] === 'warn')
+ return qsTr("Connection failed")
+ else if (bootstrap['progress'] === undefined)
+ return qsTr("Connecting\u2026")
+ else {
+ //: %1 is progress percentage, e.g. 100
+ return qsTr("Connecting\u2026 (%1%)").arg(bootstrap['progress'])
+ }
+ }
+
+ if (torControl.torStatus === TorControl.TorReady) {
+ // Indicates whether we've verified that the hidden services is connectable
+ if (userIdentity.isOnline)
+ return qsTr("Online")
+ else
+ return qsTr("Connected")
+ }
+ }
+}
diff --git a/src/ui/qml/UnreadCountBadge.qml b/src/ui/qml/UnreadCountBadge.qml
new file mode 100644
index 0000000..c35da64
--- /dev/null
+++ b/src/ui/qml/UnreadCountBadge.qml
@@ -0,0 +1,22 @@
+import QtQuick 2.0
+import QtQuick.Controls 1.0
+
+Rectangle {
+ id: badge
+ radius: 4
+ width: number.width ? Math.max(height, number.width + 4) : 0
+ height: number.height ? number.height + 4 : 0
+ color: "#d80000"
+
+ property int value
+
+ Label {
+ id: number
+ anchors.centerIn: parent
+ font.pointSize: styleHelper.pointSize - 2
+ color: "white"
+ font.bold: true
+ text: value > 0 ? (value + "") : ""
+ }
+}
+
diff --git a/src/ui/qml/dummy.qml b/src/ui/qml/dummy.qml
new file mode 100644
index 0000000..2aa67f1
--- /dev/null
+++ b/src/ui/qml/dummy.qml
@@ -0,0 +1,7 @@
+// Unused dummy QML file to specify import dependencies
+// This isn't included in the build, but is read by qmlimportscanner for static builds.
+
+import QtQuick 2.0
+import QtQuick.PrivateWidgets 1.0
+
+Item { }
diff --git a/src/ui/qml/main.qml b/src/ui/qml/main.qml
new file mode 100644
index 0000000..8829186
--- /dev/null
+++ b/src/ui/qml/main.qml
@@ -0,0 +1,142 @@
+import QtQuick 2.0
+import QtQuick.Controls 1.0
+import QtQuick.Layouts 1.0
+import QtQuick.Window 2.0
+import im.ricochet 1.0
+import "ContactWindow.js" as ContactWindow
+
+// Root non-graphical object providing window management and other logic.
+QtObject {
+ id: root
+
+ property MainWindow mainWindow: MainWindow {
+ onVisibleChanged: if (!visible) Qt.quit()
+ }
+
+ function createDialog(component, properties, parent) {
+ if (typeof(component) === "string")
+ component = Qt.createComponent(component)
+ if (component.status !== Component.Ready)
+ console.log("openDialog:", component.errorString())
+ var object = component.createObject(parent ? parent : null, (properties !== undefined) ? properties : { })
+ if (!object)
+ console.log("openDialog:", component.errorString())
+ object.closed.connect(function() { object.destroy() })
+ return object
+ }
+
+ property QtObject preferencesDialog
+ function openPreferences(page, properties) {
+ if (preferencesDialog == null) {
+ preferencesDialog = createDialog("PreferencesDialog.qml",
+ {
+ 'initialPage': page,
+ 'initialPageProperties': properties
+ }
+ )
+ preferencesDialog.closed.connect(function() { preferencesDialog = null })
+ }
+
+ preferencesDialog.visible = true
+ preferencesDialog.raise()
+ preferencesDialog.requestActivate()
+ }
+
+ property QtObject audioNotifications: audioNotificationLoader.item
+
+ Component.onCompleted: {
+ ContactWindow.createWindow = function(user) {
+ var re = createDialog("ChatWindow.qml", { 'contact': user })
+ re.x = mainWindow.x + mainWindow.width + 10
+ re.y = mainWindow.y + (mainWindow.height / 2) - (re.height / 2)
+
+ var screens = uiMain.screens
+ if ((mainWindow.Screen !== undefined) && (mainWindow.Screen.name in screens)) {
+ var currentScreen = screens[mainWindow.Screen.name]
+ var offsetX = currentScreen.left
+ var offsetY = currentScreen.top
+ re.x = re.x - offsetX + re.width <= currentScreen.width ? re.x : mainWindow.x - re.width - 10
+ re.y = re.y - offsetY + re.height <= currentScreen.height ? re.y : currentScreen.height + offsetY - re.height - 10
+ }
+
+ re.visible = true
+ return re
+ }
+
+ if (torInstance.configurationNeeded) {
+ var object = createDialog("NetworkSetupWizard.qml")
+ object.networkReady.connect(function() {
+ mainWindow.visible = true
+ object.visible = false
+ })
+ object.visible = true
+ } else {
+ mainWindow.visible = true
+ }
+ }
+
+ property list<QtObject> data: [
+ Connections {
+ target: userIdentity.contacts.incomingRequests
+ onRequestAdded: {
+ var object = createDialog("ContactRequestDialog.qml", { 'request': request }, mainWindow)
+ object.visible = true
+ }
+ },
+
+ Connections {
+ target: torInstance
+ onConfigurationNeededChanged: {
+ if (torInstance.configurationNeeded) {
+ var object = createDialog("NetworkSetupWizard.qml", { 'modality': Qt.ApplicationModal }, mainWindow)
+ object.networkReady.connect(function() { object.visible = false })
+ object.visible = true
+ }
+ }
+ },
+
+ Settings {
+ id: uiSettings
+ path: "ui"
+ },
+
+ SystemPalette {
+ id: palette
+ },
+
+ FontLoader {
+ id: iconFont
+ source: "qrc:/icons/ricochet_icons.ttf"
+ },
+
+ Item {
+ id: styleHelper
+ visible: false
+ Label { id: fakeLabel }
+ Label { id: fakeLabelSized; font.pointSize: styleHelper.pointSize > 0 ? styleHelper.pointSize : 1 }
+
+ property int pointSize: (Qt.platform.os === "windows") ? 10 : fakeLabel.font.pointSize
+ property int textHeight: fakeLabelSized.height
+ property int dialogWindowFlags: Qt.Dialog | Qt.WindowSystemMenuHint | Qt.WindowTitleHint | Qt.WindowCloseButtonHint
+ },
+
+ Timer {
+ interval: 2000
+ running: true
+ repeat: false
+ onTriggered: {
+ var pendingRequests = userIdentity.contacts.incomingRequests.requests
+ for (var i = 0; i < pendingRequests.length; i++) {
+ var object = createDialog("ContactRequestDialog.qml", { 'request': pendingRequests[i] }, mainWindow)
+ object.visible = true
+ }
+ }
+ },
+
+ Loader {
+ id: audioNotificationLoader
+ active: uiSettings.data.playAudioNotification || false
+ source: "AudioNotifications.qml"
+ }
+ ]
+}
diff --git a/src/ui/qml/qml.qrc b/src/ui/qml/qml.qrc
new file mode 100644
index 0000000..f63caa3
--- /dev/null
+++ b/src/ui/qml/qml.qrc
@@ -0,0 +1,42 @@
+<RCC>
+ <qresource prefix="/ui">
+ <file>main.qml</file>
+ <file>AddContactDialog.qml</file>
+ <file>ChatMessageArea.qml</file>
+ <file>ChatWindow.qml</file>
+ <file>ContactActions.qml</file>
+ <file>ContactList.qml</file>
+ <file>ContactListDelegate.qml</file>
+ <file>MessageDelegate.qml</file>
+ <file>PreferencesDialog.qml</file>
+ <file>PresenceIcon.qml</file>
+ <file>ContactRequestDialog.qml</file>
+ <file>ContactRequestFields.qml</file>
+ <file>TorStateWidget.qml</file>
+ <file>ContactWindow.js</file>
+ <file>NetworkSetupWizard.qml</file>
+ <file>TorConfigurationPage.qml</file>
+ <file>TorBootstrapStatus.qml</file>
+ <file>TorLogDisplay.qml</file>
+ <file>TorPreferences.qml</file>
+ <file>ContactPreferences.qml</file>
+ <file>ContactIDField.qml</file>
+ <file>AboutPreferences.qml</file>
+ <file>MessageDialogWrapper.qml</file>
+ <file>StartupStatusPage.qml</file>
+ <file>OfflineStateItem.qml</file>
+ <file>MainWindow.qml</file>
+ <file>OpenBrowserDialog.qml</file>
+ <file>Bubble.qml</file>
+ <file>PageView.qml</file>
+ <file>ChatPage.qml</file>
+ <file>MainToolBar.qml</file>
+ <file>UnreadCountBadge.qml</file>
+ <file>GeneralPreferences.qml</file>
+ <file>LanguagePreferences.qml</file>
+ <file>AudioNotifications.qml</file>
+ </qresource>
+ <qresource prefix="/text">
+ <file>../../../LICENSE</file>
+ </qresource>
+</RCC>
diff --git a/src/utils/CryptoKey.cpp b/src/utils/CryptoKey.cpp
new file mode 100644
index 0000000..ed99aaa
--- /dev/null
+++ b/src/utils/CryptoKey.cpp
@@ -0,0 +1,420 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "CryptoKey.h"
+#include "SecureRNG.h"
+#include "Useful.h"
+#include <QtDebug>
+#include <QFile>
+#include <openssl/bio.h>
+#include <openssl/pem.h>
+
+void base32_encode(char *dest, unsigned destlen, const char *src, unsigned srclen);
+bool base32_decode(char *dest, unsigned destlen, const char *src, unsigned srclen);
+
+CryptoKey::CryptoKey()
+{
+}
+
+CryptoKey::~CryptoKey()
+{
+ clear();
+}
+
+CryptoKey::Data::~Data()
+{
+ if (key)
+ {
+ RSA_free(key);
+ key = 0;
+ }
+}
+
+void CryptoKey::clear()
+{
+ d = 0;
+}
+
+bool CryptoKey::loadFromData(const QByteArray &data, KeyType type, KeyFormat format)
+{
+ RSA *key = NULL;
+ clear();
+
+ if (data.isEmpty())
+ return false;
+
+ if (format == PEM) {
+ BIO *b = BIO_new_mem_buf((void*)data.constData(), -1);
+
+ if (type == PrivateKey)
+ key = PEM_read_bio_RSAPrivateKey(b, NULL, NULL, NULL);
+ else
+ key = PEM_read_bio_RSAPublicKey(b, NULL, NULL, NULL);
+
+ BIO_free(b);
+ } else if (format == DER) {
+ const uchar *dp = reinterpret_cast<const uchar*>(data.constData());
+ if (type == PrivateKey) {
+ BUG() << "Parsing DER-encoded private keys is not implemented";
+ return false;
+ }
+
+ key = d2i_RSAPublicKey(NULL, &dp, data.size());
+ } else {
+ Q_UNREACHABLE();
+ }
+
+ if (!key) {
+ qWarning() << "Failed to parse" << (type == PrivateKey ? "private" : "public") << "key from data";
+ return false;
+ }
+
+ d = new Data(key);
+ return true;
+}
+
+bool CryptoKey::loadFromFile(const QString &path, KeyType type, KeyFormat format)
+{
+ QFile file(path);
+ if (!file.open(QIODevice::ReadOnly))
+ {
+ qWarning() << "Failed to open" << (type == PrivateKey ? "private" : "public") << "key from"
+ << path << "-" << file.errorString();
+ return false;
+ }
+
+ QByteArray data = file.readAll();
+ file.close();
+
+ return loadFromData(data, type, format);
+}
+
+bool CryptoKey::isPrivate() const
+{
+ return isLoaded() && d->key->p != 0;
+}
+
+int CryptoKey::bits() const
+{
+ return isLoaded() ? BN_num_bits(d->key->n) : 0;
+}
+
+QByteArray CryptoKey::publicKeyDigest() const
+{
+ if (!isLoaded())
+ return QByteArray();
+
+ QByteArray buf = encodedPublicKey(DER);
+
+ QByteArray re(20, 0);
+ bool ok = SHA1(reinterpret_cast<const unsigned char*>(buf.constData()), buf.size(),
+ reinterpret_cast<unsigned char*>(re.data())) != NULL;
+
+ if (!ok)
+ {
+ qWarning() << "Failed to hash public key data for digest";
+ return QByteArray();
+ }
+
+ return re;
+}
+
+QByteArray CryptoKey::encodedPublicKey(KeyFormat format) const
+{
+ if (!isLoaded())
+ return QByteArray();
+
+ if (format == PEM) {
+ BIO *b = BIO_new(BIO_s_mem());
+
+ if (!PEM_write_bio_RSAPublicKey(b, d->key)) {
+ BUG() << "Failed to encode public key in PEM format";
+ BIO_free(b);
+ return QByteArray();
+ }
+
+ BUF_MEM *buf;
+ BIO_get_mem_ptr(b, &buf);
+
+ /* Close BIO, but don't free buf. */
+ (void)BIO_set_close(b, BIO_NOCLOSE);
+ BIO_free(b);
+
+ QByteArray re((const char *)buf->data, (int)buf->length);
+ BUF_MEM_free(buf);
+ return re;
+ } else if (format == DER) {
+ uchar *buf = NULL;
+ int len = i2d_RSAPublicKey(d->key, &buf);
+ if (len <= 0 || !buf) {
+ BUG() << "Failed to encode public key in DER format";
+ return QByteArray();
+ }
+
+ QByteArray re((const char*)buf, len);
+ OPENSSL_free(buf);
+ return re;
+ } else {
+ Q_UNREACHABLE();
+ }
+
+ return QByteArray();
+}
+
+QString CryptoKey::torServiceID() const
+{
+ if (!isLoaded())
+ return QString();
+
+ QByteArray digest = publicKeyDigest();
+ if (digest.isNull())
+ return QString();
+
+ static const int hostnameDigestSize = 10;
+ static const int hostnameEncodedSize = 16;
+
+ QByteArray re(hostnameEncodedSize+1, 0);
+ base32_encode(re.data(), re.size(), digest.constData(), hostnameDigestSize);
+
+ // Chop extra null byte
+ re.chop(1);
+
+ return QString::fromLatin1(re);
+}
+
+QByteArray CryptoKey::signData(const QByteArray &data) const
+{
+ QByteArray digest(32, 0);
+ bool ok = SHA256(reinterpret_cast<const unsigned char*>(data.constData()), data.size(),
+ reinterpret_cast<unsigned char*>(digest.data())) != NULL;
+ if (!ok) {
+ qWarning() << "Digest for RSA signature failed";
+ return QByteArray();
+ }
+
+ return signSHA256(digest);
+}
+
+QByteArray CryptoKey::signSHA256(const QByteArray &digest) const
+{
+ if (!isPrivate())
+ return QByteArray();
+
+ QByteArray re(RSA_size(d->key), 0);
+ unsigned sigsize = 0;
+ int r = RSA_sign(NID_sha256, reinterpret_cast<const unsigned char*>(digest.constData()), digest.size(),
+ reinterpret_cast<unsigned char*>(re.data()), &sigsize, d->key);
+
+ if (r != 1) {
+ qWarning() << "RSA encryption failed when generating signature";
+ return QByteArray();
+ }
+
+ re.truncate(sigsize);
+ return re;
+}
+
+bool CryptoKey::verifyData(const QByteArray &data, QByteArray signature) const
+{
+ QByteArray digest(32, 0);
+ bool ok = SHA256(reinterpret_cast<const unsigned char*>(data.constData()), data.size(),
+ reinterpret_cast<unsigned char*>(digest.data())) != NULL;
+
+ if (!ok) {
+ qWarning() << "Digest for RSA verify failed";
+ return false;
+ }
+
+ return verifySHA256(digest, signature);
+}
+
+bool CryptoKey::verifySHA256(const QByteArray &digest, QByteArray signature) const
+{
+ if (!isLoaded())
+ return false;
+
+ int r = RSA_verify(NID_sha256, reinterpret_cast<const uchar*>(digest.constData()), digest.size(),
+ reinterpret_cast<uchar*>(signature.data()), signature.size(), d->key);
+ if (r != 1)
+ return false;
+ return true;
+}
+
+/* Cryptographic hash of a password as expected by Tor's HashedControlPassword */
+QByteArray torControlHashedPassword(const QByteArray &password)
+{
+ QByteArray salt = SecureRNG::random(8);
+ if (salt.isNull())
+ return QByteArray();
+
+ int count = ((quint32)16 + (96 & 15)) << ((96 >> 4) + 6);
+
+ SHA_CTX hash;
+ SHA1_Init(&hash);
+
+ QByteArray tmp = salt + password;
+ while (count)
+ {
+ int c = qMin(count, tmp.size());
+ SHA1_Update(&hash, reinterpret_cast<const void*>(tmp.constData()), c);
+ count -= c;
+ }
+
+ unsigned char md[20];
+ SHA1_Final(md, &hash);
+
+ /* 60 is the hex-encoded value of 96, which is a constant used by Tor's algorithm. */
+ return QByteArray("16:") + salt.toHex().toUpper() + QByteArray("60") +
+ QByteArray::fromRawData(reinterpret_cast<const char*>(md), 20).toHex().toUpper();
+}
+
+/* Copyright (c) 2001-2004, Roger Dingledine
+ * Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson
+ * Copyright (c) 2007-2010, The Tor Project, Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#define BASE32_CHARS "abcdefghijklmnopqrstuvwxyz234567"
+
+/* Implements base32 encoding as in rfc3548. Requires that srclen*8 is a multiple of 5. */
+void base32_encode(char *dest, unsigned destlen, const char *src, unsigned srclen)
+{
+ unsigned i, bit, v, u;
+ unsigned nbits = srclen * 8;
+
+ /* We need an even multiple of 5 bits, and enough space */
+ if ((nbits%5) != 0 || destlen > (nbits/5)+1) {
+ Q_ASSERT(false);
+ memset(dest, 0, destlen);
+ return;
+ }
+
+ for (i = 0, bit = 0; bit < nbits; ++i, bit += 5)
+ {
+ /* set v to the 16-bit value starting at src[bits/8], 0-padded. */
+ v = ((quint8) src[bit / 8]) << 8;
+ if (bit + 5 < nbits)
+ v += (quint8) src[(bit/8)+1];
+
+ /* set u to the 5-bit value at the bit'th bit of src. */
+ u = (v >> (11 - (bit % 8))) & 0x1F;
+ dest[i] = BASE32_CHARS[u];
+ }
+
+ dest[i] = '\0';
+}
+
+/* Implements base32 decoding as in rfc3548. Requires that srclen*5 is a multiple of 8. */
+bool base32_decode(char *dest, unsigned destlen, const char *src, unsigned srclen)
+{
+ unsigned int i, j, bit;
+ unsigned nbits = srclen * 5;
+
+ /* We need an even multiple of 8 bits, and enough space */
+ if ((nbits%8) != 0 || (nbits/8)+1 > destlen) {
+ Q_ASSERT(false);
+ return false;
+ }
+
+ char *tmp = new char[srclen];
+
+ /* Convert base32 encoded chars to the 5-bit values that they represent. */
+ for (j = 0; j < srclen; ++j)
+ {
+ if (src[j] > 0x60 && src[j] < 0x7B)
+ tmp[j] = src[j] - 0x61;
+ else if (src[j] > 0x31 && src[j] < 0x38)
+ tmp[j] = src[j] - 0x18;
+ else if (src[j] > 0x40 && src[j] < 0x5B)
+ tmp[j] = src[j] - 0x41;
+ else
+ {
+ delete[] tmp;
+ return false;
+ }
+ }
+
+ /* Assemble result byte-wise by applying five possible cases. */
+ for (i = 0, bit = 0; bit < nbits; ++i, bit += 8)
+ {
+ switch (bit % 40)
+ {
+ case 0:
+ dest[i] = (((quint8)tmp[(bit/5)]) << 3) + (((quint8)tmp[(bit/5)+1]) >> 2);
+ break;
+ case 8:
+ dest[i] = (((quint8)tmp[(bit/5)]) << 6) + (((quint8)tmp[(bit/5)+1]) << 1)
+ + (((quint8)tmp[(bit/5)+2]) >> 4);
+ break;
+ case 16:
+ dest[i] = (((quint8)tmp[(bit/5)]) << 4) + (((quint8)tmp[(bit/5)+1]) >> 1);
+ break;
+ case 24:
+ dest[i] = (((quint8)tmp[(bit/5)]) << 7) + (((quint8)tmp[(bit/5)+1]) << 2)
+ + (((quint8)tmp[(bit/5)+2]) >> 3);
+ break;
+ case 32:
+ dest[i] = (((quint8)tmp[(bit/5)]) << 5) + ((quint8)tmp[(bit/5)+1]);
+ break;
+ }
+ }
+
+ delete[] tmp;
+ return true;
+}
diff --git a/src/utils/CryptoKey.h b/src/utils/CryptoKey.h
new file mode 100644
index 0000000..ee88cfc
--- /dev/null
+++ b/src/utils/CryptoKey.h
@@ -0,0 +1,94 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef CRYPTOKEY_H
+#define CRYPTOKEY_H
+
+#include <QString>
+#include <QSharedData>
+#include <QExplicitlySharedDataPointer>
+
+class CryptoKey
+{
+public:
+ enum KeyType {
+ PrivateKey,
+ PublicKey
+ };
+
+ enum KeyFormat {
+ PEM,
+ DER
+ };
+
+ CryptoKey();
+ CryptoKey(const CryptoKey &other) : d(other.d) { }
+ ~CryptoKey();
+
+ bool loadFromData(const QByteArray &data, KeyType type, KeyFormat format = PEM);
+ bool loadFromFile(const QString &path, KeyType type, KeyFormat format = PEM);
+ void clear();
+
+ bool isLoaded() const { return d.data() && d->key != 0; }
+ bool isPrivate() const;
+
+ QByteArray publicKeyDigest() const;
+ QByteArray encodedPublicKey(KeyFormat format = PEM) const;
+ QString torServiceID() const;
+ int bits() const;
+
+ // Calculate and sign SHA-256 digest of data using this key and PKCS #1 v2.0 padding
+ QByteArray signData(const QByteArray &data) const;
+ // Verify a signature as per signData
+ bool verifyData(const QByteArray &data, QByteArray signature) const;
+
+ // Sign the input SHA-256 digest using this key and PKCS #1 v2.0 padding
+ QByteArray signSHA256(const QByteArray &digest) const;
+ // Verify a signature as per signSHA256
+ bool verifySHA256(const QByteArray &digest, QByteArray signature) const;
+
+private:
+ struct Data : public QSharedData
+ {
+ typedef struct rsa_st RSA;
+ RSA *key;
+
+ Data(RSA *k = 0) : key(k) { }
+ ~Data();
+ };
+
+ QExplicitlySharedDataPointer<Data> d;
+};
+
+QByteArray torControlHashedPassword(const QByteArray &password);
+
+#endif // CRYPTOKEY_H
diff --git a/src/utils/PendingOperation.cpp b/src/utils/PendingOperation.cpp
new file mode 100644
index 0000000..4a11f3e
--- /dev/null
+++ b/src/utils/PendingOperation.cpp
@@ -0,0 +1,84 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "PendingOperation.h"
+
+PendingOperation::PendingOperation(QObject *parent)
+ : QObject(parent), m_finished(false)
+{
+}
+
+bool PendingOperation::isFinished() const
+{
+ return m_finished;
+}
+
+bool PendingOperation::isSuccess() const
+{
+ return m_finished && m_errorMessage.isNull();
+}
+
+bool PendingOperation::isError() const
+{
+ return m_finished && !m_errorMessage.isNull();
+}
+
+QString PendingOperation::errorMessage() const
+{
+ return m_errorMessage;
+}
+
+void PendingOperation::finishWithError(const QString &message)
+{
+ if (message.isEmpty())
+ m_errorMessage = QStringLiteral("Unknown Error");
+ m_errorMessage = message;
+
+ if (!m_finished) {
+ m_finished = true;
+ emit finished();
+ emit error(m_errorMessage);
+ }
+}
+
+void PendingOperation::finishWithSuccess()
+{
+ Q_ASSERT(m_errorMessage.isNull());
+
+ if (!m_finished) {
+ m_finished = true;
+ emit finished();
+ if (isSuccess())
+ emit success();
+ }
+}
+
diff --git a/src/utils/PendingOperation.h b/src/utils/PendingOperation.h
new file mode 100644
index 0000000..8f776a8
--- /dev/null
+++ b/src/utils/PendingOperation.h
@@ -0,0 +1,87 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef PENDINGOPERATION_H
+#define PENDINGOPERATION_H
+
+#include <QObject>
+
+/* Represents an asynchronous operation for reporting status
+ *
+ * This class is used for asynchronous operations that report a
+ * status and errors when finished, particularly for exposing them
+ * to QML.
+ *
+ * Subclass PendingOperation to implement your operation's logic.
+ * You also need to handle the object's lifetime, for example by
+ * calling deleteLater() when finished() is emitted.
+ *
+ * PendingOperation will emit finished() and one of success() or
+ * error() when completed.
+ */
+class PendingOperation : public QObject
+{
+ Q_OBJECT
+
+ Q_PROPERTY(bool isFinished READ isFinished NOTIFY finished FINAL)
+ Q_PROPERTY(bool isSuccess READ isSuccess NOTIFY success FINAL)
+ Q_PROPERTY(bool isError READ isError NOTIFY error FINAL)
+ Q_PROPERTY(QString errorMessage READ errorMessage NOTIFY finished FINAL)
+
+public:
+ PendingOperation(QObject *parent = 0);
+
+ bool isFinished() const;
+ bool isSuccess() const;
+ bool isError() const;
+ QString errorMessage() const;
+
+signals:
+ // Always emitted once when finished, regardless of status
+ void finished();
+
+ // One of error() or success() is emitted once
+ void error(const QString &errorMessage);
+ void success();
+
+protected slots:
+ void finishWithError(const QString &errorMessage);
+ void finishWithSuccess();
+
+private:
+ bool m_finished;
+ QString m_errorMessage;
+};
+
+Q_DECLARE_METATYPE(PendingOperation*)
+
+#endif
diff --git a/src/utils/SecureRNG.cpp b/src/utils/SecureRNG.cpp
new file mode 100644
index 0000000..674ad16
--- /dev/null
+++ b/src/utils/SecureRNG.cpp
@@ -0,0 +1,146 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "SecureRNG.h"
+#include <QtDebug>
+#include <openssl/rand.h>
+#include <openssl/err.h>
+#include <limits.h>
+
+#ifdef Q_OS_WIN
+#include <Wincrypt.h>
+#endif
+
+#if QT_VERSION >= 0x040700
+#include <QElapsedTimer>
+#endif
+
+bool SecureRNG::seed()
+{
+#if QT_VERSION >= 0x040700
+ QElapsedTimer timer;
+ timer.start();
+#endif
+
+#ifdef Q_OS_WIN
+ /* RAND_poll is very unreliable on windows; with older versions of OpenSSL,
+ * it can take up to several minutes to run and has been known to crash.
+ * Even newer versions seem to take around 400ms, which is far too long for
+ * interactive startup. Random data from the windows CSP is used as a seed
+ * instead, as it should be very high quality random and fast. */
+ HCRYPTPROV provider = 0;
+ if (!CryptAcquireContext(&provider, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT))
+ {
+ qWarning() << "Failed to acquire CSP context for RNG seed:" << hex << GetLastError();
+ return false;
+ }
+
+ /* Same amount of entropy OpenSSL uses, apparently. */
+ char buf[32];
+
+ if (!CryptGenRandom(provider, sizeof(buf), reinterpret_cast<BYTE*>(buf)))
+ {
+ qWarning() << "Failed to get entropy from CSP for RNG seed: " << hex << GetLastError();
+ CryptReleaseContext(provider, 0);
+ return false;
+ }
+
+ CryptReleaseContext(provider, 0);
+
+ RAND_seed(buf, sizeof(buf));
+ memset(buf, 0, sizeof(buf));
+#else
+ if (!RAND_poll())
+ {
+ qWarning() << "OpenSSL RNG seed failed:" << ERR_get_error();
+ return false;
+ }
+#endif
+
+#if QT_VERSION >= 0x040700
+ qDebug() << "RNG seed took" << timer.elapsed() << "ms";
+#endif
+
+ return true;
+}
+
+void SecureRNG::random(char *buf, int size)
+{
+ int r = RAND_bytes(reinterpret_cast<unsigned char*>(buf), size);
+ if (r <= 0)
+ qFatal("RNG failed: %lu", ERR_get_error());
+}
+
+QByteArray SecureRNG::random(int size)
+{
+ QByteArray re(size, 0);
+ random(re.data(), size);
+ return re;
+}
+
+QByteArray SecureRNG::randomPrintable(int length)
+{
+ QByteArray re(length, 0);
+ for (int i = 0; i < re.size(); i++)
+ re[i] = randomInt(95) + 32;
+ return re;
+}
+
+unsigned SecureRNG::randomInt(unsigned max)
+{
+ unsigned cutoff = UINT_MAX - (UINT_MAX % max);
+ unsigned value = 0;
+
+ for (;;)
+ {
+ random(reinterpret_cast<char*>(&value), sizeof(value));
+ if (value < cutoff)
+ return value % max;
+ }
+}
+
+#ifndef UINT64_MAX
+#define UINT64_MAX ((quint64)-1)
+#endif
+
+quint64 SecureRNG::randomInt64(quint64 max)
+{
+ quint64 cutoff = UINT64_MAX - (UINT64_MAX % max);
+ quint64 value = 0;
+
+ for (;;)
+ {
+ random(reinterpret_cast<char*>(value), sizeof(value));
+ if (value < cutoff)
+ return value % max;
+ }
+}
diff --git a/src/utils/SecureRNG.h b/src/utils/SecureRNG.h
new file mode 100644
index 0000000..f3b2a4b
--- /dev/null
+++ b/src/utils/SecureRNG.h
@@ -0,0 +1,51 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef SECURERNG_H
+#define SECURERNG_H
+
+#include <QByteArray>
+
+class SecureRNG
+{
+public:
+ static bool seed();
+
+ static void random(char *buf, int size);
+ static QByteArray random(int size);
+
+ static QByteArray randomPrintable(int length);
+ static unsigned randomInt(unsigned max);
+ static quint64 randomInt64(quint64 max);
+};
+
+#endif // SECURERNG_H
diff --git a/src/utils/Settings.cpp b/src/utils/Settings.cpp
new file mode 100644
index 0000000..5a15e32
--- /dev/null
+++ b/src/utils/Settings.cpp
@@ -0,0 +1,553 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Settings.h"
+#include <QCoreApplication>
+#include <QJsonDocument>
+#include <QJsonParseError>
+#include <QSaveFile>
+#include <QFile>
+#include <QDir>
+#include <QFileInfo>
+#include <QTimer>
+#include <QDebug>
+#include <QPointer>
+
+class SettingsFilePrivate : public QObject
+{
+ Q_OBJECT
+
+public:
+ SettingsFile *q;
+ QString filePath;
+ QString errorMessage;
+ QTimer syncTimer;
+ QJsonObject jsonRoot;
+ SettingsObject *rootObject;
+
+ SettingsFilePrivate(SettingsFile *qp);
+ virtual ~SettingsFilePrivate();
+
+ void reset();
+ void setError(const QString &message);
+ bool checkDirPermissions(const QString &path);
+ bool readFile();
+ bool writeFile();
+
+ static QStringList splitPath(const QString &input, bool &ok);
+ QJsonValue read(const QJsonObject &base, const QStringList &path);
+ bool write(const QStringList &path, const QJsonValue &value);
+
+signals:
+ void modified(const QStringList &path, const QJsonValue &value);
+
+private slots:
+ void sync();
+};
+
+SettingsFile::SettingsFile(QObject *parent)
+ : QObject(parent), d(new SettingsFilePrivate(this))
+{
+ d->rootObject = new SettingsObject(this, QString());
+}
+
+SettingsFile::~SettingsFile()
+{
+}
+
+SettingsFilePrivate::SettingsFilePrivate(SettingsFile *qp)
+ : QObject(qp)
+ , q(qp)
+ , rootObject(0)
+{
+ syncTimer.setInterval(0);
+ syncTimer.setSingleShot(true);
+ connect(&syncTimer, &QTimer::timeout, this, &SettingsFilePrivate::sync);
+}
+
+SettingsFilePrivate::~SettingsFilePrivate()
+{
+ if (syncTimer.isActive())
+ sync();
+ delete rootObject;
+}
+
+void SettingsFilePrivate::reset()
+{
+ filePath.clear();
+ errorMessage.clear();
+
+ jsonRoot = QJsonObject();
+ emit modified(QStringList(), jsonRoot);
+}
+
+QString SettingsFile::filePath() const
+{
+ return d->filePath;
+}
+
+bool SettingsFile::setFilePath(const QString &filePath)
+{
+ if (d->filePath == filePath)
+ return hasError();
+
+ d->reset();
+ d->filePath = filePath;
+
+ QFileInfo fileInfo(filePath);
+ QDir dir(fileInfo.path());
+ if (!dir.exists() && !dir.mkpath(QStringLiteral("."))) {
+ d->setError(QStringLiteral("Cannot create directory: %1").arg(dir.path()));
+ return false;
+ }
+ d->checkDirPermissions(fileInfo.path());
+
+ if (!d->readFile())
+ return false;
+
+ return true;
+}
+
+QString SettingsFile::errorMessage() const
+{
+ return d->errorMessage;
+}
+
+bool SettingsFile::hasError() const
+{
+ return !d->errorMessage.isEmpty();
+}
+
+void SettingsFilePrivate::setError(const QString &message)
+{
+ errorMessage = message;
+ emit q->error();
+}
+
+bool SettingsFilePrivate::checkDirPermissions(const QString &path)
+{
+ static QFile::Permissions desired = QFileDevice::ReadUser | QFileDevice::WriteUser | QFileDevice::ExeUser;
+ static QFile::Permissions ignored = QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ExeOwner;
+
+ QFile file(path);
+ if ((file.permissions() & ~ignored) != desired) {
+ qDebug() << "Correcting permissions on configuration directory";
+ if (!file.setPermissions(desired)) {
+ qWarning() << "Correcting permissions on configuration directory failed";
+ return false;
+ }
+ }
+
+ return true;
+}
+
+SettingsObject *SettingsFile::root()
+{
+ return d->rootObject;
+}
+
+const SettingsObject *SettingsFile::root() const
+{
+ return d->rootObject;
+}
+
+void SettingsFilePrivate::sync()
+{
+ if (filePath.isEmpty())
+ return;
+
+ syncTimer.stop();
+ writeFile();
+}
+
+bool SettingsFilePrivate::readFile()
+{
+ QFile file(filePath);
+ if (!file.open(QIODevice::ReadWrite)) {
+ setError(file.errorString());
+ return false;
+ }
+
+ QByteArray data = file.readAll();
+ if (data.isEmpty() && (file.error() != QFileDevice::NoError || file.size() > 0)) {
+ setError(file.errorString());
+ return false;
+ }
+
+ if (data.isEmpty()) {
+ jsonRoot = QJsonObject();
+ return true;
+ }
+
+ QJsonParseError parseError;
+ QJsonDocument document = QJsonDocument::fromJson(data);
+ if (document.isNull()) {
+ setError(parseError.errorString());
+ return false;
+ }
+
+ if (!document.isObject()) {
+ setError(QStringLiteral("Invalid configuration file (expected object)"));
+ return false;
+ }
+
+ jsonRoot = document.object();
+
+ emit modified(QStringList(), jsonRoot);
+ return true;
+}
+
+bool SettingsFilePrivate::writeFile()
+{
+ QSaveFile file(filePath);
+ if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
+ setError(file.errorString());
+ return false;
+ }
+
+ QJsonDocument document(jsonRoot);
+ QByteArray data = document.toJson();
+ if (data.isEmpty() && !document.isEmpty()) {
+ setError(QStringLiteral("Encoding failure"));
+ return false;
+ }
+
+ if (file.write(data) < data.size() || !file.commit()) {
+ setError(file.errorString());
+ return false;
+ }
+
+ return true;
+}
+
+QStringList SettingsFilePrivate::splitPath(const QString &input, bool &ok)
+{
+ QStringList components = input.split(QLatin1Char('.'));
+
+ // Allow a leading '.' to simplify concatenation
+ if (!components.isEmpty() && components.first().isEmpty())
+ components.takeFirst();
+
+ // No other empty components, including a trailing .
+ foreach (const QString &word, components) {
+ if (word.isEmpty()) {
+ ok = false;
+ return QStringList();
+ }
+ }
+
+ ok = true;
+ return components;
+}
+
+QJsonValue SettingsFilePrivate::read(const QJsonObject &base, const QStringList &path)
+{
+ QJsonValue current = base;
+
+ foreach (const QString &key, path) {
+ QJsonObject object = current.toObject();
+ if (object.isEmpty() || (current = object.value(key)).isUndefined())
+ return QJsonValue::Undefined;
+ }
+
+ return current;
+}
+
+// Compare two QJsonValue to find keys that have changed,
+// recursing into objects and building paths as necessary.
+typedef QList<QPair<QStringList, QJsonValue> > ModifiedList;
+static void findModifiedRecursive(ModifiedList &modified, const QStringList &path, const QJsonValue &oldValue, const QJsonValue &newValue)
+{
+ if (oldValue.isObject() || newValue.isObject()) {
+ // If either is a non-object type, this returns an empty object
+ QJsonObject oldObject = oldValue.toObject();
+ QJsonObject newObject = newValue.toObject();
+
+ // Iterate keys of the original object and compare to new
+ for (QJsonObject::iterator it = oldObject.begin(); it != oldObject.end(); it++) {
+ QJsonValue newSubValue = newObject.value(it.key());
+ if (*it == newSubValue)
+ continue;
+
+ if ((*it).isObject() || newSubValue.isObject())
+ findModifiedRecursive(modified, QStringList() << path << it.key(), *it, newSubValue);
+ else
+ modified.append(qMakePair(QStringList() << path << it.key(), newSubValue));
+ }
+
+ // Iterate keys of the new object that may not be in original
+ for (QJsonObject::iterator it = newObject.begin(); it != newObject.end(); it++) {
+ if (oldObject.contains(it.key()))
+ continue;
+
+ if ((*it).isObject())
+ findModifiedRecursive(modified, QStringList() << path << it.key(), QJsonValue::Undefined, it.value());
+ else
+ modified.append(qMakePair(QStringList() << path << it.key(), it.value()));
+ }
+ } else
+ modified.append(qMakePair(path, newValue));
+}
+
+bool SettingsFilePrivate::write(const QStringList &path, const QJsonValue &value)
+{
+ typedef QVarLengthArray<QPair<QString,QJsonObject> > ObjectStack;
+ ObjectStack stack;
+ QJsonValue current = jsonRoot;
+ QJsonValue originalValue;
+ QString currentKey;
+
+ foreach (const QString &key, path) {
+ const QJsonObject &parent = current.toObject();
+ stack.append(qMakePair(currentKey, parent));
+ current = parent.value(key);
+ currentKey = key;
+ }
+
+ // Stack now contains parent objects starting with the root, and current
+ // is the old value. Write back changes in reverse.
+ if (current == value)
+ return false;
+ originalValue = current;
+ current = value;
+
+ ObjectStack::const_iterator it = stack.end(), begin = stack.begin();
+ while (it != begin) {
+ --it;
+ QJsonObject update = it->second;
+ update.insert(currentKey, current);
+ current = update;
+ currentKey = it->first;
+ }
+
+ // current is now the updated jsonRoot
+ jsonRoot = current.toObject();
+ syncTimer.start();
+
+ ModifiedList modified;
+ findModifiedRecursive(modified, path, originalValue, value);
+
+ for (ModifiedList::iterator it = modified.begin(); it != modified.end(); it++)
+ emit this->modified(it->first, it->second);
+
+ return true;
+}
+
+class SettingsObjectPrivate : public QObject
+{
+ Q_OBJECT
+
+public:
+ explicit SettingsObjectPrivate(SettingsObject *q);
+
+ SettingsObject *q;
+ SettingsFile *file;
+ QStringList path;
+ QJsonObject object;
+ bool invalid;
+
+ void setFile(SettingsFile *file);
+
+public slots:
+ void modified(const QStringList &absolutePath, const QJsonValue &value);
+};
+
+SettingsObject::SettingsObject(QObject *parent)
+ : QObject(parent)
+ , d(new SettingsObjectPrivate(this))
+{
+ d->setFile(defaultFile());
+ if (d->file)
+ setPath(QString());
+}
+
+SettingsObject::SettingsObject(const QString &path, QObject *parent)
+ : QObject(parent)
+ , d(new SettingsObjectPrivate(this))
+{
+ d->setFile(defaultFile());
+ setPath(path);
+}
+
+SettingsObject::SettingsObject(SettingsFile *file, const QString &path, QObject *parent)
+ : QObject(parent)
+ , d(new SettingsObjectPrivate(this))
+{
+ d->setFile(file);
+ setPath(path);
+}
+
+SettingsObject::SettingsObject(SettingsObject *base, const QString &path, QObject *parent)
+ : QObject(parent)
+ , d(new SettingsObjectPrivate(this))
+{
+ d->setFile(base->d->file);
+ setPath(base->path() + QLatin1Char('.') + path);
+}
+
+SettingsObjectPrivate::SettingsObjectPrivate(SettingsObject *qp)
+ : QObject(qp)
+ , q(qp)
+ , file(0)
+ , invalid(true)
+{
+}
+
+void SettingsObjectPrivate::setFile(SettingsFile *value)
+{
+ if (file == value)
+ return;
+
+ if (file)
+ disconnect(file, 0, this, 0);
+ file = value;
+ if (file)
+ connect(file->d, &SettingsFilePrivate::modified, this, &SettingsObjectPrivate::modified);
+}
+
+// Emit SettingsObject::modified with a relative path if path is matched
+void SettingsObjectPrivate::modified(const QStringList &key, const QJsonValue &value)
+{
+ if (key.size() < path.size())
+ return;
+
+ for (int i = 0; i < path.size(); i++) {
+ if (path[i] != key[i])
+ return;
+ }
+
+ object = file->d->read(file->d->jsonRoot, path).toObject();
+ emit q->modified(QStringList(key.mid(path.size())).join(QLatin1Char('.')), value);
+ emit q->dataChanged();
+}
+
+static QPointer<SettingsFile> defaultObjectFile;
+
+SettingsFile *SettingsObject::defaultFile()
+{
+ return defaultObjectFile;
+}
+
+void SettingsObject::setDefaultFile(SettingsFile *file)
+{
+ defaultObjectFile = file;
+}
+
+QString SettingsObject::path() const
+{
+ return d->path.join(QLatin1Char('.'));
+}
+
+void SettingsObject::setPath(const QString &input)
+{
+ bool ok = false;
+ QStringList newPath = SettingsFilePrivate::splitPath(input, ok);
+ if (!ok) {
+ d->invalid = true;
+ d->path.clear();
+ d->object = QJsonObject();
+
+ emit pathChanged();
+ emit dataChanged();
+ return;
+ }
+
+ if (!d->invalid && d->path == newPath)
+ return;
+
+ d->path = newPath;
+ if (d->file) {
+ d->invalid = false;
+ d->object = d->file->d->read(d->file->d->jsonRoot, d->path).toObject();
+ emit dataChanged();
+ }
+
+ emit pathChanged();
+}
+
+QJsonObject SettingsObject::data() const
+{
+ return d->object;
+}
+
+void SettingsObject::setData(const QJsonObject &input)
+{
+ if (d->invalid || d->object == input)
+ return;
+
+ d->object = input;
+ d->file->d->write(d->path, d->object);
+}
+
+QJsonValue SettingsObject::read(const QString &key, const QJsonValue &defaultValue) const
+{
+ bool ok = false;
+ QStringList splitKey = SettingsFilePrivate::splitPath(key, ok);
+ if (d->invalid || !ok || splitKey.isEmpty()) {
+ qDebug() << "Invalid settings read of path" << key;
+ return defaultValue;
+ }
+
+ QJsonValue ret = d->file->d->read(d->object, splitKey);
+ if (ret.isUndefined())
+ ret = defaultValue;
+ return ret;
+}
+
+void SettingsObject::write(const QString &key, const QJsonValue &value)
+{
+ bool ok = false;
+ QStringList splitKey = SettingsFilePrivate::splitPath(key, ok);
+ if (d->invalid || !ok || splitKey.isEmpty()) {
+ qDebug() << "Invalid settings write of path" << key;
+ return;
+ }
+
+ splitKey = d->path + splitKey;
+ d->file->d->write(splitKey, value);
+}
+
+void SettingsObject::unset(const QString &key)
+{
+ write(key, QJsonValue());
+}
+
+void SettingsObject::undefine()
+{
+ if (d->invalid)
+ return;
+
+ d->object = QJsonObject();
+ d->file->d->write(d->path, QJsonValue::Undefined);
+}
+
+#include "Settings.moc"
diff --git a/src/utils/Settings.h b/src/utils/Settings.h
new file mode 100644
index 0000000..79ad032
--- /dev/null
+++ b/src/utils/Settings.h
@@ -0,0 +1,257 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef SETTINGS_H
+#define SETTINGS_H
+
+#include <QObject>
+#include <QJsonValue>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QStringList>
+#include <QDateTime>
+
+class SettingsObject;
+class SettingsFilePrivate;
+class SettingsObjectPrivate;
+
+/* SettingsFile represents a JSON-encoded configuration file.
+ *
+ * SettingsFile is an API for reading, writing, and change notification
+ * on JSON-encoded settings files.
+ *
+ * Data is accessed via SettingsObject, either using the root property
+ * or by creating a SettingsObject, optionally using a base path.
+ */
+class SettingsFile : public QObject
+{
+ Q_OBJECT
+ Q_DISABLE_COPY(SettingsFile)
+
+ Q_PROPERTY(SettingsObject *root READ root CONSTANT)
+ Q_PROPERTY(QString filePath READ filePath WRITE setFilePath NOTIFY filePathChanged)
+ Q_PROPERTY(QString errorMessage READ errorMessage NOTIFY error)
+ Q_PROPERTY(bool hasError READ hasError NOTIFY error)
+
+public:
+ explicit SettingsFile(QObject *parent = 0);
+ virtual ~SettingsFile();
+
+ QString filePath() const;
+ bool setFilePath(const QString &filePath);
+
+ QString errorMessage() const;
+ bool hasError() const;
+
+ SettingsObject *root();
+ const SettingsObject *root() const;
+
+signals:
+ void filePathChanged();
+ void error();
+
+private:
+ SettingsFilePrivate *d;
+
+ friend class SettingsObject;
+ friend class SettingsObjectPrivate;
+};
+
+/* SettingsObject reads and writes data within a SettingsFile
+ *
+ * A SettingsObject is associated with a SettingsFile and represents an object
+ * tree within that file. It refers to the JSON object tree using a path
+ * notation with keys separated by '.'. For example:
+ *
+ * {
+ * "one": {
+ * "two": {
+ * "three": "value"
+ * }
+ * }
+ * }
+ *
+ * With this data, a SettingsObject with an empty path can read with the path
+ * "one.two.three", and a SettingsObject with a path of "one.two" can simply
+ * read or write on "three".
+ *
+ * Multiple SettingsObjects may be created for the same path, and will be kept
+ * synchronized with changes. The modified signal is emitted for all changes
+ * affecting keys within a path, including writes of object trees and from other
+ * instances.
+ */
+class SettingsObject : public QObject
+{
+ Q_OBJECT
+ Q_DISABLE_COPY(SettingsObject)
+
+ Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged)
+ Q_PROPERTY(QJsonObject data READ data WRITE setData NOTIFY dataChanged)
+
+public:
+ explicit SettingsObject(QObject *parent = 0);
+ explicit SettingsObject(const QString &path, QObject *parent = 0);
+ explicit SettingsObject(SettingsFile *file, const QString &path, QObject *parent = 0);
+ explicit SettingsObject(SettingsObject *base, const QString &path, QObject *parent = 0);
+
+ /* Specify a SettingsFile to use by default on SettingsObject instances.
+ *
+ * After calling setDefaultFile, a SettingsObject created without any file, e.g.:
+ *
+ * SettingsObject settings;
+ * SettingsObject animals(QStringLiteral("animals"));
+ *
+ * Will use the specified SettingsFile instance by default. This is a convenience
+ * over passing around instances of SettingsFile in application use cases, and is
+ * particularly useful for QML.
+ */
+ static SettingsFile *defaultFile();
+ static void setDefaultFile(SettingsFile *file);
+
+ QString path() const;
+ void setPath(const QString &path);
+
+ QJsonObject data() const;
+ void setData(const QJsonObject &data);
+
+ Q_INVOKABLE QJsonValue read(const QString &key, const QJsonValue &defaultValue = QJsonValue::Undefined) const;
+ template<typename T> T read(const QString &key) const;
+ Q_INVOKABLE void write(const QString &key, const QJsonValue &value);
+ template<typename T> void write(const QString &key, const T &value);
+ Q_INVOKABLE void unset(const QString &key);
+
+ // const char* key overloads
+ QJsonValue read(const char *key, const QJsonValue &defaultValue = QJsonValue::Undefined) const
+ {
+ return read(QString::fromLatin1(key), defaultValue);
+ }
+ template<typename T> T read(const char *key) const
+ {
+ return read<T>(QString::fromLatin1(key));
+ }
+ void write(const char *key, const QJsonValue &value)
+ {
+ write(QString::fromLatin1(key), value);
+ }
+ template<typename T> void write(const char *key, const T &value)
+ {
+ write<T>(QString::fromLatin1(key), value);
+ }
+ void unset(const char *key)
+ {
+ unset(QString::fromLatin1(key));
+ }
+
+ Q_INVOKABLE void undefine();
+
+signals:
+ void pathChanged();
+ void dataChanged();
+
+ void modified(const QString &path, const QJsonValue &value);
+
+private:
+ SettingsObjectPrivate *d;
+};
+
+template<typename T> inline void SettingsObject::write(const QString &key, const T &value)
+{
+ write(key, QJsonValue(value));
+}
+
+template<> inline QString SettingsObject::read<QString>(const QString &key) const
+{
+ return read(key).toString();
+}
+
+template<> inline QJsonArray SettingsObject::read<QJsonArray>(const QString &key) const
+{
+ return read(key).toArray();
+}
+
+template<> inline QJsonObject SettingsObject::read<QJsonObject>(const QString &key) const
+{
+ return read(key).toObject();
+}
+
+template<> inline double SettingsObject::read<double>(const QString &key) const
+{
+ return read(key).toDouble();
+}
+
+template<> inline int SettingsObject::read<int>(const QString &key) const
+{
+ return read(key).toInt();
+}
+
+template<> inline bool SettingsObject::read<bool>(const QString &key) const
+{
+ return read(key).toBool();
+}
+
+template<> inline QDateTime SettingsObject::read<QDateTime>(const QString &key) const
+{
+ QString value = read(key).toString();
+ if (value.isEmpty())
+ return QDateTime();
+ return QDateTime::fromString(value, Qt::ISODate).toLocalTime();
+}
+
+template<> inline void SettingsObject::write<QDateTime>(const QString &key, const QDateTime &value)
+{
+ write(key, QJsonValue(value.toUTC().toString(Qt::ISODate)));
+}
+
+// Explicitly store value encoded as base64. Decodes and casts implicitly to QByteArray for reads.
+class Base64Encode
+{
+public:
+ explicit Base64Encode(const QByteArray &value) : d(value) { }
+ operator QByteArray() { return d; }
+ QByteArray encoded() const { return d.toBase64(); }
+
+private:
+ QByteArray d;
+};
+
+template<> inline Base64Encode SettingsObject::read<Base64Encode>(const QString &key) const
+{
+ return Base64Encode(QByteArray::fromBase64(read(key).toString().toLatin1()));
+}
+
+template<> inline void SettingsObject::write<Base64Encode>(const QString &key, const Base64Encode &value)
+{
+ write(key, QJsonValue(QString::fromLatin1(value.encoded())));
+}
+
+#endif
+
diff --git a/src/utils/StringUtil.cpp b/src/utils/StringUtil.cpp
new file mode 100644
index 0000000..8215d13
--- /dev/null
+++ b/src/utils/StringUtil.cpp
@@ -0,0 +1,118 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "StringUtil.h"
+
+QByteArray quotedString(const QByteArray &string)
+{
+ QByteArray out;
+ out.reserve(string.size() * 2);
+
+ out.append('"');
+
+ for (int i = 0; i < string.size(); ++i)
+ {
+ switch (string[i])
+ {
+ case '"':
+ out.append("\\\"");
+ break;
+ case '\\':
+ out.append("\\\\");
+ break;
+ default:
+ out.append(string[i]);
+ break;
+ }
+ }
+
+ out.append('"');
+ return out;
+}
+
+QByteArray unquotedString(const QByteArray &string)
+{
+ if (string.size() < 2 || string[0] != '"')
+ return string;
+
+ QByteArray out;
+ out.reserve(string.size() - 2);
+
+ for (int i = 1; i < string.size(); ++i)
+ {
+ switch (string[i])
+ {
+ case '\\':
+ if (++i < string.size())
+ out.append(string[i]);
+ break;
+ case '"':
+ return out;
+ default:
+ out.append(string[i]);
+ }
+ }
+
+ return out;
+}
+
+QList<QByteArray> splitQuotedStrings(const QByteArray &input, char separator)
+{
+ QList<QByteArray> out;
+ bool inquote = false;
+ int start = 0;
+
+ for (int i = 0; i < input.size(); ++i)
+ {
+ switch (input[i])
+ {
+ case '"':
+ inquote = !inquote;
+ break;
+ case '\\':
+ if (inquote)
+ ++i;
+ break;
+ }
+
+ if (!inquote && input[i] == separator)
+ {
+ out.append(input.mid(start, i - start));
+ start = i+1;
+ }
+ }
+
+ if (start < input.size())
+ out.append(input.mid(start));
+
+ return out;
+}
diff --git a/src/utils/StringUtil.h b/src/utils/StringUtil.h
new file mode 100644
index 0000000..c86d2c6
--- /dev/null
+++ b/src/utils/StringUtil.h
@@ -0,0 +1,46 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef STRINGUTIL_H
+#define STRINGUTIL_H
+
+#include <QByteArray>
+#include <QList>
+
+QByteArray quotedString(const QByteArray &string);
+
+/* Return the unquoted contents of a string, either until an end quote or an unescaped separator character. */
+QByteArray unquotedString(const QByteArray &string);
+
+QList<QByteArray> splitQuotedStrings(const QByteArray &input, char separator);
+
+#endif // STRINGUTIL_H
diff --git a/src/utils/Useful.h b/src/utils/Useful.h
new file mode 100644
index 0000000..6bb6e44
--- /dev/null
+++ b/src/utils/Useful.h
@@ -0,0 +1,71 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef UTILS_USEFUL_H
+#define UTILS_USEFUL_H
+
+#include <QtGlobal>
+#include <QDebug>
+
+/* Print a warning for bug conditions, and assert on a debug build.
+ *
+ * This should be used in place of Q_ASSERT for bug conditions, along
+ * with a proper error case for release-mode builds. For example:
+ *
+ * if (!connection || !user) {
+ * BUG() << "Request" << request << "should have a connection and user";
+ * return false;
+ * }
+ *
+ * Do not confuse bugs with actual error cases; BUG() should never be
+ * triggered unless the code or logic is wrong.
+ */
+#if !defined(QT_NO_DEBUG) || defined(QT_FORCE_ASSERTS)
+# define BUG() Explode(__FILE__,__LINE__), qWarning() << "BUG:"
+namespace {
+class Explode
+{
+public:
+ const char *file;
+ int line;
+ Explode(const char *file, int line) : file(file), line(line) { }
+ ~Explode() {
+ qt_assert("something broke!", file, line);
+ }
+};
+}
+#else
+# define BUG() qWarning() << "BUG:"
+#endif
+
+#endif
+
diff --git a/tests/cryptokey/tst_cryptokey.cpp b/tests/cryptokey/tst_cryptokey.cpp
new file mode 100644
index 0000000..48888da
--- /dev/null
+++ b/tests/cryptokey/tst_cryptokey.cpp
@@ -0,0 +1,205 @@
+/* Ricochet - https://ricochet.im/
+ * Copyright (C) 2014, John Brooks <john.brooks at dereferenced.net>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the names of the copyright owners nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <QtTest>
+#include "utils/CryptoKey.h"
+
+class TestCryptoKey : public QObject
+{
+ Q_OBJECT
+
+private slots:
+ void load();
+ void publicKeyDigest();
+ void encodedPublicKey();
+ void torServiceID();
+ void sign();
+};
+
+const char *alice =
+ "-----BEGIN RSA PRIVATE KEY-----\n"
+ "MIICXQIBAAKBgQDAS9nLWyK0jWZ8yduqVEhSyZRplTaeUpGWYRi14n1C4sjO6nqm\n"
+ "ES31UCGDH4nIor2R/XMJCJkJwK+t2XrtiH+jUEHwUGhnMkm3hW5NHt5g39s9YK7l\n"
+ "xD39O8N2tHUycVq8guhrb1WBQ2/bmZ85nOIuBDZxIuVQZA1U1L6rWGvm+wIDAQAB\n"
+ "AoGAewYL6JX9thVgpCVga7BQNObSFFpp/xBEJDkqXfLwwIHmhrpsjSIgjPke94yN\n"
+ "0daMAYJsvjLJ9ftYaZjhlGXngbBJiAU95gcZoTAsn2hNJP22ndGuhi6WEKhYwRxK\n"
+ "U5d+3Khzy/ysuoay7DSVtpSmpiacWPSiiptEkxNbcbGba8ECQQDeEGoPASmxZoh4\n"
+ "I2JNQkqSwMKsOZpp/SJhnmLCPoA1oDwlGtu4HF7t9hBXeyIXgLvbfJudFEa+LqR7\n"
+ "wrKQPn0fAkEA3a7cR7eSRNu1ak7gVfQfnP4tFl3+7UC2hUqVHLA5ks4pLl7/ITa+\n"
+ "3P04SOs3WpvZJHYJ+hi/anqEPYrD/3B+pQJBAKmjnnHh8IjODDjCxyjAGJntWYoZ\n"
+ "4yVOtEIgrc830delley+jNUkDzz3+dnqfcu4k0oD8hjYUYaduRe2T5Szt/8CQQDC\n"
+ "EVt8WUNujp0R9P1FohKu4IFeLGmJD/b5V2KUm927HEpG8xkM3Z1XX0KP64MpCnid\n"
+ "B80SKeog8CKmsb2F+NiVAkBT1CEAdiFYtf72hnZCLBw5HrqpN+zjw00GjtlrmmNV\n"
+ "+ILb/YRp5flCY5Se95ExzQqRKzvK5iJg0yEOVF0OcbO+\n"
+ "-----END RSA PRIVATE KEY-----";
+const char *aliceDigest = "623a1ffc94d8f8edcd5e47fbd45e08deb911d1bc";
+const char *aliceTorID = "mi5b77eu3d4o3tk6";
+const char *aliceSignedTestData = "23fdcd5c7d40b44a7e49619d9048c81931166a0adb80c8981cc8f9a9e02c3923d5fba6d92ea03dc672d009a5fe1be2b582fb935076f880d9aa55511c33620d2aa23336b579dd7ccd1dbf4c845e4100a114d8ac20dd47229e876444f79d5152456a8e26fefa67a12436b3c33728a2ff7cb12250c486f786647574e48bb9208f64";
+
+const char *bob =
+ "-----BEGIN RSA PUBLIC KEY-----\n"
+ "MIGJAoGBAMP8GyAg/kzwXizpUWjWIMw/lvDffXjsxcq1qmZWZxXJQH/oE8bX+WAf\n"
+ "VS8iUHVqTykubR0W3QNL6aWSZKBqDQUTN0QBJUF4qdkg3x56C0kwcWa+seDMAvJw\n"
+ "pcHK9wN7mtWHIhFwhikP//NylrY1MaUxcPjvOKcdJ90k988nnmpZAgMBAAE=\n"
+ "-----END RSA PUBLIC KEY-----\n";
+const char *bobDigest = "b4780cabdfc3593004431644977cf73bf8475848";
+const char *bobTorID = "wr4azk67ynmtabcd";
+
+void TestCryptoKey::load()
+{
+ CryptoKey key;
+ QVERIFY(!key.isLoaded());
+
+ // Private key
+ QVERIFY(key.loadFromData(alice, CryptoKey::PrivateKey));
+ QVERIFY(key.isLoaded());
+ QVERIFY(key.isPrivate());
+ QCOMPARE(key.bits(), 1024);
+ key.clear();
+ QVERIFY(!key.isLoaded());
+
+ // Public key
+ QVERIFY(key.loadFromData(bob, CryptoKey::PublicKey));
+ QVERIFY(key.isLoaded());
+ QVERIFY(!key.isPrivate());
+ QCOMPARE(key.bits(), 1024);
+
+ // DER public key
+ QByteArray derEncoded = key.encodedPublicKey(CryptoKey::DER);
+ key.clear();
+ QVERIFY(key.loadFromData(derEncoded, CryptoKey::PublicKey, CryptoKey::DER));
+ QCOMPARE(key.encodedPublicKey(CryptoKey::DER), derEncoded);
+ key.clear();
+
+ // Invalid key
+ QVERIFY(!key.loadFromData(QByteArray(alice).mid(0, 150), CryptoKey::PrivateKey));
+ QVERIFY(!key.isLoaded());
+
+ // Invalid DER key
+ QVERIFY(!key.loadFromData(derEncoded.mid(0, derEncoded.size()-2), CryptoKey::PublicKey, CryptoKey::DER));
+ QVERIFY(!key.isLoaded());
+
+ // Empty key
+ QVERIFY(!key.loadFromData("", CryptoKey::PublicKey));
+ QVERIFY(!key.isLoaded());
+}
+
+void TestCryptoKey::publicKeyDigest()
+{
+ CryptoKey key;
+ QVERIFY(key.loadFromData(bob, CryptoKey::PublicKey));
+ QCOMPARE(key.publicKeyDigest().toHex(), QByteArray(bobDigest));
+
+ key.clear();
+ QVERIFY(key.loadFromData(alice, CryptoKey::PrivateKey));
+ QCOMPARE(key.publicKeyDigest().toHex(), QByteArray(aliceDigest));
+}
+
+void TestCryptoKey::encodedPublicKey()
+{
+ CryptoKey key;
+ QVERIFY(key.loadFromData(bob, CryptoKey::PublicKey));
+
+ QByteArray pemEncoded = key.encodedPublicKey(CryptoKey::PEM);
+ QVERIFY(pemEncoded.contains("BEGIN RSA PUBLIC KEY"));
+
+ QByteArray derEncoded = key.encodedPublicKey(CryptoKey::DER);
+ QCOMPARE(derEncoded.size(), 140);
+
+ CryptoKey key2;
+ QVERIFY(key2.loadFromData(pemEncoded, CryptoKey::PublicKey));
+ QCOMPARE(key.encodedPublicKey(), key2.encodedPublicKey());
+ QCOMPARE(key.publicKeyDigest(), key2.publicKeyDigest());
+
+ CryptoKey key3;
+ QVERIFY(key3.loadFromData(derEncoded, CryptoKey::PublicKey, CryptoKey::DER));
+ QCOMPARE(key.encodedPublicKey(), key3.encodedPublicKey());
+ QCOMPARE(key.publicKeyDigest(), key3.publicKeyDigest());
+
+ // Doesn't contain a private key
+ CryptoKey key4;
+ QVERIFY(!key4.loadFromData(pemEncoded, CryptoKey::PrivateKey));
+}
+
+void TestCryptoKey::torServiceID()
+{
+ CryptoKey key;
+ QVERIFY(key.loadFromData(bob, CryptoKey::PublicKey));
+
+ QString id = key.torServiceID();
+ QCOMPARE(id.size(), 16);
+ QCOMPARE(id, QLatin1String(bobTorID));
+}
+
+void TestCryptoKey::sign()
+{
+ CryptoKey key;
+ QVERIFY(key.loadFromData(alice, CryptoKey::PrivateKey));
+
+ QByteArray data = "test data";
+ QByteArray data2 = "different";
+
+ // Good signature
+ QByteArray signature = key.signData(data);
+ QVERIFY(!signature.isEmpty());
+ QVERIFY(key.verifyData(data, signature));
+
+ // Bad signature
+ QVERIFY(!key.verifyData(data2, signature));
+
+ // Corrupt signature
+ QVERIFY(!key.verifyData(data, signature.mid(0, signature.size() - 10)));
+
+ // Wrong public key
+ CryptoKey key2;
+ QVERIFY(key2.loadFromData(bob, CryptoKey::PublicKey));
+ QVERIFY(!key2.verifyData(data, signature));
+
+ // Compare to signSHA256
+ QByteArray dataDigest = QCryptographicHash::hash(data, QCryptographicHash::Sha256);
+ QByteArray signature2 = key.signSHA256(dataDigest);
+ QVERIFY(!signature2.isEmpty());
+ // signSHA256 and verifySHA256
+ QVERIFY(key.verifySHA256(dataDigest, signature2));
+ // signSHA256 and verifyData
+ QVERIFY(key.verifyData(data, signature2));
+ // signData and verifySHA256
+ QVERIFY(key.verifySHA256(dataDigest, signature));
+
+ // Compare to precomputed signature
+ QByteArray signaturep = QByteArray::fromHex(aliceSignedTestData);
+ QVERIFY(key.verifyData(data, signaturep));
+ QVERIFY(key.verifySHA256(dataDigest, signaturep));
+}
+
+QTEST_MAIN(TestCryptoKey)
+#include "tst_cryptokey.moc"
diff --git a/tests/cryptokey/tst_cryptokey.pro b/tests/cryptokey/tst_cryptokey.pro
new file mode 100644
index 0000000..4dfc438
--- /dev/null
+++ b/tests/cryptokey/tst_cryptokey.pro
@@ -0,0 +1,24 @@
+include(../tests.pri)
+
+SOURCES += tst_cryptokey.cpp \
+ $${SRC}/utils/CryptoKey.cpp \
+ $${SRC}/utils/SecureRNG.cpp
+
+unix:!macx {
+ !isEmpty(OPENSSLDIR) {
+ INCLUDEPATH += $${OPENSSLDIR}/include
+ LIBS += -L$${OPENSSLDIR}/lib -lcrypto
+ } else {
+ CONFIG += link_pkgconfig
+ PKGCONFIG += libcrypto
+ }
+}
+win32 {
+ isEmpty(OPENSSLDIR):error(You must pass OPENSSLDIR=path/to/openssl to qmake on this platform)
+ INCLUDEPATH += $${OPENSSLDIR}/include
+ LIBS += -L$${OPENSSLDIR}/lib -llibeay32
+
+ # required by openssl
+ LIBS += -lUser32 -lGdi32 -ladvapi32
+}
+macx:LIBS += -lcrypto
diff --git a/tests/tests.pri b/tests/tests.pri
new file mode 100644
index 0000000..7ee54c6
--- /dev/null
+++ b/tests/tests.pri
@@ -0,0 +1,8 @@
+TEMPLATE = app
+QT += testlib
+CONFIG -= app_bundle
+CONFIG += testcase
+
+SRC = ../../src/
+INCLUDEPATH += $${SRC}
+
diff --git a/tests/tests.pro b/tests/tests.pro
new file mode 100644
index 0000000..88d92d9
--- /dev/null
+++ b/tests/tests.pro
@@ -0,0 +1,2 @@
+TEMPLATE = subdirs
+SUBDIRS += cryptokey
diff --git a/translation/embedded.qrc b/translation/embedded.qrc
new file mode 100644
index 0000000..c5cb2f2
--- /dev/null
+++ b/translation/embedded.qrc
@@ -0,0 +1,21 @@
+<RCC>
+ <qresource prefix="/lang">
+ <file>ricochet_en.qm</file>
+ <file>ricochet_it.qm</file>
+ <file>ricochet_es.qm</file>
+ <file>ricochet_da.qm</file>
+ <file>ricochet_pt_BR.qm</file>
+ <file>ricochet_de.qm</file>
+ <file>ricochet_bg.qm</file>
+ <file>ricochet_cs.qm</file>
+ <file>ricochet_fi.qm</file>
+ <file>ricochet_fr.qm</file>
+ <file>ricochet_ru.qm</file>
+ <file>ricochet_uk.qm</file>
+ <file>ricochet_tr.qm</file>
+ <file>ricochet_nl_NL.qm</file>
+ <file>ricochet_fil_PH.qm</file>
+ <file>ricochet_sv.qm</file>
+ <file>ricochet_pl.qm</file>
+ </qresource>
+</RCC>
diff --git a/translation/inno/Bulgarian.isl b/translation/inno/Bulgarian.isl
new file mode 100644
index 0000000..9b37287
--- /dev/null
+++ b/translation/inno/Bulgarian.isl
@@ -0,0 +1,337 @@
+; *** Inno Setup version 5.5.3+ Bulgarian messages ***
+; Mikhail Balabanov <mishob at abv.bg>
+;
+; �� �� ��������� ������� �� ���� ����, ������������ �� �����������, ��������:
+; http://www.jrsoftware.org/files/istrans/
+;
+; ���������: ������ ����������, �� ��������� ����� (.) � ���� �� ���������,
+; ����� �����, ������ Inno Setup �� ������ ����������� (����������� �� �����
+; �� ������ �� ��������� �� ��� �����).
+
+[LangOptions]
+; �������� ��� ������ �� ����� �����. ������� ��, �� ��� ������ � ���������
+; ������� "[LangOptions]" �� �������� ����.
+LanguageName=<0431><044A><043B><0433><0430><0440><0441><043A><0438>
+LanguageID=$0402
+LanguageCodePage=1251
+; ��� ������, �� ����� ����������, ������� ��������� ��������� ��� ������ ��
+; ������, �������� �� �������� ����������� ������ ��-���� � �� ���������
+; ������ ������ �����.
+;DialogFontName=
+;DialogFontSize=8
+;WelcomeFontName=Verdana
+;WelcomeFontSize=12
+;TitleFontName=Arial
+;TitleFontSize=29
+;CopyrightFontName=Arial
+;CopyrightFontSize=8
+
+[Messages]
+
+; *** �������� �� ������������
+SetupAppTitle=�����������
+SetupWindowTitle=����������� �� %1
+UninstallAppTitle=�������������
+UninstallAppFullTitle=������������� �� %1
+
+; *** �������� �� ��� ���
+InformationTitle=����������
+ConfirmTitle=������������
+ErrorTitle=������
+
+; *** ��������� �� ���������� �����
+SetupLdrStartupMessage=�� �� ��������� %1. ������� �� �� ����������?
+LdrCannotCreateTemp=�� � �������� �� �� ������� �������� ����. ������������� �� ����������
+LdrCannotExecTemp=�� � �������� �� �� �������� ���� �� ���������� ����������. ������������� �� ����������
+
+; *** ��������� �� ������ ��� ����������
+LastErrorMessage=%1.%n%n������ %2: %3
+SetupFileMissing=������ %1 ������ �� ��������������� ����������. ����, ���������� �������� ��� �� �������� � ���� ����� �� ����������.
+SetupFileCorrupt=��������������� ������� �� ���������. ����, �������� �� � ���� ����� �� ����������.
+SetupFileCorruptOrWrongVer=��������������� ������� �� ��������� ��� ������������ � ���� ������ �� �����������. ����, ���������� �������� ��� �� �������� � ���� ����� �� ����������.
+InvalidParameter=� ��������� ��� � ������� ��������� ���������:%n%n%1
+SetupAlreadyRunning=������������ ���� �� ���������.
+WindowsVersionNotSupported=���������� �� �������� �������� �� Windows, � ����� ������ ���������� ��.
+WindowsServicePackRequired=���������� ������� %1 Service Pack %2 ��� ��-���.
+NotOnThisPlatform=���������� �� ���� �� �� ��������� ��� %1.
+OnlyOnThisPlatform=���������� ������ �� �� ��������� ��� %1.
+OnlyOnTheseArchitectures=���������� ���� �� �� ��������� ���� ��� ������ �� Windows �� �������� ���������� �����������:%n%n%1
+MissingWOW64APIs=������������ �� ��� ������ �� Windows �� ������� ����������������, ���������� �� 64-������ �����������. �� �� ���������� ��������, ������������ Service Pack %1.
+WinVersionTooLowError=���������� ������� %1 ������ %2 ��� ��-����.
+WinVersionTooHighError=���������� �� ���� �� ���� ����������� � %1 ������ %2 ��� ��-����.
+AdminPrivilegesRequired=�� �� ����������� ����������, ������ �� ������� ���� �������������.
+PowerUserPrivilegesRequired=�� �� ����������� ����������, ������ �� ������� ���� ������������� ��� ���������� � ��������� �����.
+SetupAppRunningError=������������ ��������, �� %1 �� ��������� � �������.%n%n����, ��������� ������ ����� �� ���������� � ��������� "OK", �� �� ����������, ��� "Cancel" �� �����.
+UninstallAppRunningError=�������������� ��������, �� %1 �� ��������� � �������.%n%n����, ��������� ������ ����� �� ���������� � ��������� "OK", �� �� ����������, ��� "Cancel" �� �����.
+
+; *** ����� ������
+ErrorCreatingDir=�� � �������� �� �� ������� ���������� "%1"
+ErrorTooManyFilesInDir=�� � �������� �� �� ������� ���� � ������������ "%1", ��� ���� �� ������� ������ ����� �������
+
+; *** ��������� �� ��� ��� �� �����������
+ExitSetupTitle=��������� �� �����������
+ExitSetupMessage=������������� �� � ���������. ��� ��������� ����, ���������� ���� �� ���� �����������.%n%n��-����� ������ ������ �� ���������� �����������, �� �� ��������� �������������.%n%n��������� �� �����������?
+AboutSetupMenuItem=&�� �����������...
+AboutSetupTitle=�� �����������
+AboutSetupMessage=%1 ������ %2%n%3%n%n�����������:%n%4
+AboutSetupNote=
+TranslatorNote=������ �� ���������: ������ ���������
+
+; *** ������
+ButtonBack=< ��&���
+ButtonNext=��&���� >
+ButtonInstall=&�����������
+ButtonOK=OK
+ButtonCancel=�����
+ButtonYes=&��
+ButtonYesToAll=�� �� &������
+ButtonNo=&��
+ButtonNoToAll=�� �� �&�����
+ButtonFinish=&������
+ButtonBrowse=���&����...
+ButtonWizardBrowse=���&����...
+ButtonNewFolder=&���� �����
+
+; *** ��������� � ���������� �������� �� ����� �� ����
+SelectLanguageTitle=����� �� ���� �� �����������
+SelectLanguageLabel=�������� ��� ���� �� �������� � �����������:
+
+; *** �������� �� ��� ��� �� ���������
+ClickNext=��������� "������", �� �� ����������, ��� "�����" �� ��������� �� �����������.
+BeveledLabel=
+BrowseDialogTitle=������� �� �����
+BrowseDialogLabel=�������� ����� �� ������ ������ � ��������� "OK".
+NewFolderName=���� �����
+
+; *** �������� "����� �����" �� ���������
+WelcomeLabel1=����� ����� ��� ��������� �� ����������� �� [name]
+WelcomeLabel2=���������� �� ��������� [name/ver] ��� ����� ��������.%n%n���������� �� �� ��������� ������ �������� ����������, ����� �� ����������.
+
+; *** �������� "������" �� ���������
+WizardPassword=������
+PasswordLabel1=������������ � �������� � ������.
+PasswordLabel3=����, �������� �������� � ��������� "������", �� �� ����������. ������ � ����� ����� �� �� ��������.
+PasswordEditLabel=&������:
+IncorrectPassword=���������� �� ��� ������ � ����������. ����, �������� ������.
+
+; *** �������� "����������� ������������" �� ���������
+WizardLicense=����������� ������������
+LicenseLabel=����, ��������� �������� ����� ����������, ����� �� ����������.
+LicenseLabel3=����, ��������� �������� ����������� ������������. ����� ������������� �� ��������, ������ �� �������� ��������� �� ��������������.
+LicenseAccepted=�&������ ��������������
+LicenseNotAccepted=&�� ������� ��������������
+
+; *** �������� "����������" �� ���������
+WizardInfoBefore=����������
+InfoBeforeLabel=����, ��������� �������� ����� ����������, ����� �� ����������.
+InfoBeforeClickLabel=������ ��� ����� �� ����������, ��������� "������".
+WizardInfoAfter=����������
+InfoAfterLabel=����, ��������� �������� ����� ����������, ����� �� ����������.
+InfoAfterClickLabel=������ ��� ����� �� ����������, ��������� "������".
+
+; *** �������� "����� �� ����������" �� ���������
+WizardUserInfo=����� �� ����������
+UserInfoDesc=����, �������� ������ �����.
+UserInfoName=&���:
+UserInfoOrg=&�����������:
+UserInfoSerial=&������ �����:
+UserInfoNameRequired=������ �� �������� ���.
+
+; *** �������� "����� �� ���������������" �� ���������
+WizardSelectDir=����� �� ���������������
+SelectDirDesc=���� �� �� ��������� [name]?
+SelectDirLabel3=[name] �� �� ��������� � �������� �����.
+SelectDirBrowseLabel=��������� "������", �� �� ����������. �� �� �������� ����� �����, ��������� "�������".
+DiskSpaceMBLabel=�������� �� ���� [mb] �� �������� ������� ������������.
+CannotInstallToNetworkDrive=������������ �� ���� �� ��������� �� ������� ����������.
+CannotInstallToUNCPath=������������ �� ���� �� ��������� � UNC ���.
+InvalidPath=������ �� �������� ����� ��� � ����� �� ����������, ��������:%n%nC:\APP%n%n��� UNC ��� ��� ����:%n%n\\������\��������� �����
+InvalidDrive=��������� �� ��� ���������� ��� ��������� UNC ����� �� ���������� ��� �� � ��������. ����, �������� �����.
+DiskSpaceWarningTitle=�������� �� ������� ������������
+DiskSpaceWarning=������������� ������� %1 �� �������� �����, �� �� ��������� ���������� ��� ���� %2 ��.%n%n������� �� ��� ��� �� ����������?
+DirNameTooLong=������ ����� ��� �� ����� ��� ���.
+InvalidDirName=����� �� ����� � ���������.
+BadDirName32=������� �� ����� �� ����� �� �������� �������� �����:%n%n%1
+DirExistsTitle=������� ����������
+DirExists=�������:%n%n%1%n%n���� ����������. ������� �� ��� ��� �� ����������� � ���?
+DirDoesntExistTitle=������� �� ����������
+DirDoesntExist=�������:%n%n%1%n%n�� ����������. ������� �� �� ���� ���������?
+
+; *** �������� "����� �� ����������" �� ���������
+WizardSelectComponents=����� �� ����������
+SelectComponentsDesc=��� ���������� �� ����� �����������?
+SelectComponentsLabel2=�������� ������������, ����� ������� �� �����������, � �������� ����������. ��������� "������", ������ ��� ����� �� ����������.
+FullInstallation=����� ����������
+; �� ���������� �� ����������� "Compact" ���� "Minimal" (��� �� ������� "Minimal" �� ����� ����)
+CompactInstallation=��������� ����������
+CustomInstallation=���������� �� �����
+NoUninstallWarningTitle=������������ �����������
+NoUninstallWarning=������������ ��������, �� �������� ���������� �� ���� ����������� � ��������a:%n%n%1%n%n����������� �� ���� ���������� ���� �� �� �����������.%n%n������� �� ��� ��� �� ����������?
+ComponentSize1=%1 ��
+ComponentSize2=%1 ��
+ComponentsDiskSpaceMBLabel=����������� ����� ������� ���� [mb] �� ������� ������������.
+
+; *** �������� "����� �� ������������ ������" �� ���������
+WizardSelectTasks=����� �� ������������ ������
+SelectTasksDesc=��� ������������ ������ �� ����� ���������?
+SelectTasksLabel2=�������� ��� ������������ ������ ������� �� �� �������� ��� ����������� �� [name], ���� ����� ��������� "������".
+
+; *** �������� "����� �� ����� � ������ "�����" �� ���������
+WizardSelectProgramGroup=����� �� ����� � ������ "�����"
+SelectStartMenuFolderDesc=���� �� ����� ��������� ������� ������ �� ����������?
+SelectStartMenuFolderLabel3=������������ �� ������� ����� ������ � �������� ����� �� ������ "�����".
+SelectStartMenuFolderBrowseLabel=��������� "������", �� �� ����������. �� �� �������� ����� �����, ��������� "�������".
+MustEnterGroupName=������ �� �������� ��� �� �����.
+GroupNameTooLong=������ ����� ��� �� ����� ��� ���.
+InvalidGroupName=����� �� ����� � ���������.
+BadGroupName=����� �� ����� �� ���� �� ������� �������� �����:%n%n%1
+NoProgramGroupCheck2=�&���������� ��� ����� � ������ "�����"
+
+; *** �������� "��������� �� �����������" �� ���������
+WizardReady=��������� �� �����������
+ReadyLabel1=������������ � ����� �� ��������� [name] ��� ����� ��������.
+ReadyLabel2a=��������� "�����������", �� �� ����������, ��� "�����" �� ������� ��� ������� �� ����� ���������.
+ReadyLabel2b=��������� "�����������", �� �� ���������� � �������������.
+ReadyMemoUserInfo=����� �� ����������:
+ReadyMemoDir=���������������:
+ReadyMemoType=��� ����������:
+ReadyMemoComponents=������� ����������:
+ReadyMemoGroup=����� � ������ "�����":
+ReadyMemoTasks=������������ ������:
+
+; *** �������� "���������� �� �����������" �� ���������
+WizardPreparing=���������� �� �����������
+PreparingDesc=������������ �� �������� �� ��������� [name] ��� ����� ��������.
+PreviousInstallNotCompleted=����������� ��� ���������� �� �������� �������� �� � ���������. ������������� ���������, �� �� ���� �������� �� �������.%n%n���� ���� ������������, ����������� ����������� ������, �� �� ��������� ������������� �� [name].
+CannotContinue=������������� �� ���� �� ��������. ����, ��������� "�����" �� �����.
+ApplicationsFound=�������� ���������� ��������� �������, ����� ������ �� ����� �������� �� �����������. ���������� �� �� ��������� �� ����������� ����������� �� ������� ������������.
+ApplicationsFound2=�������� ���������� ��������� �������, ����� ������ �� ����� �������� �� �����������. ���������� �� �� ��������� �� ����������� ����������� �� ������� ������������. ���� ���� �� ������������� �� ���� �������� ���� �� �������������� ��.
+CloseApplications=������������ �� �� �������� &�����������
+DontCloseApplications=������������ �� &�� �� ��������
+ErrorCloseApplications=�� �� �������� �� �� �������� ����������� ������ ����������. ���������� �� ����� �� ����������, �� ��������� ������ ����������, ���������� �������, ����� ������������ ������ �� ������.
+
+; *** �������� "�����������" �� ���������
+WizardInstalling=�����������
+InstallingLabel=����, ��������� ������ [name] �� ��������� ��� ����� ��������.
+
+; *** �������� "������������� �������" �� ���������
+FinishedHeadingLabel=���������� �� ����������� �� [name] �������
+FinishedLabelNoIcons=������������� �� [name] ��� ����� �������� �������.
+FinishedLabel=������������� �� [name] ��� ����� �������� �������. ������ �� ���������� ������������ ���� ������������� �����.
+ClickFinish=��������� "������", �� �� ��������� �����������.
+FinishedRestartLabel=������������ ������ �� ���������� ���������, �� �� ������� ������������� �� [name]. ������� �� �� ������������ ����?
+FinishedRestartMessage=������������ ������ �� ���������� ���������, �� �� ������� ������������� �� [name].%n%n������� �� �� ������������ ����?
+ShowReadmeCheck=��, ����� �� ��������� ����� README
+YesRadio=&��, ���� ���������� �� ���������� ����
+NoRadio=&��, �� ����������� ��������� ��-�����
+; �������� �� �������� � "���������� �� MyProg.exe"
+RunEntryExec=���������� �� %1
+; �������� �� �������� � "����������� �� Readme.txt"
+RunEntryShellExec=����������� �� %1
+
+; *** �������� �� ���� �� "������������ ������� ������� �������"
+ChangeDiskTitle=������������ ������� ������� �������
+SelectDiskLabel2=����, ��������� ������� %1 � ��������� "��".%n%n��� ��������� �� �������� �� ������� � �������� �� ���������� ��-���� �����, �������� ��������� ��� �� ��� ��� ��������� "�������".
+PathLabel=�&��:
+FileNotInDir2=������ "%1" �� �� ������� � "%2". ����, ��������� ��������� ������� ��� �������� ����� �����.
+SelectDirectoryLabel=����, �������� ���������������� �� ��������� �������.
+
+; *** ��������� �� ���� "�����������"
+SetupAborted=������������� �� � ���������.%n%n����, ���������� �������� � ����������� ����������� ������.
+EntryAbortRetryIgnore=��������� "Retry" �� �������� ����, "Ignore" �� ����������� ������� �������� ��� "Abort" �� ������������ �� �������������.
+
+; *** ��������� �� ���� �� �������������
+StatusClosingApplications=�������� �� ����������...
+StatusCreateDirs=�������� �� ����������...
+StatusExtractFiles=�������� �� �������...
+StatusCreateIcons=�������� �� ����� ������...
+StatusCreateIniEntries=�������� �� ������ � INI ����...
+StatusCreateRegistryEntries=�������� �� ������ � ���������...
+StatusRegisterFiles=����������� �� �������...
+StatusSavingUninstall=�������� �� ����� �� �������������...
+StatusRunProgram=������������ ���������...
+StatusRestartingApplications=����������� �� ����������...
+StatusRollback=��������� �� �������...
+
+; *** ������ �� ��� ���
+ErrorInternal2=�������� ������: %1
+ErrorFunctionFailedNoCode=��������� ���������� �� %1
+ErrorFunctionFailed=��������� ���������� �� %1; ��� �� ��������: %2
+ErrorFunctionFailedWithMessage=��������� ���������� �� %1; ��� �� ��������: %2.%n%3
+ErrorExecutingProgram=�� � �������� �� �� �������� ����:%n%1
+
+; *** ������, �������� � ���������
+ErrorRegOpenKey=������ ��� �������� �� ���� � ���������:%n%1\%2
+ErrorRegCreateKey=������ ��� ��������� �� ���� � ���������:%n%1\%2
+ErrorRegWriteKey=������ ��� ������ � ���� �� ���������:%n%1\%2
+
+; *** ������, �������� � INI �������
+ErrorIniEntry=������ ��� ��������� �� INI ����� ��� ����� "%1".
+
+; *** ������ ��� �������� �� �������
+FileAbortRetryIgnore=��������� "Retry" �� �������� ����, "Ignore" �� ���������� �� ����� (�� �� ����������) ��� "Abort" �� ������������ �� �������������.
+FileAbortRetryIgnore2=��������� "Retry" �� �������� ����, "Ignore" �� ����������� (�� �� ����������) ��� "Abort" �� ������������ �� �������������.
+SourceIsCorrupted=������ - �������� � ��������
+SourceDoesntExist=������ - �������� "%1" �� ����������
+ExistingFileReadOnly=�������������� ���� ��� ������� "���� �� ������".%n%n��������� "Retry" �� ���������� �� �������� � �������� ����, "Ignore" �� ���������� �� ����� ��� "Abort" �� ������������ �� �������������.
+ErrorReadingExistingDest=������ ��� ���� �� ������ �� ����������� ����:
+FileExists=������ ���� ����������.%n%n������� �� ������������ �� �� ���������?
+ExistingFileNewer=�������������� ���� � ��-��� �� ����, ����� ������������ �� ������ �� ���������. ���������� �� �� �� ��������.%n%n������� �� �� �������� ������������� ����?
+ErrorChangingAttr=������ ��� ���� �� ����� �� �������� �� ����������� ����:
+ErrorCreatingTemp=������ ��� ���� �� ��������� �� ���� � �������� ����������:
+ErrorReadingSource=������ ��� ���� �� ������ �� ���� - ��������:
+ErrorCopying=������ ��� ���� �� �������� �� ����:
+ErrorReplacingExistingFile=������ ��� ���� �� ���������� �� ����������� ����:
+ErrorRestartReplace=��������� �������� ����������:
+ErrorRenamingTemp=������ ��� ���� �� ������������ �� ���� � �������� ����������:
+ErrorRegisterServer=�� � �������� �� �� ���������� ���������� �� ��� DLL/OCX: %1
+ErrorRegSvr32Failed=��������� ���������� �� RegSvr32 � ��� �� ����� %1
+ErrorRegisterTypeLib=�� � �������� �� �� ���������� ���������� �� ������: %1
+
+; *** ������ ���� �����������
+ErrorOpeningReadme=�������� ������ ��� ���� �� �������� �� ����� README.
+ErrorRestartingComputer=������������ �� � � ��������� �� ���������� ���������. ����, ��������� �� �����.
+
+; *** ��������� �� �������������
+UninstallNotFound=������ "%1" �� ����������. ��������������� � ����������.
+UninstallOpenError=������ "%1" �� ���� �� �� ������. ��������������� � ����������
+UninstallUnsupportedVer=�������� �� ��������������� ���� �� ������������� "%1" �� �� ���������� �� ���� ������ �� �������������. ��������������� � ����������
+UninstallUnknownEntry=������ �� �������� ����� (%1) � ��������������� ���� �� �������������
+ConfirmUninstall=�������� �� ������� �� ���������� ������� %1 � ������ ��������� ����������?
+UninstallOnlyOnWin64=���������� ���� �� ���� ������������� ���� ��� 64-����� Windows.
+OnlyAdminCanUninstall=���������� ���� �� ���� ���������� ���� �� ���������� � ���������������� �����.
+UninstallStatusLabel=����, ��������� ������������ �� %1 �� ����� �������� �� ��������.
+UninstalledAll=%1 ���� ���������� ������� �� ����� ��������.
+UninstalledMost=��������������� �� %1 �������.%n%n������������ �� ����� �������� �� �� ��������. ������ �� �� ���������� �����.
+UninstalledAndNeedsRestart=�� �� �������� ��������������� �� %1, ������ �� ������������ ����� ��������.%n%n������� �� �� ������������ ����?
+UninstallDataCorrupted=������ "%1" � ��������. ��������������� � ����������
+
+; *** ��������� �� ���� "�������������"
+ConfirmDeleteSharedFileTitle=���������� �� �������� ����?
+ConfirmDeleteSharedFile2=��������� ������, �� �������� �������� ���� ���� �� �� ������ �� ����� ��������. ������� �� �������������� �� �� ��������?%n%n��� ����� �������� ��� ��� ������ ����� � ��� ���� ������, ���������� ���� �� ���� �� ������ ��������. ��� �� ���������, �������� "��". ���������� �� ����� � ��������� � ���������.
+SharedFileNameLabel=��� �� �����:
+SharedFileLocationLabel=��������������:
+WizardUninstalling=��� �� ���������������
+StatusUninstalling=%1 �� �����������...
+
+; *** ��������� �� ��������� ������� �� ���������
+ShutdownBlockReasonInstallingApp=��������� �� %1.
+ShutdownBlockReasonUninstallingApp=����������� �� %1.
+
+; ��������������� ��������� ��-���� �� �� ������� �� ����� ����������, ��
+; ��� �������� ������ � ����������� ��, �������� ����� ������ �� �� ���������.
+
+[CustomMessages]
+
+NameAndVersion=%1, ������ %2
+AdditionalIcons=������������ �����:
+CreateDesktopIcon=����� �� &�������� ����
+CreateQuickLaunchIcon=����� � ������� "&����� ����������"
+ProgramOnTheWeb=%1 � ��������
+UninstallProgram=������������� �� %1
+LaunchProgram=���������� �� %1
+AssocFileExtension=&��������� �� %1 � ��������� ���������� %2
+AssocingFileExtension=%1 �� ������� � ��������� ���������� %2...
+AutoStartProgramGroupDescription=����������:
+AutoStartProgram=����������� ���������� �� %1
+AddonHostProgramNotFound=%1 �� �� �������� � ��������� �� ��� �����.%n%n������� �� ��� ��� �� ����������?
diff --git a/translation/inno/Swedish.isl b/translation/inno/Swedish.isl
new file mode 100644
index 0000000..d2c4d43
--- /dev/null
+++ b/translation/inno/Swedish.isl
@@ -0,0 +1,339 @@
+; *** Inno Setup version 5.5.3+ Swedish messages ***
+;
+; To download user-contributed translations of this file, go to:
+; http://www.jrsoftware.org/files/istrans/
+;
+; Note: When translating this text, do not add periods (.) to the end of
+; messages that didn't have them already, because on those messages Inno
+; Setup adds the periods automatically (appending a period would result in
+; two periods being displayed).
+;
+; Translated by christer_1 at hotmail.com (Christer Toivonen)
+;
+
+[LangOptions]
+; The following three entries are very important. Be sure to read and
+; understand the '[LangOptions] section' topic in the help file.
+LanguageName=Svenska
+LanguageID=$041D
+LanguageCodePage=1252
+; If the language you are translating to requires special font faces or
+; sizes, uncomment any of the following entries and change them accordingly.
+;DialogFontName=
+;DialogFontSize=8
+;WelcomeFontName=Verdana
+;WelcomeFontSize=12
+;TitleFontName=Arial
+;TitleFontSize=29
+;CopyrightFontName=Arial
+;CopyrightFontSize=8
+
+[Messages]
+
+; *** Application titles
+SetupAppTitle=Installationsprogram
+SetupWindowTitle=Installationsprogram f�r %1
+UninstallAppTitle=Avinstallation
+UninstallAppFullTitle=%1 Avinstallation
+
+; *** Misc. common
+InformationTitle=Information
+ConfirmTitle=Bekr�fta
+ErrorTitle=Fel
+
+; *** SetupLdr messages
+SetupLdrStartupMessage=%1 kommer att installeras. Vill du forts�tta?
+LdrCannotCreateTemp=Kan ej skapa en tempor�r fil. Installationen avbryts
+LdrCannotExecTemp=Kan inte k�ra fil i tempor�r katalog. Installationen avbryts
+
+; *** Startup error messages
+LastErrorMessage=%1.%n%nFel %2: %3
+SetupFileMissing=Filen %1 saknas i installationskatalogen. R�tta till problemet eller h�mta en ny kopia av programmet.
+SetupFileCorrupt=Installationsfilerna �r felaktiga. H�mta en ny kopia av programmet
+SetupFileCorruptOrWrongVer=Installationsfilerna �r felaktiga, eller st�mmer ej �verens med denna version av installationsprogrammet. R�tta till felet eller h�mta en ny programkopia.
+InvalidParameter=En ogiltig parameter angavs p� kommandoraden:%n%n%1
+SetupAlreadyRunning=Setup k�rs redan.
+WindowsVersionNotSupported=Programmet st�djer inte den version av Windows som k�rs p� datorn.
+WindowsServicePackRequired=Programmet kr�ver %1 Service Pack %2 eller nyare.
+NotOnThisPlatform=Detta program kan ej k�ras p� %1.
+OnlyOnThisPlatform=Detta program m�ste ha %1.
+OnlyOnTheseArchitectures=Detta program kan bara installeras p� Windows versioner med f�ljande processorarkitekturer:%n%n%1
+MissingWOW64APIs=Den versionen av Windows du k�r har inte den funktionalitet installationsprogrammet beh�ver f�r att genomf�ra en 64-bitars installation. R�tta till problemet genom att installera Service Pack %1.
+WinVersionTooLowError=Detta program kr�ver %1, version %2 eller senare.
+WinVersionTooHighError=Programmet kan inte installeras p� %1 version %2 eller senare.
+AdminPrivilegesRequired=Du m�ste vara inloggad som administrat�r n�r du installerar detta program.
+PowerUserPrivilegesRequired=Du m�ste vara inloggad som administrat�r eller medlem av gruppen Privilegierade anv�ndare (Power Users) n�r du installerar detta program.
+SetupAppRunningError=Installationsprogrammet har uppt�ckt att %1 �r ig�ng.%n%nAvsluta det angivna programmet nu. Klicka sedan p� OK f�r att g� vidare, eller p� Avbryt f�r att avsluta.
+UninstallAppRunningError=Avinstalleraren har uppt�ckt att %1 k�rs f�r tillf�llet.%n%nSt�ng all �ppna instanser av det nu, klicka sedan p� OK f�r att g� vidare, eller p� Avbryt f�r att avsluta.
+
+; *** Misc. errors
+ErrorCreatingDir=Kunde inte skapa katalogen "%1"
+ErrorTooManyFilesInDir=Kunde inte skapa en fil i katalogen "%1" d�rf�r att den inneh�ller f�r m�nga filer
+
+; *** Setup common messages
+ExitSetupTitle=Avsluta installationen
+ExitSetupMessage=Installationen �r inte f�rdig. Om du avslutar nu, kommer programmet inte att installeras.%n%nDu kan k�ra installationsprogrammet vid ett senare tillf�lle f�r att slutf�ra installationen.%n%nVill du avbryta installationen?
+AboutSetupMenuItem=&Om installationsprogrammet...
+AboutSetupTitle=Om installationsprogrammet
+AboutSetupMessage=%1 version %2%n%3%n%n%1 hemsida:%n%4
+AboutSetupNote=Svensk �vers�ttning �r gjord av dickg at go.to 1999, 2002%n%nUppdatering till 3.0.2+ av peter at peterandlinda.com, 4.+ av stefan at bodingh.se
+TranslatorNote=
+
+; *** Buttons
+ButtonBack=< &Tillbaka
+ButtonNext=&N�sta >
+ButtonInstall=&Installera
+ButtonOK=OK
+ButtonCancel=Avbryt
+ButtonYes=&Ja
+ButtonYesToAll=Ja till &Allt
+ButtonNo=&Nej
+ButtonNoToAll=N&ej till allt
+ButtonFinish=&Slutf�r
+ButtonBrowse=&Bl�ddra...
+ButtonWizardBrowse=&Bl�ddra...
+ButtonNewFolder=Skapa ny katalog
+
+; *** "Select Language" dialog messages
+SelectLanguageTitle=V�lj spr�k f�r installationen
+SelectLanguageLabel=V�lj spr�k som skall anv�ndas under installationen:
+
+; *** Common wizard text
+ClickNext=Klicka p� N�sta f�r att forts�tta eller p� Avbryt f�r att avsluta installationen.
+BeveledLabel=
+BrowseDialogTitle=V�lj katalog
+BrowseDialogLabel=V�lj en katalog i listan nedan, klicka sedan p� OK.
+NewFolderName=Ny katalog
+
+; *** "Welcome" wizard page
+WelcomeLabel1=V�lkommen till installationsprogrammet f�r [name].
+WelcomeLabel2=Detta kommer att installera [name/ver] p� din dator.%n%nDet rekommenderas att du avslutar alla andra program innan du forts�tter. Det f�rebygger konflikter under installationens g�ng.
+
+; *** "Password" wizard page
+WizardPassword=L�senord
+PasswordLabel1=Denna installation �r skyddad med l�senord.
+PasswordLabel3=Var god ange l�senordet, klicka sedan p� N�sta f�r att forts�tta. L�senord skiljer p� versaler/gemener.
+PasswordEditLabel=&L�senord:
+IncorrectPassword=L�senordet du angav �r inkorrekt. F�rs�k igen.
+
+; *** "License Agreement" wizard page
+WizardLicense=Licensavtal
+LicenseLabel=Var god och l�s f�ljande viktiga information innan du forts�tter.
+LicenseLabel3=Var god och l�s f�ljande licensavtal. Du m�ste acceptera villkoren i avtalet innan du kan forts�tta med installationen.
+LicenseAccepted=Jag &accepterar avtalet
+LicenseNotAccepted=Jag accepterar &inte avtalet
+
+; *** "Information" wizard pages
+WizardInfoBefore=Information
+InfoBeforeLabel=Var god l�s f�ljande viktiga information innan du forts�tter.
+InfoBeforeClickLabel=N�r du �r klar att forts�tta med installationen klickar du p� N�sta.
+WizardInfoAfter=Information
+InfoAfterLabel=Var god l�s f�ljande viktiga information innan du forts�tter.
+InfoAfterClickLabel=N�r du �r klar att forts�tta med installationen klickar du p� N�sta.
+
+; *** "User Information" wizard page
+WizardUserInfo=Anv�ndarinformation
+UserInfoDesc=Var god och fyll i f�ljande uppgifter.
+UserInfoName=&Namn:
+UserInfoOrg=&Organisation:
+UserInfoSerial=&Serienummer:
+UserInfoNameRequired=Du m�ste fylla i ett namn.
+
+; *** "Select Destination Directory" wizard page
+WizardSelectDir=V�lj installationsplats
+SelectDirDesc=Var skall [name] installeras?
+SelectDirLabel3=Installationsprogrammet kommer att installera [name] i f�ljande katalog
+SelectDirBrowseLabel=F�r att forts�tta klickar du p� N�sta. Om du vill v�lja en annan katalog klickar du p� Bl�ddra.
+DiskSpaceMBLabel=Programmet kr�ver minst [mb] MB h�rddiskutrymme.
+CannotInstallToNetworkDrive=Setup kan inte installeras p� n�tverksdisk.
+CannotInstallToUNCPath=Setup kan inte installeras p� UNC s�kv�g.
+InvalidPath=Du m�ste skriva en fullst�ndig s�kv�g med enhetsbeteckning; till exempel:%n%nC:\Program%n%neller en UNC-s�kv�g i formatet:%n%n\\server\resurs
+InvalidDrive=Enheten du har valt finns inte eller �r inte tillg�nglig. V�lj en annan.
+DiskSpaceWarningTitle=Ej tillr�ckligt med diskutrymme
+DiskSpaceWarning=Installationsprogrammet beh�ver �tminstone %1 KB ledigt diskutrymme f�r installationen, men den valda enheten har bara %2 KB tillg�ngligt.%n%nVill du forts�tta �nd�?
+DirNameTooLong=Katalogens namn eller s�kv�g �r f�r l�ng.
+InvalidDirName=Katalogen du har valt �r inte tillg�nglig.
+BadDirName32=Katalogens namn f�r ej inneh�lla n�got av f�ljande tecken:%n%n%1
+DirExistsTitle=Katalogen finns
+DirExists=Katalogen:%n%n%1%n%nfinns redan. Vill du �nd� forts�tta installationen till den valda katalogen?
+DirDoesntExistTitle=Katalogen finns inte
+DirDoesntExist=Katalogen:%n%n%1%n%nfinns inte. Vill du skapa den?
+
+; *** "Select Components" wizard page
+WizardSelectComponents=V�lj komponenter
+SelectComponentsDesc=Vilka komponenter skall installeras?
+SelectComponentsLabel2=V�lj de komponenter som du vill ska installeras; avmarkera de komponenter som du inte vill ha. Klicka sedan p� N�sta n�r du �r klar att forts�tta.
+FullInstallation=Fullst�ndig installation
+; if possible don't translate 'Compact' as 'Minimal' (I mean 'Minimal' in your language)
+CompactInstallation=Kompakt installation
+CustomInstallation=Anpassad installation
+NoUninstallWarningTitle=Komponenter finns
+NoUninstallWarning=Installationsprogrammet har uppt�ckt att f�ljande komponenter redan finns installerade p� din dator:%n%n%1%n%nAtt avmarkera dessa komponenter kommer inte att avinstallera dom.%n%nVill du forts�tta �nd�?
+ComponentSize1=%1 KB
+ComponentSize2=%1 MB
+ComponentsDiskSpaceMBLabel=Aktuella val kr�ver minst [mb] MB diskutrymme.
+
+; *** "Select Additional Tasks" wizard page
+WizardSelectTasks=V�lj extra uppgifter
+SelectTasksDesc=Vilka extra uppgifter skall utf�ras?
+SelectTasksLabel2=Markera ytterligare uppgifter att utf�ra vid installation av [name], tryck sedan p� N�sta.
+
+; *** "Select Start Menu Folder" wizard page
+WizardSelectProgramGroup=V�lj Startmenykatalogen
+SelectStartMenuFolderDesc=Var skall installationsprogrammet placera programmets genv�gar?
+SelectStartMenuFolderLabel3=Installationsprogrammet kommer att skapa programmets genv�gar i f�ljande katalog.
+SelectStartMenuFolderBrowseLabel=F�r att forts�tta klickar du p� N�sta. Om du vill v�lja en annan katalog, klickar du p� Bl�ddra.
+MustEnterGroupName=Du m�ste ange en katalog.
+GroupNameTooLong=Katalogens namn eller s�kv�g �r f�r l�ng.
+InvalidGroupName=Katalogen du har valt �r inte tillg�nglig.
+BadGroupName=Katalognamnet kan inte inneh�lla n�gon av f�ljande tecken:%n%n%1
+NoProgramGroupCheck2=&Skapa ingen Startmenykatalog
+
+; *** "Ready to Install" wizard page
+WizardReady=Redo att installera
+ReadyLabel1=Installationsprogrammet �r nu redo att installera [name] p� din dator.
+ReadyLabel2a=Tryck p� Installera om du vill forts�tta, eller p� g� Tillbaka om du vill granska eller �ndra p� n�got.
+ReadyLabel2b=V�lj Installera f�r att p�b�rja installationen.
+ReadyMemoUserInfo=Anv�ndarinformation:
+ReadyMemoDir=Installationsplats:
+ReadyMemoType=Installationstyp:
+ReadyMemoComponents=Valda komponenter:
+ReadyMemoGroup=Startmenykatalog:
+ReadyMemoTasks=Extra uppgifter:
+
+; *** "Preparing to Install" wizard page
+WizardPreparing=F�rbereder installationen
+PreparingDesc=Installationsprogrammet f�rbereder installationen av [name] p� din dator.
+PreviousInstallNotCompleted=Installationen/avinstallationen av ett tidigare program har inte slutf�rts. Du m�ste starta om datorn f�r att avsluta den installationen.%n%nEfter att ha startat om datorn k�r du installationsprogrammet igen f�r att slutf�ra installationen av [name].
+CannotContinue=Installationsprogrammet kan inte forts�tta. Klicka p� Avbryt f�r att avsluta.
+ApplicationsFound=F�ljande program anv�nder filer som m�ste uppdateras av Setup. Vi rekommenderar att du l�ter Setup automatiskt st�nga dessa program.
+ApplicationsFound2=F�ljande program anv�nder filer som m�ste uppdateras av Setup. Vi rekommenderar att du l�ter Setup automatiskt st�nga dessa program. Efter installationen kommer Setup att f�rs�ka starta programmen igen.
+CloseApplications=&St�ng programmen automatiskt
+DontCloseApplications=&St�ng inte programmen
+ErrorCloseApplications=Installationsprogrammet kunde inte st�nga alla program. Innan installationen forts�tter rekommenderar vi att du st�nger alla program som anv�nder filer som Setup beh�ver uppdatera.
+
+; *** "Installing" wizard page
+WizardInstalling=Installerar
+InstallingLabel=V�nta medan [name] installeras p� din dator.
+
+; *** "Setup Completed" wizard page
+FinishedHeadingLabel=Avslutar installationen av [name]
+FinishedLabelNoIcons=[name] har nu installerats p� din dator.
+FinishedLabel=[name] har nu installerats p� din dator. Programmet kan startas genom att v�lja n�gon av ikonerna.
+ClickFinish=V�lj Slutf�r f�r att avsluta installationen.
+FinishedRestartLabel=F�r att slutf�ra installationen av [name], m�ste datorn startas om. Vill du starta om nu?
+FinishedRestartMessage=F�r att slutf�ra installationen av [name], m�ste datorn startas om.%n%nVill du starta om datorn nu?
+ShowReadmeCheck=Ja, jag vill se filen L�S MIG
+YesRadio=&Ja, jag vill starta om datorn nu
+NoRadio=&Nej, jag startar sj�lv om datorn senare
+; used for example as 'Run MyProg.exe'
+RunEntryExec=K�r %1
+; used for example as 'View Readme.txt'
+RunEntryShellExec=L�s %1
+
+; *** "Setup Needs the Next Disk" stuff
+ChangeDiskTitle=Installationsprogrammet beh�ver n�sta diskett
+SelectDiskLabel2=Var god s�tt i diskett %1 och tryck OK.%n%nOm filerna kan hittas i en annan katalog �n den som visas nedan, skriv in r�tt s�kv�g eller v�lj Bl�ddra.
+PathLabel=&S�kv�g:
+FileNotInDir2=Kunde inte hitta filen "%1" i "%2". Var god s�tt i korrekt diskett eller v�lj en annan katalog.
+SelectDirectoryLabel=Var god ange s�kv�gen f�r n�sta diskett.
+
+; *** Installation phase messages
+SetupAborted=Installationen slutf�rdes inte.%n%nVar god r�tta till felet och k�r installationen igen.
+EntryAbortRetryIgnore=V�lj F�rs�k igen eller Ignorera f�r att forts�tta �nd�, eller v�lj Avbryt f�r att avbryta installationen.
+
+; *** Installation status messages
+StatusClosingApplications=St�nger program...
+StatusCreateDirs=Skapar kataloger...
+StatusExtractFiles=Packar upp filer...
+StatusCreateIcons=Skapar programikoner...
+StatusCreateIniEntries=Skriver INI-v�rden...
+StatusCreateRegistryEntries=Skriver register-v�rden...
+StatusRegisterFiles=Registrerar filer...
+StatusSavingUninstall=Sparar information f�r avinstallation...
+StatusRunProgram=Slutf�r installationen...
+StatusRestartingApplications=Startar om program...
+StatusRollback=�terst�ller �ndringar...
+
+; *** Misc. errors
+ErrorInternal2=Internt fel: %1
+ErrorFunctionFailedNoCode=%1 misslyckades
+ErrorFunctionFailed=%1 misslyckades; kod %2
+ErrorFunctionFailedWithMessage=%1 misslyckades; kod %2.%n%3
+ErrorExecutingProgram=Kan inte k�ra filen:%n%1
+
+; *** Registry errors
+ErrorRegOpenKey=Fel vid �ppning av registernyckel:%n%1\%2
+ErrorRegCreateKey=Kan ej skapa registernyckel:%n%1\%2
+ErrorRegWriteKey=Kan ej skriva till registernyckel:%n%1\%2
+
+; *** INI errors
+ErrorIniEntry=Kan inte skriva nytt INI-v�rde i filen "%1".
+
+; *** File copying errors
+FileAbortRetryIgnore=V�lj F�rs�k igen eller Ignorera f�r att hoppa �ver denna fil (ej rekommenderat), eller v�lj Avbryt installationen.
+FileAbortRetryIgnore2=V�lj F�rs�k igen eller Ignorera och forts�tt �nd� (ej rekommenderat), eller v�lj Avbryt installationen.
+SourceIsCorrupted=K�llfilen �r felaktig
+SourceDoesntExist=K�llfilen "%1" finns inte
+ExistingFileReadOnly=Den nuvarande filen �r skrivskyddad.%n%nV�lj F�rs�k igen f�r att ta bort skrivskyddet, Ignorera f�r att hoppa �ver denna fil, eller v�lj Avbryt installationen.
+ErrorReadingExistingDest=Ett fel uppstod vid f�rs�k att l�sa den befintliga filen:
+FileExists=Filen finns redan.%n%nVill du skriva �ver den?
+ExistingFileNewer=Den befintliga filen �r nyare �n den som ska installeras. Du rekommenderas att beh�lla den befintliga filen. %n%nVill Du beh�lla den befintliga filen?
+ErrorChangingAttr=Ett fel uppstod vid f�rs�k att �ndra attribut p� den befintliga filen:
+ErrorCreatingTemp=Ett fel uppstod vid ett f�rs�k att skapa installationskatalogen:
+ErrorReadingSource=Ett fel uppstod vid ett f�rs�k att l�sa k�llfilen:
+ErrorCopying=Ett fel uppstod vid kopiering av filen:
+ErrorReplacingExistingFile=Ett fel uppstod vid ett f�rs�k att ers�tta den befintliga filen:
+ErrorRestartReplace=�terstartaErs�tt misslyckades:
+ErrorRenamingTemp=Ett fel uppstod vid ett f�rs�k att byta namn p� en fil i installationskatalogen:
+ErrorRegisterServer=Kunde inte registrera DLL/OCX: %1
+ErrorRegSvr32Failed=RegSvr32 misslyckades med felkod %1
+ErrorRegisterTypeLib=Kunde inte registrera typbibliotek: %1
+
+; *** Post-installation errors
+ErrorOpeningReadme=Ett fel uppstod vid �ppnandet av L�S MIG-filen.
+ErrorRestartingComputer=Installationsprogrammet kunde inte starta om datorn. Var god g�r det manuellt.
+
+; *** Uninstaller messages
+UninstallNotFound=Filen "%1" finns inte. Kan inte avinstallera.
+UninstallOpenError=Filen "%1" kan inte �ppnas. Kan inte avinstallera.
+UninstallUnsupportedVer=Avinstallationsloggen "%1" �r i ett format som denna version inte k�nner igen. Kan ej avinstallera
+UninstallUnknownEntry=En ok�nd rad (%1) hittades i avinstallationsloggen
+ConfirmUninstall=�r du s�ker p� att du vill ta bort %1 och alla tillh�rande komponenter?
+UninstallOnlyOnWin64=Denna installation kan endast avinstalleras p� en 64-bitarsversion av Windows.
+OnlyAdminCanUninstall=Denna installation kan endast avinstalleras av en anv�ndare med administrativa r�ttigheter.
+UninstallStatusLabel=Var god och v�nta medan %1 tas bort fr�n din dator.
+UninstalledAll=%1 �r nu borttaget fr�n din dator.
+UninstalledMost=Avinstallationen av %1 �r nu klar.%n%nEn del filer/kataloger gick ej att ta bort. Dessa kan tas bort manuellt.
+UninstalledAndNeedsRestart=F�r att slutf�ra avinstallationen av %1 m�ste datorn startas om.%n%nVill du starta om nu?
+UninstallDataCorrupted=Filen "%1" �r felaktig. Kan inte avinstallera
+
+; *** Uninstallation phase messages
+ConfirmDeleteSharedFileTitle=Ta bort delad fil?
+ConfirmDeleteSharedFile2=Systemet indikerar att f�ljande delade fil inte l�ngre anv�nds av n�gra program. Vill du ta bort den delade filen?%n%n%1%n%nOm n�got program fortfarande anv�nder denna fil och den raderas, kommer programmet kanske att sluta fungera. Om du �r os�ker, v�lj Nej. Att l�ta filen ligga kvar i systemet kommer inte att orsaka n�gon skada.
+SharedFileNameLabel=Filnamn:
+SharedFileLocationLabel=Plats:
+WizardUninstalling=Avinstallationsstatus
+StatusUninstalling=Avinstallerar %1...
+
+; *** Shutdown block reasons
+ShutdownBlockReasonInstallingApp=Installerar %1.
+ShutdownBlockReasonUninstallingApp=Avinstallerar %1.
+
+; The custom messages below aren't used by Setup itself, but if you make
+; use of them in your scripts, you'll want to translate them.
+
+[CustomMessages]
+
+NameAndVersion=%1 version %2
+AdditionalIcons=�terst�ende ikoner:
+CreateDesktopIcon=Skapa en ikon p� skrivbordet
+CreateQuickLaunchIcon=Skapa en ikon i Snabbstartf�ltet
+ProgramOnTheWeb=%1 p� Webben
+UninstallProgram=Avinstallera %1
+LaunchProgram=Starta %1
+AssocFileExtension=Associera %1 med %2 filnamnstill�gg
+AssocingFileExtension=Associerar %1 med %2 filnamnstill�gg...
+AutoStartProgramGroupDescription=Autostart:
+AutoStartProgram=Starta automatiskt %1
+AddonHostProgramNotFound=%1 kunde inte hittas i katalogen du valde.%n%nVill du forts�tta �nd�?
diff --git a/translation/installer_bg.isl b/translation/installer_bg.isl
new file mode 100644
index 0000000..119f9e6
--- /dev/null
+++ b/translation/installer_bg.isl
@@ -0,0 +1,14 @@
+[Messages]
+WelcomeLabel2=���� �� ��������� ������� �� ��������� ��.
+[CustomMessages]
+AppTitle=������� - �������� ��� ��������
+UninstallShortcut=�������� �������
+RunShortcut=��������� �������
+PortableDesc=����� �� ��������� ����������?
+PortableText=������� ���� �� ���� ����������� �� ��������� ��, ��� �������� � ��������� �����. ����������� ���������� ���� �� ���� ������� ����� �������� ��� ������ �� ������� ����� �� ��������� ����.
+PortableTitle=����� �� ����������
+PortableOptInstall=���������� (��������������)
+PortableOptExtract=�������� (���������)
+BtnExtract=�������
+ExtractDirText=������� �� ���� �������� � �������� �����
+ExtractDirDesc=���� �� ���� ������� �������?
diff --git a/translation/installer_cs.isl b/translation/installer_cs.isl
new file mode 100644
index 0000000..97ac313
--- /dev/null
+++ b/translation/installer_cs.isl
@@ -0,0 +1,14 @@
+[Messages]
+WelcomeLabel2=Instalace programu Ricochet na v� po��ta�.
+[CustomMessages]
+AppTitle=Ricochet - anonymn� instantn� komunik�tor
+UninstallShortcut=Odinstalovat Ricochet
+RunShortcut=Spustit Ricochet
+PortableDesc=Chcete nainstalovat p�enosnou instalaci?
+PortableText=Ricochet lze nainstalovat na v� syst�m, nebo rozbalit do p�enosn� slo�ky soubor�. P�enosnou instalaci lze p�en�et mezi n�kolika po��ta�i nebo zabezpe�it na za�ifrovan�m pevn�m disku.
+PortableTitle=Zp�sob instalace
+PortableOptInstall=Instalovat (doporu�eno)
+PortableOptExtract=Rozbalit (p�enosn�)
+BtnExtract=Rozbalit
+ExtractDirText=Ricochet bude rozbalen do n�sleduj�c�ho adres��e
+ExtractDirDesc=Kam m� b�t Ricochet rozbalen?
diff --git a/translation/installer_da.isl b/translation/installer_da.isl
new file mode 100644
index 0000000..4a5a40d
--- /dev/null
+++ b/translation/installer_da.isl
@@ -0,0 +1,14 @@
+[Messages]
+WelcomeLabel2=Dette vil installere Ricochet p� din computer.
+[CustomMessages]
+AppTitle=Ricochet - Anonym Instant Messaging
+UninstallShortcut=Afinstaller Ricochet
+RunShortcut=Start Ricochet
+PortableDesc=Vil du have en portabel installation?
+PortableText=Ricochet kan installeres p� dit system, eller udpakkes til en portabel mappe. Den portable installation kan flyttes mellem computere eller holdes sikker p� en krypteret harddisk.
+PortableTitle=Installationstilstand
+PortableOptInstall=Installer (Anbefalt)
+PortableOptExtract=Udpak (Portabel)
+BtnExtract=Udpak
+ExtractDirText=Rirochet vil blive udpakket i f�lgende mappe
+ExtractDirDesc=Hvor skal Ricochet udpakkes?
diff --git a/translation/installer_de.isl b/translation/installer_de.isl
new file mode 100644
index 0000000..76ed998
--- /dev/null
+++ b/translation/installer_de.isl
@@ -0,0 +1,14 @@
+[Messages]
+WelcomeLabel2=Dies installiert Ricochet auf Ihrem Computer.
+[CustomMessages]
+AppTitle=Ricochet - Anonymes Instant Messaging
+UninstallShortcut=Ricochet deinstallieren
+RunShortcut=Ricochet starten
+PortableDesc=Wollen Sie Ricochet als Portable-Variante installieren?
+PortableText=Ricochet kann auf Ihrem System installiert, oder in einen Ordner als Portable-Variante entpackt werden. Die Portable-Variante kann zwischen versch. Computern bewegt werden ebenso wie auf einem USB-Stick.
+PortableTitle=Installationsmodus
+PortableOptInstall=Installieren (Empfohlen)
+PortableOptExtract=Entpacken (Portable)
+BtnExtract=Entpacken
+ExtractDirText=Ricochet wird in den folgenden Ordner entpackt
+ExtractDirDesc=Wo soll Ricochet entpackt werden?
diff --git a/translation/installer_en.isl b/translation/installer_en.isl
new file mode 100644
index 0000000..e65f407
--- /dev/null
+++ b/translation/installer_en.isl
@@ -0,0 +1,14 @@
+[Messages]
+WelcomeLabel2=This will install Ricochet on your computer.
+[CustomMessages]
+AppTitle=Ricochet - Anonymous Instant Messaging
+UninstallShortcut=Uninstall Ricochet
+RunShortcut=Launch Ricochet
+PortableDesc=Do you want a portable installation?
+PortableText=Ricochet can be installed on your system, or extracted to a portable folder. The portable installation can be moved between computers or kept secure on an encrypted harddrive.
+PortableTitle=Installation Mode
+PortableOptInstall=Install (Recommended)
+PortableOptExtract=Extract (Portable)
+BtnExtract=Extract
+ExtractDirText=Ricochet will be extracted into the following folder
+ExtractDirDesc=Where should Ricochet be extracted?
diff --git a/translation/installer_es.isl b/translation/installer_es.isl
new file mode 100644
index 0000000..d579a9e
--- /dev/null
+++ b/translation/installer_es.isl
@@ -0,0 +1,14 @@
+[Messages]
+WelcomeLabel2=Esto instalar� Ricochet en su computadora.
+[CustomMessages]
+AppTitle=Ricochet - Mensajer�a Instant�nea An�nima
+UninstallShortcut=Desinstalar Ricochet
+RunShortcut=Ejecutar Ricochet
+PortableDesc=�Quiere una instalaci�n portable?
+PortableText=Ricochet puede ser instalado en su sistema, o extra�do a una carpeta portable. La instalaci�n portable pude ser trasladada entre computadoras, o permanecer segura en un disco duro cifrado.
+PortableTitle=Modo de Instalaci�n
+PortableOptInstall=Instalar (Recomendado)
+PortableOptExtract=Extraer (Portable)
+BtnExtract=Extraer
+ExtractDirText=Ricochet ser� extra�do en la siguiente carpeta
+ExtractDirDesc=�D�nde debe extraerse Ricochet?
diff --git a/translation/installer_fi.isl b/translation/installer_fi.isl
new file mode 100644
index 0000000..b1a3925
--- /dev/null
+++ b/translation/installer_fi.isl
@@ -0,0 +1,14 @@
+[Messages]
+WelcomeLabel2=Tietokoneeseesi asennetaan Ricochet ohjelmisto
+[CustomMessages]
+AppTitle=Ricochet - Anonyymi Pikaviestin
+UninstallShortcut=Poista Ricochet
+RunShortcut=K�ynnist� Ricochet
+PortableDesc=Asennetaanko ohjelmisto siirrett�v�lle tietov�lineelle?
+PortableText=Ricochet voidaan asentaa kiinte�lle kovalevylle, purkaa siirrett�v�ksi tai vain purkaa haluttuun kansioon. Siirrett�v� vaihtoehto mahdollistaa ohjelmiston k�yt�n useissa eri tietokoneissa ja ohjelmiston tallentamisen salatuille tietov�lineille.
+PortableTitle=Asennustyyppi
+PortableOptInstall=Asenna (Suositeltu vaihtoehto)
+PortableOptExtract=Pura (siirrett�v� asennus)
+BtnExtract=Pura kansioon
+ExtractDirText=Ricochet puretaan seuraavaan sijaintiin
+ExtractDirDesc=Mihin sijaintiin Ricochet puretaan?
diff --git a/translation/installer_fr.isl b/translation/installer_fr.isl
new file mode 100644
index 0000000..d3c782b
--- /dev/null
+++ b/translation/installer_fr.isl
@@ -0,0 +1,14 @@
+[Messages]
+WelcomeLabel2=Ceci va installer Ricochet sur votre ordinateur.
+[CustomMessages]
+AppTitle=Ricochet - Messagerie instantan�e anonyme
+UninstallShortcut=D�sinstaller Ricochet
+RunShortcut=D�marrer Ricochet
+PortableDesc=Voulez-vous une installation portable ?
+PortableText=Ricochet peut �tre install� sur votre syst�me, ou extrait dans un dossier portable. L'installation portable peut �tre d�plac�e et utilis�e sur plusieurs ordinateurs ou conserv�e de fa�on s�curis�e sur un disque dur crypt�.
+PortableTitle=Mode d'installation
+PortableOptInstall=Installation rapide (Recommand�)
+PortableOptExtract=Extraction (Portable)
+BtnExtract=Extraire
+ExtractDirText=Ricochet sera d�compress� dans le dossier suivant :
+ExtractDirDesc=O� Ricochet devrait �tre d�compress� ?
diff --git a/translation/installer_it.isl b/translation/installer_it.isl
new file mode 100644
index 0000000..c3c441a
--- /dev/null
+++ b/translation/installer_it.isl
@@ -0,0 +1,14 @@
+[Messages]
+WelcomeLabel2=Questo installer� Ricochet sul tuo computer
+[CustomMessages]
+AppTitle=Ricochet - Messaggistica Istantanea Anonima
+UninstallShortcut=Disinstalla Ricochet
+RunShortcut=Avvia Ricochet
+PortableDesc=Vuoi un installazione portabile?
+PortableText=Ricochet pu� essere installato sul tuo sistema, o estratto in una cartella portabile. L'installazione portabile pu� essere spostata fra computer o tenuta al sicuro su un disco rigido crittografato.
+PortableTitle=Modalit� di installazione
+PortableOptInstall=Installazione (Consigliato)
+PortableOptExtract=Estrazione (Portabile)
+BtnExtract=Estrai
+ExtractDirText=Ricochet verr� estratto nella seguente cartella
+ExtractDirDesc=Dove dovrebbe essere estratto Ricochet?
diff --git a/translation/installer_nl_NL.isl b/translation/installer_nl_NL.isl
new file mode 100644
index 0000000..8fc5f28
--- /dev/null
+++ b/translation/installer_nl_NL.isl
@@ -0,0 +1,14 @@
+[Messages]
+WelcomeLabel2=Hiermee installeer je Ricochet op je computer.
+[CustomMessages]
+AppTitle=Ricochet - Anonieme Instant Messaging
+UninstallShortcut=De-installeer Ricochet
+RunShortcut=Start Ricochet
+PortableDesc=Wil je een portable installatie?
+PortableText=Ricochet kan worden ge�nstalleerd op je computer, of uitgepakt in een portable map. De portable installatie kan worden verplaatst tussen computers of veilig worden bewaard op een versleutelde harde schijf.
+PortableTitle=Installatiemodus
+PortableOptInstall=Installeren (aanbevolen)
+PortableOptExtract=Uitpakken (Portable)
+BtnExtract=Uitpakken
+ExtractDirText=Ricochet wordt uitgepakt in de volgende map
+ExtractDirDesc=Waar moet Ricochet worden uitgepakt?
diff --git a/translation/installer_pl.isl b/translation/installer_pl.isl
new file mode 100644
index 0000000..7efcb70
--- /dev/null
+++ b/translation/installer_pl.isl
@@ -0,0 +1,14 @@
+[Messages]
+WelcomeLabel2=Program Ricochet zostanie zainstalowany na Twoim komputerze.
+[CustomMessages]
+AppTitle=Ricochet - Anonimowy Komunikator
+UninstallShortcut=Odinstaluj Ricochet
+RunShortcut=Uruchom Ricochet
+PortableDesc=Czy chcesz zainstalowa� program w wersji przeno�nej?
+PortableText=Ricochet mo�e zosta� zainstalowany w Twoim systemie albo rozpakowany do przeno�nego katalogu. Instalacja przeno�na mo�e by� przechowywana na wielu komputerach lub zabezpieczona na zaszyfrowanym dysku.
+PortableTitle=Tryb instalacji
+PortableOptInstall=Instaluj (Zalecane)
+PortableOptExtract=Rozpakuj (Wersja przeno�na)
+BtnExtract=Rozpakuj
+ExtractDirText=Ricochet zostanie rozpakowany do katalogu
+ExtractDirDesc=Gdzie rozpakowa� Ricochet?
diff --git a/translation/installer_pt_BR.isl b/translation/installer_pt_BR.isl
new file mode 100644
index 0000000..373ca9b
--- /dev/null
+++ b/translation/installer_pt_BR.isl
@@ -0,0 +1,14 @@
+[Messages]
+WelcomeLabel2=Isso vai instalar o Ricochet no seu computador.
+[CustomMessages]
+AppTitle=Ricochet - Mensagens Instant�neas An�nimas
+UninstallShortcut=Desinstalar o Ricochet
+RunShortcut=Executar o Ricochet
+PortableDesc=Voc� deseja uma instala��o port�til?
+PortableText=O Ricochet pode ser instalado no seu sistema, ou extra�do para uma pasta port�til. A instala��o port�til pode ser movida entre computadores ou mantida segura em um disco r�gido criptografado.
+PortableTitle=Modo de Instala��o
+PortableOptInstall=Instalar (Recomendado)
+PortableOptExtract=Extrair (Port�til)
+BtnExtract=Extrair
+ExtractDirText=O Ricochet ser� extra�do na seguinte pasta
+ExtractDirDesc=Onde o Ricochet deveria ser extra�do?
diff --git a/translation/installer_ru.isl b/translation/installer_ru.isl
new file mode 100644
index 0000000..4f049a9
--- /dev/null
+++ b/translation/installer_ru.isl
@@ -0,0 +1,14 @@
+[Messages]
+WelcomeLabel2=��� ��������� ������� ��� ���������� Ricochet �� ��� ���������.
+[CustomMessages]
+AppTitle=Ricochet - ��������� ����������
+UninstallShortcut=������� Ricochet
+RunShortcut=��������� Ricochet
+PortableDesc=����� ��� ��������� ��� ��� ����������������?
+PortableText=Ricochet ����� ���� ���������� � ������� ��� ������� ��������� ��� �������� � ��������� �����. ����� ���������� �� ������� ��������� Ricochet �� ����-�����, ������ ��������� ��� ������������� ���������.
+PortableTitle=��� ���������
+PortableOptInstall=���������� (�������������)
+PortableOptExtract=������� (����������� ������)
+BtnExtract=�������
+ExtractDirText=Ricochet ����� �������� � ��������� ���� �����
+ExtractDirDesc=���� �� ������ ������� Ricochet?
diff --git a/translation/installer_sv.isl b/translation/installer_sv.isl
new file mode 100644
index 0000000..1a097f7
--- /dev/null
+++ b/translation/installer_sv.isl
@@ -0,0 +1,14 @@
+[Messages]
+WelcomeLabel2=Det h�r kommer att installera Ricochet p� din dator.
+[CustomMessages]
+AppTitle=Ricochet - Anonym direktkommunikation
+UninstallShortcut=Avinstallera Ricochet
+RunShortcut=Starta Ricochet
+PortableDesc=Vill du ha en portabel installation?
+PortableText=Ricochet kan installeras p� ditt system eller packas upp till en portabel mapp. Den portabla mappen kan flyttas mellan datorer eller lagras s�kert p� en krypterad disk.
+PortableTitle=Installationstyp
+PortableOptInstall=Installera (rekommenderas)
+PortableOptExtract=Packa upp (portabel)
+BtnExtract=Packa upp
+ExtractDirText=Ricochet packas upp till f�ljande mapp
+ExtractDirDesc=Var skall Ricochet packas upp?
diff --git a/translation/installer_tr.isl b/translation/installer_tr.isl
new file mode 100644
index 0000000..28f7387
--- /dev/null
+++ b/translation/installer_tr.isl
@@ -0,0 +1,14 @@
+[Messages]
+WelcomeLabel2=Bu yaz�l�m, bilgisayar�n�za Ricochet y�kleyecek.
+[CustomMessages]
+AppTitle=Ricochet - Anonim Anl�k Mesajla�ma
+UninstallShortcut=Ricochet Kald�r
+RunShortcut=Ricochet Ba�lat
+PortableDesc=Ta��nabilir kurulum istiyor musunuz?
+PortableText=Ricochet, sisteminize y�klenebilir veya ta��nabilir bir klas�re ��kar�labilir. Ta��nabilir kurulum, bilgisayarlar aras� ta��nabilir veya �ifreli sabit diskte g�venli saklanabilir.
+PortableTitle=Kurulum Kipi
+PortableOptInstall=Kur (�nerilen)
+PortableOptExtract=��kar (Ta��nabilir)
+BtnExtract=��kar
+ExtractDirText=Ricochet a�a��daki klas�re ��kar�lacak
+ExtractDirDesc=Ricochet nereye ��kar�ls�n?
diff --git a/translation/installer_uk.isl b/translation/installer_uk.isl
new file mode 100644
index 0000000..4c23059
--- /dev/null
+++ b/translation/installer_uk.isl
@@ -0,0 +1,14 @@
+[Messages]
+WelcomeLabel2=���� ��������� ��������� Ricochet �� ��� ����'����.
+[CustomMessages]
+AppTitle=Ricochet - �� ������ ����� �����������
+UninstallShortcut=�������� Ricochet
+RunShortcut=��������� Ricochet
+PortableDesc=������ �������� ���������� ���������?
+PortableText=Ricochet ���� ���� ������������ � ���� ������� ��� ������������ �� ���������� �����. ���������� ������������ �� ����� ���������� �� �� ����'������� �� ������� � ������� �� ���������� �����.
+PortableTitle=����� ������������
+PortableOptInstall=���������� (�������������)
+PortableOptExtract=����������� (����������)
+BtnExtract=������������
+ExtractDirText=Ricochet ���� ������������ � �������� �����
+ExtractDirDesc=���� ��� ����������� Ricochet?
diff --git a/translation/ricochet_bg.ts b/translation/ricochet_bg.ts
new file mode 100644
index 0000000..7d91572
--- /dev/null
+++ b/translation/ricochet_bg.ts
@@ -0,0 +1,694 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="bg">
+<context>
+ <name>AboutPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/AboutPreferences.qml" line="14"/>
+ <source>Ricochet %1</source>
+ <extracomment>%1 version, e.g. 1.0.0</extracomment>
+ <translation>Рикошет %1</translation>
+ </message>
+</context>
+<context>
+ <name>AddContactDialog</name>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="51"/>
+ <source>Share your Ricochet ID to allow connection requests</source>
+ <translation>Сподели своят Рикошет Идентификатор, за да позволиш нови покани</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="107"/>
+ <source>Cancel</source>
+ <translation>Откажи</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="112"/>
+ <source>Add</source>
+ <translation>Добави</translation>
+ </message>
+</context>
+<context>
+ <name>ContactActions</name>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="40"/>
+ <source>Open Window</source>
+ <translation>Отвори Прозорец</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="44"/>
+ <source>Details...</source>
+ <translation>Допълнитлно...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="48"/>
+ <source>Rename</source>
+ <translation>Преименувай</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="53"/>
+ <source>Remove</source>
+ <translation>Премахни</translation>
+ </message>
+</context>
+<context>
+ <name>ContactIDField</name>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="40"/>
+ <source><b>%1</b> is already your contact</source>
+ <translation><b>%1</b> вече е твой контакт</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="42"/>
+ <source>You can't add yourself as a contact</source>
+ <translation>Не може да добавиш себе си като контакт</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="44"/>
+ <source>Enter an ID starting with <b>ricochet:</b></source>
+ <translation>Въведи Идентификатор с <b>рикошет:</b></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="79"/>
+ <source>Copied to clipboard</source>
+ <translation>Копирано</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="91"/>
+ <source>Copy</source>
+ <translation>Копирай</translation>
+ </message>
+</context>
+<context>
+ <name>ContactList</name>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="75"/>
+ <source>Online</source>
+ <translation>На линия</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="76"/>
+ <source>Offline</source>
+ <translation>Отписан</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="77"/>
+ <source>Requests</source>
+ <translation>Молби</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="78"/>
+ <source>Rejected</source>
+ <translation>Отхвърлени</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="79"/>
+ <source>Outdated</source>
+ <translation>Стара версия</translation>
+ </message>
+</context>
+<context>
+ <name>ContactPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="97"/>
+ <source>Date added:</source>
+ <translation>Дата на добавяне:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="104"/>
+ <source>Last seen:</source>
+ <translation>Последно видян:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="113"/>
+ <source>Request:</source>
+ <translation>Покана:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="122"/>
+ <source>Pending connection</source>
+ <translation>В очакване на връзка</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="123"/>
+ <source>Delivered</source>
+ <translation>Доставено</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="124"/>
+ <source>Accepted</source>
+ <translation>Прието</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="125"/>
+ <source>Error</source>
+ <translation>Грешка</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="126"/>
+ <source>Rejected</source>
+ <translation>Отхвърлено</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="130"/>
+ <source>%1 (Connected)</source>
+ <extracomment>%1 status, e.g. "Accepted"</extracomment>
+ <translation>%1 (Свързани)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="136"/>
+ <source>Response:</source>
+ <translation>Отговор:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="158"/>
+ <source>Rename</source>
+ <translation>Преименувай</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="165"/>
+ <source>Remove</source>
+ <translation>Премахни</translation>
+ </message>
+</context>
+<context>
+ <name>ContactRequestDialog</name>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="55"/>
+ <source>Someone new is asking to connect to you</source>
+ <translation>Нов човек те моли да се свържете</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="102"/>
+ <source>Reject</source>
+ <translation>Отхвърли</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="107"/>
+ <source>Accept</source>
+ <translation>Приеми</translation>
+ </message>
+</context>
+<context>
+ <name>ContactRequestFields</name>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="17"/>
+ <source>ID:</source>
+ <translation>Индентификатор:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="29"/>
+ <source>Name:</source>
+ <translation>Име:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="40"/>
+ <source>Message:</source>
+ <translation>Съобщение:</translation>
+ </message>
+</context>
+<context>
+ <name>GeneralPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="12"/>
+ <source>Use a single window for conversations</source>
+ <translation>Използвай само един прозорец за всички разговори</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="20"/>
+ <source>Open links in default browser without prompting</source>
+ <translation>Отваряй връзките в браузъра по подразбиране без да искаш разрешение</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="28"/>
+ <source>Play audio notifications</source>
+ <translation>Използвай звукови известия</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="37"/>
+ <source>Volume</source>
+ <translation>Звук</translation>
+ </message>
+</context>
+<context>
+ <name>LanguagePreferences</name>
+ <message>
+ <location filename="../src/ui/qml/LanguagePreferences.qml" line="22"/>
+ <source>Select Language</source>
+ <translation>Избери език</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/LanguagePreferences.qml" line="52"/>
+ <source>Restart Ricochet to apply changes</source>
+ <translation>Рестартирай Richchet, за да приложиш промените</translation>
+ </message>
+</context>
+<context>
+ <name>LanguagesModel</name>
+ <message>
+ <location filename="../src/ui/LanguagesModel.cpp" line="43"/>
+ <source>System default</source>
+ <translation>Настройки по подразбиране</translation>
+ </message>
+</context>
+<context>
+ <name>Main</name>
+ <message>
+ <location filename="../src/main.cpp" line="73"/>
+ <source>Ricochet Error</source>
+ <translation>Рикошет Грешка</translation>
+ </message>
+</context>
+<context>
+ <name>MainToolBar</name>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="19"/>
+ <source>Add Contact</source>
+ <translation>Добави Контакт</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="28"/>
+ <source>Preferences</source>
+ <translation>Настройки</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="75"/>
+ <source>Click to add contacts</source>
+ <translation>Кликни, за да добавиш контакт</translation>
+ </message>
+</context>
+<context>
+ <name>MainWindow</name>
+ <message>
+ <location filename="../src/ui/MainWindow.cpp" line="149"/>
+ <source>Remove %1</source>
+ <translation>Премахни %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/MainWindow.cpp" line="150"/>
+ <source>Do you want to permanently remove %1?</source>
+ <translation>Наистина ли искаш да премахнеш %1 завинаги?</translation>
+ </message>
+</context>
+<context>
+ <name>MessageDelegate</name>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="26"/>
+ <source>%1 is offline</source>
+ <extracomment>%1 nickname</extracomment>
+ <translation>%1 е отписан</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="134"/>
+ <source>Copy ID</source>
+ <translation>Копирай Идентификатора</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="134"/>
+ <source>Copy Link</source>
+ <translation>Копирай Връзката</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="139"/>
+ <source>Open with Browser</source>
+ <translation>Отвори в Браузъра</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="153"/>
+ <source>Add as Contact</source>
+ <translation>Доабви като Контакт</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="165"/>
+ <source>Copy Message</source>
+ <translation>Копирай съобщение</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="172"/>
+ <source>Copy Selection</source>
+ <translation>Копирай селекцията</translation>
+ </message>
+</context>
+<context>
+ <name>MessageDialogWrapper</name>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="7"/>
+ <source>Remove %1</source>
+ <translation>Премахни %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="9"/>
+ <source>Do you want to permanently remove %1?</source>
+ <extracomment>%1 nickname</extracomment>
+ <translation>Наистина ли искаш да премахнеш %1 завинаги?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="10"/>
+ <source>This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request.</source>
+ <translation>Този контакт повече няма да може да ти пише, и ще бъде уведомен за премахването. Той може да реши да ти прати нова покана за връзка.</translation>
+ </message>
+</context>
+<context>
+ <name>NetworkSetupWizard</name>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="100"/>
+ <source>This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network.</source>
+ <translation>Интернет връзката на този компютър не е възпрепятствана. Искам да се свържа директно с Тор мрежата.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="107"/>
+ <source>Connect</source>
+ <translation>Свържи</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="124"/>
+ <source>This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings.</source>
+ <translation>Интернет връзката на този компютър е цензурирана, филтрирана или минава през прокси. Трябва да променя настройките на мрежата .</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="131"/>
+ <source>Configure</source>
+ <translation>Настрой</translation>
+ </message>
+</context>
+<context>
+ <name>OfflineStateItem</name>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="107"/>
+ <source>Configure</source>
+ <translation>Настрой</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="115"/>
+ <source>Details</source>
+ <translation>Допълнително</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="143"/>
+ <source>Connection failed</source>
+ <translation>Неуспешна връзка</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="169"/>
+ <source>Connecting…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Свързване...</translation>
+ </message>
+</context>
+<context>
+ <name>OpenBrowserDialog</name>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="39"/>
+ <source><b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead.</source>
+ <translation><b>Внимание!</b> Отварянето на връзки с браузъра по подразбиране може да прекрати сигурната и анонимността ти.<br><br>Вместо това може да го <a href='.'>копираш</a>.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="58"/>
+ <source>Don't ask again for links from %1</source>
+ <translation>Не искай повече връзки от %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="64"/>
+ <source>Don't ask again for any links (not recommended!)</source>
+ <translation>Не искай повече никакви връзки (не се препоръчва!)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="71"/>
+ <source>Open Browser</source>
+ <translation>Отвори Браузъра</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="83"/>
+ <source>Cancel</source>
+ <translation>Откажи</translation>
+ </message>
+</context>
+<context>
+ <name>PreferencesDialog</name>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="12"/>
+ <source>Ricochet Preferences</source>
+ <translation>Настройки на Рикошет</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="41"/>
+ <source>General</source>
+ <translation>Главни</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="46"/>
+ <source>Language</source>
+ <translation>Език</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="51"/>
+ <source>Contacts</source>
+ <translation>Контакти</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="56"/>
+ <source>Tor</source>
+ <translation>Тор</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="61"/>
+ <source>About</source>
+ <translation>За Програмта</translation>
+ </message>
+</context>
+<context>
+ <name>QCocoaMenuItem</name>
+ <message>
+ <location filename="../src/ui/qml/MainWindow.qml" line="29"/>
+ <source>Preference</source>
+ <translation>Настройки</translation>
+ </message>
+</context>
+<context>
+ <name>StartupStatusPage</name>
+ <message>
+ <location filename="../src/ui/qml/StartupStatusPage.qml" line="18"/>
+ <source>The Tor process was not started successfully. This is most likely an installation or system error.</source>
+ <translation>Неуспешно стартиране на Тор процесът. Това най-вероятно се дължи на инсталационна или системна грешка.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/StartupStatusPage.qml" line="49"/>
+ <source>Quit</source>
+ <translation>Изход</translation>
+ </message>
+</context>
+<context>
+ <name>TorBootstrapStatus</name>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="17"/>
+ <source>Connecting to the Tor network…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Свързване с Тор мрежата...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="50"/>
+ <source>Back</source>
+ <translation>Назад</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="57"/>
+ <source>Hide details</source>
+ <translation>Скрий допълнителната информация</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="57"/>
+ <source>Show details</source>
+ <translation>Покажи допълнителна информация</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="69"/>
+ <source>Done</source>
+ <translation>Готово</translation>
+ </message>
+</context>
+<context>
+ <name>TorConfigurationPage</name>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="72"/>
+ <source>Does this computer need a proxy to access the internet?</source>
+ <translation>Този компютър нуждае ли се от прокси, за да се свърже с интернет?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="84"/>
+ <source>Proxy type:</source>
+ <translation>Вид Прокси:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="89"/>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="91"/>
+ <source>None</source>
+ <translation>Никакво</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="106"/>
+ <source>Address:</source>
+ <translation>Адрес:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="115"/>
+ <source>IP address or hostname</source>
+ <translation>IP адрес или hostname</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="118"/>
+ <source>Port:</source>
+ <translation>Порт:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="129"/>
+ <source>Username:</source>
+ <translation>Потребителско Име:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="139"/>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="149"/>
+ <source>Optional</source>
+ <translation>Незадължително</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="142"/>
+ <source>Password:</source>
+ <translation>Парола:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="159"/>
+ <source>Does this computer's Internet connection go through a firewall that only allows connections to certain ports?</source>
+ <translation>Минава ли интернет връзката на този компютър през firewall програма, която позволява връзка само през определени портове?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="170"/>
+ <source>Allowed ports:</source>
+ <translation>Разрешени портове:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="177"/>
+ <source>Example: 80,443</source>
+ <translation>Пример: 80,443</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="188"/>
+ <source>If this computer's Internet connection is censored, you will need to obtain and use bridge relays.</source>
+ <translation>Ако интернет връзката на този компютър е цензурирана, ще се наложи да намериш и използваш bridge relays.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="197"/>
+ <source>Enter one or more bridge relays (one per line):</source>
+ <translation>Въведи едно или повече bridge relays (по едно на ред):</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="212"/>
+ <source>Back</source>
+ <translation>Назад</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="219"/>
+ <source>Connect</source>
+ <translation>Свържи</translation>
+ </message>
+</context>
+<context>
+ <name>TorPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="24"/>
+ <source>Running:</source>
+ <translation>Изпълнява:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="27"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="29"/>
+ <source>Yes</source>
+ <translation>Да</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="27"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="29"/>
+ <source>No</source>
+ <translation>Не</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <source>External</source>
+ <translation>Външен</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="26"/>
+ <source>Control connected:</source>
+ <translation>Control connected:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="28"/>
+ <source>Circuits established:</source>
+ <translation>Circuits established:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="30"/>
+ <source>Hidden service:</source>
+ <translation>Hidden service:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="31"/>
+ <source>Online</source>
+ <translation>На Линия</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="31"/>
+ <source>Offline</source>
+ <translation>Отписан</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="32"/>
+ <source>Version:</source>
+ <translation>Версия:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="59"/>
+ <source>Error: <b>%1</b></source>
+ <extracomment>%1 is error message</extracomment>
+ <translation>Грешка: <b>%1</b></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="75"/>
+ <source>Configure</source>
+ <translation>Настрой</translation>
+ </message>
+</context>
+<context>
+ <name>TorStateWidget</name>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="9"/>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="20"/>
+ <source>Connection failed</source>
+ <translation>Неуспешна връзка</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="12"/>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="22"/>
+ <source>Connecting…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Свързване...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="25"/>
+ <source>Connecting… (%1%)</source>
+ <extracomment>%1 is progress percentage, e.g. 100</extracomment>
+ <translation>Свързване... (%1%)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="32"/>
+ <source>Online</source>
+ <translation>На линия</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="34"/>
+ <source>Connected</source>
+ <translation>Свързан</translation>
+ </message>
+</context>
+</TS>
diff --git a/translation/ricochet_cs.ts b/translation/ricochet_cs.ts
new file mode 100644
index 0000000..90d76c8
--- /dev/null
+++ b/translation/ricochet_cs.ts
@@ -0,0 +1,694 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="cs">
+<context>
+ <name>AboutPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/AboutPreferences.qml" line="14"/>
+ <source>Ricochet %1</source>
+ <extracomment>%1 version, e.g. 1.0.0</extracomment>
+ <translation>Ricochet %1</translation>
+ </message>
+</context>
+<context>
+ <name>AddContactDialog</name>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="51"/>
+ <source>Share your Ricochet ID to allow connection requests</source>
+ <translation>Sdílejte vaše Richochet ID pro umožnění zádostí připojení</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="107"/>
+ <source>Cancel</source>
+ <translation>Zrušit</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="112"/>
+ <source>Add</source>
+ <translation>Přidat</translation>
+ </message>
+</context>
+<context>
+ <name>ContactActions</name>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="40"/>
+ <source>Open Window</source>
+ <translation>Otevřít okno</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="44"/>
+ <source>Details...</source>
+ <translation>Podrobnosti...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="48"/>
+ <source>Rename</source>
+ <translation>Přejmenovat</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="53"/>
+ <source>Remove</source>
+ <translation>Odstranit</translation>
+ </message>
+</context>
+<context>
+ <name>ContactIDField</name>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="40"/>
+ <source><b>%1</b> is already your contact</source>
+ <translation><b>%1</b> již je v seznamu kontaktů</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="42"/>
+ <source>You can't add yourself as a contact</source>
+ <translation>Nelze přidat vlastní adresu na seznam kontaktů</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="44"/>
+ <source>Enter an ID starting with <b>ricochet:</b></source>
+ <translation>Zadejte ID začínající slovem <b>ricochet:</b></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="79"/>
+ <source>Copied to clipboard</source>
+ <translation>Zkopírováno do schránky</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="91"/>
+ <source>Copy</source>
+ <translation>Zkopírovat</translation>
+ </message>
+</context>
+<context>
+ <name>ContactList</name>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="75"/>
+ <source>Online</source>
+ <translation>Online</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="76"/>
+ <source>Offline</source>
+ <translation>Offline</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="77"/>
+ <source>Requests</source>
+ <translation>Žádosti</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="78"/>
+ <source>Rejected</source>
+ <translation>Zamítnuto</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="79"/>
+ <source>Outdated</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>ContactPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="97"/>
+ <source>Date added:</source>
+ <translation>Přidáno dne:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="104"/>
+ <source>Last seen:</source>
+ <translation>Naposledy online:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="113"/>
+ <source>Request:</source>
+ <translation>Žádost:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="122"/>
+ <source>Pending connection</source>
+ <translation>Čeká se na připojení</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="123"/>
+ <source>Delivered</source>
+ <translation>Doručeno</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="124"/>
+ <source>Accepted</source>
+ <translation>Přijato</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="125"/>
+ <source>Error</source>
+ <translation>Chyba</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="126"/>
+ <source>Rejected</source>
+ <translation>Zamítnuto</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="130"/>
+ <source>%1 (Connected)</source>
+ <extracomment>%1 status, e.g. "Accepted"</extracomment>
+ <translation>%1 (Připojeno)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="136"/>
+ <source>Response:</source>
+ <translation>Odpověď:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="158"/>
+ <source>Rename</source>
+ <translation type="unfinished">Přejmenovat</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="165"/>
+ <source>Remove</source>
+ <translation>Odstranit</translation>
+ </message>
+</context>
+<context>
+ <name>ContactRequestDialog</name>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="55"/>
+ <source>Someone new is asking to connect to you</source>
+ <translation>Někdo nový se s vámi chce spojit</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="102"/>
+ <source>Reject</source>
+ <translation>Zamítnout</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="107"/>
+ <source>Accept</source>
+ <translation>Přijmout</translation>
+ </message>
+</context>
+<context>
+ <name>ContactRequestFields</name>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="17"/>
+ <source>ID:</source>
+ <translation>ID:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="29"/>
+ <source>Name:</source>
+ <translation>Jméno:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="40"/>
+ <source>Message:</source>
+ <translation>Zpráva:</translation>
+ </message>
+</context>
+<context>
+ <name>GeneralPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="12"/>
+ <source>Use a single window for conversations</source>
+ <translation>Používat totéž okno pro všechny konverzace</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="20"/>
+ <source>Open links in default browser without prompting</source>
+ <translation>Otevírat odkazy v defaultním prohlížeči bez dotazování</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="28"/>
+ <source>Play audio notifications</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="37"/>
+ <source>Volume</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>LanguagePreferences</name>
+ <message>
+ <location filename="../src/ui/qml/LanguagePreferences.qml" line="22"/>
+ <source>Select Language</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/LanguagePreferences.qml" line="52"/>
+ <source>Restart Ricochet to apply changes</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>LanguagesModel</name>
+ <message>
+ <location filename="../src/ui/LanguagesModel.cpp" line="43"/>
+ <source>System default</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>Main</name>
+ <message>
+ <location filename="../src/main.cpp" line="73"/>
+ <source>Ricochet Error</source>
+ <translation>Chyba programu Ricochet</translation>
+ </message>
+</context>
+<context>
+ <name>MainToolBar</name>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="19"/>
+ <source>Add Contact</source>
+ <translation>Přidat kontakt</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="28"/>
+ <source>Preferences</source>
+ <translation>Nastavení</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="75"/>
+ <source>Click to add contacts</source>
+ <translation>Klikněte pro přidání kontaktů</translation>
+ </message>
+</context>
+<context>
+ <name>MainWindow</name>
+ <message>
+ <location filename="../src/ui/MainWindow.cpp" line="149"/>
+ <source>Remove %1</source>
+ <translation>Odstranit %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/MainWindow.cpp" line="150"/>
+ <source>Do you want to permanently remove %1?</source>
+ <translation>Chcete trvale odstranit kontakt %1?</translation>
+ </message>
+</context>
+<context>
+ <name>MessageDelegate</name>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="26"/>
+ <source>%1 is offline</source>
+ <extracomment>%1 nickname</extracomment>
+ <translation>%1 je offline</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="134"/>
+ <source>Copy ID</source>
+ <translation>Kopírovat ID</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="134"/>
+ <source>Copy Link</source>
+ <translation>Kopírovat odkaz</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="139"/>
+ <source>Open with Browser</source>
+ <translation>Otevřít v prohlížeči</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="153"/>
+ <source>Add as Contact</source>
+ <translation>Přidat jako kontakt</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="165"/>
+ <source>Copy Message</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="172"/>
+ <source>Copy Selection</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>MessageDialogWrapper</name>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="7"/>
+ <source>Remove %1</source>
+ <translation>Odstranit %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="9"/>
+ <source>Do you want to permanently remove %1?</source>
+ <extracomment>%1 nickname</extracomment>
+ <translation>Chcete trvale odstranit kontakt %1?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="10"/>
+ <source>This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request.</source>
+ <translation>Tento kontakt vám nebude moci posílat zprávy a bude informován o odstranění ze seznamu kontaktů. Může však zaslat novou žádost o spojení.</translation>
+ </message>
+</context>
+<context>
+ <name>NetworkSetupWizard</name>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="100"/>
+ <source>This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network.</source>
+ <translation>Tento počítač se připojuje na internet bez jakýchkoli překážek. Chci se připojit přímo na síť Tor. </translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="107"/>
+ <source>Connect</source>
+ <translation>Připojit</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="124"/>
+ <source>This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings.</source>
+ <translation>Připojení tohoto počítače na internet je cenzurováno, filtrováno nebo vedeno přes proxy. Potřebuji upravit nastavení sítě.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="131"/>
+ <source>Configure</source>
+ <translation>Nastavit</translation>
+ </message>
+</context>
+<context>
+ <name>OfflineStateItem</name>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="107"/>
+ <source>Configure</source>
+ <translation>Nastavit</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="115"/>
+ <source>Details</source>
+ <translation>Podrobnosti</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="143"/>
+ <source>Connection failed</source>
+ <translation>Připojení selhalo</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="169"/>
+ <source>Connecting…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Připojuje se...</translation>
+ </message>
+</context>
+<context>
+ <name>OpenBrowserDialog</name>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="39"/>
+ <source><b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead.</source>
+ <translation><b>-Upozornění!</b> Otvírání odkazů v defaultním prohlížeči naruší vaši bezpečnost a anonymitu.<br><br>Můžete místo toho odkaz <a href='.'>zkopírovat do schránky</a>.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="58"/>
+ <source>Don't ask again for links from %1</source>
+ <translation>Znovu nepožadovat odkazy od kontaktu %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="64"/>
+ <source>Don't ask again for any links (not recommended!)</source>
+ <translation>Znovu nepožadovat žádné odkazy (nedoporučuje se!)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="71"/>
+ <source>Open Browser</source>
+ <translation>Otevřít prohlížeč</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="83"/>
+ <source>Cancel</source>
+ <translation>Zrušit</translation>
+ </message>
+</context>
+<context>
+ <name>PreferencesDialog</name>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="12"/>
+ <source>Ricochet Preferences</source>
+ <translation>Nastavení programu Ricochet</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="41"/>
+ <source>General</source>
+ <translation>Obecné</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="46"/>
+ <source>Language</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="51"/>
+ <source>Contacts</source>
+ <translation>Kontakty</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="56"/>
+ <source>Tor</source>
+ <translation>Tor</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="61"/>
+ <source>About</source>
+ <translation>O programu</translation>
+ </message>
+</context>
+<context>
+ <name>QCocoaMenuItem</name>
+ <message>
+ <location filename="../src/ui/qml/MainWindow.qml" line="29"/>
+ <source>Preference</source>
+ <translation>Nastavení</translation>
+ </message>
+</context>
+<context>
+ <name>StartupStatusPage</name>
+ <message>
+ <location filename="../src/ui/qml/StartupStatusPage.qml" line="18"/>
+ <source>The Tor process was not started successfully. This is most likely an installation or system error.</source>
+ <translation>Proces programu Tor nebyl úspěšně spuštěn. Pravděpodobně se jedná o chybu instalace či systému.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/StartupStatusPage.qml" line="49"/>
+ <source>Quit</source>
+ <translation>Ukončit</translation>
+ </message>
+</context>
+<context>
+ <name>TorBootstrapStatus</name>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="17"/>
+ <source>Connecting to the Tor network…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Připojuje se k síti Tor…</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="50"/>
+ <source>Back</source>
+ <translation>Zpět</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="57"/>
+ <source>Hide details</source>
+ <translation>Skrýt podrobnosti</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="57"/>
+ <source>Show details</source>
+ <translation>Zobrazit podrobnosti</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="69"/>
+ <source>Done</source>
+ <translation>Hotovo</translation>
+ </message>
+</context>
+<context>
+ <name>TorConfigurationPage</name>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="72"/>
+ <source>Does this computer need a proxy to access the internet?</source>
+ <translation>Potřebuje tento počítač pro přístup na internet proxy?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="84"/>
+ <source>Proxy type:</source>
+ <translation>Typ proxy:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="89"/>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="91"/>
+ <source>None</source>
+ <translation>Žádný</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="106"/>
+ <source>Address:</source>
+ <translation>Adresa:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="115"/>
+ <source>IP address or hostname</source>
+ <translation>IP adresa nebo hostname</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="118"/>
+ <source>Port:</source>
+ <translation>Port:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="129"/>
+ <source>Username:</source>
+ <translation>Username:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="139"/>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="149"/>
+ <source>Optional</source>
+ <translation>Volitelné</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="142"/>
+ <source>Password:</source>
+ <translation>Heslo:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="159"/>
+ <source>Does this computer's Internet connection go through a firewall that only allows connections to certain ports?</source>
+ <translation>Prochází připojení tohoto počítače na internet přes firewall, jenž umožňuje připojení pouze na určité porty?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="170"/>
+ <source>Allowed ports:</source>
+ <translation>Povolené porty:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="177"/>
+ <source>Example: 80,443</source>
+ <translation>Příklad: 80,443</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="188"/>
+ <source>If this computer's Internet connection is censored, you will need to obtain and use bridge relays.</source>
+ <translation>Je-li připojení tohoto počítače na internet cenzurováno, potřebujete získat a používat bridge relays.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="197"/>
+ <source>Enter one or more bridge relays (one per line):</source>
+ <translation>Zadejte jeden či více bridge relays (na jednotlivé řádky):</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="212"/>
+ <source>Back</source>
+ <translation>Zpět</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="219"/>
+ <source>Connect</source>
+ <translation>Připojit</translation>
+ </message>
+</context>
+<context>
+ <name>TorPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="24"/>
+ <source>Running:</source>
+ <translation>Spuštěno:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="27"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="29"/>
+ <source>Yes</source>
+ <translation>Ano</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="27"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="29"/>
+ <source>No</source>
+ <translation>Ne</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <source>External</source>
+ <translation>Externí</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="26"/>
+ <source>Control connected:</source>
+ <translation>Kontrola připojena:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="28"/>
+ <source>Circuits established:</source>
+ <translation>Okruhy sestaveny:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="30"/>
+ <source>Hidden service:</source>
+ <translation>Skrytá služba:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="31"/>
+ <source>Online</source>
+ <translation>Online</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="31"/>
+ <source>Offline</source>
+ <translation>Offline</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="32"/>
+ <source>Version:</source>
+ <translation>Verze:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="59"/>
+ <source>Error: <b>%1</b></source>
+ <extracomment>%1 is error message</extracomment>
+ <translation>Chyba: <b>%1</b></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="75"/>
+ <source>Configure</source>
+ <translation>Nastavit</translation>
+ </message>
+</context>
+<context>
+ <name>TorStateWidget</name>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="9"/>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="20"/>
+ <source>Connection failed</source>
+ <translation>Připojení selhalo</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="12"/>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="22"/>
+ <source>Connecting…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Připojuje se...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="25"/>
+ <source>Connecting… (%1%)</source>
+ <extracomment>%1 is progress percentage, e.g. 100</extracomment>
+ <translation>Připojuje se… (%1%)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="32"/>
+ <source>Online</source>
+ <translation>Online</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="34"/>
+ <source>Connected</source>
+ <translation>Připojeno</translation>
+ </message>
+</context>
+</TS>
diff --git a/translation/ricochet_da.ts b/translation/ricochet_da.ts
new file mode 100644
index 0000000..097fd5e
--- /dev/null
+++ b/translation/ricochet_da.ts
@@ -0,0 +1,694 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="da">
+<context>
+ <name>AboutPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/AboutPreferences.qml" line="14"/>
+ <source>Ricochet %1</source>
+ <extracomment>%1 version, e.g. 1.0.0</extracomment>
+ <translation>Ricochet %1</translation>
+ </message>
+</context>
+<context>
+ <name>AddContactDialog</name>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="51"/>
+ <source>Share your Ricochet ID to allow connection requests</source>
+ <translation>Del dit Ricochet ID for at tillade kontaktanmodninger</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="107"/>
+ <source>Cancel</source>
+ <translation>Annuller</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="112"/>
+ <source>Add</source>
+ <translation>Tilføj</translation>
+ </message>
+</context>
+<context>
+ <name>ContactActions</name>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="40"/>
+ <source>Open Window</source>
+ <translation>Åben Vindue</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="44"/>
+ <source>Details...</source>
+ <translation>Detaljer...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="48"/>
+ <source>Rename</source>
+ <translation>Omdøb</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="53"/>
+ <source>Remove</source>
+ <translation>Fjern</translation>
+ </message>
+</context>
+<context>
+ <name>ContactIDField</name>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="40"/>
+ <source><b>%1</b> is already your contact</source>
+ <translation><b>%1</b> er allerede på din kontaktliste</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="42"/>
+ <source>You can't add yourself as a contact</source>
+ <translation>Du kan ikke tilføje dig selv som kontaktperson</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="44"/>
+ <source>Enter an ID starting with <b>ricochet:</b></source>
+ <translation>Skriv et ID der begynder med <b>ricochet:</b></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="79"/>
+ <source>Copied to clipboard</source>
+ <translation>Kopieret til clipboardet</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="91"/>
+ <source>Copy</source>
+ <translation>Kopier</translation>
+ </message>
+</context>
+<context>
+ <name>ContactList</name>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="75"/>
+ <source>Online</source>
+ <translation>Online</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="76"/>
+ <source>Offline</source>
+ <translation>Offline</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="77"/>
+ <source>Requests</source>
+ <translation>Anmodninger</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="78"/>
+ <source>Rejected</source>
+ <translation>Afvist</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="79"/>
+ <source>Outdated</source>
+ <translation>Uddateret</translation>
+ </message>
+</context>
+<context>
+ <name>ContactPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="97"/>
+ <source>Date added:</source>
+ <translation>Dato tilføjet:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="104"/>
+ <source>Last seen:</source>
+ <translation>Sidst set:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="113"/>
+ <source>Request:</source>
+ <translation>Anmodning:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="122"/>
+ <source>Pending connection</source>
+ <translation>Afventer forbindelse</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="123"/>
+ <source>Delivered</source>
+ <translation>Modtaget</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="124"/>
+ <source>Accepted</source>
+ <translation>Accepteret</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="125"/>
+ <source>Error</source>
+ <translation>Fejl</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="126"/>
+ <source>Rejected</source>
+ <translation>Afvist</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="130"/>
+ <source>%1 (Connected)</source>
+ <extracomment>%1 status, e.g. "Accepted"</extracomment>
+ <translation>%1 (Forbundet)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="136"/>
+ <source>Response:</source>
+ <translation>Tilbagesvar:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="158"/>
+ <source>Rename</source>
+ <translation>Omdøb</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="165"/>
+ <source>Remove</source>
+ <translation>Fjern</translation>
+ </message>
+</context>
+<context>
+ <name>ContactRequestDialog</name>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="55"/>
+ <source>Someone new is asking to connect to you</source>
+ <translation>Du har en ny kontaktanmodning</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="102"/>
+ <source>Reject</source>
+ <translation>Afvis</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="107"/>
+ <source>Accept</source>
+ <translation>Acceptér</translation>
+ </message>
+</context>
+<context>
+ <name>ContactRequestFields</name>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="17"/>
+ <source>ID:</source>
+ <translation>ID:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="29"/>
+ <source>Name:</source>
+ <translation>Navn:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="40"/>
+ <source>Message:</source>
+ <translation>Besked:</translation>
+ </message>
+</context>
+<context>
+ <name>GeneralPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="12"/>
+ <source>Use a single window for conversations</source>
+ <translation>Brug et samlet vindue til samtaler</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="20"/>
+ <source>Open links in default browser without prompting</source>
+ <translation>Åben links i browser uden at spørge</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="28"/>
+ <source>Play audio notifications</source>
+ <translation>Afspil lydnotifikationer</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="37"/>
+ <source>Volume</source>
+ <translation>Lydstyrke</translation>
+ </message>
+</context>
+<context>
+ <name>LanguagePreferences</name>
+ <message>
+ <location filename="../src/ui/qml/LanguagePreferences.qml" line="22"/>
+ <source>Select Language</source>
+ <translation>Vælg sprog</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/LanguagePreferences.qml" line="52"/>
+ <source>Restart Ricochet to apply changes</source>
+ <translation>Genstart Ricochet for at anvende nye indstillinger</translation>
+ </message>
+</context>
+<context>
+ <name>LanguagesModel</name>
+ <message>
+ <location filename="../src/ui/LanguagesModel.cpp" line="43"/>
+ <source>System default</source>
+ <translation>Standardindstillinger</translation>
+ </message>
+</context>
+<context>
+ <name>Main</name>
+ <message>
+ <location filename="../src/main.cpp" line="73"/>
+ <source>Ricochet Error</source>
+ <translation>Ricochet Fejl</translation>
+ </message>
+</context>
+<context>
+ <name>MainToolBar</name>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="19"/>
+ <source>Add Contact</source>
+ <translation>Tilføj Kontaktperson</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="28"/>
+ <source>Preferences</source>
+ <translation>Indstillinger</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="75"/>
+ <source>Click to add contacts</source>
+ <translation>Klik for at tilføje kontaktpersoner</translation>
+ </message>
+</context>
+<context>
+ <name>MainWindow</name>
+ <message>
+ <location filename="../src/ui/MainWindow.cpp" line="149"/>
+ <source>Remove %1</source>
+ <translation>Fjern %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/MainWindow.cpp" line="150"/>
+ <source>Do you want to permanently remove %1?</source>
+ <translation>Vil du fjerne %1 permanent?</translation>
+ </message>
+</context>
+<context>
+ <name>MessageDelegate</name>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="26"/>
+ <source>%1 is offline</source>
+ <extracomment>%1 nickname</extracomment>
+ <translation>%1 er offline</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="134"/>
+ <source>Copy ID</source>
+ <translation>Kopiér ID</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="134"/>
+ <source>Copy Link</source>
+ <translation>Kopiér Link</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="139"/>
+ <source>Open with Browser</source>
+ <translation>Åben med Browser</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="153"/>
+ <source>Add as Contact</source>
+ <translation>Tilføj som Kontaktperson</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="165"/>
+ <source>Copy Message</source>
+ <translation>Kopiér besked</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="172"/>
+ <source>Copy Selection</source>
+ <translation>Kopiér markeret tekst</translation>
+ </message>
+</context>
+<context>
+ <name>MessageDialogWrapper</name>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="7"/>
+ <source>Remove %1</source>
+ <translation>Fjern %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="9"/>
+ <source>Do you want to permanently remove %1?</source>
+ <extracomment>%1 nickname</extracomment>
+ <translation>Vil du fjerne %1 permanent?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="10"/>
+ <source>This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request.</source>
+ <translation>Denne kontaktperson vil ikke længere være i stand til at sende dig beskeder, og vil blive notificeret om fjernelsen. Kontaktpersonen kan vælge at sende dig en ny anmodning.</translation>
+ </message>
+</context>
+<context>
+ <name>NetworkSetupWizard</name>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="100"/>
+ <source>This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network.</source>
+ <translation>Denne computers internetadgang er uhindret. Jeg vil gerne forbinde direkte til Tor-netværket.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="107"/>
+ <source>Connect</source>
+ <translation>Forbind</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="124"/>
+ <source>This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings.</source>
+ <translation>Denne computers internetadgang er censureret, filtreret eller proxied. Jeg har brug for at konfigurere netværksindstillinger.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="131"/>
+ <source>Configure</source>
+ <translation>Konfigurer</translation>
+ </message>
+</context>
+<context>
+ <name>OfflineStateItem</name>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="107"/>
+ <source>Configure</source>
+ <translation>Konfigurer</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="115"/>
+ <source>Details</source>
+ <translation>Detaljer</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="143"/>
+ <source>Connection failed</source>
+ <translation>Forbindelse mislykkedes</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="169"/>
+ <source>Connecting…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Forbinder...</translation>
+ </message>
+</context>
+<context>
+ <name>OpenBrowserDialog</name>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="39"/>
+ <source><b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead.</source>
+ <translation><b>Advarsel!</b> Ved at åbne links med din browser vil du forøge din sikkerhed og anonymitet.<br><br>Du kan <a href='.'>kopiere adressen til clipboardet</a> i stedet.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="58"/>
+ <source>Don't ask again for links from %1</source>
+ <translation>Spørg ikke igen om links fra %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="64"/>
+ <source>Don't ask again for any links (not recommended!)</source>
+ <translation>Spørg ikke om nogen links (ikke anbefalt!)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="71"/>
+ <source>Open Browser</source>
+ <translation>Åben Browser</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="83"/>
+ <source>Cancel</source>
+ <translation>Annuller</translation>
+ </message>
+</context>
+<context>
+ <name>PreferencesDialog</name>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="12"/>
+ <source>Ricochet Preferences</source>
+ <translation>Indstillinger for Ricochet</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="41"/>
+ <source>General</source>
+ <translation>Generelt</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="46"/>
+ <source>Language</source>
+ <translation>Sprog</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="51"/>
+ <source>Contacts</source>
+ <translation>Kontaktpersoner</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="56"/>
+ <source>Tor</source>
+ <translation>Tor</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="61"/>
+ <source>About</source>
+ <translation>Om</translation>
+ </message>
+</context>
+<context>
+ <name>QCocoaMenuItem</name>
+ <message>
+ <location filename="../src/ui/qml/MainWindow.qml" line="29"/>
+ <source>Preference</source>
+ <translation>Indstilling</translation>
+ </message>
+</context>
+<context>
+ <name>StartupStatusPage</name>
+ <message>
+ <location filename="../src/ui/qml/StartupStatusPage.qml" line="18"/>
+ <source>The Tor process was not started successfully. This is most likely an installation or system error.</source>
+ <translation>Tor-processen kunne ikke starte. Dette er sandsynligvis en installations- eller systemfejl.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/StartupStatusPage.qml" line="49"/>
+ <source>Quit</source>
+ <translation>Afslut</translation>
+ </message>
+</context>
+<context>
+ <name>TorBootstrapStatus</name>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="17"/>
+ <source>Connecting to the Tor network…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Forbinder til Tor-netværket...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="50"/>
+ <source>Back</source>
+ <translation>Tilbage</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="57"/>
+ <source>Hide details</source>
+ <translation>Skjul detaljer</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="57"/>
+ <source>Show details</source>
+ <translation>Vis detaljer</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="69"/>
+ <source>Done</source>
+ <translation>Færdig</translation>
+ </message>
+</context>
+<context>
+ <name>TorConfigurationPage</name>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="72"/>
+ <source>Does this computer need a proxy to access the internet?</source>
+ <translation>Skal denne computer forbinde gennem en proxy for at tilgå internettet?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="84"/>
+ <source>Proxy type:</source>
+ <translation>Proxy type:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="89"/>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="91"/>
+ <source>None</source>
+ <translation>Ingen</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="106"/>
+ <source>Address:</source>
+ <translation>Adresse:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="115"/>
+ <source>IP address or hostname</source>
+ <translation>IP-adresse eller værtsnavn</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="118"/>
+ <source>Port:</source>
+ <translation>Port:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="129"/>
+ <source>Username:</source>
+ <translation>Brugernavn:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="139"/>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="149"/>
+ <source>Optional</source>
+ <translation>Valgfrit</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="142"/>
+ <source>Password:</source>
+ <translation>Adgangskode:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="159"/>
+ <source>Does this computer's Internet connection go through a firewall that only allows connections to certain ports?</source>
+ <translation>Går denne computers internetforbindelse gennem en firewall der kun tillader forbindelser på visse porte?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="170"/>
+ <source>Allowed ports:</source>
+ <translation>Tilladte porte:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="177"/>
+ <source>Example: 80,443</source>
+ <translation>Eksempel: 80,443</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="188"/>
+ <source>If this computer's Internet connection is censored, you will need to obtain and use bridge relays.</source>
+ <translation>Hvis denne computers internetforbindelse er censureret, er du nødt til at finde og benytte bridge relæer.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="197"/>
+ <source>Enter one or more bridge relays (one per line):</source>
+ <translation>Indtast et eller flere bridge relæer (et per linje):</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="212"/>
+ <source>Back</source>
+ <translation>Tilbage</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="219"/>
+ <source>Connect</source>
+ <translation>Forbind</translation>
+ </message>
+</context>
+<context>
+ <name>TorPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="24"/>
+ <source>Running:</source>
+ <translation>Kører:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="27"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="29"/>
+ <source>Yes</source>
+ <translation>Ja</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="27"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="29"/>
+ <source>No</source>
+ <translation>Nej</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <source>External</source>
+ <translation>Ekstern</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="26"/>
+ <source>Control connected:</source>
+ <translation>Kontrol forbundet:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="28"/>
+ <source>Circuits established:</source>
+ <translation>Kredsløb etableret:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="30"/>
+ <source>Hidden service:</source>
+ <translation>Skjult service:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="31"/>
+ <source>Online</source>
+ <translation>Online</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="31"/>
+ <source>Offline</source>
+ <translation>Offline</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="32"/>
+ <source>Version:</source>
+ <translation>Version:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="59"/>
+ <source>Error: <b>%1</b></source>
+ <extracomment>%1 is error message</extracomment>
+ <translation>Fejl: <b>%1</b></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="75"/>
+ <source>Configure</source>
+ <translation>Konfigurér</translation>
+ </message>
+</context>
+<context>
+ <name>TorStateWidget</name>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="9"/>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="20"/>
+ <source>Connection failed</source>
+ <translation>Forbindelse mislykkedes</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="12"/>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="22"/>
+ <source>Connecting…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Forbinder...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="25"/>
+ <source>Connecting… (%1%)</source>
+ <extracomment>%1 is progress percentage, e.g. 100</extracomment>
+ <translation>Forbinder... (%1)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="32"/>
+ <source>Online</source>
+ <translation>Online</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="34"/>
+ <source>Connected</source>
+ <translation>Forbundet</translation>
+ </message>
+</context>
+</TS>
diff --git a/translation/ricochet_de.ts b/translation/ricochet_de.ts
new file mode 100644
index 0000000..8788bf3
--- /dev/null
+++ b/translation/ricochet_de.ts
@@ -0,0 +1,694 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="de">
+<context>
+ <name>AboutPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/AboutPreferences.qml" line="14"/>
+ <source>Ricochet %1</source>
+ <extracomment>%1 version, e.g. 1.0.0</extracomment>
+ <translation>Ricochet %1</translation>
+ </message>
+</context>
+<context>
+ <name>AddContactDialog</name>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="51"/>
+ <source>Share your Ricochet ID to allow connection requests</source>
+ <translation>Teile deine Ricochet ID, um Verbindungsanfragen zu erlauben</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="107"/>
+ <source>Cancel</source>
+ <translation>Abbrechen</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="112"/>
+ <source>Add</source>
+ <translation>Hinzufügen</translation>
+ </message>
+</context>
+<context>
+ <name>ContactActions</name>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="40"/>
+ <source>Open Window</source>
+ <translation>Fenster öffnen</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="44"/>
+ <source>Details...</source>
+ <translation>Details...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="48"/>
+ <source>Rename</source>
+ <translation>Umbenennen</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="53"/>
+ <source>Remove</source>
+ <translation>Entfernen</translation>
+ </message>
+</context>
+<context>
+ <name>ContactIDField</name>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="40"/>
+ <source><b>%1</b> is already your contact</source>
+ <translation><b>%1</b> ist bereits dein Kontakt</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="42"/>
+ <source>You can't add yourself as a contact</source>
+ <translation>Du kannst dich nicht selbst als Kontakt hinzufügen</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="44"/>
+ <source>Enter an ID starting with <b>ricochet:</b></source>
+ <translation>Gebe eine ID an, beginnend mit <b>ricochet:<b></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="79"/>
+ <source>Copied to clipboard</source>
+ <translation>In die Zwischenablage kopiert</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="91"/>
+ <source>Copy</source>
+ <translation>Kopieren</translation>
+ </message>
+</context>
+<context>
+ <name>ContactList</name>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="75"/>
+ <source>Online</source>
+ <translation>Online</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="76"/>
+ <source>Offline</source>
+ <translation>Offline</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="77"/>
+ <source>Requests</source>
+ <translation>Anfragen</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="78"/>
+ <source>Rejected</source>
+ <translation>Abgewiesen</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="79"/>
+ <source>Outdated</source>
+ <translation>veraltet</translation>
+ </message>
+</context>
+<context>
+ <name>ContactPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="97"/>
+ <source>Date added:</source>
+ <translation>Hinzugefügt am:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="104"/>
+ <source>Last seen:</source>
+ <translation>Zuletzt gesehen:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="113"/>
+ <source>Request:</source>
+ <translation>Anfrage:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="122"/>
+ <source>Pending connection</source>
+ <translation>Laufende Verbindung</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="123"/>
+ <source>Delivered</source>
+ <translation>Gesendet</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="124"/>
+ <source>Accepted</source>
+ <translation>Akzeptiert</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="125"/>
+ <source>Error</source>
+ <translation>Fehler</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="126"/>
+ <source>Rejected</source>
+ <translation>Abgewiesen</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="130"/>
+ <source>%1 (Connected)</source>
+ <extracomment>%1 status, e.g. "Accepted"</extracomment>
+ <translation>%1 (Verbunden)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="136"/>
+ <source>Response:</source>
+ <translation>Antwort:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="158"/>
+ <source>Rename</source>
+ <translation>Umbenennen</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="165"/>
+ <source>Remove</source>
+ <translation>Entfernen</translation>
+ </message>
+</context>
+<context>
+ <name>ContactRequestDialog</name>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="55"/>
+ <source>Someone new is asking to connect to you</source>
+ <translation>Jemand unbekanntes möchte sich zu Ihnen verbinden</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="102"/>
+ <source>Reject</source>
+ <translation>Abweisen</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="107"/>
+ <source>Accept</source>
+ <translation>Annehmen</translation>
+ </message>
+</context>
+<context>
+ <name>ContactRequestFields</name>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="17"/>
+ <source>ID:</source>
+ <translation>ID:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="29"/>
+ <source>Name:</source>
+ <translation>Name:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="40"/>
+ <source>Message:</source>
+ <translation>Nachricht:</translation>
+ </message>
+</context>
+<context>
+ <name>GeneralPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="12"/>
+ <source>Use a single window for conversations</source>
+ <translation>Nutze ein einzelnes Fenster für Unterhaltungen</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="20"/>
+ <source>Open links in default browser without prompting</source>
+ <translation>Öffne Links im Standardbrowser ohne Nachfrage</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="28"/>
+ <source>Play audio notifications</source>
+ <translation>Benachrichtigungston abspielen</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="37"/>
+ <source>Volume</source>
+ <translation>Lautstärke</translation>
+ </message>
+</context>
+<context>
+ <name>LanguagePreferences</name>
+ <message>
+ <location filename="../src/ui/qml/LanguagePreferences.qml" line="22"/>
+ <source>Select Language</source>
+ <translation>Sprache auswählen</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/LanguagePreferences.qml" line="52"/>
+ <source>Restart Ricochet to apply changes</source>
+ <translation>Starte Ricochet neu, um Änderungung anzuwenden</translation>
+ </message>
+</context>
+<context>
+ <name>LanguagesModel</name>
+ <message>
+ <location filename="../src/ui/LanguagesModel.cpp" line="43"/>
+ <source>System default</source>
+ <translation>Systemvorgabe</translation>
+ </message>
+</context>
+<context>
+ <name>Main</name>
+ <message>
+ <location filename="../src/main.cpp" line="73"/>
+ <source>Ricochet Error</source>
+ <translation>Ricochet Fehler</translation>
+ </message>
+</context>
+<context>
+ <name>MainToolBar</name>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="19"/>
+ <source>Add Contact</source>
+ <translation>Kontakt hinzufügen</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="28"/>
+ <source>Preferences</source>
+ <translation>Einstellungen</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="75"/>
+ <source>Click to add contacts</source>
+ <translation>Klicken, um Kontakte hinzuzufügen</translation>
+ </message>
+</context>
+<context>
+ <name>MainWindow</name>
+ <message>
+ <location filename="../src/ui/MainWindow.cpp" line="149"/>
+ <source>Remove %1</source>
+ <translation>Entferne %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/MainWindow.cpp" line="150"/>
+ <source>Do you want to permanently remove %1?</source>
+ <translation>Möchten Sie 1% permanent entfernen?</translation>
+ </message>
+</context>
+<context>
+ <name>MessageDelegate</name>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="26"/>
+ <source>%1 is offline</source>
+ <extracomment>%1 nickname</extracomment>
+ <translation>%1 ist offline</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="134"/>
+ <source>Copy ID</source>
+ <translation>Kopiere ID</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="134"/>
+ <source>Copy Link</source>
+ <translation>Kopiere Link</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="139"/>
+ <source>Open with Browser</source>
+ <translation>Im Browser öffnen</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="153"/>
+ <source>Add as Contact</source>
+ <translation>Als Kontakt hinzufügen</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="165"/>
+ <source>Copy Message</source>
+ <translation>Nachricht kopieren</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="172"/>
+ <source>Copy Selection</source>
+ <translation>Auswahl kopieren</translation>
+ </message>
+</context>
+<context>
+ <name>MessageDialogWrapper</name>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="7"/>
+ <source>Remove %1</source>
+ <translation>Entferne %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="9"/>
+ <source>Do you want to permanently remove %1?</source>
+ <extracomment>%1 nickname</extracomment>
+ <translation>Möchten Sie %1 permanent entfernen?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="10"/>
+ <source>This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request.</source>
+ <translation>Dieser Kontakt wird Ihnen nicht mehr schreiben können, und wird über das Entfernen in Kenntnis gesetzt. Eine neue Verbindungsanfrage kann von ihm/ihr gesendet werden.</translation>
+ </message>
+</context>
+<context>
+ <name>NetworkSetupWizard</name>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="100"/>
+ <source>This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network.</source>
+ <translation>Die Internetverbindung dieses Rechners ist frei von Hindernissen. Ich möchte gerne eine direkte Verbindung zum Tor-Netzwerk herstellen.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="107"/>
+ <source>Connect</source>
+ <translation>Verbinden</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="124"/>
+ <source>This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings.</source>
+ <translation>Die Internetverbindung dieses Rechners ist zensiert, gefiltert oder vermittelt.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="131"/>
+ <source>Configure</source>
+ <translation>Konfigurieren</translation>
+ </message>
+</context>
+<context>
+ <name>OfflineStateItem</name>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="107"/>
+ <source>Configure</source>
+ <translation>Konfigurieren</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="115"/>
+ <source>Details</source>
+ <translation>Details</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="143"/>
+ <source>Connection failed</source>
+ <translation>Verbindung gescheitert</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="169"/>
+ <source>Connecting…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Verbindet...</translation>
+ </message>
+</context>
+<context>
+ <name>OpenBrowserDialog</name>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="39"/>
+ <source><b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead.</source>
+ <translation><b>Warnung!</b> Links mit deinem Standardbrowser zu öffnen kann deine Sicherheit und Anonymität beeinträchtigen.<br> <br>Du kannst stattdessen <a href='.'>in die Zwischenablage kopieren.</a></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="58"/>
+ <source>Don't ask again for links from %1</source>
+ <translation>Nicht mehr fragen bei Links von %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="64"/>
+ <source>Don't ask again for any links (not recommended!)</source>
+ <translation>Für keinen Link mehr nachfragen (nicht empfohlen!)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="71"/>
+ <source>Open Browser</source>
+ <translation>Browser öffnen</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="83"/>
+ <source>Cancel</source>
+ <translation>Abbrechen</translation>
+ </message>
+</context>
+<context>
+ <name>PreferencesDialog</name>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="12"/>
+ <source>Ricochet Preferences</source>
+ <translation>Ricochet Einstellungen</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="41"/>
+ <source>General</source>
+ <translation>Allgemein</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="46"/>
+ <source>Language</source>
+ <translation>Sprache</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="51"/>
+ <source>Contacts</source>
+ <translation>Kontakte</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="56"/>
+ <source>Tor</source>
+ <translation>Tor</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="61"/>
+ <source>About</source>
+ <translation>Über</translation>
+ </message>
+</context>
+<context>
+ <name>QCocoaMenuItem</name>
+ <message>
+ <location filename="../src/ui/qml/MainWindow.qml" line="29"/>
+ <source>Preference</source>
+ <translation>Einstellung</translation>
+ </message>
+</context>
+<context>
+ <name>StartupStatusPage</name>
+ <message>
+ <location filename="../src/ui/qml/StartupStatusPage.qml" line="18"/>
+ <source>The Tor process was not started successfully. This is most likely an installation or system error.</source>
+ <translation>Der Tor-Prozess konnte nicht gestartet werden. Es handelt sich wahrscheinlich um einen Installations- oder einen Systemfehler.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/StartupStatusPage.qml" line="49"/>
+ <source>Quit</source>
+ <translation>Beenden</translation>
+ </message>
+</context>
+<context>
+ <name>TorBootstrapStatus</name>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="17"/>
+ <source>Connecting to the Tor network…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Verbinde zum Tor-Netzwerk...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="50"/>
+ <source>Back</source>
+ <translation>Zurück</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="57"/>
+ <source>Hide details</source>
+ <translation>Details verstecken</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="57"/>
+ <source>Show details</source>
+ <translation>Details anzeigen</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="69"/>
+ <source>Done</source>
+ <translation>Fertig</translation>
+ </message>
+</context>
+<context>
+ <name>TorConfigurationPage</name>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="72"/>
+ <source>Does this computer need a proxy to access the internet?</source>
+ <translation>Benötigt dieser Rechner einen Proxy um sich mit dem Internet zu verbinden?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="84"/>
+ <source>Proxy type:</source>
+ <translation>Proxytyp:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="89"/>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="91"/>
+ <source>None</source>
+ <translation>Keiner</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="106"/>
+ <source>Address:</source>
+ <translation>Adresse:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="115"/>
+ <source>IP address or hostname</source>
+ <translation>IP-Adresse oder Rechnername</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="118"/>
+ <source>Port:</source>
+ <translation>Port:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="129"/>
+ <source>Username:</source>
+ <translation>Benutzername:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="139"/>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="149"/>
+ <source>Optional</source>
+ <translation>Fakultativ</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="142"/>
+ <source>Password:</source>
+ <translation>Passwort:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="159"/>
+ <source>Does this computer's Internet connection go through a firewall that only allows connections to certain ports?</source>
+ <translation>Geht die Verbindung dieses Rechners durch eine Firewall, die nur Verbindungen zu manchen Ports erlaubt?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="170"/>
+ <source>Allowed ports:</source>
+ <translation>Erlaubte Ports:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="177"/>
+ <source>Example: 80,443</source>
+ <translation>Beispiel: 80, 443</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="188"/>
+ <source>If this computer's Internet connection is censored, you will need to obtain and use bridge relays.</source>
+ <translation>Sollte die Internetverbindung dieses Rechners zensiert sein, werden Sie Brücken-Relays finden und nutzen müssen.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="197"/>
+ <source>Enter one or more bridge relays (one per line):</source>
+ <translation>Geben Sie ein oder mehrere Brücken-Relays an (eins pro Zeile):</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="212"/>
+ <source>Back</source>
+ <translation>Zurück</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="219"/>
+ <source>Connect</source>
+ <translation>Verbinden</translation>
+ </message>
+</context>
+<context>
+ <name>TorPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="24"/>
+ <source>Running:</source>
+ <translation>Läuft:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="27"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="29"/>
+ <source>Yes</source>
+ <translation>Ja</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="27"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="29"/>
+ <source>No</source>
+ <translation>Nein</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <source>External</source>
+ <translation>Extern</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="26"/>
+ <source>Control connected:</source>
+ <translation>Control verbunden:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="28"/>
+ <source>Circuits established:</source>
+ <translation>Circuits aufgebaut:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="30"/>
+ <source>Hidden service:</source>
+ <translation>Versteckter Service:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="31"/>
+ <source>Online</source>
+ <translation>Online</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="31"/>
+ <source>Offline</source>
+ <translation>Offline</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="32"/>
+ <source>Version:</source>
+ <translation>Version:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="59"/>
+ <source>Error: <b>%1</b></source>
+ <extracomment>%1 is error message</extracomment>
+ <translation>Fehler: <b>%1</b></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="75"/>
+ <source>Configure</source>
+ <translation>Konfigurieren</translation>
+ </message>
+</context>
+<context>
+ <name>TorStateWidget</name>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="9"/>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="20"/>
+ <source>Connection failed</source>
+ <translation>Verbindung gescheitert</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="12"/>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="22"/>
+ <source>Connecting…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Verbinde...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="25"/>
+ <source>Connecting… (%1%)</source>
+ <extracomment>%1 is progress percentage, e.g. 100</extracomment>
+ <translation>Verbinde... (%1%)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="32"/>
+ <source>Online</source>
+ <translation>Online</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="34"/>
+ <source>Connected</source>
+ <translation>Verbunden</translation>
+ </message>
+</context>
+</TS>
diff --git a/translation/ricochet_en.ts b/translation/ricochet_en.ts
new file mode 100644
index 0000000..6e945a5
--- /dev/null
+++ b/translation/ricochet_en.ts
@@ -0,0 +1,694 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="en_US">
+<context>
+ <name>AboutPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/AboutPreferences.qml" line="14"/>
+ <source>Ricochet %1</source>
+ <extracomment>%1 version, e.g. 1.0.0</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>AddContactDialog</name>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="51"/>
+ <source>Share your Ricochet ID to allow connection requests</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="107"/>
+ <source>Cancel</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="112"/>
+ <source>Add</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>ContactActions</name>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="40"/>
+ <source>Open Window</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="44"/>
+ <source>Details...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="48"/>
+ <source>Rename</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="53"/>
+ <source>Remove</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>ContactIDField</name>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="40"/>
+ <source><b>%1</b> is already your contact</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="42"/>
+ <source>You can't add yourself as a contact</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="44"/>
+ <source>Enter an ID starting with <b>ricochet:</b></source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="79"/>
+ <source>Copied to clipboard</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="91"/>
+ <source>Copy</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>ContactList</name>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="75"/>
+ <source>Online</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="76"/>
+ <source>Offline</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="77"/>
+ <source>Requests</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="78"/>
+ <source>Rejected</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="79"/>
+ <source>Outdated</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>ContactPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="97"/>
+ <source>Date added:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="104"/>
+ <source>Last seen:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="113"/>
+ <source>Request:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="122"/>
+ <source>Pending connection</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="123"/>
+ <source>Delivered</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="124"/>
+ <source>Accepted</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="125"/>
+ <source>Error</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="126"/>
+ <source>Rejected</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="130"/>
+ <source>%1 (Connected)</source>
+ <extracomment>%1 status, e.g. "Accepted"</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="136"/>
+ <source>Response:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="158"/>
+ <source>Rename</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="165"/>
+ <source>Remove</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>ContactRequestDialog</name>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="55"/>
+ <source>Someone new is asking to connect to you</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="102"/>
+ <source>Reject</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="107"/>
+ <source>Accept</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>ContactRequestFields</name>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="17"/>
+ <source>ID:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="29"/>
+ <source>Name:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="40"/>
+ <source>Message:</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>GeneralPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="12"/>
+ <source>Use a single window for conversations</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="20"/>
+ <source>Open links in default browser without prompting</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="28"/>
+ <source>Play audio notifications</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="37"/>
+ <source>Volume</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>LanguagePreferences</name>
+ <message>
+ <location filename="../src/ui/qml/LanguagePreferences.qml" line="22"/>
+ <source>Select Language</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/LanguagePreferences.qml" line="52"/>
+ <source>Restart Ricochet to apply changes</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>LanguagesModel</name>
+ <message>
+ <location filename="../src/ui/LanguagesModel.cpp" line="43"/>
+ <source>System default</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>Main</name>
+ <message>
+ <location filename="../src/main.cpp" line="73"/>
+ <source>Ricochet Error</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>MainToolBar</name>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="19"/>
+ <source>Add Contact</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="28"/>
+ <source>Preferences</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="75"/>
+ <source>Click to add contacts</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>MainWindow</name>
+ <message>
+ <location filename="../src/ui/MainWindow.cpp" line="149"/>
+ <source>Remove %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/MainWindow.cpp" line="150"/>
+ <source>Do you want to permanently remove %1?</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>MessageDelegate</name>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="26"/>
+ <source>%1 is offline</source>
+ <extracomment>%1 nickname</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="134"/>
+ <source>Copy ID</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="134"/>
+ <source>Copy Link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="139"/>
+ <source>Open with Browser</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="153"/>
+ <source>Add as Contact</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="165"/>
+ <source>Copy Message</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="172"/>
+ <source>Copy Selection</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>MessageDialogWrapper</name>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="7"/>
+ <source>Remove %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="9"/>
+ <source>Do you want to permanently remove %1?</source>
+ <extracomment>%1 nickname</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="10"/>
+ <source>This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request.</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>NetworkSetupWizard</name>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="100"/>
+ <source>This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="107"/>
+ <source>Connect</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="124"/>
+ <source>This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="131"/>
+ <source>Configure</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>OfflineStateItem</name>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="107"/>
+ <source>Configure</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="115"/>
+ <source>Details</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="143"/>
+ <source>Connection failed</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="169"/>
+ <source>Connecting…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>OpenBrowserDialog</name>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="39"/>
+ <source><b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="58"/>
+ <source>Don't ask again for links from %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="64"/>
+ <source>Don't ask again for any links (not recommended!)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="71"/>
+ <source>Open Browser</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="83"/>
+ <source>Cancel</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PreferencesDialog</name>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="12"/>
+ <source>Ricochet Preferences</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="41"/>
+ <source>General</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="46"/>
+ <source>Language</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="51"/>
+ <source>Contacts</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="56"/>
+ <source>Tor</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="61"/>
+ <source>About</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>QCocoaMenuItem</name>
+ <message>
+ <location filename="../src/ui/qml/MainWindow.qml" line="29"/>
+ <source>Preference</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>StartupStatusPage</name>
+ <message>
+ <location filename="../src/ui/qml/StartupStatusPage.qml" line="18"/>
+ <source>The Tor process was not started successfully. This is most likely an installation or system error.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/StartupStatusPage.qml" line="49"/>
+ <source>Quit</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>TorBootstrapStatus</name>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="17"/>
+ <source>Connecting to the Tor network…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="50"/>
+ <source>Back</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="57"/>
+ <source>Hide details</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="57"/>
+ <source>Show details</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="69"/>
+ <source>Done</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>TorConfigurationPage</name>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="72"/>
+ <source>Does this computer need a proxy to access the internet?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="84"/>
+ <source>Proxy type:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="89"/>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="91"/>
+ <source>None</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="106"/>
+ <source>Address:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="115"/>
+ <source>IP address or hostname</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="118"/>
+ <source>Port:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="129"/>
+ <source>Username:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="139"/>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="149"/>
+ <source>Optional</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="142"/>
+ <source>Password:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="159"/>
+ <source>Does this computer's Internet connection go through a firewall that only allows connections to certain ports?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="170"/>
+ <source>Allowed ports:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="177"/>
+ <source>Example: 80,443</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="188"/>
+ <source>If this computer's Internet connection is censored, you will need to obtain and use bridge relays.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="197"/>
+ <source>Enter one or more bridge relays (one per line):</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="212"/>
+ <source>Back</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="219"/>
+ <source>Connect</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>TorPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="24"/>
+ <source>Running:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="27"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="29"/>
+ <source>Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="27"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="29"/>
+ <source>No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <source>External</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="26"/>
+ <source>Control connected:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="28"/>
+ <source>Circuits established:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="30"/>
+ <source>Hidden service:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="31"/>
+ <source>Online</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="31"/>
+ <source>Offline</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="32"/>
+ <source>Version:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="59"/>
+ <source>Error: <b>%1</b></source>
+ <extracomment>%1 is error message</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="75"/>
+ <source>Configure</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>TorStateWidget</name>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="9"/>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="20"/>
+ <source>Connection failed</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="12"/>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="22"/>
+ <source>Connecting…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="25"/>
+ <source>Connecting… (%1%)</source>
+ <extracomment>%1 is progress percentage, e.g. 100</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="32"/>
+ <source>Online</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="34"/>
+ <source>Connected</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
diff --git a/translation/ricochet_es.ts b/translation/ricochet_es.ts
new file mode 100644
index 0000000..729dda9
--- /dev/null
+++ b/translation/ricochet_es.ts
@@ -0,0 +1,694 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="es">
+<context>
+ <name>AboutPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/AboutPreferences.qml" line="14"/>
+ <source>Ricochet %1</source>
+ <extracomment>%1 version, e.g. 1.0.0</extracomment>
+ <translation>Ricochet %1</translation>
+ </message>
+</context>
+<context>
+ <name>AddContactDialog</name>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="51"/>
+ <source>Share your Ricochet ID to allow connection requests</source>
+ <translation>Comparta su ID (identificador) de Ricochet para permitir solicitudes de conexión</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="107"/>
+ <source>Cancel</source>
+ <translation>Cancelar</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="112"/>
+ <source>Add</source>
+ <translation>Añadir</translation>
+ </message>
+</context>
+<context>
+ <name>ContactActions</name>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="40"/>
+ <source>Open Window</source>
+ <translation>Abrir Ventana</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="44"/>
+ <source>Details...</source>
+ <translation>Detalles...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="48"/>
+ <source>Rename</source>
+ <translation>Renombrar</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="53"/>
+ <source>Remove</source>
+ <translation>Eliminar</translation>
+ </message>
+</context>
+<context>
+ <name>ContactIDField</name>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="40"/>
+ <source><b>%1</b> is already your contact</source>
+ <translation><b>%1</b> ya es su contacto</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="42"/>
+ <source>You can't add yourself as a contact</source>
+ <translation>No puede añadirse a usted mismo como un contacto</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="44"/>
+ <source>Enter an ID starting with <b>ricochet:</b></source>
+ <translation>Introduzca un ID comience con <b>ricochet:</b></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="79"/>
+ <source>Copied to clipboard</source>
+ <translation>Copiado al portapapeles</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="91"/>
+ <source>Copy</source>
+ <translation>Copiar</translation>
+ </message>
+</context>
+<context>
+ <name>ContactList</name>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="75"/>
+ <source>Online</source>
+ <translation>En línea</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="76"/>
+ <source>Offline</source>
+ <translation>Fuera de línea</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="77"/>
+ <source>Requests</source>
+ <translation>Solicitudes</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="78"/>
+ <source>Rejected</source>
+ <translation>Rechazada</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="79"/>
+ <source>Outdated</source>
+ <translation>Caducada</translation>
+ </message>
+</context>
+<context>
+ <name>ContactPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="97"/>
+ <source>Date added:</source>
+ <translation>Añadido desde:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="104"/>
+ <source>Last seen:</source>
+ <translation>Visto por última vez:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="113"/>
+ <source>Request:</source>
+ <translation>Solicitud:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="122"/>
+ <source>Pending connection</source>
+ <translation>Conexión pendiente</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="123"/>
+ <source>Delivered</source>
+ <translation>Entregado</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="124"/>
+ <source>Accepted</source>
+ <translation>Aceptado</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="125"/>
+ <source>Error</source>
+ <translation>Error</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="126"/>
+ <source>Rejected</source>
+ <translation>Rechazado</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="130"/>
+ <source>%1 (Connected)</source>
+ <extracomment>%1 status, e.g. "Accepted"</extracomment>
+ <translation>%1 (Conectado)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="136"/>
+ <source>Response:</source>
+ <translation>Respuesta:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="158"/>
+ <source>Rename</source>
+ <translation>Renombrar</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="165"/>
+ <source>Remove</source>
+ <translation>Eliminar</translation>
+ </message>
+</context>
+<context>
+ <name>ContactRequestDialog</name>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="55"/>
+ <source>Someone new is asking to connect to you</source>
+ <translation>Alguien nuevo está pidiendo conectarse con usted</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="102"/>
+ <source>Reject</source>
+ <translation>Rechazar</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="107"/>
+ <source>Accept</source>
+ <translation>Aceptar</translation>
+ </message>
+</context>
+<context>
+ <name>ContactRequestFields</name>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="17"/>
+ <source>ID:</source>
+ <translation>ID (identificador):</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="29"/>
+ <source>Name:</source>
+ <translation>Nombre:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="40"/>
+ <source>Message:</source>
+ <translation>Mensaje:</translation>
+ </message>
+</context>
+<context>
+ <name>GeneralPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="12"/>
+ <source>Use a single window for conversations</source>
+ <translation>Usar una única ventana para conversaciones</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="20"/>
+ <source>Open links in default browser without prompting</source>
+ <translation>Abrir enlaces en el navegador predeterminado sin pedir confirmación</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="28"/>
+ <source>Play audio notifications</source>
+ <translation>Reproducir notificaciones de audio</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="37"/>
+ <source>Volume</source>
+ <translation>Volumen</translation>
+ </message>
+</context>
+<context>
+ <name>LanguagePreferences</name>
+ <message>
+ <location filename="../src/ui/qml/LanguagePreferences.qml" line="22"/>
+ <source>Select Language</source>
+ <translation>Seleccionar idioma</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/LanguagePreferences.qml" line="52"/>
+ <source>Restart Ricochet to apply changes</source>
+ <translation>Reiniciar Ricochet para aplicar los cambios</translation>
+ </message>
+</context>
+<context>
+ <name>LanguagesModel</name>
+ <message>
+ <location filename="../src/ui/LanguagesModel.cpp" line="43"/>
+ <source>System default</source>
+ <translation>Predeterminado del sistema</translation>
+ </message>
+</context>
+<context>
+ <name>Main</name>
+ <message>
+ <location filename="../src/main.cpp" line="73"/>
+ <source>Ricochet Error</source>
+ <translation>Error de Ricochet</translation>
+ </message>
+</context>
+<context>
+ <name>MainToolBar</name>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="19"/>
+ <source>Add Contact</source>
+ <translation>Añadir Contacto</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="28"/>
+ <source>Preferences</source>
+ <translation>Preferencias</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="75"/>
+ <source>Click to add contacts</source>
+ <translation>Haga clic para añadir contactos</translation>
+ </message>
+</context>
+<context>
+ <name>MainWindow</name>
+ <message>
+ <location filename="../src/ui/MainWindow.cpp" line="149"/>
+ <source>Remove %1</source>
+ <translation>Eliminar %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/MainWindow.cpp" line="150"/>
+ <source>Do you want to permanently remove %1?</source>
+ <translation>¿Quiere eliminar permanentemente a %1?</translation>
+ </message>
+</context>
+<context>
+ <name>MessageDelegate</name>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="26"/>
+ <source>%1 is offline</source>
+ <extracomment>%1 nickname</extracomment>
+ <translation>%1 está fuera de línea</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="134"/>
+ <source>Copy ID</source>
+ <translation>Copiar ID (identificador)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="134"/>
+ <source>Copy Link</source>
+ <translation>Copiar Enlace</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="139"/>
+ <source>Open with Browser</source>
+ <translation>Abrir con el Navegador</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="153"/>
+ <source>Add as Contact</source>
+ <translation>Añadir como Contacto</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="165"/>
+ <source>Copy Message</source>
+ <translation>Copiar mensaje</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="172"/>
+ <source>Copy Selection</source>
+ <translation>Copiar selección</translation>
+ </message>
+</context>
+<context>
+ <name>MessageDialogWrapper</name>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="7"/>
+ <source>Remove %1</source>
+ <translation>Eliminar %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="9"/>
+ <source>Do you want to permanently remove %1?</source>
+ <extracomment>%1 nickname</extracomment>
+ <translation>¿Quiere eliminar permanentemente a %1?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="10"/>
+ <source>This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request.</source>
+ <translation>Este contacto ya no podrá enviarle mensajes, y será notificado sobre la eliminación. Pueden elegir enviar una nueva solicitud de conexión.</translation>
+ </message>
+</context>
+<context>
+ <name>NetworkSetupWizard</name>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="100"/>
+ <source>This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network.</source>
+ <translation>La conexión a Internet de este equipo está libre de obstáculos. Me gustaría conectar directamente a la red Tor.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="107"/>
+ <source>Connect</source>
+ <translation>Conectar</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="124"/>
+ <source>This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings.</source>
+ <translation>La conexión a Internet de este equipo está censurada, filtrada, o proxyficada. Necesito configurar las preferencias de red.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="131"/>
+ <source>Configure</source>
+ <translation>Configurar</translation>
+ </message>
+</context>
+<context>
+ <name>OfflineStateItem</name>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="107"/>
+ <source>Configure</source>
+ <translation>Configurar</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="115"/>
+ <source>Details</source>
+ <translation>Detalles</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="143"/>
+ <source>Connection failed</source>
+ <translation>Conexión fallida</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="169"/>
+ <source>Connecting…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Conectando...</translation>
+ </message>
+</context>
+<context>
+ <name>OpenBrowserDialog</name>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="39"/>
+ <source><b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead.</source>
+ <translation><b>¡Advertencia!</b> Abrir enlaces con su navegador predeterminado perjudicará su seguridad y anonimato. <br><br>En su lugar puede <a href='.'>copiarlos al portapapeles</a>.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="58"/>
+ <source>Don't ask again for links from %1</source>
+ <translation>No volver a preguntar por enlaces desde %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="64"/>
+ <source>Don't ask again for any links (not recommended!)</source>
+ <translation>No volver a preguntar por ningún enlace (¡no recomendado!)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="71"/>
+ <source>Open Browser</source>
+ <translation>Abrir Navegador</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="83"/>
+ <source>Cancel</source>
+ <translation>Cancelar</translation>
+ </message>
+</context>
+<context>
+ <name>PreferencesDialog</name>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="12"/>
+ <source>Ricochet Preferences</source>
+ <translation>Preferencias de Ricochet</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="41"/>
+ <source>General</source>
+ <translation>General</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="46"/>
+ <source>Language</source>
+ <translation>Idioma</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="51"/>
+ <source>Contacts</source>
+ <translation>Contactos</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="56"/>
+ <source>Tor</source>
+ <translation>Tor</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="61"/>
+ <source>About</source>
+ <translation>Acerca de</translation>
+ </message>
+</context>
+<context>
+ <name>QCocoaMenuItem</name>
+ <message>
+ <location filename="../src/ui/qml/MainWindow.qml" line="29"/>
+ <source>Preference</source>
+ <translation>Preferencia</translation>
+ </message>
+</context>
+<context>
+ <name>StartupStatusPage</name>
+ <message>
+ <location filename="../src/ui/qml/StartupStatusPage.qml" line="18"/>
+ <source>The Tor process was not started successfully. This is most likely an installation or system error.</source>
+ <translation>El proceso Tor no se inició con éxito. Lo más probable es que esto sea un error de instalación o de sistema.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/StartupStatusPage.qml" line="49"/>
+ <source>Quit</source>
+ <translation>Salir</translation>
+ </message>
+</context>
+<context>
+ <name>TorBootstrapStatus</name>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="17"/>
+ <source>Connecting to the Tor network…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Conectando a la red Tor...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="50"/>
+ <source>Back</source>
+ <translation>Atrás</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="57"/>
+ <source>Hide details</source>
+ <translation>Ocultar detalles</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="57"/>
+ <source>Show details</source>
+ <translation>Mostrar detalles</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="69"/>
+ <source>Done</source>
+ <translation>Hecho</translation>
+ </message>
+</context>
+<context>
+ <name>TorConfigurationPage</name>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="72"/>
+ <source>Does this computer need a proxy to access the internet?</source>
+ <translation>¿Este equipo necesita un proxy para acceder a Internet?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="84"/>
+ <source>Proxy type:</source>
+ <translation>Tipo de proxy:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="89"/>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="91"/>
+ <source>None</source>
+ <translation>Ninguno</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="106"/>
+ <source>Address:</source>
+ <translation>Dirección:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="115"/>
+ <source>IP address or hostname</source>
+ <translation>Dirección IP o nombre del equipo</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="118"/>
+ <source>Port:</source>
+ <translation>Puerto:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="129"/>
+ <source>Username:</source>
+ <translation>Nombre de usuario:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="139"/>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="149"/>
+ <source>Optional</source>
+ <translation>Opcional</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="142"/>
+ <source>Password:</source>
+ <translation>Contraseña:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="159"/>
+ <source>Does this computer's Internet connection go through a firewall that only allows connections to certain ports?</source>
+ <translation>¿La conexión a Internet de este equipo va a través de un cortafuegos (firewall) que sólo permite conexiones a ciertos puertos?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="170"/>
+ <source>Allowed ports:</source>
+ <translation>Puertos permitidos:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="177"/>
+ <source>Example: 80,443</source>
+ <translation>Ejemplo: 80,443</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="188"/>
+ <source>If this computer's Internet connection is censored, you will need to obtain and use bridge relays.</source>
+ <translation>Si la conexión a Internet de este equipo está bajo censura, necesitará obtener y usar repetidores puente (bridge relays).</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="197"/>
+ <source>Enter one or more bridge relays (one per line):</source>
+ <translation>Introduzca uno o más repetidores puente (uno por línea):</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="212"/>
+ <source>Back</source>
+ <translation>Atrás</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="219"/>
+ <source>Connect</source>
+ <translation>Conectar</translation>
+ </message>
+</context>
+<context>
+ <name>TorPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="24"/>
+ <source>Running:</source>
+ <translation>Ejecutar:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="27"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="29"/>
+ <source>Yes</source>
+ <translation>Sí</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="27"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="29"/>
+ <source>No</source>
+ <translation>No</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <source>External</source>
+ <translation>Externo</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="26"/>
+ <source>Control connected:</source>
+ <translation>Control conectado:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="28"/>
+ <source>Circuits established:</source>
+ <translation>Circuitos establecidos:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="30"/>
+ <source>Hidden service:</source>
+ <translation>Servicio oculto:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="31"/>
+ <source>Online</source>
+ <translation>En línea</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="31"/>
+ <source>Offline</source>
+ <translation>Fuera de línea</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="32"/>
+ <source>Version:</source>
+ <translation>Versión:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="59"/>
+ <source>Error: <b>%1</b></source>
+ <extracomment>%1 is error message</extracomment>
+ <translation>Error: <b>%1</b></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="75"/>
+ <source>Configure</source>
+ <translation>Configurar</translation>
+ </message>
+</context>
+<context>
+ <name>TorStateWidget</name>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="9"/>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="20"/>
+ <source>Connection failed</source>
+ <translation>Conexión fallida</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="12"/>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="22"/>
+ <source>Connecting…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Conectando...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="25"/>
+ <source>Connecting… (%1%)</source>
+ <extracomment>%1 is progress percentage, e.g. 100</extracomment>
+ <translation>Conectando... (%1%)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="32"/>
+ <source>Online</source>
+ <translation>En línea</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="34"/>
+ <source>Connected</source>
+ <translation>Conectado</translation>
+ </message>
+</context>
+</TS>
diff --git a/translation/ricochet_fi.ts b/translation/ricochet_fi.ts
new file mode 100644
index 0000000..d445544
--- /dev/null
+++ b/translation/ricochet_fi.ts
@@ -0,0 +1,694 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="fi">
+<context>
+ <name>AboutPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/AboutPreferences.qml" line="14"/>
+ <source>Ricochet %1</source>
+ <extracomment>%1 version, e.g. 1.0.0</extracomment>
+ <translation>Ricochet %1</translation>
+ </message>
+</context>
+<context>
+ <name>AddContactDialog</name>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="51"/>
+ <source>Share your Ricochet ID to allow connection requests</source>
+ <translation>Jaa oma ID kontakteillesi, että he voivat lähettää yhteyspyynnön</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="107"/>
+ <source>Cancel</source>
+ <translation>Peruuta</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="112"/>
+ <source>Add</source>
+ <translation>Lisää</translation>
+ </message>
+</context>
+<context>
+ <name>ContactActions</name>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="40"/>
+ <source>Open Window</source>
+ <translation>Avaa ikkuna</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="44"/>
+ <source>Details...</source>
+ <translation>Tiedot...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="48"/>
+ <source>Rename</source>
+ <translation>Nimeä uudelleen</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="53"/>
+ <source>Remove</source>
+ <translation>Poista</translation>
+ </message>
+</context>
+<context>
+ <name>ContactIDField</name>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="40"/>
+ <source><b>%1</b> is already your contact</source>
+ <translation><b>%1</b> on jo kontaktisi</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="42"/>
+ <source>You can't add yourself as a contact</source>
+ <translation>Et voi lisätä itseäsi kontaktiksi</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="44"/>
+ <source>Enter an ID starting with <b>ricochet:</b></source>
+ <translation>Anna ID joka alkaa <b>ricochet:</b></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="79"/>
+ <source>Copied to clipboard</source>
+ <translation>Kopioitu leikepöydälle</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="91"/>
+ <source>Copy</source>
+ <translation>Kopioi</translation>
+ </message>
+</context>
+<context>
+ <name>ContactList</name>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="75"/>
+ <source>Online</source>
+ <translation>Linjoilla</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="76"/>
+ <source>Offline</source>
+ <translation>Ei linjoilla</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="77"/>
+ <source>Requests</source>
+ <translation>Pyynnöt</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="78"/>
+ <source>Rejected</source>
+ <translation>Torjuttu</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="79"/>
+ <source>Outdated</source>
+ <translation>Vanhentunut</translation>
+ </message>
+</context>
+<context>
+ <name>ContactPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="97"/>
+ <source>Date added:</source>
+ <translation>Lisätty:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="104"/>
+ <source>Last seen:</source>
+ <translation>Nähty viimeksi:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="113"/>
+ <source>Request:</source>
+ <translation>Pyyntö:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="122"/>
+ <source>Pending connection</source>
+ <translation>Odotetaan yhteyttä</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="123"/>
+ <source>Delivered</source>
+ <translation>Toimitettu</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="124"/>
+ <source>Accepted</source>
+ <translation>Hyväksytty</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="125"/>
+ <source>Error</source>
+ <translation>Virhe</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="126"/>
+ <source>Rejected</source>
+ <translation>Torjuttu</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="130"/>
+ <source>%1 (Connected)</source>
+ <extracomment>%1 status, e.g. "Accepted"</extracomment>
+ <translation>%1 (Yhdistetty)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="136"/>
+ <source>Response:</source>
+ <translation>Vastaus:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="158"/>
+ <source>Rename</source>
+ <translation>Nimeä uudelleen</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="165"/>
+ <source>Remove</source>
+ <translation>Poista</translation>
+ </message>
+</context>
+<context>
+ <name>ContactRequestDialog</name>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="55"/>
+ <source>Someone new is asking to connect to you</source>
+ <translation>Sinulle on uusi yhteyspyyntö</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="102"/>
+ <source>Reject</source>
+ <translation>Torju</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="107"/>
+ <source>Accept</source>
+ <translation>Hyväksy</translation>
+ </message>
+</context>
+<context>
+ <name>ContactRequestFields</name>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="17"/>
+ <source>ID:</source>
+ <translation>Saajan ID:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="29"/>
+ <source>Name:</source>
+ <translation>Nimi:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="40"/>
+ <source>Message:</source>
+ <translation>Viestisi:</translation>
+ </message>
+</context>
+<context>
+ <name>GeneralPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="12"/>
+ <source>Use a single window for conversations</source>
+ <translation>Käytä yhtä ikkunaa keskusteluille</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="20"/>
+ <source>Open links in default browser without prompting</source>
+ <translation>Avaa linkit oletusselaimella ilman vahvistusta</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="28"/>
+ <source>Play audio notifications</source>
+ <translation>Soita huomioäänimerkkejä</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="37"/>
+ <source>Volume</source>
+ <translation>Äänenvoimakkuus</translation>
+ </message>
+</context>
+<context>
+ <name>LanguagePreferences</name>
+ <message>
+ <location filename="../src/ui/qml/LanguagePreferences.qml" line="22"/>
+ <source>Select Language</source>
+ <translation>Valitse kieli</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/LanguagePreferences.qml" line="52"/>
+ <source>Restart Ricochet to apply changes</source>
+ <translation>Käynnistä Ricochet uudelleen ottaaksesi tehdyt muutokset käyttöön</translation>
+ </message>
+</context>
+<context>
+ <name>LanguagesModel</name>
+ <message>
+ <location filename="../src/ui/LanguagesModel.cpp" line="43"/>
+ <source>System default</source>
+ <translation>Systeemin oletus</translation>
+ </message>
+</context>
+<context>
+ <name>Main</name>
+ <message>
+ <location filename="../src/main.cpp" line="73"/>
+ <source>Ricochet Error</source>
+ <translation>Ricochetin virhe</translation>
+ </message>
+</context>
+<context>
+ <name>MainToolBar</name>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="19"/>
+ <source>Add Contact</source>
+ <translation>Lisää kontakti</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="28"/>
+ <source>Preferences</source>
+ <translation>Asetukset</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="75"/>
+ <source>Click to add contacts</source>
+ <translation>Klikkaa lisätäksesi kontakteja</translation>
+ </message>
+</context>
+<context>
+ <name>MainWindow</name>
+ <message>
+ <location filename="../src/ui/MainWindow.cpp" line="149"/>
+ <source>Remove %1</source>
+ <translation>Poista %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/MainWindow.cpp" line="150"/>
+ <source>Do you want to permanently remove %1?</source>
+ <translation>Haluatko poistaa tämän pysyvästi: %1?</translation>
+ </message>
+</context>
+<context>
+ <name>MessageDelegate</name>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="26"/>
+ <source>%1 is offline</source>
+ <extracomment>%1 nickname</extracomment>
+ <translation>%1 ei ole linjoilla</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="134"/>
+ <source>Copy ID</source>
+ <translation>Kopioi ID</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="134"/>
+ <source>Copy Link</source>
+ <translation>Kopioi linkki</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="139"/>
+ <source>Open with Browser</source>
+ <translation>Avaa selaimessa</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="153"/>
+ <source>Add as Contact</source>
+ <translation>Lisää kontaktiksi</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="165"/>
+ <source>Copy Message</source>
+ <translation>Kopioi viesti</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="172"/>
+ <source>Copy Selection</source>
+ <translation>Kopioi valinta</translation>
+ </message>
+</context>
+<context>
+ <name>MessageDialogWrapper</name>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="7"/>
+ <source>Remove %1</source>
+ <translation>Poista %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="9"/>
+ <source>Do you want to permanently remove %1?</source>
+ <extracomment>%1 nickname</extracomment>
+ <translation>Haluatko poistaa tämän pysyvästi: %1?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="10"/>
+ <source>This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request.</source>
+ <translation>Tämä kontakti ei voi enää viestiä kanssasi ja hän saa tästä tiedon. Kontakti voi halutessaan lähettää sinulle uuden yhteyspyynnön.</translation>
+ </message>
+</context>
+<context>
+ <name>NetworkSetupWizard</name>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="100"/>
+ <source>This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network.</source>
+ <translation>Tämän tietokoneen internet-yhteydelle ei ole rajoittavia esteitä. Haluan muodostaa suoran yhteyden Tor-verkkoon.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="107"/>
+ <source>Connect</source>
+ <translation>Yhdistä</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="124"/>
+ <source>This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings.</source>
+ <translation>Tämän tietokoneen internet-yhteyttä on sensuroitu tai filtteröity tai yhteys käyttää välityspalvelimia. Minun täytyy määrittää yhteysasetukset.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="131"/>
+ <source>Configure</source>
+ <translation>Määritä</translation>
+ </message>
+</context>
+<context>
+ <name>OfflineStateItem</name>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="107"/>
+ <source>Configure</source>
+ <translation>Määritä</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="115"/>
+ <source>Details</source>
+ <translation>Tiedot</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="143"/>
+ <source>Connection failed</source>
+ <translation>Yhteys epäonnistui</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="169"/>
+ <source>Connecting…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Yhdistetään...</translation>
+ </message>
+</context>
+<context>
+ <name>OpenBrowserDialog</name>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="39"/>
+ <source><b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead.</source>
+ <translation><b>Varoitus!</b> Linkkien avaaminen oletusselaimessasi voi vaarantaa tietoturvaasi ja yksityisyyttäsi.<br><br>Voit <a href='.'>kopioida linkin leikepöydälle</a> tämän sijaan.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="58"/>
+ <source>Don't ask again for links from %1</source>
+ <translation>Älä kysy uudestaan linkeistä joiden lähteenä on %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="64"/>
+ <source>Don't ask again for any links (not recommended!)</source>
+ <translation>Älä kysy uudestaan mistään linkeistä (Ei suositella!)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="71"/>
+ <source>Open Browser</source>
+ <translation>Avaa selain</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="83"/>
+ <source>Cancel</source>
+ <translation>Peruuta</translation>
+ </message>
+</context>
+<context>
+ <name>PreferencesDialog</name>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="12"/>
+ <source>Ricochet Preferences</source>
+ <translation>Ricochet asetukset</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="41"/>
+ <source>General</source>
+ <translation>Yleistä</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="46"/>
+ <source>Language</source>
+ <translation>Kieli</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="51"/>
+ <source>Contacts</source>
+ <translation>Kontaktit</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="56"/>
+ <source>Tor</source>
+ <translation>Tor</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="61"/>
+ <source>About</source>
+ <translation>Tietoa</translation>
+ </message>
+</context>
+<context>
+ <name>QCocoaMenuItem</name>
+ <message>
+ <location filename="../src/ui/qml/MainWindow.qml" line="29"/>
+ <source>Preference</source>
+ <translation>Asetus</translation>
+ </message>
+</context>
+<context>
+ <name>StartupStatusPage</name>
+ <message>
+ <location filename="../src/ui/qml/StartupStatusPage.qml" line="18"/>
+ <source>The Tor process was not started successfully. This is most likely an installation or system error.</source>
+ <translation>Tor-prosessin käynnistys ei onnistunut. Tämä voi johtua asennus- tai järjestelmävirheestä.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/StartupStatusPage.qml" line="49"/>
+ <source>Quit</source>
+ <translation>Lopeta</translation>
+ </message>
+</context>
+<context>
+ <name>TorBootstrapStatus</name>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="17"/>
+ <source>Connecting to the Tor network…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Yhdistetään Tor-verkkoon...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="50"/>
+ <source>Back</source>
+ <translation>Palaa</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="57"/>
+ <source>Hide details</source>
+ <translation>Piilota tiedot</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="57"/>
+ <source>Show details</source>
+ <translation>Näytä tiedot</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="69"/>
+ <source>Done</source>
+ <translation>Valmis</translation>
+ </message>
+</context>
+<context>
+ <name>TorConfigurationPage</name>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="72"/>
+ <source>Does this computer need a proxy to access the internet?</source>
+ <translation>Tarvitseeko tämä tietokone välityspalvelimen internet-yhteyden luomista varten?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="84"/>
+ <source>Proxy type:</source>
+ <translation>Välityspalvelimen tyyppi:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="89"/>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="91"/>
+ <source>None</source>
+ <translation>Ei mitään</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="106"/>
+ <source>Address:</source>
+ <translation>Osoite:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="115"/>
+ <source>IP address or hostname</source>
+ <translation>IP-osoite tai isäntä:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="118"/>
+ <source>Port:</source>
+ <translation>Portti:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="129"/>
+ <source>Username:</source>
+ <translation>Käyttäjänimi:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="139"/>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="149"/>
+ <source>Optional</source>
+ <translation>Vaihtoehtoinen</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="142"/>
+ <source>Password:</source>
+ <translation>Salasana:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="159"/>
+ <source>Does this computer's Internet connection go through a firewall that only allows connections to certain ports?</source>
+ <translation>Suojaako tämän tietokoneen internet-yhteyttä palomuuri, joka sallii liikenteen vain ennalta määrätyistä porteista?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="170"/>
+ <source>Allowed ports:</source>
+ <translation>Sallitut portit:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="177"/>
+ <source>Example: 80,443</source>
+ <translation>Esimerkiksi: 80,443</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="188"/>
+ <source>If this computer's Internet connection is censored, you will need to obtain and use bridge relays.</source>
+ <translation>Jos tämän tietokoneen internet-yhteyttä on sensuroitu, sinun täytyy käyttää sillattuja reitittimiä</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="197"/>
+ <source>Enter one or more bridge relays (one per line):</source>
+ <translation>Anna yksi tai useampi reititin (erottele rivinvaihdolla):</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="212"/>
+ <source>Back</source>
+ <translation>Palaa</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="219"/>
+ <source>Connect</source>
+ <translation>Yhdistä</translation>
+ </message>
+</context>
+<context>
+ <name>TorPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="24"/>
+ <source>Running:</source>
+ <translation>Käynnissä:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="27"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="29"/>
+ <source>Yes</source>
+ <translation>Kyllä</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="27"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="29"/>
+ <source>No</source>
+ <translation>Ei</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <source>External</source>
+ <translation>Ulkoinen</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="26"/>
+ <source>Control connected:</source>
+ <translation>Ohjaus kytketty:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="28"/>
+ <source>Circuits established:</source>
+ <translation>Alueet perustettu:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="30"/>
+ <source>Hidden service:</source>
+ <translation>Piilotettu palvelu:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="31"/>
+ <source>Online</source>
+ <translation>Kyllä - Online</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="31"/>
+ <source>Offline</source>
+ <translation>ei - Offline</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="32"/>
+ <source>Version:</source>
+ <translation>Versio:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="59"/>
+ <source>Error: <b>%1</b></source>
+ <extracomment>%1 is error message</extracomment>
+ <translation>Virhe: <b>%1</b></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="75"/>
+ <source>Configure</source>
+ <translation>Määritä</translation>
+ </message>
+</context>
+<context>
+ <name>TorStateWidget</name>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="9"/>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="20"/>
+ <source>Connection failed</source>
+ <translation>Yhteys epäonnistui</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="12"/>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="22"/>
+ <source>Connecting…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Yhdistetään...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="25"/>
+ <source>Connecting… (%1%)</source>
+ <extracomment>%1 is progress percentage, e.g. 100</extracomment>
+ <translation>Yhdistetään… (%1%)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="32"/>
+ <source>Online</source>
+ <translation>Online</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="34"/>
+ <source>Connected</source>
+ <translation>Yhdistetty</translation>
+ </message>
+</context>
+</TS>
diff --git a/translation/ricochet_fil_PH.ts b/translation/ricochet_fil_PH.ts
new file mode 100644
index 0000000..7200514
--- /dev/null
+++ b/translation/ricochet_fil_PH.ts
@@ -0,0 +1,695 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="tl_PH">
+<context>
+ <name>AboutPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/AboutPreferences.qml" line="14"/>
+ <source>Ricochet %1</source>
+ <extracomment>%1 version, e.g. 1.0.0</extracomment>
+ <translation>Ricochet %1</translation>
+ </message>
+</context>
+<context>
+ <name>AddContactDialog</name>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="51"/>
+ <source>Share your Ricochet ID to allow connection requests</source>
+ <translation>Ipamahagi ang iyong Ricochet ID para makatanggap ng mga kahilingan ng koneksyon</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="107"/>
+ <source>Cancel</source>
+ <translation>Kanselahin</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="112"/>
+ <source>Add</source>
+ <translation>Magdagdag</translation>
+ </message>
+</context>
+<context>
+ <name>ContactActions</name>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="40"/>
+ <source>Open Window</source>
+ <translation>Magbukas ng Window</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="44"/>
+ <source>Details...</source>
+ <translation>Detalye...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="48"/>
+ <source>Rename</source>
+ <translation>Palitan ang pangalan</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="53"/>
+ <source>Remove</source>
+ <translation>Tanggalin</translation>
+ </message>
+</context>
+<context>
+ <name>ContactIDField</name>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="40"/>
+ <source><b>%1</b> is already your contact</source>
+ <translation>Si <b>%1</b> ay nasa iyong mga contact na.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="42"/>
+ <source>You can't add yourself as a contact</source>
+ <translation>Hindi mo maaaring idagdag ang iyong sarili bilang isang contact</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="44"/>
+ <source>Enter an ID starting with <b>ricochet:</b></source>
+ <translation>Magpasok ng isang ID na nagsisimula sa <b>ricochet:</ b></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="79"/>
+ <source>Copied to clipboard</source>
+ <translation>Kinopya sa clipboard</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="91"/>
+ <source>Copy</source>
+ <translation>Kopyahin</translation>
+ </message>
+</context>
+<context>
+ <name>ContactList</name>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="75"/>
+ <source>Online</source>
+ <translation>Online</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="76"/>
+ <source>Offline</source>
+ <translation>Offline</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="77"/>
+ <source>Requests</source>
+ <translation>Mga Kahilingan</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="78"/>
+ <source>Rejected</source>
+ <translation>Tinanggihan</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="79"/>
+ <source>Outdated</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>ContactPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="97"/>
+ <source>Date added:</source>
+ <translation>Petsa nang idinagdag:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="104"/>
+ <source>Last seen:</source>
+ <translation>Huling nakita:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="113"/>
+ <source>Request:</source>
+ <translation>Hiling:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="122"/>
+ <source>Pending connection</source>
+ <translation>Naghihintay na mga koneksyon</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="123"/>
+ <source>Delivered</source>
+ <translation>Naihatid</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="124"/>
+ <source>Accepted</source>
+ <translation>Natanggap</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="125"/>
+ <source>Error</source>
+ <translation>Pagkakamali</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="126"/>
+ <source>Rejected</source>
+ <translation>Tinanggihan</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="130"/>
+ <source>%1 (Connected)</source>
+ <extracomment>%1 status, e.g. "Accepted"</extracomment>
+ <translation>%1 (Nakakonekta)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="136"/>
+ <source>Response:</source>
+ <translation>Tugon:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="158"/>
+ <source>Rename</source>
+ <translation type="unfinished">Palitan ang pangalan</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="165"/>
+ <source>Remove</source>
+ <translation>Tanggalin</translation>
+ </message>
+</context>
+<context>
+ <name>ContactRequestDialog</name>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="55"/>
+ <source>Someone new is asking to connect to you</source>
+ <translation>May bagong tao na nagtatanong upang kumonekta sa iyo</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="102"/>
+ <source>Reject</source>
+ <translation>Tanggihan</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="107"/>
+ <source>Accept</source>
+ <translation>Tanggapin</translation>
+ </message>
+</context>
+<context>
+ <name>ContactRequestFields</name>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="17"/>
+ <source>ID:</source>
+ <translation>ID:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="29"/>
+ <source>Name:</source>
+ <translation>Pangalan:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="40"/>
+ <source>Message:</source>
+ <translation>Mensahe:</translation>
+ </message>
+</context>
+<context>
+ <name>GeneralPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="12"/>
+ <source>Use a single window for conversations</source>
+ <translation>Gumamit ng solong window para sa mga pag-uusap</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="20"/>
+ <source>Open links in default browser without prompting</source>
+ <translation>Buksan ang mga link sa default na browser nang walang pagdikta</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="28"/>
+ <source>Play audio notifications</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="37"/>
+ <source>Volume</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>LanguagePreferences</name>
+ <message>
+ <location filename="../src/ui/qml/LanguagePreferences.qml" line="22"/>
+ <source>Select Language</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/LanguagePreferences.qml" line="52"/>
+ <source>Restart Ricochet to apply changes</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>LanguagesModel</name>
+ <message>
+ <location filename="../src/ui/LanguagesModel.cpp" line="43"/>
+ <source>System default</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>Main</name>
+ <message>
+ <location filename="../src/main.cpp" line="73"/>
+ <source>Ricochet Error</source>
+ <translation>Pagkakamali sa Ricochet</translation>
+ </message>
+</context>
+<context>
+ <name>MainToolBar</name>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="19"/>
+ <source>Add Contact</source>
+ <translation>Magdagdag ng Contact</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="28"/>
+ <source>Preferences</source>
+ <translation>Kagustuhan</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="75"/>
+ <source>Click to add contacts</source>
+ <translation>I-click upang magdagdag ng contacts</translation>
+ </message>
+</context>
+<context>
+ <name>MainWindow</name>
+ <message>
+ <location filename="../src/ui/MainWindow.cpp" line="149"/>
+ <source>Remove %1</source>
+ <translation>Tanggalin si %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/MainWindow.cpp" line="150"/>
+ <source>Do you want to permanently remove %1?</source>
+ <translation>Gusto mo bang permanenteng tanggalin si %1?</translation>
+ </message>
+</context>
+<context>
+ <name>MessageDelegate</name>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="26"/>
+ <source>%1 is offline</source>
+ <extracomment>%1 nickname</extracomment>
+ <translation>Si %1 ay offline</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="134"/>
+ <source>Copy ID</source>
+ <translation>Kopyahin ang ID</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="134"/>
+ <source>Copy Link</source>
+ <translation>Kopyahin ang Link</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="139"/>
+ <source>Open with Browser</source>
+ <translation>Buksan gamit ang Browser</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="153"/>
+ <source>Add as Contact</source>
+ <translation>Idagdag bilang Contact</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="165"/>
+ <source>Copy Message</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="172"/>
+ <source>Copy Selection</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>MessageDialogWrapper</name>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="7"/>
+ <source>Remove %1</source>
+ <translation>Tanggalin si %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="9"/>
+ <source>Do you want to permanently remove %1?</source>
+ <extracomment>%1 nickname</extracomment>
+ <translation>Gusto mo bang permanenteng tanggalin si %1?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="10"/>
+ <source>This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request.</source>
+ <translation>Ang contact na ito ay hindi na magagawang magpadala ng mensahe sa iyo, siya ay aabisuhan tungkol sa pag-alis. Maaari nilang piliin na magpadala ng bagong kahilingan ng koneksyon.</translation>
+ </message>
+</context>
+<context>
+ <name>NetworkSetupWizard</name>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="100"/>
+ <source>This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network.</source>
+ <translation>Ang koneksyon sa Internet ng computer na ito ay walang hadlang. Gusto kong direktang kumonekta sa Tor network.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="107"/>
+ <source>Connect</source>
+ <translation>Ikonekta</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="124"/>
+ <source>This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings.</source>
+ <translation>Ang koneksyon sa Internet ng computer na ito ay censored, na-filter, o proxied. Kailangan ko i-configure ang mga setting ng network.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="131"/>
+ <source>Configure</source>
+ <translation>I-configure</translation>
+ </message>
+</context>
+<context>
+ <name>OfflineStateItem</name>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="107"/>
+ <source>Configure</source>
+ <translation>I-configure</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="115"/>
+ <source>Details</source>
+ <translation>Detalye</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="143"/>
+ <source>Connection failed</source>
+ <translation>Nabigo ang koneksyon</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="169"/>
+ <source>Connecting…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Kumukonekta...</translation>
+ </message>
+</context>
+<context>
+ <name>OpenBrowserDialog</name>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="39"/>
+ <source><b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead.</source>
+ <translation><b>Babala!</ b> Pagbukas ng mga link gamit ang iyong default na browser ay makakapinsala sa iyong seguridad at anonymity.<br>
+<br> Maaari mong <a href='.'>kopyahin sa clipboard</a> sa halip.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="58"/>
+ <source>Don't ask again for links from %1</source>
+ <translation>Huwag nang tatanungin ulit para sa mga link mula kay %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="64"/>
+ <source>Don't ask again for any links (not recommended!)</source>
+ <translation>Huwag nang tatanungin ulit para sa anumang mga link (hindi inirerekomenda!)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="71"/>
+ <source>Open Browser</source>
+ <translation>Buksan ang Browser</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="83"/>
+ <source>Cancel</source>
+ <translation>Kanselahin</translation>
+ </message>
+</context>
+<context>
+ <name>PreferencesDialog</name>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="12"/>
+ <source>Ricochet Preferences</source>
+ <translation>Kagustuhan sa Ricochet</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="41"/>
+ <source>General</source>
+ <translation>Pangkalahatan</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="46"/>
+ <source>Language</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="51"/>
+ <source>Contacts</source>
+ <translation>Mga Contact</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="56"/>
+ <source>Tor</source>
+ <translation>Tor</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="61"/>
+ <source>About</source>
+ <translation>Patungkol</translation>
+ </message>
+</context>
+<context>
+ <name>QCocoaMenuItem</name>
+ <message>
+ <location filename="../src/ui/qml/MainWindow.qml" line="29"/>
+ <source>Preference</source>
+ <translation>Kagustuhan</translation>
+ </message>
+</context>
+<context>
+ <name>StartupStatusPage</name>
+ <message>
+ <location filename="../src/ui/qml/StartupStatusPage.qml" line="18"/>
+ <source>The Tor process was not started successfully. This is most likely an installation or system error.</source>
+ <translation>Ang proseso ng Tor ay hindi matagumpay na nakapagsimula. Ito ay pinaka-malamang na pagkakamali sa pag-install o pagkakamali sa sistema.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/StartupStatusPage.qml" line="49"/>
+ <source>Quit</source>
+ <translation>Mag-quit</translation>
+ </message>
+</context>
+<context>
+ <name>TorBootstrapStatus</name>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="17"/>
+ <source>Connecting to the Tor network…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Kumokonekta sa Tor network...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="50"/>
+ <source>Back</source>
+ <translation>Bumalik</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="57"/>
+ <source>Hide details</source>
+ <translation>Itago ang mga detalye</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="57"/>
+ <source>Show details</source>
+ <translation>Ipakita ang mga detalye</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="69"/>
+ <source>Done</source>
+ <translation>Tapos na</translation>
+ </message>
+</context>
+<context>
+ <name>TorConfigurationPage</name>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="72"/>
+ <source>Does this computer need a proxy to access the internet?</source>
+ <translation>Kailangan ba ng computer na ito ng proxy upang ma-access ang internet?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="84"/>
+ <source>Proxy type:</source>
+ <translation>Uri ng Proxy:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="89"/>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="91"/>
+ <source>None</source>
+ <translation>Wala</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="106"/>
+ <source>Address:</source>
+ <translation>Address:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="115"/>
+ <source>IP address or hostname</source>
+ <translation>IP address o hostname</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="118"/>
+ <source>Port:</source>
+ <translation>Port:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="129"/>
+ <source>Username:</source>
+ <translation>Username:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="139"/>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="149"/>
+ <source>Optional</source>
+ <translation>Opsyonal</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="142"/>
+ <source>Password:</source>
+ <translation>Password:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="159"/>
+ <source>Does this computer's Internet connection go through a firewall that only allows connections to certain ports?</source>
+ <translation>Pumupunta ba ang koneksyon sa Internet ng computer na ito sa pamamagitan ng isang firewall na nagbibigay-daan lamang ng mga koneksyon sa ilang mga ports?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="170"/>
+ <source>Allowed ports:</source>
+ <translation>Pinayagan na ports:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="177"/>
+ <source>Example: 80,443</source>
+ <translation>Halimbawa: 80, 443</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="188"/>
+ <source>If this computer's Internet connection is censored, you will need to obtain and use bridge relays.</source>
+ <translation>Kung ang koneksyon sa Internet ng computer na ito ay censored, kakailanganin mo na makuha at gumamit ng bridge relays.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="197"/>
+ <source>Enter one or more bridge relays (one per line):</source>
+ <translation>Magpasok ng isa o higit pang mga bridge relays (isa bawat linya):</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="212"/>
+ <source>Back</source>
+ <translation>Bumalik</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="219"/>
+ <source>Connect</source>
+ <translation>Kumonekta</translation>
+ </message>
+</context>
+<context>
+ <name>TorPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="24"/>
+ <source>Running:</source>
+ <translation>Tumatakbo:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="27"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="29"/>
+ <source>Yes</source>
+ <translation>Oo</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="27"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="29"/>
+ <source>No</source>
+ <translation>Hindi</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <source>External</source>
+ <translation>Panlabas</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="26"/>
+ <source>Control connected:</source>
+ <translation>Naakakonekta ang Control:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="28"/>
+ <source>Circuits established:</source>
+ <translation>Naitatag ang Circuits:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="30"/>
+ <source>Hidden service:</source>
+ <translation>Nakatagong serbisyo:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="31"/>
+ <source>Online</source>
+ <translation>Online</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="31"/>
+ <source>Offline</source>
+ <translation>Offline</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="32"/>
+ <source>Version:</source>
+ <translation>Bersyon:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="59"/>
+ <source>Error: <b>%1</b></source>
+ <extracomment>%1 is error message</extracomment>
+ <translation>Pagkakamali: <b>%1</b></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="75"/>
+ <source>Configure</source>
+ <translation>I-configure</translation>
+ </message>
+</context>
+<context>
+ <name>TorStateWidget</name>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="9"/>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="20"/>
+ <source>Connection failed</source>
+ <translation>Nabigo ang koneksyon</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="12"/>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="22"/>
+ <source>Connecting…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Kumukonekta...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="25"/>
+ <source>Connecting… (%1%)</source>
+ <extracomment>%1 is progress percentage, e.g. 100</extracomment>
+ <translation>Kumukonekta… (%1%)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="32"/>
+ <source>Online</source>
+ <translation>Online</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="34"/>
+ <source>Connected</source>
+ <translation>Konektado</translation>
+ </message>
+</context>
+</TS>
diff --git a/translation/ricochet_fr.ts b/translation/ricochet_fr.ts
new file mode 100644
index 0000000..406fa2d
--- /dev/null
+++ b/translation/ricochet_fr.ts
@@ -0,0 +1,694 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="fr">
+<context>
+ <name>AboutPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/AboutPreferences.qml" line="14"/>
+ <source>Ricochet %1</source>
+ <extracomment>%1 version, e.g. 1.0.0</extracomment>
+ <translation>Ricochet %1</translation>
+ </message>
+</context>
+<context>
+ <name>AddContactDialog</name>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="51"/>
+ <source>Share your Ricochet ID to allow connection requests</source>
+ <translation>Partagez votre ID Ricochet pour autoriser les demandes de connexion</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="107"/>
+ <source>Cancel</source>
+ <translation>Annuler</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="112"/>
+ <source>Add</source>
+ <translation>Ajouter</translation>
+ </message>
+</context>
+<context>
+ <name>ContactActions</name>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="40"/>
+ <source>Open Window</source>
+ <translation>Ouvrir la fenêtre</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="44"/>
+ <source>Details...</source>
+ <translation>Détails...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="48"/>
+ <source>Rename</source>
+ <translation>Renommer</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="53"/>
+ <source>Remove</source>
+ <translation>Supprimer</translation>
+ </message>
+</context>
+<context>
+ <name>ContactIDField</name>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="40"/>
+ <source><b>%1</b> is already your contact</source>
+ <translation><b>%1</b> est déjà votre contact</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="42"/>
+ <source>You can't add yourself as a contact</source>
+ <translation>Vous ne pouvez pas vous ajouter vous-même en tant que contact</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="44"/>
+ <source>Enter an ID starting with <b>ricochet:</b></source>
+ <translation>Entrez un ID commençant par <b>ricochet:</b></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="79"/>
+ <source>Copied to clipboard</source>
+ <translation>Copié dans le presse-papier</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="91"/>
+ <source>Copy</source>
+ <translation>Copier</translation>
+ </message>
+</context>
+<context>
+ <name>ContactList</name>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="75"/>
+ <source>Online</source>
+ <translation>Connecté</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="76"/>
+ <source>Offline</source>
+ <translation>Déconnecté</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="77"/>
+ <source>Requests</source>
+ <translation>Requêtes</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="78"/>
+ <source>Rejected</source>
+ <translation>Rejeté</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="79"/>
+ <source>Outdated</source>
+ <translation>Caduc</translation>
+ </message>
+</context>
+<context>
+ <name>ContactPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="97"/>
+ <source>Date added:</source>
+ <translation>Date d'ajout :</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="104"/>
+ <source>Last seen:</source>
+ <translation>Dernière apparition :</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="113"/>
+ <source>Request:</source>
+ <translation>Requête : </translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="122"/>
+ <source>Pending connection</source>
+ <translation>En attente de connexion</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="123"/>
+ <source>Delivered</source>
+ <translation>Délivré</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="124"/>
+ <source>Accepted</source>
+ <translation>Accepté</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="125"/>
+ <source>Error</source>
+ <translation>Erreur</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="126"/>
+ <source>Rejected</source>
+ <translation>Rejeté</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="130"/>
+ <source>%1 (Connected)</source>
+ <extracomment>%1 status, e.g. "Accepted"</extracomment>
+ <translation>%1 (Connecté)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="136"/>
+ <source>Response:</source>
+ <translation>Réponse :</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="158"/>
+ <source>Rename</source>
+ <translation>Renommer</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="165"/>
+ <source>Remove</source>
+ <translation>Supprimer</translation>
+ </message>
+</context>
+<context>
+ <name>ContactRequestDialog</name>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="55"/>
+ <source>Someone new is asking to connect to you</source>
+ <translation>Une nouvelle personne demande à se connecter à vous</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="102"/>
+ <source>Reject</source>
+ <translation>Refuser</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="107"/>
+ <source>Accept</source>
+ <translation>Accepter</translation>
+ </message>
+</context>
+<context>
+ <name>ContactRequestFields</name>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="17"/>
+ <source>ID:</source>
+ <translation>ID :</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="29"/>
+ <source>Name:</source>
+ <translation>Nom : </translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="40"/>
+ <source>Message:</source>
+ <translation>Message : </translation>
+ </message>
+</context>
+<context>
+ <name>GeneralPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="12"/>
+ <source>Use a single window for conversations</source>
+ <translation>Utiliser une seule fenêtre pour les conversations</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="20"/>
+ <source>Open links in default browser without prompting</source>
+ <translation>Ouvrir les hyperliens directement dans le navigateur par défaut sans demander à chaque fois</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="28"/>
+ <source>Play audio notifications</source>
+ <translation>Jouer les notifications sonores</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="37"/>
+ <source>Volume</source>
+ <translation>Volume</translation>
+ </message>
+</context>
+<context>
+ <name>LanguagePreferences</name>
+ <message>
+ <location filename="../src/ui/qml/LanguagePreferences.qml" line="22"/>
+ <source>Select Language</source>
+ <translation>Choisir la langue</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/LanguagePreferences.qml" line="52"/>
+ <source>Restart Ricochet to apply changes</source>
+ <translation>Redémarrer l'application pour applique les modifications</translation>
+ </message>
+</context>
+<context>
+ <name>LanguagesModel</name>
+ <message>
+ <location filename="../src/ui/LanguagesModel.cpp" line="43"/>
+ <source>System default</source>
+ <translation>Par défaut du système </translation>
+ </message>
+</context>
+<context>
+ <name>Main</name>
+ <message>
+ <location filename="../src/main.cpp" line="73"/>
+ <source>Ricochet Error</source>
+ <translation>Erreur de Ricochet</translation>
+ </message>
+</context>
+<context>
+ <name>MainToolBar</name>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="19"/>
+ <source>Add Contact</source>
+ <translation>Ajouter un contact</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="28"/>
+ <source>Preferences</source>
+ <translation>Préférences</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="75"/>
+ <source>Click to add contacts</source>
+ <translation>Cliquez pour ajouter des contacts</translation>
+ </message>
+</context>
+<context>
+ <name>MainWindow</name>
+ <message>
+ <location filename="../src/ui/MainWindow.cpp" line="149"/>
+ <source>Remove %1</source>
+ <translation>Supprimer %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/MainWindow.cpp" line="150"/>
+ <source>Do you want to permanently remove %1?</source>
+ <translation>Voulez-vous supprimer %1 définitivement ?</translation>
+ </message>
+</context>
+<context>
+ <name>MessageDelegate</name>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="26"/>
+ <source>%1 is offline</source>
+ <extracomment>%1 nickname</extracomment>
+ <translation>%1 est hors-ligne</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="134"/>
+ <source>Copy ID</source>
+ <translation>Copier ID</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="134"/>
+ <source>Copy Link</source>
+ <translation>Copier l'hyperlien</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="139"/>
+ <source>Open with Browser</source>
+ <translation>Ouvrir avec le navigateur</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="153"/>
+ <source>Add as Contact</source>
+ <translation>Ajouter comme contact</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="165"/>
+ <source>Copy Message</source>
+ <translation>Copier le message</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="172"/>
+ <source>Copy Selection</source>
+ <translation>Copier la sélection</translation>
+ </message>
+</context>
+<context>
+ <name>MessageDialogWrapper</name>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="7"/>
+ <source>Remove %1</source>
+ <translation>Supprimer %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="9"/>
+ <source>Do you want to permanently remove %1?</source>
+ <extracomment>%1 nickname</extracomment>
+ <translation>Voulez-vous supprimer %1 définitivement ?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="10"/>
+ <source>This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request.</source>
+ <translation>Ce contact ne sera plus en mesure de vous envoyer des messages et sera informé du retrait. Il pourra toutefois vous envoyer une nouvelle demande de connexion.</translation>
+ </message>
+</context>
+<context>
+ <name>NetworkSetupWizard</name>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="100"/>
+ <source>This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network.</source>
+ <translation>La connexion internet de cet ordinateur est libre d'obstacles. Je voudrais me connecter directement au réseau Tor.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="107"/>
+ <source>Connect</source>
+ <translation>Connexion</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="124"/>
+ <source>This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings.</source>
+ <translation>La connexion internet de cet ordinateur est censurée, filtrée, ou derrière un proxy. J'ai besoin de configurer les paramètres réseau.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="131"/>
+ <source>Configure</source>
+ <translation>Configurer</translation>
+ </message>
+</context>
+<context>
+ <name>OfflineStateItem</name>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="107"/>
+ <source>Configure</source>
+ <translation>Configurer</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="115"/>
+ <source>Details</source>
+ <translation>Détails</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="143"/>
+ <source>Connection failed</source>
+ <translation>Connexion échouée</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="169"/>
+ <source>Connecting…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Connexion...</translation>
+ </message>
+</context>
+<context>
+ <name>OpenBrowserDialog</name>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="39"/>
+ <source><b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead.</source>
+ <translation><b>Attention !</b> Ouvrir des hyperliens avec votre navigateur par défaut pourrait nuire à votre sécurité et votre anonymat.<br><br>Vous pouvez <a href='.'>copier dans le presse-papier</a> à la place.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="58"/>
+ <source>Don't ask again for links from %1</source>
+ <translation>Ne plus demander pour des hyperliens provenant de %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="64"/>
+ <source>Don't ask again for any links (not recommended!)</source>
+ <translation>Ne plus demander à nouveau pour tous les hyperliens (non recommandé)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="71"/>
+ <source>Open Browser</source>
+ <translation>Ouvrir le navigateur</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="83"/>
+ <source>Cancel</source>
+ <translation>Annuler</translation>
+ </message>
+</context>
+<context>
+ <name>PreferencesDialog</name>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="12"/>
+ <source>Ricochet Preferences</source>
+ <translation>Préférences de Ricochet</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="41"/>
+ <source>General</source>
+ <translation>Général</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="46"/>
+ <source>Language</source>
+ <translation>Langue</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="51"/>
+ <source>Contacts</source>
+ <translation>Contacts</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="56"/>
+ <source>Tor</source>
+ <translation>Tor</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="61"/>
+ <source>About</source>
+ <translation>À propos</translation>
+ </message>
+</context>
+<context>
+ <name>QCocoaMenuItem</name>
+ <message>
+ <location filename="../src/ui/qml/MainWindow.qml" line="29"/>
+ <source>Preference</source>
+ <translation>Préférences</translation>
+ </message>
+</context>
+<context>
+ <name>StartupStatusPage</name>
+ <message>
+ <location filename="../src/ui/qml/StartupStatusPage.qml" line="18"/>
+ <source>The Tor process was not started successfully. This is most likely an installation or system error.</source>
+ <translation>Le processus Tor n'a pas démarré avec succès. C'est probablement dû à une erreur d'installation ou du système.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/StartupStatusPage.qml" line="49"/>
+ <source>Quit</source>
+ <translation>Quitter</translation>
+ </message>
+</context>
+<context>
+ <name>TorBootstrapStatus</name>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="17"/>
+ <source>Connecting to the Tor network…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Connexion au réseau Tor...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="50"/>
+ <source>Back</source>
+ <translation>Retour</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="57"/>
+ <source>Hide details</source>
+ <translation>Cacher les détails</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="57"/>
+ <source>Show details</source>
+ <translation>Afficher les détails</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="69"/>
+ <source>Done</source>
+ <translation>Terminé</translation>
+ </message>
+</context>
+<context>
+ <name>TorConfigurationPage</name>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="72"/>
+ <source>Does this computer need a proxy to access the internet?</source>
+ <translation>Est-ce que cet ordinateur à besoin d'un proxy pour accéder à Internet ?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="84"/>
+ <source>Proxy type:</source>
+ <translation>Type de proxy :</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="89"/>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="91"/>
+ <source>None</source>
+ <translation>Aucun</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="106"/>
+ <source>Address:</source>
+ <translation>Adresse : </translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="115"/>
+ <source>IP address or hostname</source>
+ <translation>Adresse IP ou nom d'hôte</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="118"/>
+ <source>Port:</source>
+ <translation>Port : </translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="129"/>
+ <source>Username:</source>
+ <translation>Nom d'utilisateur : </translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="139"/>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="149"/>
+ <source>Optional</source>
+ <translation>En option</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="142"/>
+ <source>Password:</source>
+ <translation>Mot de passe : </translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="159"/>
+ <source>Does this computer's Internet connection go through a firewall that only allows connections to certain ports?</source>
+ <translation>Est-ce que la connexion Internet de cet ordinateur passe à travers un pare-feu qui autorise uniquement les connexions à certains ports ?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="170"/>
+ <source>Allowed ports:</source>
+ <translation>Ports autorisés :</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="177"/>
+ <source>Example: 80,443</source>
+ <translation>Exemple : 80,443</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="188"/>
+ <source>If this computer's Internet connection is censored, you will need to obtain and use bridge relays.</source>
+ <translation>Si la connexion Internet de cet ordinateur est censurée, vous allez devoir obtenir et utiliser un pont Tor.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="197"/>
+ <source>Enter one or more bridge relays (one per line):</source>
+ <translation>Entrez un ou plusieurs ponts Tor (un par ligne) :</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="212"/>
+ <source>Back</source>
+ <translation>Retour</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="219"/>
+ <source>Connect</source>
+ <translation>Connexion</translation>
+ </message>
+</context>
+<context>
+ <name>TorPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="24"/>
+ <source>Running:</source>
+ <translation>En cours :</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="27"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="29"/>
+ <source>Yes</source>
+ <translation>Oui</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="27"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="29"/>
+ <source>No</source>
+ <translation>Non</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <source>External</source>
+ <translation>Externe</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="26"/>
+ <source>Control connected:</source>
+ <translation>Contrôle connecté :</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="28"/>
+ <source>Circuits established:</source>
+ <translation>Circuits établis :</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="30"/>
+ <source>Hidden service:</source>
+ <translation>Service caché :</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="31"/>
+ <source>Online</source>
+ <translation>En ligne</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="31"/>
+ <source>Offline</source>
+ <translation>Hors-ligne</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="32"/>
+ <source>Version:</source>
+ <translation>Version :</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="59"/>
+ <source>Error: <b>%1</b></source>
+ <extracomment>%1 is error message</extracomment>
+ <translation>Erreur : <b>%1</b></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="75"/>
+ <source>Configure</source>
+ <translation>Configurer</translation>
+ </message>
+</context>
+<context>
+ <name>TorStateWidget</name>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="9"/>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="20"/>
+ <source>Connection failed</source>
+ <translation>Connexion échouée</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="12"/>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="22"/>
+ <source>Connecting…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Connexion...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="25"/>
+ <source>Connecting… (%1%)</source>
+ <extracomment>%1 is progress percentage, e.g. 100</extracomment>
+ <translation>Connexion… (%1%)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="32"/>
+ <source>Online</source>
+ <translation>En ligne</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="34"/>
+ <source>Connected</source>
+ <translation>Connecté</translation>
+ </message>
+</context>
+</TS>
diff --git a/translation/ricochet_it.ts b/translation/ricochet_it.ts
new file mode 100644
index 0000000..238525e
--- /dev/null
+++ b/translation/ricochet_it.ts
@@ -0,0 +1,694 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="it">
+<context>
+ <name>AboutPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/AboutPreferences.qml" line="14"/>
+ <source>Ricochet %1</source>
+ <extracomment>%1 version, e.g. 1.0.0</extracomment>
+ <translation>Ricochet %1</translation>
+ </message>
+</context>
+<context>
+ <name>AddContactDialog</name>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="51"/>
+ <source>Share your Ricochet ID to allow connection requests</source>
+ <translation>Condividi il tuo Ricochet ID per permettere le richieste di connessione</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="107"/>
+ <source>Cancel</source>
+ <translation>Annulla</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="112"/>
+ <source>Add</source>
+ <translation>Aggiungere</translation>
+ </message>
+</context>
+<context>
+ <name>ContactActions</name>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="40"/>
+ <source>Open Window</source>
+ <translation>Apri Finestra</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="44"/>
+ <source>Details...</source>
+ <translation>Dettagli...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="48"/>
+ <source>Rename</source>
+ <translation>Rinominare</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="53"/>
+ <source>Remove</source>
+ <translation>Rimuovere</translation>
+ </message>
+</context>
+<context>
+ <name>ContactIDField</name>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="40"/>
+ <source><b>%1</b> is already your contact</source>
+ <translation><b>%1</b> è già un tuo contatto</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="42"/>
+ <source>You can't add yourself as a contact</source>
+ <translation>Non puoi aggiungere te stesso come contatto</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="44"/>
+ <source>Enter an ID starting with <b>ricochet:</b></source>
+ <translation>Inserisci un ID che inizi con <b>ricochet:</b></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="79"/>
+ <source>Copied to clipboard</source>
+ <translation>Copiato negli appunti</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="91"/>
+ <source>Copy</source>
+ <translation>Copia</translation>
+ </message>
+</context>
+<context>
+ <name>ContactList</name>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="75"/>
+ <source>Online</source>
+ <translation>Connesso</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="76"/>
+ <source>Offline</source>
+ <translation>Disconnesso</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="77"/>
+ <source>Requests</source>
+ <translation>Richieste</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="78"/>
+ <source>Rejected</source>
+ <translation>Rifiutato</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="79"/>
+ <source>Outdated</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>ContactPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="97"/>
+ <source>Date added:</source>
+ <translation>Data di inserimento:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="104"/>
+ <source>Last seen:</source>
+ <translation>Ultimo accesso:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="113"/>
+ <source>Request:</source>
+ <translation>Richiesta:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="122"/>
+ <source>Pending connection</source>
+ <translation>Connessione in sospeso</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="123"/>
+ <source>Delivered</source>
+ <translation>Consegnato</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="124"/>
+ <source>Accepted</source>
+ <translation>Accettato</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="125"/>
+ <source>Error</source>
+ <translation>Errore</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="126"/>
+ <source>Rejected</source>
+ <translation>Rifiutato</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="130"/>
+ <source>%1 (Connected)</source>
+ <extracomment>%1 status, e.g. "Accepted"</extracomment>
+ <translation>%1 (Collegato)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="136"/>
+ <source>Response:</source>
+ <translation>Risposta:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="158"/>
+ <source>Rename</source>
+ <translation type="unfinished">Rinominare</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="165"/>
+ <source>Remove</source>
+ <translation>Rimuovere</translation>
+ </message>
+</context>
+<context>
+ <name>ContactRequestDialog</name>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="55"/>
+ <source>Someone new is asking to connect to you</source>
+ <translation>Qualcuno di nuovo sta chiedendo di connettersi a te</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="102"/>
+ <source>Reject</source>
+ <translation>Rifiuta</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="107"/>
+ <source>Accept</source>
+ <translation>Accetta</translation>
+ </message>
+</context>
+<context>
+ <name>ContactRequestFields</name>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="17"/>
+ <source>ID:</source>
+ <translation>ID:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="29"/>
+ <source>Name:</source>
+ <translation>Nome:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="40"/>
+ <source>Message:</source>
+ <translation>Messaggio:</translation>
+ </message>
+</context>
+<context>
+ <name>GeneralPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="12"/>
+ <source>Use a single window for conversations</source>
+ <translation>Usa una finestra singola per le conversazioni</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="20"/>
+ <source>Open links in default browser without prompting</source>
+ <translation>Apri link nel browser di default senza chiedere</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="28"/>
+ <source>Play audio notifications</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="37"/>
+ <source>Volume</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>LanguagePreferences</name>
+ <message>
+ <location filename="../src/ui/qml/LanguagePreferences.qml" line="22"/>
+ <source>Select Language</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/LanguagePreferences.qml" line="52"/>
+ <source>Restart Ricochet to apply changes</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>LanguagesModel</name>
+ <message>
+ <location filename="../src/ui/LanguagesModel.cpp" line="43"/>
+ <source>System default</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>Main</name>
+ <message>
+ <location filename="../src/main.cpp" line="73"/>
+ <source>Ricochet Error</source>
+ <translation>Errore Ricochet</translation>
+ </message>
+</context>
+<context>
+ <name>MainToolBar</name>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="19"/>
+ <source>Add Contact</source>
+ <translation>Aggiungere Contatto</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="28"/>
+ <source>Preferences</source>
+ <translation>Preferenze</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="75"/>
+ <source>Click to add contacts</source>
+ <translation>Clicca per aggiungere contatti</translation>
+ </message>
+</context>
+<context>
+ <name>MainWindow</name>
+ <message>
+ <location filename="../src/ui/MainWindow.cpp" line="149"/>
+ <source>Remove %1</source>
+ <translation>Rimuovere %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/MainWindow.cpp" line="150"/>
+ <source>Do you want to permanently remove %1?</source>
+ <translation>Vuoi rimuovere perennemente %1?</translation>
+ </message>
+</context>
+<context>
+ <name>MessageDelegate</name>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="26"/>
+ <source>%1 is offline</source>
+ <extracomment>%1 nickname</extracomment>
+ <translation>%1 è sconnesso</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="134"/>
+ <source>Copy ID</source>
+ <translation>Copia ID</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="134"/>
+ <source>Copy Link</source>
+ <translation>Copia Link</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="139"/>
+ <source>Open with Browser</source>
+ <translation>Apri con il Browser</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="153"/>
+ <source>Add as Contact</source>
+ <translation>Aggiungi come Contatto</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="165"/>
+ <source>Copy Message</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="172"/>
+ <source>Copy Selection</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>MessageDialogWrapper</name>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="7"/>
+ <source>Remove %1</source>
+ <translation>Rimuovere %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="9"/>
+ <source>Do you want to permanently remove %1?</source>
+ <extracomment>%1 nickname</extracomment>
+ <translation>Vuoi rimuovere perennemente %1?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="10"/>
+ <source>This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request.</source>
+ <translation>Questo contatto non sarà più in grado di inviarti messaggi e sarà notificato di questo. Potrebbe decidere di mandare una nuova richiesta.</translation>
+ </message>
+</context>
+<context>
+ <name>NetworkSetupWizard</name>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="100"/>
+ <source>This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network.</source>
+ <translation>La connessione a internet di questo computer è libera da ostacoli. Vorrei connettermi direttamente alla rete Tor.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="107"/>
+ <source>Connect</source>
+ <translation>Collegare</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="124"/>
+ <source>This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings.</source>
+ <translation>La connessione a internet di questo computer è soggetta a censure, filtri o proxy. Devo configurare le impostazioni del network.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="131"/>
+ <source>Configure</source>
+ <translation>Configurare</translation>
+ </message>
+</context>
+<context>
+ <name>OfflineStateItem</name>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="107"/>
+ <source>Configure</source>
+ <translation>Configurare</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="115"/>
+ <source>Details</source>
+ <translation>Dettagli</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="143"/>
+ <source>Connection failed</source>
+ <translation>Connessione fallita</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="169"/>
+ <source>Connecting…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Collegamento...</translation>
+ </message>
+</context>
+<context>
+ <name>OpenBrowserDialog</name>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="39"/>
+ <source><b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead.</source>
+ <translation><b>Attenzione!</b> Aprire link con il tuo browser di default danneggerà la tua sicurezza e anonimia.<br><br>Puoi <a href='.'>copiare negli appunti</a> invece.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="58"/>
+ <source>Don't ask again for links from %1</source>
+ <translation>Non chiedere ancora per link da %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="64"/>
+ <source>Don't ask again for any links (not recommended!)</source>
+ <translation>Non chiedere più per ogni link (non raccomandato)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="71"/>
+ <source>Open Browser</source>
+ <translation>Apri Browser</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="83"/>
+ <source>Cancel</source>
+ <translation>Annulla</translation>
+ </message>
+</context>
+<context>
+ <name>PreferencesDialog</name>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="12"/>
+ <source>Ricochet Preferences</source>
+ <translation>Preferenze Ricochet</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="41"/>
+ <source>General</source>
+ <translation>Generali</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="46"/>
+ <source>Language</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="51"/>
+ <source>Contacts</source>
+ <translation>Contatti</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="56"/>
+ <source>Tor</source>
+ <translation>Tor</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="61"/>
+ <source>About</source>
+ <translation>Riguardo a </translation>
+ </message>
+</context>
+<context>
+ <name>QCocoaMenuItem</name>
+ <message>
+ <location filename="../src/ui/qml/MainWindow.qml" line="29"/>
+ <source>Preference</source>
+ <translation>Preferenza</translation>
+ </message>
+</context>
+<context>
+ <name>StartupStatusPage</name>
+ <message>
+ <location filename="../src/ui/qml/StartupStatusPage.qml" line="18"/>
+ <source>The Tor process was not started successfully. This is most likely an installation or system error.</source>
+ <translation>Tor non è stato avviato correttamente. Probabilmente si tratta di un errore di sistema o di installazione.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/StartupStatusPage.qml" line="49"/>
+ <source>Quit</source>
+ <translation>Esci</translation>
+ </message>
+</context>
+<context>
+ <name>TorBootstrapStatus</name>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="17"/>
+ <source>Connecting to the Tor network…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Collegamento al network Tor...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="50"/>
+ <source>Back</source>
+ <translation>Indietro</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="57"/>
+ <source>Hide details</source>
+ <translation>Nascondi dettagli</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="57"/>
+ <source>Show details</source>
+ <translation>Mostra dettagli</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="69"/>
+ <source>Done</source>
+ <translation>Fatto</translation>
+ </message>
+</context>
+<context>
+ <name>TorConfigurationPage</name>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="72"/>
+ <source>Does this computer need a proxy to access the internet?</source>
+ <translation>Questo computer necessita di un proxy per accedere a internet?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="84"/>
+ <source>Proxy type:</source>
+ <translation>Tipo di proxy:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="89"/>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="91"/>
+ <source>None</source>
+ <translation>Nessuno</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="106"/>
+ <source>Address:</source>
+ <translation>Indirizzo:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="115"/>
+ <source>IP address or hostname</source>
+ <translation>Indirizzo IP o nome host</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="118"/>
+ <source>Port:</source>
+ <translation>Porta:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="129"/>
+ <source>Username:</source>
+ <translation>Nome utente:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="139"/>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="149"/>
+ <source>Optional</source>
+ <translation>Opzionale</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="142"/>
+ <source>Password:</source>
+ <translation>Password:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="159"/>
+ <source>Does this computer's Internet connection go through a firewall that only allows connections to certain ports?</source>
+ <translation>La connessione di questo computer passa attraverso un firewall che permette la connessione solo ad alcune porte?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="170"/>
+ <source>Allowed ports:</source>
+ <translation>Porte permesse:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="177"/>
+ <source>Example: 80,443</source>
+ <translation>Esempio: 80,443</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="188"/>
+ <source>If this computer's Internet connection is censored, you will need to obtain and use bridge relays.</source>
+ <translation>Se la connessione a internet di questo computer è censurata dovrai ottenere ed usare dei bridge relay</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="197"/>
+ <source>Enter one or more bridge relays (one per line):</source>
+ <translation>Inserisci uno o più bridge relay (uno per linea):</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="212"/>
+ <source>Back</source>
+ <translation>Indietro</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="219"/>
+ <source>Connect</source>
+ <translation>Collegare</translation>
+ </message>
+</context>
+<context>
+ <name>TorPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="24"/>
+ <source>Running:</source>
+ <translation>In esecuzione:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="27"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="29"/>
+ <source>Yes</source>
+ <translation>Si</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="27"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="29"/>
+ <source>No</source>
+ <translation>No</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <source>External</source>
+ <translation>Esterno</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="26"/>
+ <source>Control connected:</source>
+ <translation>Controllo connesso:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="28"/>
+ <source>Circuits established:</source>
+ <translation>Circuiti stabiliti:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="30"/>
+ <source>Hidden service:</source>
+ <translation>Hidden service:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="31"/>
+ <source>Online</source>
+ <translation>Connesso</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="31"/>
+ <source>Offline</source>
+ <translation>Disconnesso</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="32"/>
+ <source>Version:</source>
+ <translation>Versione:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="59"/>
+ <source>Error: <b>%1</b></source>
+ <extracomment>%1 is error message</extracomment>
+ <translation>Errore: <b>%1</b></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="75"/>
+ <source>Configure</source>
+ <translation>Configurare</translation>
+ </message>
+</context>
+<context>
+ <name>TorStateWidget</name>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="9"/>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="20"/>
+ <source>Connection failed</source>
+ <translation>Connessione fallita</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="12"/>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="22"/>
+ <source>Connecting…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Collegamento...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="25"/>
+ <source>Connecting… (%1%)</source>
+ <extracomment>%1 is progress percentage, e.g. 100</extracomment>
+ <translation>Collegamento... (%1%)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="32"/>
+ <source>Online</source>
+ <translation>Connesso</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="34"/>
+ <source>Connected</source>
+ <translation>Connesso</translation>
+ </message>
+</context>
+</TS>
diff --git a/translation/ricochet_nl_NL.ts b/translation/ricochet_nl_NL.ts
new file mode 100644
index 0000000..e041990
--- /dev/null
+++ b/translation/ricochet_nl_NL.ts
@@ -0,0 +1,694 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="nl_NL">
+<context>
+ <name>AboutPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/AboutPreferences.qml" line="14"/>
+ <source>Ricochet %1</source>
+ <extracomment>%1 version, e.g. 1.0.0</extracomment>
+ <translation>Ricochet %1</translation>
+ </message>
+</context>
+<context>
+ <name>AddContactDialog</name>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="51"/>
+ <source>Share your Ricochet ID to allow connection requests</source>
+ <translation>Deel je Ricochet ID om verbindingsverzoeken mogelijk te maken</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="107"/>
+ <source>Cancel</source>
+ <translation>Annuleren</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="112"/>
+ <source>Add</source>
+ <translation>Toevoegen</translation>
+ </message>
+</context>
+<context>
+ <name>ContactActions</name>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="40"/>
+ <source>Open Window</source>
+ <translation>Open venster</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="44"/>
+ <source>Details...</source>
+ <translation>Details...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="48"/>
+ <source>Rename</source>
+ <translation>Hernoemen</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="53"/>
+ <source>Remove</source>
+ <translation>Verwijderen</translation>
+ </message>
+</context>
+<context>
+ <name>ContactIDField</name>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="40"/>
+ <source><b>%1</b> is already your contact</source>
+ <translation><b>%1</b> is al een contact</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="42"/>
+ <source>You can't add yourself as a contact</source>
+ <translation>Je kunt jezelf niet als contact toevoegen</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="44"/>
+ <source>Enter an ID starting with <b>ricochet:</b></source>
+ <translation>Geef een ID op dat begint met <b>ricochet:</b></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="79"/>
+ <source>Copied to clipboard</source>
+ <translation>Gekopieerd naar klembord</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="91"/>
+ <source>Copy</source>
+ <translation>Kopieer</translation>
+ </message>
+</context>
+<context>
+ <name>ContactList</name>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="75"/>
+ <source>Online</source>
+ <translation>Online</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="76"/>
+ <source>Offline</source>
+ <translation>Offline</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="77"/>
+ <source>Requests</source>
+ <translation>Aanvragen</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="78"/>
+ <source>Rejected</source>
+ <translation>Afgewezen</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="79"/>
+ <source>Outdated</source>
+ <translation>Verouderd</translation>
+ </message>
+</context>
+<context>
+ <name>ContactPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="97"/>
+ <source>Date added:</source>
+ <translation>Datum toegevoegd:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="104"/>
+ <source>Last seen:</source>
+ <translation>Laatst gezien:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="113"/>
+ <source>Request:</source>
+ <translation>Aanvraag:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="122"/>
+ <source>Pending connection</source>
+ <translation>Verbinding wachtend</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="123"/>
+ <source>Delivered</source>
+ <translation>Afgeleverd</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="124"/>
+ <source>Accepted</source>
+ <translation>Geaccepteerd</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="125"/>
+ <source>Error</source>
+ <translation>Fout</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="126"/>
+ <source>Rejected</source>
+ <translation>Afgewezen</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="130"/>
+ <source>%1 (Connected)</source>
+ <extracomment>%1 status, e.g. "Accepted"</extracomment>
+ <translation>%1 (verbonden)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="136"/>
+ <source>Response:</source>
+ <translation>Antwoord:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="158"/>
+ <source>Rename</source>
+ <translation>Hernoemen</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="165"/>
+ <source>Remove</source>
+ <translation>Verwijder</translation>
+ </message>
+</context>
+<context>
+ <name>ContactRequestDialog</name>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="55"/>
+ <source>Someone new is asking to connect to you</source>
+ <translation>Een nieuwe contactpersoon wil met je verbinden</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="102"/>
+ <source>Reject</source>
+ <translation>Afwijzen</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="107"/>
+ <source>Accept</source>
+ <translation>Accepteren</translation>
+ </message>
+</context>
+<context>
+ <name>ContactRequestFields</name>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="17"/>
+ <source>ID:</source>
+ <translation>ID:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="29"/>
+ <source>Name:</source>
+ <translation>Naam:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="40"/>
+ <source>Message:</source>
+ <translation>Bericht:</translation>
+ </message>
+</context>
+<context>
+ <name>GeneralPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="12"/>
+ <source>Use a single window for conversations</source>
+ <translation>Gebruik een enkel venster voor gesprekken</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="20"/>
+ <source>Open links in default browser without prompting</source>
+ <translation>Open links in standaardbrowser zonder te vragen </translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="28"/>
+ <source>Play audio notifications</source>
+ <translation>Afspelen audio meldingen</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="37"/>
+ <source>Volume</source>
+ <translation>Volume</translation>
+ </message>
+</context>
+<context>
+ <name>LanguagePreferences</name>
+ <message>
+ <location filename="../src/ui/qml/LanguagePreferences.qml" line="22"/>
+ <source>Select Language</source>
+ <translation>Kies taal</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/LanguagePreferences.qml" line="52"/>
+ <source>Restart Ricochet to apply changes</source>
+ <translation>Herstart Ricochet om wijzigingen door te voeren</translation>
+ </message>
+</context>
+<context>
+ <name>LanguagesModel</name>
+ <message>
+ <location filename="../src/ui/LanguagesModel.cpp" line="43"/>
+ <source>System default</source>
+ <translation>Systeemstandaard</translation>
+ </message>
+</context>
+<context>
+ <name>Main</name>
+ <message>
+ <location filename="../src/main.cpp" line="73"/>
+ <source>Ricochet Error</source>
+ <translation>Ricochet fout</translation>
+ </message>
+</context>
+<context>
+ <name>MainToolBar</name>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="19"/>
+ <source>Add Contact</source>
+ <translation>Toevoegen contact</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="28"/>
+ <source>Preferences</source>
+ <translation>Voorkeuren</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="75"/>
+ <source>Click to add contacts</source>
+ <translation>Klikken om contacten toe te voegen</translation>
+ </message>
+</context>
+<context>
+ <name>MainWindow</name>
+ <message>
+ <location filename="../src/ui/MainWindow.cpp" line="149"/>
+ <source>Remove %1</source>
+ <translation>Verwijder %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/MainWindow.cpp" line="150"/>
+ <source>Do you want to permanently remove %1?</source>
+ <translation>Wil je %1 permanent verwijderen?</translation>
+ </message>
+</context>
+<context>
+ <name>MessageDelegate</name>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="26"/>
+ <source>%1 is offline</source>
+ <extracomment>%1 nickname</extracomment>
+ <translation>%1 is offline</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="134"/>
+ <source>Copy ID</source>
+ <translation>Kopieer ID</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="134"/>
+ <source>Copy Link</source>
+ <translation>Kopieer link</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="139"/>
+ <source>Open with Browser</source>
+ <translation>Open met browser</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="153"/>
+ <source>Add as Contact</source>
+ <translation>Toevoegen als contact</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="165"/>
+ <source>Copy Message</source>
+ <translation>Kopiëren bericht</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="172"/>
+ <source>Copy Selection</source>
+ <translation>Kopiëren selectie</translation>
+ </message>
+</context>
+<context>
+ <name>MessageDialogWrapper</name>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="7"/>
+ <source>Remove %1</source>
+ <translation>Verwijder %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="9"/>
+ <source>Do you want to permanently remove %1?</source>
+ <extracomment>%1 nickname</extracomment>
+ <translation>Wil je %1 permanent verwijderen?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="10"/>
+ <source>This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request.</source>
+ <translation>Deze contactpersoon kan je niet langer berichten sturen en wordt geïnformeerd over het verwijderen. Hij/zij kan een nieuwe aanvraag doen.</translation>
+ </message>
+</context>
+<context>
+ <name>NetworkSetupWizard</name>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="100"/>
+ <source>This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network.</source>
+ <translation>De internetverbinding van deze computer kent geen belemmeringen. Ik wil rechtstreeks verbinden met het TOR netwerk.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="107"/>
+ <source>Connect</source>
+ <translation>Verbinden</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="124"/>
+ <source>This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings.</source>
+ <translation>De internetverbinding van deze computer is gecensureerd, gefilterd of geproxied. Ik moet de netwerkinstellingen configureren.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="131"/>
+ <source>Configure</source>
+ <translation>Configureren</translation>
+ </message>
+</context>
+<context>
+ <name>OfflineStateItem</name>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="107"/>
+ <source>Configure</source>
+ <translation>Configureren</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="115"/>
+ <source>Details</source>
+ <translation>Details</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="143"/>
+ <source>Connection failed</source>
+ <translation>Verbinding mislukt</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="169"/>
+ <source>Connecting…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Verbinden...</translation>
+ </message>
+</context>
+<context>
+ <name>OpenBrowserDialog</name>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="39"/>
+ <source><b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead.</source>
+ <translation><b>Waarschuwing!</b> Het openen van links met je standaard browser schaadt je beveliging en anonimiteit.<br><br>Je kunt in plaats daarvan <a href='.'>kopiëren maar het klembord</a>.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="58"/>
+ <source>Don't ask again for links from %1</source>
+ <translation>Vraag niet opnieuw om links van %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="64"/>
+ <source>Don't ask again for any links (not recommended!)</source>
+ <translation>Vraag niet meer naar links (niet aanbevolen!)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="71"/>
+ <source>Open Browser</source>
+ <translation>Open Browser</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="83"/>
+ <source>Cancel</source>
+ <translation>Annuleren</translation>
+ </message>
+</context>
+<context>
+ <name>PreferencesDialog</name>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="12"/>
+ <source>Ricochet Preferences</source>
+ <translation>Ricochet voorkeuren</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="41"/>
+ <source>General</source>
+ <translation>Algemeen</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="46"/>
+ <source>Language</source>
+ <translation>Taal</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="51"/>
+ <source>Contacts</source>
+ <translation>Contacten</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="56"/>
+ <source>Tor</source>
+ <translation>Tor</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="61"/>
+ <source>About</source>
+ <translation>Over</translation>
+ </message>
+</context>
+<context>
+ <name>QCocoaMenuItem</name>
+ <message>
+ <location filename="../src/ui/qml/MainWindow.qml" line="29"/>
+ <source>Preference</source>
+ <translation>Voorkeur</translation>
+ </message>
+</context>
+<context>
+ <name>StartupStatusPage</name>
+ <message>
+ <location filename="../src/ui/qml/StartupStatusPage.qml" line="18"/>
+ <source>The Tor process was not started successfully. This is most likely an installation or system error.</source>
+ <translation>Het TOR proces is niet succesvol gestart. Dit komt waarschijnlijk door een installatie- of systeemfout.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/StartupStatusPage.qml" line="49"/>
+ <source>Quit</source>
+ <translation>Afsluiten</translation>
+ </message>
+</context>
+<context>
+ <name>TorBootstrapStatus</name>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="17"/>
+ <source>Connecting to the Tor network…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Verbinden met het TOR netwerk...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="50"/>
+ <source>Back</source>
+ <translation>Terug</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="57"/>
+ <source>Hide details</source>
+ <translation>Verberg details</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="57"/>
+ <source>Show details</source>
+ <translation>Toon details</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="69"/>
+ <source>Done</source>
+ <translation>Gedaan</translation>
+ </message>
+</context>
+<context>
+ <name>TorConfigurationPage</name>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="72"/>
+ <source>Does this computer need a proxy to access the internet?</source>
+ <translation>Heeft deze computer een proxy nodig om te verbinden met het internet?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="84"/>
+ <source>Proxy type:</source>
+ <translation>Proxy type:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="89"/>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="91"/>
+ <source>None</source>
+ <translation>Geen</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="106"/>
+ <source>Address:</source>
+ <translation>Adres:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="115"/>
+ <source>IP address or hostname</source>
+ <translation>IP adres of servernaam</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="118"/>
+ <source>Port:</source>
+ <translation>Poort:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="129"/>
+ <source>Username:</source>
+ <translation>Gebruikersnaam:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="139"/>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="149"/>
+ <source>Optional</source>
+ <translation>Optioneel</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="142"/>
+ <source>Password:</source>
+ <translation>Wachtwoord:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="159"/>
+ <source>Does this computer's Internet connection go through a firewall that only allows connections to certain ports?</source>
+ <translation>Gaat de internetverbinding van deze computer door een firewall die alleen verbindingen naar bepaalde poorten toestaat?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="170"/>
+ <source>Allowed ports:</source>
+ <translation>Toegestane poorten:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="177"/>
+ <source>Example: 80,443</source>
+ <translation>Voorbeeld: 80,443</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="188"/>
+ <source>If this computer's Internet connection is censored, you will need to obtain and use bridge relays.</source>
+ <translation>Als de internetverbinding van deze computer is gecensureerd, moet je bridge relays vinden en gebruiken.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="197"/>
+ <source>Enter one or more bridge relays (one per line):</source>
+ <translation>Geen een of meer bridge relays op (een per regel):</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="212"/>
+ <source>Back</source>
+ <translation>Terug</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="219"/>
+ <source>Connect</source>
+ <translation>Verbinden</translation>
+ </message>
+</context>
+<context>
+ <name>TorPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="24"/>
+ <source>Running:</source>
+ <translation>Draaiend:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="27"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="29"/>
+ <source>Yes</source>
+ <translation>Ja</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="27"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="29"/>
+ <source>No</source>
+ <translation>Nee</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <source>External</source>
+ <translation>Extern</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="26"/>
+ <source>Control connected:</source>
+ <translation>Control verbonden:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="28"/>
+ <source>Circuits established:</source>
+ <translation>Circuits aangelegd:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="30"/>
+ <source>Hidden service:</source>
+ <translation>Verborgen service:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="31"/>
+ <source>Online</source>
+ <translation>Online</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="31"/>
+ <source>Offline</source>
+ <translation>Offline</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="32"/>
+ <source>Version:</source>
+ <translation>Versie:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="59"/>
+ <source>Error: <b>%1</b></source>
+ <extracomment>%1 is error message</extracomment>
+ <translation>Fout: <b>%1</b></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="75"/>
+ <source>Configure</source>
+ <translation>Configureren</translation>
+ </message>
+</context>
+<context>
+ <name>TorStateWidget</name>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="9"/>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="20"/>
+ <source>Connection failed</source>
+ <translation>Verbinding mislukt</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="12"/>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="22"/>
+ <source>Connecting…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Verbinden...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="25"/>
+ <source>Connecting… (%1%)</source>
+ <extracomment>%1 is progress percentage, e.g. 100</extracomment>
+ <translation>Verbinden… (%1%)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="32"/>
+ <source>Online</source>
+ <translation>Online</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="34"/>
+ <source>Connected</source>
+ <translation>Verbonden</translation>
+ </message>
+</context>
+</TS>
diff --git a/translation/ricochet_pl.ts b/translation/ricochet_pl.ts
new file mode 100644
index 0000000..2378cdd
--- /dev/null
+++ b/translation/ricochet_pl.ts
@@ -0,0 +1,694 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="pl">
+<context>
+ <name>AboutPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/AboutPreferences.qml" line="14"/>
+ <source>Ricochet %1</source>
+ <extracomment>%1 version, e.g. 1.0.0</extracomment>
+ <translation>Ricochet %1</translation>
+ </message>
+</context>
+<context>
+ <name>AddContactDialog</name>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="51"/>
+ <source>Share your Ricochet ID to allow connection requests</source>
+ <translation>Udostępnij swój identyfikator Ricochet, aby umożliwić łączenie się z Tobą.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="107"/>
+ <source>Cancel</source>
+ <translation>Anuluj</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="112"/>
+ <source>Add</source>
+ <translation>Dodaj</translation>
+ </message>
+</context>
+<context>
+ <name>ContactActions</name>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="40"/>
+ <source>Open Window</source>
+ <translation>Otwórz okno</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="44"/>
+ <source>Details...</source>
+ <translation>Szczegóły...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="48"/>
+ <source>Rename</source>
+ <translation>Zmień nazwę</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="53"/>
+ <source>Remove</source>
+ <translation>Usuń</translation>
+ </message>
+</context>
+<context>
+ <name>ContactIDField</name>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="40"/>
+ <source><b>%1</b> is already your contact</source>
+ <translation><b>%1</b> jest już w Twoich kontaktach</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="42"/>
+ <source>You can't add yourself as a contact</source>
+ <translation>Nie możesz dodać siebie do kontaktów</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="44"/>
+ <source>Enter an ID starting with <b>ricochet:</b></source>
+ <translation>Wpisz ID zaczynające się od <b>ricochet:</b></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="79"/>
+ <source>Copied to clipboard</source>
+ <translation>Skopiowano do schowka</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="91"/>
+ <source>Copy</source>
+ <translation>Kopiuj</translation>
+ </message>
+</context>
+<context>
+ <name>ContactList</name>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="75"/>
+ <source>Online</source>
+ <translation>Połączeni</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="76"/>
+ <source>Offline</source>
+ <translation>Rozłączeni</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="77"/>
+ <source>Requests</source>
+ <translation>Prośby o dodanie</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="78"/>
+ <source>Rejected</source>
+ <translation>Odrzucone</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="79"/>
+ <source>Outdated</source>
+ <translation>Przeterminowane</translation>
+ </message>
+</context>
+<context>
+ <name>ContactPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="97"/>
+ <source>Date added:</source>
+ <translation>Dołączył:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="104"/>
+ <source>Last seen:</source>
+ <translation>Ostatnio widziany:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="113"/>
+ <source>Request:</source>
+ <translation>Prośba o dodanie:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="122"/>
+ <source>Pending connection</source>
+ <translation>Oczekuje na połączenie</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="123"/>
+ <source>Delivered</source>
+ <translation>Dostarczony</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="124"/>
+ <source>Accepted</source>
+ <translation>Zaakceptowany</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="125"/>
+ <source>Error</source>
+ <translation>Błąd</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="126"/>
+ <source>Rejected</source>
+ <translation>Odrzucony</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="130"/>
+ <source>%1 (Connected)</source>
+ <extracomment>%1 status, e.g. "Accepted"</extracomment>
+ <translation>%1 (Połączony)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="136"/>
+ <source>Response:</source>
+ <translation>Odpowiedź:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="158"/>
+ <source>Rename</source>
+ <translation>Zmień nazwę</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="165"/>
+ <source>Remove</source>
+ <translation>Usuń</translation>
+ </message>
+</context>
+<context>
+ <name>ContactRequestDialog</name>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="55"/>
+ <source>Someone new is asking to connect to you</source>
+ <translation>Ktoś chce się z Tobą skontaktować</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="102"/>
+ <source>Reject</source>
+ <translation>Odrzuć</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="107"/>
+ <source>Accept</source>
+ <translation>Akceptuj</translation>
+ </message>
+</context>
+<context>
+ <name>ContactRequestFields</name>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="17"/>
+ <source>ID:</source>
+ <translation>ID:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="29"/>
+ <source>Name:</source>
+ <translation>Nazwa:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="40"/>
+ <source>Message:</source>
+ <translation>Wiadomość:</translation>
+ </message>
+</context>
+<context>
+ <name>GeneralPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="12"/>
+ <source>Use a single window for conversations</source>
+ <translation>Używaj pojedynczego okno do rozmów</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="20"/>
+ <source>Open links in default browser without prompting</source>
+ <translation>Otwórz linki w domyślnej przeglądarce bez pytania</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="28"/>
+ <source>Play audio notifications</source>
+ <translation>Włącz powiadomienia dźwiękowe</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="37"/>
+ <source>Volume</source>
+ <translation>Głośność</translation>
+ </message>
+</context>
+<context>
+ <name>LanguagePreferences</name>
+ <message>
+ <location filename="../src/ui/qml/LanguagePreferences.qml" line="22"/>
+ <source>Select Language</source>
+ <translation>Wybierz język</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/LanguagePreferences.qml" line="52"/>
+ <source>Restart Ricochet to apply changes</source>
+ <translation>Aby zaakceptować zmiany, uruchom Ricochet ponownie</translation>
+ </message>
+</context>
+<context>
+ <name>LanguagesModel</name>
+ <message>
+ <location filename="../src/ui/LanguagesModel.cpp" line="43"/>
+ <source>System default</source>
+ <translation>Domyślny w systemie</translation>
+ </message>
+</context>
+<context>
+ <name>Main</name>
+ <message>
+ <location filename="../src/main.cpp" line="73"/>
+ <source>Ricochet Error</source>
+ <translation>Błąd programu Ricochet</translation>
+ </message>
+</context>
+<context>
+ <name>MainToolBar</name>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="19"/>
+ <source>Add Contact</source>
+ <translation>Dodaj kontakt</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="28"/>
+ <source>Preferences</source>
+ <translation>Ustawienia</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="75"/>
+ <source>Click to add contacts</source>
+ <translation>Kliknij, aby dodać do kontaktów</translation>
+ </message>
+</context>
+<context>
+ <name>MainWindow</name>
+ <message>
+ <location filename="../src/ui/MainWindow.cpp" line="149"/>
+ <source>Remove %1</source>
+ <translation>Usuń %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/MainWindow.cpp" line="150"/>
+ <source>Do you want to permanently remove %1?</source>
+ <translation>Czy chcesz na stałe usunąć %1?</translation>
+ </message>
+</context>
+<context>
+ <name>MessageDelegate</name>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="26"/>
+ <source>%1 is offline</source>
+ <extracomment>%1 nickname</extracomment>
+ <translation>%1 jest rozłączony</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="134"/>
+ <source>Copy ID</source>
+ <translation>Skopiuj ID</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="134"/>
+ <source>Copy Link</source>
+ <translation>Skopiuj link</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="139"/>
+ <source>Open with Browser</source>
+ <translation>Otwórz w przeglądarce</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="153"/>
+ <source>Add as Contact</source>
+ <translation>Dodaj jako kontakt</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="165"/>
+ <source>Copy Message</source>
+ <translation>Kopiuj wiadomość</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="172"/>
+ <source>Copy Selection</source>
+ <translation>Kopiuj zaznaczenie</translation>
+ </message>
+</context>
+<context>
+ <name>MessageDialogWrapper</name>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="7"/>
+ <source>Remove %1</source>
+ <translation>Usuń %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="9"/>
+ <source>Do you want to permanently remove %1?</source>
+ <extracomment>%1 nickname</extracomment>
+ <translation>Czy chcesz na stałe usunąć %1?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="10"/>
+ <source>This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request.</source>
+ <translation>Ten kontakt nie będzie mógł więcej wysyłać do Ciebie wiadomości i zostanie poinformowany o usunięciu. Może wysłać Ci nową prośbę o połączenie.</translation>
+ </message>
+</context>
+<context>
+ <name>NetworkSetupWizard</name>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="100"/>
+ <source>This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network.</source>
+ <translation>Połączenie internetowe tego kompuera jest wolne od przeszkód. Chcę połączyć się bezpośrednio z siecią Tor.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="107"/>
+ <source>Connect</source>
+ <translation>Połącz</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="124"/>
+ <source>This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings.</source>
+ <translation>Połączenie internetowe tego komputera jest cenzurowane, filtrowane lub trasowane przez serwer proxy. Muszę skonfigurować ustawienia sieciowe.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="131"/>
+ <source>Configure</source>
+ <translation>Konfiguruj</translation>
+ </message>
+</context>
+<context>
+ <name>OfflineStateItem</name>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="107"/>
+ <source>Configure</source>
+ <translation>Konfiguruj</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="115"/>
+ <source>Details</source>
+ <translation>Szczegóły</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="143"/>
+ <source>Connection failed</source>
+ <translation>Połączenie nie powiodło się</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="169"/>
+ <source>Connecting…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Łączenie…</translation>
+ </message>
+</context>
+<context>
+ <name>OpenBrowserDialog</name>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="39"/>
+ <source><b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead.</source>
+ <translation><b>Uwaga!</b> Otwieranie linków za pomocą domyślnej przeglądarki spowoduje utratę bezpieczeństwa i anonimowości. Zamiast tego możesz <a href='.'>skopiować go do schowka</a></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="58"/>
+ <source>Don't ask again for links from %1</source>
+ <translation>Nie pytaj ponownie dla linków od %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="64"/>
+ <source>Don't ask again for any links (not recommended!)</source>
+ <translation>Nigdy nie pytaj ponownie (niezalecane!)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="71"/>
+ <source>Open Browser</source>
+ <translation>Otwórz przeglądarkę</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="83"/>
+ <source>Cancel</source>
+ <translation>Anuluj</translation>
+ </message>
+</context>
+<context>
+ <name>PreferencesDialog</name>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="12"/>
+ <source>Ricochet Preferences</source>
+ <translation>Ustawienia Ricocheta</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="41"/>
+ <source>General</source>
+ <translation>Główne</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="46"/>
+ <source>Language</source>
+ <translation>Język</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="51"/>
+ <source>Contacts</source>
+ <translation>Kontakty</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="56"/>
+ <source>Tor</source>
+ <translation>Tor</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="61"/>
+ <source>About</source>
+ <translation>O programie</translation>
+ </message>
+</context>
+<context>
+ <name>QCocoaMenuItem</name>
+ <message>
+ <location filename="../src/ui/qml/MainWindow.qml" line="29"/>
+ <source>Preference</source>
+ <translation>Ustawienie</translation>
+ </message>
+</context>
+<context>
+ <name>StartupStatusPage</name>
+ <message>
+ <location filename="../src/ui/qml/StartupStatusPage.qml" line="18"/>
+ <source>The Tor process was not started successfully. This is most likely an installation or system error.</source>
+ <translation>Tor nie został uruchomiony poprawnie. Najprawdopodobniej oznacza to błąd instalacji lub systemu.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/StartupStatusPage.qml" line="49"/>
+ <source>Quit</source>
+ <translation>Wyjdź</translation>
+ </message>
+</context>
+<context>
+ <name>TorBootstrapStatus</name>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="17"/>
+ <source>Connecting to the Tor network…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Łączenie z siecią Tor…</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="50"/>
+ <source>Back</source>
+ <translation>Wróć</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="57"/>
+ <source>Hide details</source>
+ <translation>Ukryj szczegóły</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="57"/>
+ <source>Show details</source>
+ <translation>Pokaż szczegóły</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="69"/>
+ <source>Done</source>
+ <translation>Zrobione</translation>
+ </message>
+</context>
+<context>
+ <name>TorConfigurationPage</name>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="72"/>
+ <source>Does this computer need a proxy to access the internet?</source>
+ <translation>Czy ten komputer potrzebuje serwera proxy, aby łączyć się z Internetem?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="84"/>
+ <source>Proxy type:</source>
+ <translation>Typ proxy:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="89"/>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="91"/>
+ <source>None</source>
+ <translation>Brak</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="106"/>
+ <source>Address:</source>
+ <translation>Adres:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="115"/>
+ <source>IP address or hostname</source>
+ <translation>Adres IP lub nazwa hosta</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="118"/>
+ <source>Port:</source>
+ <translation>Port:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="129"/>
+ <source>Username:</source>
+ <translation>Nazwa użytkownika:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="139"/>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="149"/>
+ <source>Optional</source>
+ <translation>Opcjonalny</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="142"/>
+ <source>Password:</source>
+ <translation>Hasło:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="159"/>
+ <source>Does this computer's Internet connection go through a firewall that only allows connections to certain ports?</source>
+ <translation>Czy połączenie internetowe tego komputera jest kierowane przez firewall, który pozwala na łączenie się tylko z wybranymi portami?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="170"/>
+ <source>Allowed ports:</source>
+ <translation>Dozwolone porty:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="177"/>
+ <source>Example: 80,443</source>
+ <translation>Na przykład: 80,443</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="188"/>
+ <source>If this computer's Internet connection is censored, you will need to obtain and use bridge relays.</source>
+ <translation>Jeżeli połączenie internetowe tego komputera jest cenzurowane, będziesz musiał uzyskać dostęp do węzła pośredniego (<i>bridge relay</i>).</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="197"/>
+ <source>Enter one or more bridge relays (one per line):</source>
+ <translation>Podaj jeden lub więcej węzłów pośrednich (jeden w linii):</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="212"/>
+ <source>Back</source>
+ <translation>Wróć</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="219"/>
+ <source>Connect</source>
+ <translation>Połącz</translation>
+ </message>
+</context>
+<context>
+ <name>TorPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="24"/>
+ <source>Running:</source>
+ <translation>Uruchomione:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="27"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="29"/>
+ <source>Yes</source>
+ <translation>Tak</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="27"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="29"/>
+ <source>No</source>
+ <translation>Nie</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <source>External</source>
+ <translation>Zewnętrzny</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="26"/>
+ <source>Control connected:</source>
+ <translation>Połączony z portem kontrolnym:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="28"/>
+ <source>Circuits established:</source>
+ <translation>Obwody utworzone:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="30"/>
+ <source>Hidden service:</source>
+ <translation>Ukryte usługi:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="31"/>
+ <source>Online</source>
+ <translation>Połączony</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="31"/>
+ <source>Offline</source>
+ <translation>Rozłączony</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="32"/>
+ <source>Version:</source>
+ <translation>Wersja:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="59"/>
+ <source>Error: <b>%1</b></source>
+ <extracomment>%1 is error message</extracomment>
+ <translation>Błąd: <b>%1</b></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="75"/>
+ <source>Configure</source>
+ <translation>Konfiguruj</translation>
+ </message>
+</context>
+<context>
+ <name>TorStateWidget</name>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="9"/>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="20"/>
+ <source>Connection failed</source>
+ <translation>Połączenie nie powiodło się</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="12"/>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="22"/>
+ <source>Connecting…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Łączenie…</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="25"/>
+ <source>Connecting… (%1%)</source>
+ <extracomment>%1 is progress percentage, e.g. 100</extracomment>
+ <translation>Łączenie… (%1%)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="32"/>
+ <source>Online</source>
+ <translation>Połączony</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="34"/>
+ <source>Connected</source>
+ <translation>Połączony</translation>
+ </message>
+</context>
+</TS>
diff --git a/translation/ricochet_pt_BR.ts b/translation/ricochet_pt_BR.ts
new file mode 100644
index 0000000..d1f6af0
--- /dev/null
+++ b/translation/ricochet_pt_BR.ts
@@ -0,0 +1,694 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="pt_BR">
+<context>
+ <name>AboutPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/AboutPreferences.qml" line="14"/>
+ <source>Ricochet %1</source>
+ <extracomment>%1 version, e.g. 1.0.0</extracomment>
+ <translation>Ricochet %1</translation>
+ </message>
+</context>
+<context>
+ <name>AddContactDialog</name>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="51"/>
+ <source>Share your Ricochet ID to allow connection requests</source>
+ <translation>Compartilhe seu ID Ricochet para permitir pedidos de conexão</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="107"/>
+ <source>Cancel</source>
+ <translation>Cancelar</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="112"/>
+ <source>Add</source>
+ <translation>Adicionar</translation>
+ </message>
+</context>
+<context>
+ <name>ContactActions</name>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="40"/>
+ <source>Open Window</source>
+ <translation>Abrir Janela</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="44"/>
+ <source>Details...</source>
+ <translation>Detalhes...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="48"/>
+ <source>Rename</source>
+ <translation>Renomear</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="53"/>
+ <source>Remove</source>
+ <translation>Remover</translation>
+ </message>
+</context>
+<context>
+ <name>ContactIDField</name>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="40"/>
+ <source><b>%1</b> is already your contact</source>
+ <translation><b>%1</b> já é seu contato</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="42"/>
+ <source>You can't add yourself as a contact</source>
+ <translation>Você não pode adicionar a si mesmo como um contato</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="44"/>
+ <source>Enter an ID starting with <b>ricochet:</b></source>
+ <translation>Entre uma ID começando com <b>ricochet:</b></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="79"/>
+ <source>Copied to clipboard</source>
+ <translation>Copiado para a área de transferência</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="91"/>
+ <source>Copy</source>
+ <translation>Copiar</translation>
+ </message>
+</context>
+<context>
+ <name>ContactList</name>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="75"/>
+ <source>Online</source>
+ <translation>Online</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="76"/>
+ <source>Offline</source>
+ <translation>Offline</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="77"/>
+ <source>Requests</source>
+ <translation>Pedidos</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="78"/>
+ <source>Rejected</source>
+ <translation>Rejeitado</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="79"/>
+ <source>Outdated</source>
+ <translation>Desatualizado</translation>
+ </message>
+</context>
+<context>
+ <name>ContactPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="97"/>
+ <source>Date added:</source>
+ <translation>Adicionado em:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="104"/>
+ <source>Last seen:</source>
+ <translation>Visto pela última vez:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="113"/>
+ <source>Request:</source>
+ <translation>Pedido:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="122"/>
+ <source>Pending connection</source>
+ <translation>Conexão pendente</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="123"/>
+ <source>Delivered</source>
+ <translation>Entregue</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="124"/>
+ <source>Accepted</source>
+ <translation>Aceito</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="125"/>
+ <source>Error</source>
+ <translation>Erro</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="126"/>
+ <source>Rejected</source>
+ <translation>Rejeitado</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="130"/>
+ <source>%1 (Connected)</source>
+ <extracomment>%1 status, e.g. "Accepted"</extracomment>
+ <translation>%1 (Conectado)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="136"/>
+ <source>Response:</source>
+ <translation>Resposta:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="158"/>
+ <source>Rename</source>
+ <translation>Renomear</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="165"/>
+ <source>Remove</source>
+ <translation>Remover</translation>
+ </message>
+</context>
+<context>
+ <name>ContactRequestDialog</name>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="55"/>
+ <source>Someone new is asking to connect to you</source>
+ <translation>Alguém novo está pedindo para se conectar com você</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="102"/>
+ <source>Reject</source>
+ <translation>Rejeitar</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="107"/>
+ <source>Accept</source>
+ <translation>Aceitar</translation>
+ </message>
+</context>
+<context>
+ <name>ContactRequestFields</name>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="17"/>
+ <source>ID:</source>
+ <translation>ID:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="29"/>
+ <source>Name:</source>
+ <translation>Nome:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="40"/>
+ <source>Message:</source>
+ <translation>Mensagem:</translation>
+ </message>
+</context>
+<context>
+ <name>GeneralPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="12"/>
+ <source>Use a single window for conversations</source>
+ <translation>Usar somente uma janela para conversas</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="20"/>
+ <source>Open links in default browser without prompting</source>
+ <translation>Abrir links no navegador padrão sem pedir confirmação</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="28"/>
+ <source>Play audio notifications</source>
+ <translation>Tocar notificações de áudio</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="37"/>
+ <source>Volume</source>
+ <translation>Volume</translation>
+ </message>
+</context>
+<context>
+ <name>LanguagePreferences</name>
+ <message>
+ <location filename="../src/ui/qml/LanguagePreferences.qml" line="22"/>
+ <source>Select Language</source>
+ <translation>Selecionar Linguagem</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/LanguagePreferences.qml" line="52"/>
+ <source>Restart Ricochet to apply changes</source>
+ <translation>Reiniciar Ricochet para aplicar mudanças</translation>
+ </message>
+</context>
+<context>
+ <name>LanguagesModel</name>
+ <message>
+ <location filename="../src/ui/LanguagesModel.cpp" line="43"/>
+ <source>System default</source>
+ <translation>Padrão do sistema</translation>
+ </message>
+</context>
+<context>
+ <name>Main</name>
+ <message>
+ <location filename="../src/main.cpp" line="73"/>
+ <source>Ricochet Error</source>
+ <translation>Erro do Ricochet</translation>
+ </message>
+</context>
+<context>
+ <name>MainToolBar</name>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="19"/>
+ <source>Add Contact</source>
+ <translation>Adicionar Contato</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="28"/>
+ <source>Preferences</source>
+ <translation>Preferências</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="75"/>
+ <source>Click to add contacts</source>
+ <translation>Clique para adicionar contatos</translation>
+ </message>
+</context>
+<context>
+ <name>MainWindow</name>
+ <message>
+ <location filename="../src/ui/MainWindow.cpp" line="149"/>
+ <source>Remove %1</source>
+ <translation>Remover %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/MainWindow.cpp" line="150"/>
+ <source>Do you want to permanently remove %1?</source>
+ <translation>Você deseja remover %1 permanentemente?</translation>
+ </message>
+</context>
+<context>
+ <name>MessageDelegate</name>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="26"/>
+ <source>%1 is offline</source>
+ <extracomment>%1 nickname</extracomment>
+ <translation>%1 está offline</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="134"/>
+ <source>Copy ID</source>
+ <translation>Copiar ID</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="134"/>
+ <source>Copy Link</source>
+ <translation>Copiar Link</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="139"/>
+ <source>Open with Browser</source>
+ <translation>Abrir no Navegador</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="153"/>
+ <source>Add as Contact</source>
+ <translation>Adicionar como Contato</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="165"/>
+ <source>Copy Message</source>
+ <translation>Copiar Mensagem</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="172"/>
+ <source>Copy Selection</source>
+ <translation>Copiar Seleção</translation>
+ </message>
+</context>
+<context>
+ <name>MessageDialogWrapper</name>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="7"/>
+ <source>Remove %1</source>
+ <translation>Remover %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="9"/>
+ <source>Do you want to permanently remove %1?</source>
+ <extracomment>%1 nickname</extracomment>
+ <translation>Você deseja remover %1 permanentemente?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="10"/>
+ <source>This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request.</source>
+ <translation>Este contato não poderá mais te enviar mensagens, e será notificado sobre a remoção. Ele pode escolher enviar novo pedido de conexão.</translation>
+ </message>
+</context>
+<context>
+ <name>NetworkSetupWizard</name>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="100"/>
+ <source>This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network.</source>
+ <translation>A conexão à internet deste computador está livre de obstáculos. Quero me conectar diretamente à rede Tor.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="107"/>
+ <source>Connect</source>
+ <translation>Conectar</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="124"/>
+ <source>This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings.</source>
+ <translation>A conexão à internet deste computador é censurada, filtrada, ou passa por proxy. Preciso configurar a rede.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="131"/>
+ <source>Configure</source>
+ <translation>Configurar</translation>
+ </message>
+</context>
+<context>
+ <name>OfflineStateItem</name>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="107"/>
+ <source>Configure</source>
+ <translation>Configurar</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="115"/>
+ <source>Details</source>
+ <translation>Detalhes</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="143"/>
+ <source>Connection failed</source>
+ <translation>A conexão falhou</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="169"/>
+ <source>Connecting…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Conectando...</translation>
+ </message>
+</context>
+<context>
+ <name>OpenBrowserDialog</name>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="39"/>
+ <source><b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead.</source>
+ <translation><b>Atenção!</b> Abrir links com o seu navegador pode danificar sua segurança e anonimidade.<br><br>Ao invés disso, você pode <a href='.'>copiar para a área de transferência</a>.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="58"/>
+ <source>Don't ask again for links from %1</source>
+ <translation>Não perguntar de novo para links de %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="64"/>
+ <source>Don't ask again for any links (not recommended!)</source>
+ <translation>Não perguntar de novo para quaisquer links (não recomendado!)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="71"/>
+ <source>Open Browser</source>
+ <translation>Abrir Navegador</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="83"/>
+ <source>Cancel</source>
+ <translation>Cancelar</translation>
+ </message>
+</context>
+<context>
+ <name>PreferencesDialog</name>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="12"/>
+ <source>Ricochet Preferences</source>
+ <translation>Preferências do Ricochet</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="41"/>
+ <source>General</source>
+ <translation>Geral</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="46"/>
+ <source>Language</source>
+ <translation>Idioma</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="51"/>
+ <source>Contacts</source>
+ <translation>Contatos</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="56"/>
+ <source>Tor</source>
+ <translation>Tor</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="61"/>
+ <source>About</source>
+ <translation>Sobre</translation>
+ </message>
+</context>
+<context>
+ <name>QCocoaMenuItem</name>
+ <message>
+ <location filename="../src/ui/qml/MainWindow.qml" line="29"/>
+ <source>Preference</source>
+ <translation>Preferência</translation>
+ </message>
+</context>
+<context>
+ <name>StartupStatusPage</name>
+ <message>
+ <location filename="../src/ui/qml/StartupStatusPage.qml" line="18"/>
+ <source>The Tor process was not started successfully. This is most likely an installation or system error.</source>
+ <translation>O processo do Tor não foi iniciado com sucesso. Muito provavelmente isso é um erro de instalação ou de sistema.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/StartupStatusPage.qml" line="49"/>
+ <source>Quit</source>
+ <translation>Sair</translation>
+ </message>
+</context>
+<context>
+ <name>TorBootstrapStatus</name>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="17"/>
+ <source>Connecting to the Tor network…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Conectando à rede Tor...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="50"/>
+ <source>Back</source>
+ <translation>Voltar</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="57"/>
+ <source>Hide details</source>
+ <translation>Esconder detalhes</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="57"/>
+ <source>Show details</source>
+ <translation>Mostrar detalhes</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="69"/>
+ <source>Done</source>
+ <translation>Feito</translation>
+ </message>
+</context>
+<context>
+ <name>TorConfigurationPage</name>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="72"/>
+ <source>Does this computer need a proxy to access the internet?</source>
+ <translation>Este computador precisa de um proxy para acessar a internet?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="84"/>
+ <source>Proxy type:</source>
+ <translation>Tipo de proxy:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="89"/>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="91"/>
+ <source>None</source>
+ <translation>Nenhum</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="106"/>
+ <source>Address:</source>
+ <translation>Endereço:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="115"/>
+ <source>IP address or hostname</source>
+ <translation>Endereço IP ou nome do host</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="118"/>
+ <source>Port:</source>
+ <translation>Porta:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="129"/>
+ <source>Username:</source>
+ <translation>Usuário:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="139"/>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="149"/>
+ <source>Optional</source>
+ <translation>Opcional</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="142"/>
+ <source>Password:</source>
+ <translation>Senha:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="159"/>
+ <source>Does this computer's Internet connection go through a firewall that only allows connections to certain ports?</source>
+ <translation>A conexão à internet deste computador passa por um firewall que só permite conexões a certas portas?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="170"/>
+ <source>Allowed ports:</source>
+ <translation>Portas permitidas:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="177"/>
+ <source>Example: 80,443</source>
+ <translation>Exemplo: 80,443</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="188"/>
+ <source>If this computer's Internet connection is censored, you will need to obtain and use bridge relays.</source>
+ <translation>Se a conexão à internet deste computador é censurada, você precisará obter e usar bridge relays.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="197"/>
+ <source>Enter one or more bridge relays (one per line):</source>
+ <translation>Entre um ou mais bridge relays (um por linha):</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="212"/>
+ <source>Back</source>
+ <translation>Voltar</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="219"/>
+ <source>Connect</source>
+ <translation>Conectar</translation>
+ </message>
+</context>
+<context>
+ <name>TorPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="24"/>
+ <source>Running:</source>
+ <translation>Executando:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="27"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="29"/>
+ <source>Yes</source>
+ <translation>Sim</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="27"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="29"/>
+ <source>No</source>
+ <translation>Não</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <source>External</source>
+ <translation>Externo</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="26"/>
+ <source>Control connected:</source>
+ <translation>Controle conectado:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="28"/>
+ <source>Circuits established:</source>
+ <translation>Circuitos estabelecidos:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="30"/>
+ <source>Hidden service:</source>
+ <translation>Serviço escondido:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="31"/>
+ <source>Online</source>
+ <translation>Online</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="31"/>
+ <source>Offline</source>
+ <translation>Offline</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="32"/>
+ <source>Version:</source>
+ <translation>Versão:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="59"/>
+ <source>Error: <b>%1</b></source>
+ <extracomment>%1 is error message</extracomment>
+ <translation>Erro: <b>%1</b></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="75"/>
+ <source>Configure</source>
+ <translation>Configurar</translation>
+ </message>
+</context>
+<context>
+ <name>TorStateWidget</name>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="9"/>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="20"/>
+ <source>Connection failed</source>
+ <translation>A conexão falhou</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="12"/>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="22"/>
+ <source>Connecting…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Conectando...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="25"/>
+ <source>Connecting… (%1%)</source>
+ <extracomment>%1 is progress percentage, e.g. 100</extracomment>
+ <translation>Conectando... (%1)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="32"/>
+ <source>Online</source>
+ <translation>Online</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="34"/>
+ <source>Connected</source>
+ <translation>Conectado</translation>
+ </message>
+</context>
+</TS>
diff --git a/translation/ricochet_ru.ts b/translation/ricochet_ru.ts
new file mode 100644
index 0000000..8fb1624
--- /dev/null
+++ b/translation/ricochet_ru.ts
@@ -0,0 +1,694 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="ru">
+<context>
+ <name>AboutPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/AboutPreferences.qml" line="14"/>
+ <source>Ricochet %1</source>
+ <extracomment>%1 version, e.g. 1.0.0</extracomment>
+ <translation>Ricochet %1</translation>
+ </message>
+</context>
+<context>
+ <name>AddContactDialog</name>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="51"/>
+ <source>Share your Ricochet ID to allow connection requests</source>
+ <translation>Сообщите свой ID, чтобы собеседник узнал Вас</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="107"/>
+ <source>Cancel</source>
+ <translation>Отмена</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="112"/>
+ <source>Add</source>
+ <translation>Добавить</translation>
+ </message>
+</context>
+<context>
+ <name>ContactActions</name>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="40"/>
+ <source>Open Window</source>
+ <translation>Открыть окно</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="44"/>
+ <source>Details...</source>
+ <translation>Подробнее...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="48"/>
+ <source>Rename</source>
+ <translation>Переименовать</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="53"/>
+ <source>Remove</source>
+ <translation>Удалить</translation>
+ </message>
+</context>
+<context>
+ <name>ContactIDField</name>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="40"/>
+ <source><b>%1</b> is already your contact</source>
+ <translation><b>%1</b> уже в Вашем списке контактов</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="42"/>
+ <source>You can't add yourself as a contact</source>
+ <translation>Вы не можете добавить себя в свой же список контактов</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="44"/>
+ <source>Enter an ID starting with <b>ricochet:</b></source>
+ <translation>Введите ID, начинающийся с <b>ricochet:</b></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="79"/>
+ <source>Copied to clipboard</source>
+ <translation>Скопировано в буфер</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="91"/>
+ <source>Copy</source>
+ <translation>Копировать</translation>
+ </message>
+</context>
+<context>
+ <name>ContactList</name>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="75"/>
+ <source>Online</source>
+ <translation>В сети</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="76"/>
+ <source>Offline</source>
+ <translation>Не в сети</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="77"/>
+ <source>Requests</source>
+ <translation>Запросы</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="78"/>
+ <source>Rejected</source>
+ <translation>Отказано</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="79"/>
+ <source>Outdated</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>ContactPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="97"/>
+ <source>Date added:</source>
+ <translation>Добавлен:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="104"/>
+ <source>Last seen:</source>
+ <translation>Последний раз замечен:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="113"/>
+ <source>Request:</source>
+ <translation>Запрос:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="122"/>
+ <source>Pending connection</source>
+ <translation>Ожидаем соединение</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="123"/>
+ <source>Delivered</source>
+ <translation>Доставлено</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="124"/>
+ <source>Accepted</source>
+ <translation>Принято</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="125"/>
+ <source>Error</source>
+ <translation>Ошибка</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="126"/>
+ <source>Rejected</source>
+ <translation>Отказано</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="130"/>
+ <source>%1 (Connected)</source>
+ <extracomment>%1 status, e.g. "Accepted"</extracomment>
+ <translation>%1 (Соединено)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="136"/>
+ <source>Response:</source>
+ <translation>Ответ:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="158"/>
+ <source>Rename</source>
+ <translation type="unfinished">Переименовать</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="165"/>
+ <source>Remove</source>
+ <translation>Удалить</translation>
+ </message>
+</context>
+<context>
+ <name>ContactRequestDialog</name>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="55"/>
+ <source>Someone new is asking to connect to you</source>
+ <translation>Новый человек хочет пообщаться с Вами</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="102"/>
+ <source>Reject</source>
+ <translation>Отказать</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="107"/>
+ <source>Accept</source>
+ <translation>Принять</translation>
+ </message>
+</context>
+<context>
+ <name>ContactRequestFields</name>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="17"/>
+ <source>ID:</source>
+ <translation>ID:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="29"/>
+ <source>Name:</source>
+ <translation>Имя:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="40"/>
+ <source>Message:</source>
+ <translation>Сообщение:</translation>
+ </message>
+</context>
+<context>
+ <name>GeneralPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="12"/>
+ <source>Use a single window for conversations</source>
+ <translation>Использовать одно окно для разговоров</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="20"/>
+ <source>Open links in default browser without prompting</source>
+ <translation>Открывать ссылки в браузере по умолчанию без запроса</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="28"/>
+ <source>Play audio notifications</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="37"/>
+ <source>Volume</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>LanguagePreferences</name>
+ <message>
+ <location filename="../src/ui/qml/LanguagePreferences.qml" line="22"/>
+ <source>Select Language</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/LanguagePreferences.qml" line="52"/>
+ <source>Restart Ricochet to apply changes</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>LanguagesModel</name>
+ <message>
+ <location filename="../src/ui/LanguagesModel.cpp" line="43"/>
+ <source>System default</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>Main</name>
+ <message>
+ <location filename="../src/main.cpp" line="73"/>
+ <source>Ricochet Error</source>
+ <translation>Ошибка Ricochet</translation>
+ </message>
+</context>
+<context>
+ <name>MainToolBar</name>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="19"/>
+ <source>Add Contact</source>
+ <translation>Добавить контакт</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="28"/>
+ <source>Preferences</source>
+ <translation>Настройки</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="75"/>
+ <source>Click to add contacts</source>
+ <translation>Нажмите, чтобы добавить контакты</translation>
+ </message>
+</context>
+<context>
+ <name>MainWindow</name>
+ <message>
+ <location filename="../src/ui/MainWindow.cpp" line="149"/>
+ <source>Remove %1</source>
+ <translation>Удалить %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/MainWindow.cpp" line="150"/>
+ <source>Do you want to permanently remove %1?</source>
+ <translation>Вы действительно хотите удалить %1 навсегда?</translation>
+ </message>
+</context>
+<context>
+ <name>MessageDelegate</name>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="26"/>
+ <source>%1 is offline</source>
+ <extracomment>%1 nickname</extracomment>
+ <translation>%1 не в сети</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="134"/>
+ <source>Copy ID</source>
+ <translation>Копировать ID</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="134"/>
+ <source>Copy Link</source>
+ <translation>Копировать ссылку</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="139"/>
+ <source>Open with Browser</source>
+ <translation>Открыть в браузере</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="153"/>
+ <source>Add as Contact</source>
+ <translation>Добавить в список контактов</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="165"/>
+ <source>Copy Message</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="172"/>
+ <source>Copy Selection</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>MessageDialogWrapper</name>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="7"/>
+ <source>Remove %1</source>
+ <translation>Удалить %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="9"/>
+ <source>Do you want to permanently remove %1?</source>
+ <extracomment>%1 nickname</extracomment>
+ <translation>Вы действительно хотите удалить %1 навсегда?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="10"/>
+ <source>This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request.</source>
+ <translation>Этот контакт больше не сможет писать Вам и будет уведомлен об удалении. Тем не менее, он сможет послать Вам новый запрос.</translation>
+ </message>
+</context>
+<context>
+ <name>NetworkSetupWizard</name>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="100"/>
+ <source>This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network.</source>
+ <translation>Соединение на этом компьютере свободно от цензуры и фильтров. Я хочу подключиться к сети Tor напрямую.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="107"/>
+ <source>Connect</source>
+ <translation>Соединиться</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="124"/>
+ <source>This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings.</source>
+ <translation>Соединение на этом компьютере подвергается цензуре, фильтрации либо проксируется. Мне нужно настроить дополнительные параметры соединения.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="131"/>
+ <source>Configure</source>
+ <translation>Настроить</translation>
+ </message>
+</context>
+<context>
+ <name>OfflineStateItem</name>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="107"/>
+ <source>Configure</source>
+ <translation>Настроить</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="115"/>
+ <source>Details</source>
+ <translation>Подробности</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="143"/>
+ <source>Connection failed</source>
+ <translation>Не удалось соединиться</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="169"/>
+ <source>Connecting…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Соединение...</translation>
+ </message>
+</context>
+<context>
+ <name>OpenBrowserDialog</name>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="39"/>
+ <source><b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead.</source>
+ <translation><b>Внимание!</b> Открывая ссылки браузером по умолчанию Вы можете нарушить Вашу безопасность и анонимность.<br><br>Вместо этого Вы можете <a href='.'>скопировать ссылку</a>.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="58"/>
+ <source>Don't ask again for links from %1</source>
+ <translation>Не спрашивать при открытии последующих ссылок от %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="64"/>
+ <source>Don't ask again for any links (not recommended!)</source>
+ <translation>Не спрашивать при открытии всех последующих ссылок (не рекомендуется!)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="71"/>
+ <source>Open Browser</source>
+ <translation>Открыть браузер</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="83"/>
+ <source>Cancel</source>
+ <translation>Отмена</translation>
+ </message>
+</context>
+<context>
+ <name>PreferencesDialog</name>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="12"/>
+ <source>Ricochet Preferences</source>
+ <translation>Настройки Ricochet</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="41"/>
+ <source>General</source>
+ <translation>Основные</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="46"/>
+ <source>Language</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="51"/>
+ <source>Contacts</source>
+ <translation>Контакты</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="56"/>
+ <source>Tor</source>
+ <translation>Tor</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="61"/>
+ <source>About</source>
+ <translation>О программе</translation>
+ </message>
+</context>
+<context>
+ <name>QCocoaMenuItem</name>
+ <message>
+ <location filename="../src/ui/qml/MainWindow.qml" line="29"/>
+ <source>Preference</source>
+ <translation>Настройки</translation>
+ </message>
+</context>
+<context>
+ <name>StartupStatusPage</name>
+ <message>
+ <location filename="../src/ui/qml/StartupStatusPage.qml" line="18"/>
+ <source>The Tor process was not started successfully. This is most likely an installation or system error.</source>
+ <translation>Процесс Tor не был запущен успешно. Скорее всего, причина в неправильной установке или системной ошибке.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/StartupStatusPage.qml" line="49"/>
+ <source>Quit</source>
+ <translation>Выход</translation>
+ </message>
+</context>
+<context>
+ <name>TorBootstrapStatus</name>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="17"/>
+ <source>Connecting to the Tor network…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Подсоединяемся к сети Tor…</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="50"/>
+ <source>Back</source>
+ <translation>Назад</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="57"/>
+ <source>Hide details</source>
+ <translation>Скрыть подробности</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="57"/>
+ <source>Show details</source>
+ <translation>Показать подробности</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="69"/>
+ <source>Done</source>
+ <translation>Готово</translation>
+ </message>
+</context>
+<context>
+ <name>TorConfigurationPage</name>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="72"/>
+ <source>Does this computer need a proxy to access the internet?</source>
+ <translation>Этот компьютер использует прокси для доступа в Интернет?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="84"/>
+ <source>Proxy type:</source>
+ <translation>Тип прокси:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="89"/>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="91"/>
+ <source>None</source>
+ <translation>Нет</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="106"/>
+ <source>Address:</source>
+ <translation>Адрес:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="115"/>
+ <source>IP address or hostname</source>
+ <translation>IP адрес или имя хоста</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="118"/>
+ <source>Port:</source>
+ <translation>Порт:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="129"/>
+ <source>Username:</source>
+ <translation>Имя пользователя:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="139"/>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="149"/>
+ <source>Optional</source>
+ <translation>Необязательно</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="142"/>
+ <source>Password:</source>
+ <translation>Пароль:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="159"/>
+ <source>Does this computer's Internet connection go through a firewall that only allows connections to certain ports?</source>
+ <translation>Соединение данного компьютера фильтруется фаерволом, который разрешает доступ только к определенным портам?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="170"/>
+ <source>Allowed ports:</source>
+ <translation>Открытые порты:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="177"/>
+ <source>Example: 80,443</source>
+ <translation>Например: 80,443</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="188"/>
+ <source>If this computer's Internet connection is censored, you will need to obtain and use bridge relays.</source>
+ <translation>Если соединение этого компьютера подвергается цензуре, Вам необходимо получить и использовать ретрансляторы Tor.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="197"/>
+ <source>Enter one or more bridge relays (one per line):</source>
+ <translation>Введите один ретранслятор или более (по одному на строку):</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="212"/>
+ <source>Back</source>
+ <translation>Назад</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="219"/>
+ <source>Connect</source>
+ <translation>Соединить</translation>
+ </message>
+</context>
+<context>
+ <name>TorPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="24"/>
+ <source>Running:</source>
+ <translation>Запущен:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="27"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="29"/>
+ <source>Yes</source>
+ <translation>Да</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="27"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="29"/>
+ <source>No</source>
+ <translation>Нет</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <source>External</source>
+ <translation>Внешний</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="26"/>
+ <source>Control connected:</source>
+ <translation>Контроль присоединенных:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="28"/>
+ <source>Circuits established:</source>
+ <translation>Цепи установлены:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="30"/>
+ <source>Hidden service:</source>
+ <translation>Скрытый сервис:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="31"/>
+ <source>Online</source>
+ <translation>В сети</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="31"/>
+ <source>Offline</source>
+ <translation>Не в сети</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="32"/>
+ <source>Version:</source>
+ <translation>Версия:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="59"/>
+ <source>Error: <b>%1</b></source>
+ <extracomment>%1 is error message</extracomment>
+ <translation>Ошибка: <b>%1</b></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="75"/>
+ <source>Configure</source>
+ <translation>Настроить</translation>
+ </message>
+</context>
+<context>
+ <name>TorStateWidget</name>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="9"/>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="20"/>
+ <source>Connection failed</source>
+ <translation>Не удалось соединиться</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="12"/>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="22"/>
+ <source>Connecting…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Соединение…</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="25"/>
+ <source>Connecting… (%1%)</source>
+ <extracomment>%1 is progress percentage, e.g. 100</extracomment>
+ <translation>Соединение… (%1%)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="32"/>
+ <source>Online</source>
+ <translation>В сети</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="34"/>
+ <source>Connected</source>
+ <translation>Соединено</translation>
+ </message>
+</context>
+</TS>
diff --git a/translation/ricochet_sv.ts b/translation/ricochet_sv.ts
new file mode 100644
index 0000000..cf3a44d
--- /dev/null
+++ b/translation/ricochet_sv.ts
@@ -0,0 +1,694 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="sv">
+<context>
+ <name>AboutPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/AboutPreferences.qml" line="14"/>
+ <source>Ricochet %1</source>
+ <extracomment>%1 version, e.g. 1.0.0</extracomment>
+ <translation>Ricochet %1</translation>
+ </message>
+</context>
+<context>
+ <name>AddContactDialog</name>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="51"/>
+ <source>Share your Ricochet ID to allow connection requests</source>
+ <translation>Dela ditt Ricochet-ID för att tillåta anslutningsförfrågningar</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="107"/>
+ <source>Cancel</source>
+ <translation>Avbryt</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="112"/>
+ <source>Add</source>
+ <translation>Lägg till</translation>
+ </message>
+</context>
+<context>
+ <name>ContactActions</name>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="40"/>
+ <source>Open Window</source>
+ <translation>Öppna fönster</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="44"/>
+ <source>Details...</source>
+ <translation>Detaljer...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="48"/>
+ <source>Rename</source>
+ <translation>Ändra namn</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="53"/>
+ <source>Remove</source>
+ <translation>Ta bort</translation>
+ </message>
+</context>
+<context>
+ <name>ContactIDField</name>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="40"/>
+ <source><b>%1</b> is already your contact</source>
+ <translation><b>%1</b> finns redan bland dina kontakter</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="42"/>
+ <source>You can't add yourself as a contact</source>
+ <translation>Du kan inte lägga till dig själv som kontakt</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="44"/>
+ <source>Enter an ID starting with <b>ricochet:</b></source>
+ <translation>Ange ett ID som startar med <b>ricochet:</b></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="79"/>
+ <source>Copied to clipboard</source>
+ <translation>Kopierat till urklipp</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="91"/>
+ <source>Copy</source>
+ <translation>Kopiera</translation>
+ </message>
+</context>
+<context>
+ <name>ContactList</name>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="75"/>
+ <source>Online</source>
+ <translation>Online</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="76"/>
+ <source>Offline</source>
+ <translation>Offline</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="77"/>
+ <source>Requests</source>
+ <translation>Förfrågningar</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="78"/>
+ <source>Rejected</source>
+ <translation>Nekad</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="79"/>
+ <source>Outdated</source>
+ <translation>Inaktuell</translation>
+ </message>
+</context>
+<context>
+ <name>ContactPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="97"/>
+ <source>Date added:</source>
+ <translation>Datum tillagd:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="104"/>
+ <source>Last seen:</source>
+ <translation>Sågs senast:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="113"/>
+ <source>Request:</source>
+ <translation>Förfrågan:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="122"/>
+ <source>Pending connection</source>
+ <translation>Inväntar anslutning</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="123"/>
+ <source>Delivered</source>
+ <translation>Levererad</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="124"/>
+ <source>Accepted</source>
+ <translation>Accepterat</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="125"/>
+ <source>Error</source>
+ <translation>Fel</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="126"/>
+ <source>Rejected</source>
+ <translation>Nekad</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="130"/>
+ <source>%1 (Connected)</source>
+ <extracomment>%1 status, e.g. "Accepted"</extracomment>
+ <translation>%1 (Ansluten)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="136"/>
+ <source>Response:</source>
+ <translation>Svar:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="158"/>
+ <source>Rename</source>
+ <translation>Ändra namn</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="165"/>
+ <source>Remove</source>
+ <translation>Ta bort</translation>
+ </message>
+</context>
+<context>
+ <name>ContactRequestDialog</name>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="55"/>
+ <source>Someone new is asking to connect to you</source>
+ <translation>Någon ber att få kontakt med dig</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="102"/>
+ <source>Reject</source>
+ <translation>Avböj</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="107"/>
+ <source>Accept</source>
+ <translation>Acceptera</translation>
+ </message>
+</context>
+<context>
+ <name>ContactRequestFields</name>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="17"/>
+ <source>ID:</source>
+ <translation>ID:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="29"/>
+ <source>Name:</source>
+ <translation>Namn:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="40"/>
+ <source>Message:</source>
+ <translation>Meddelande:</translation>
+ </message>
+</context>
+<context>
+ <name>GeneralPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="12"/>
+ <source>Use a single window for conversations</source>
+ <translation>Använd ett enda fönster för konversationer</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="20"/>
+ <source>Open links in default browser without prompting</source>
+ <translation>Öppna länkar i standardwebbläsaren utan att fråga</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="28"/>
+ <source>Play audio notifications</source>
+ <translation>Spela ljudnotiser</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="37"/>
+ <source>Volume</source>
+ <translation>Volym</translation>
+ </message>
+</context>
+<context>
+ <name>LanguagePreferences</name>
+ <message>
+ <location filename="../src/ui/qml/LanguagePreferences.qml" line="22"/>
+ <source>Select Language</source>
+ <translation>Välj språk</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/LanguagePreferences.qml" line="52"/>
+ <source>Restart Ricochet to apply changes</source>
+ <translation>Starta om Ricochet för att aktivera ändringarna</translation>
+ </message>
+</context>
+<context>
+ <name>LanguagesModel</name>
+ <message>
+ <location filename="../src/ui/LanguagesModel.cpp" line="43"/>
+ <source>System default</source>
+ <translation>Systemets förval</translation>
+ </message>
+</context>
+<context>
+ <name>Main</name>
+ <message>
+ <location filename="../src/main.cpp" line="73"/>
+ <source>Ricochet Error</source>
+ <translation>Ricochet fel</translation>
+ </message>
+</context>
+<context>
+ <name>MainToolBar</name>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="19"/>
+ <source>Add Contact</source>
+ <translation>Lägg till kontakt</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="28"/>
+ <source>Preferences</source>
+ <translation>Inställningar</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="75"/>
+ <source>Click to add contacts</source>
+ <translation>Klicka för att lägga till kontakter</translation>
+ </message>
+</context>
+<context>
+ <name>MainWindow</name>
+ <message>
+ <location filename="../src/ui/MainWindow.cpp" line="149"/>
+ <source>Remove %1</source>
+ <translation>Ta bort %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/MainWindow.cpp" line="150"/>
+ <source>Do you want to permanently remove %1?</source>
+ <translation>Vill du ta bort %1 permanent?</translation>
+ </message>
+</context>
+<context>
+ <name>MessageDelegate</name>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="26"/>
+ <source>%1 is offline</source>
+ <extracomment>%1 nickname</extracomment>
+ <translation>%1 är offline</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="134"/>
+ <source>Copy ID</source>
+ <translation>Kopiera ID</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="134"/>
+ <source>Copy Link</source>
+ <translation>Kopiera länk</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="139"/>
+ <source>Open with Browser</source>
+ <translation>Öppna med webbläsare</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="153"/>
+ <source>Add as Contact</source>
+ <translation>Lägg till som kontakt</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="165"/>
+ <source>Copy Message</source>
+ <translation>Kopiera meddelande</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="172"/>
+ <source>Copy Selection</source>
+ <translation>Kopiera markering</translation>
+ </message>
+</context>
+<context>
+ <name>MessageDialogWrapper</name>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="7"/>
+ <source>Remove %1</source>
+ <translation>Ta bort %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="9"/>
+ <source>Do you want to permanently remove %1?</source>
+ <extracomment>%1 nickname</extracomment>
+ <translation>Vill du ta bort %1 permanent?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="10"/>
+ <source>This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request.</source>
+ <translation>Den här kontakten kommer inte längre att kunna skicka meddelanden till dig, och kommer att bli informerad om borttagningen. De kan välja att skicka en ny kontaktförfrågan.</translation>
+ </message>
+</context>
+<context>
+ <name>NetworkSetupWizard</name>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="100"/>
+ <source>This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network.</source>
+ <translation>Den här datorn kan ansluta till Internet obehindrat. Jag vill ansluta direkt till Tor-nätverket.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="107"/>
+ <source>Connect</source>
+ <translation>Anslut</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="124"/>
+ <source>This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings.</source>
+ <translation>Den här datorns Internetanslutning är censurerad, filtrerad eller kräver användning av en proxy. Jag behöver konfigurera nätverksinställningarna.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="131"/>
+ <source>Configure</source>
+ <translation>Konfigurera</translation>
+ </message>
+</context>
+<context>
+ <name>OfflineStateItem</name>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="107"/>
+ <source>Configure</source>
+ <translation>Konfigurera</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="115"/>
+ <source>Details</source>
+ <translation>Detaljer</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="143"/>
+ <source>Connection failed</source>
+ <translation>Anslutning misslyckades</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="169"/>
+ <source>Connecting…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Ansluter...</translation>
+ </message>
+</context>
+<context>
+ <name>OpenBrowserDialog</name>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="39"/>
+ <source><b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead.</source>
+ <translation><b>Varning!</b> Att öppna länkar med din standardwebbläsare skadar din säkerhet och anonymitet.<br><br>Du kan <a href='.'>kopiera till urklipp</a> istället.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="58"/>
+ <source>Don't ask again for links from %1</source>
+ <translation>Fråga inte igen för länkar från %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="64"/>
+ <source>Don't ask again for any links (not recommended!)</source>
+ <translation>Fråga inte igen för några länkar (rekommenderas inte!)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="71"/>
+ <source>Open Browser</source>
+ <translation>Öppna webbläsare</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="83"/>
+ <source>Cancel</source>
+ <translation>Avbryt</translation>
+ </message>
+</context>
+<context>
+ <name>PreferencesDialog</name>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="12"/>
+ <source>Ricochet Preferences</source>
+ <translation>Ricochet inställningar</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="41"/>
+ <source>General</source>
+ <translation>Allmänt</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="46"/>
+ <source>Language</source>
+ <translation>Språk</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="51"/>
+ <source>Contacts</source>
+ <translation>Kontakter</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="56"/>
+ <source>Tor</source>
+ <translation>Tor</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="61"/>
+ <source>About</source>
+ <translation>Om</translation>
+ </message>
+</context>
+<context>
+ <name>QCocoaMenuItem</name>
+ <message>
+ <location filename="../src/ui/qml/MainWindow.qml" line="29"/>
+ <source>Preference</source>
+ <translation>Inställning</translation>
+ </message>
+</context>
+<context>
+ <name>StartupStatusPage</name>
+ <message>
+ <location filename="../src/ui/qml/StartupStatusPage.qml" line="18"/>
+ <source>The Tor process was not started successfully. This is most likely an installation or system error.</source>
+ <translation>Tor-processen startade inte korrekt. Detta är högst sannolikt ett installations- eller systemfel.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/StartupStatusPage.qml" line="49"/>
+ <source>Quit</source>
+ <translation>Avsluta</translation>
+ </message>
+</context>
+<context>
+ <name>TorBootstrapStatus</name>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="17"/>
+ <source>Connecting to the Tor network…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Ansluter till Tor-nätverket...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="50"/>
+ <source>Back</source>
+ <translation>Tillbaka</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="57"/>
+ <source>Hide details</source>
+ <translation>Göm detaljer</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="57"/>
+ <source>Show details</source>
+ <translation>Visa detaljer</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="69"/>
+ <source>Done</source>
+ <translation>Klar</translation>
+ </message>
+</context>
+<context>
+ <name>TorConfigurationPage</name>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="72"/>
+ <source>Does this computer need a proxy to access the internet?</source>
+ <translation>Behöver den här datorn använda en proxy för att ansluta till Internet?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="84"/>
+ <source>Proxy type:</source>
+ <translation>Typ av proxy:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="89"/>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="91"/>
+ <source>None</source>
+ <translation>Ingen</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="106"/>
+ <source>Address:</source>
+ <translation>Adress:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="115"/>
+ <source>IP address or hostname</source>
+ <translation>IP-adress eller värdnamn</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="118"/>
+ <source>Port:</source>
+ <translation>Port:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="129"/>
+ <source>Username:</source>
+ <translation>Användarnamn:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="139"/>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="149"/>
+ <source>Optional</source>
+ <translation>Krävs ej</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="142"/>
+ <source>Password:</source>
+ <translation>Lösenord:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="159"/>
+ <source>Does this computer's Internet connection go through a firewall that only allows connections to certain ports?</source>
+ <translation>Går den här datorns Internetanslutning genom en brandvägg som bara tillåter anslutningar på vissa portar?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="170"/>
+ <source>Allowed ports:</source>
+ <translation>Tillåtna portar:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="177"/>
+ <source>Example: 80,443</source>
+ <translation>Exempel: 80,443</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="188"/>
+ <source>If this computer's Internet connection is censored, you will need to obtain and use bridge relays.</source>
+ <translation>Om den här datorns Internetanslutning är censurerad måste du skaffa och använda en brygga till Tor ("bridge relay").</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="197"/>
+ <source>Enter one or more bridge relays (one per line):</source>
+ <translation>Ange en eller flera bryggor (en per rad):</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="212"/>
+ <source>Back</source>
+ <translation>Tillbaka</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="219"/>
+ <source>Connect</source>
+ <translation>Anslut</translation>
+ </message>
+</context>
+<context>
+ <name>TorPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="24"/>
+ <source>Running:</source>
+ <translation>Körs:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="27"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="29"/>
+ <source>Yes</source>
+ <translation>Ja</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="27"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="29"/>
+ <source>No</source>
+ <translation>Nej</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <source>External</source>
+ <translation>Extern</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="26"/>
+ <source>Control connected:</source>
+ <translation>Kontroll ansluten:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="28"/>
+ <source>Circuits established:</source>
+ <translation>Kretsar etablerade:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="30"/>
+ <source>Hidden service:</source>
+ <translation>Dold tjänst:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="31"/>
+ <source>Online</source>
+ <translation>Online</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="31"/>
+ <source>Offline</source>
+ <translation>Offline</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="32"/>
+ <source>Version:</source>
+ <translation>Version:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="59"/>
+ <source>Error: <b>%1</b></source>
+ <extracomment>%1 is error message</extracomment>
+ <translation>Fel: <b>%1</b></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="75"/>
+ <source>Configure</source>
+ <translation>Konfigurera</translation>
+ </message>
+</context>
+<context>
+ <name>TorStateWidget</name>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="9"/>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="20"/>
+ <source>Connection failed</source>
+ <translation>Anslutning misslyckades</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="12"/>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="22"/>
+ <source>Connecting…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Ansluter...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="25"/>
+ <source>Connecting… (%1%)</source>
+ <extracomment>%1 is progress percentage, e.g. 100</extracomment>
+ <translation>Ansluter… (%1%)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="32"/>
+ <source>Online</source>
+ <translation>Online</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="34"/>
+ <source>Connected</source>
+ <translation>Ansluten</translation>
+ </message>
+</context>
+</TS>
diff --git a/translation/ricochet_tr.ts b/translation/ricochet_tr.ts
new file mode 100644
index 0000000..6f5a755
--- /dev/null
+++ b/translation/ricochet_tr.ts
@@ -0,0 +1,706 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="tr">
+<context>
+ <name>AboutPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/AboutPreferences.qml" line="14"/>
+ <source>Ricochet %1</source>
+ <extracomment>%1 version, e.g. 1.0.0</extracomment>
+ <translation>Ricochet %1</translation>
+ </message>
+</context>
+<context>
+ <name>AddContactDialog</name>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="51"/>
+ <source>Share your Ricochet ID to allow connection requests</source>
+ <translation>Bağlantı isteklerine izin vermek için Ricochet ID paylaşın</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="107"/>
+ <source>Cancel</source>
+ <translation>İptal</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="112"/>
+ <source>Add</source>
+ <translation>Ekle</translation>
+ </message>
+</context>
+<context>
+ <name>ContactActions</name>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="40"/>
+ <source>Open Window</source>
+ <translation>Yeni Pencere</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="44"/>
+ <source>Details...</source>
+ <translation>Ayrıntılar...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="48"/>
+ <source>Rename</source>
+ <translation>Yeniden adlandır</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="53"/>
+ <source>Remove</source>
+ <translation>Çıkar</translation>
+ </message>
+</context>
+<context>
+ <name>ContactIDField</name>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="40"/>
+ <source><b>%1</b> is already your contact</source>
+ <translation><b>%1</b> zaten sizde ekli</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="42"/>
+ <source>You can't add yourself as a contact</source>
+ <translation>Kendinizi ekleyemezsiniz</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="44"/>
+ <source>Enter an ID starting with <b>ricochet:</b></source>
+ <translation><b>ricochet:</b> ile başlayan bir ID girin</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="79"/>
+ <source>Copied to clipboard</source>
+ <translation>Panoya kopyalandı</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="91"/>
+ <source>Copy</source>
+ <translation>Kopyala</translation>
+ </message>
+</context>
+<context>
+ <name>ContactList</name>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="75"/>
+ <source>Online</source>
+ <translation>Çevrimiçi</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="76"/>
+ <source>Offline</source>
+ <translation>Çevrimdışı</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="77"/>
+ <source>Requests</source>
+ <translation>İstekler</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="78"/>
+ <source>Rejected</source>
+ <translation>Reddedildi</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="79"/>
+ <source>Outdated</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>ContactPreferences</name>
+ <message>
+ <source>Nickname:</source>
+ <translation type="vanished">Takma ad:</translation>
+ </message>
+ <message>
+ <source>ID:</source>
+ <translation type="vanished">ID:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="97"/>
+ <source>Date added:</source>
+ <translation>Eklenme tarihi:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="104"/>
+ <source>Last seen:</source>
+ <translation>Son görülme tarihi:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="113"/>
+ <source>Request:</source>
+ <translation>İstek:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="122"/>
+ <source>Pending connection</source>
+ <translation>Beklemedeki bağlantı</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="123"/>
+ <source>Delivered</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="124"/>
+ <source>Accepted</source>
+ <translation>Kabul edildi</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="125"/>
+ <source>Error</source>
+ <translation>Hata</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="126"/>
+ <source>Rejected</source>
+ <translation>Reddedildi</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="130"/>
+ <source>%1 (Connected)</source>
+ <extracomment>%1 status, e.g. "Accepted"</extracomment>
+ <translation>%1 (Bağlandı)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="136"/>
+ <source>Response:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="158"/>
+ <source>Rename</source>
+ <translation type="unfinished">Yeniden adlandır</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="165"/>
+ <source>Remove</source>
+ <translation>Çıkar</translation>
+ </message>
+</context>
+<context>
+ <name>ContactRequestDialog</name>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="55"/>
+ <source>Someone new is asking to connect to you</source>
+ <translation>Birisi, size bağlantı kurmak için soruyor</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="102"/>
+ <source>Reject</source>
+ <translation>Reddet</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="107"/>
+ <source>Accept</source>
+ <translation>Kabul et</translation>
+ </message>
+</context>
+<context>
+ <name>ContactRequestFields</name>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="17"/>
+ <source>ID:</source>
+ <translation>ID:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="29"/>
+ <source>Name:</source>
+ <translation>Ad:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="40"/>
+ <source>Message:</source>
+ <translation>Mesaj:</translation>
+ </message>
+</context>
+<context>
+ <name>GeneralPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="12"/>
+ <source>Use a single window for conversations</source>
+ <translation>Görüşmeler için tek pencere kullan</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="20"/>
+ <source>Open links in default browser without prompting</source>
+ <translation>Bağlantıları, bana sormadan varsayılan tarayıcıda aç</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="28"/>
+ <source>Play audio notifications</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="37"/>
+ <source>Volume</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>LanguagePreferences</name>
+ <message>
+ <location filename="../src/ui/qml/LanguagePreferences.qml" line="22"/>
+ <source>Select Language</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/LanguagePreferences.qml" line="52"/>
+ <source>Restart Ricochet to apply changes</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>LanguagesModel</name>
+ <message>
+ <location filename="../src/ui/LanguagesModel.cpp" line="43"/>
+ <source>System default</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>Main</name>
+ <message>
+ <location filename="../src/main.cpp" line="73"/>
+ <source>Ricochet Error</source>
+ <translation>Ricochet Hata</translation>
+ </message>
+</context>
+<context>
+ <name>MainToolBar</name>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="19"/>
+ <source>Add Contact</source>
+ <translation>Kişi Ekle</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="28"/>
+ <source>Preferences</source>
+ <translation>Tercihler</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="75"/>
+ <source>Click to add contacts</source>
+ <translation>Kişileri eklemek için tıklayın</translation>
+ </message>
+</context>
+<context>
+ <name>MainWindow</name>
+ <message>
+ <location filename="../src/ui/MainWindow.cpp" line="149"/>
+ <source>Remove %1</source>
+ <translation>%1 Çıkar</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/MainWindow.cpp" line="150"/>
+ <source>Do you want to permanently remove %1?</source>
+ <translation>Kalıcı olarak %1 çıkarmak istiyor musunuz?</translation>
+ </message>
+</context>
+<context>
+ <name>MessageDelegate</name>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="26"/>
+ <source>%1 is offline</source>
+ <extracomment>%1 nickname</extracomment>
+ <translation>%1 çevrimdışı</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="134"/>
+ <source>Copy ID</source>
+ <translation>ID Kopyala</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="134"/>
+ <source>Copy Link</source>
+ <translation>Bağlantı Kopyala</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="139"/>
+ <source>Open with Browser</source>
+ <translation>Tarayıcı ile Aç</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="153"/>
+ <source>Add as Contact</source>
+ <translation>Kişi olarak Ekle</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="165"/>
+ <source>Copy Message</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="172"/>
+ <source>Copy Selection</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Copy</source>
+ <translation type="vanished">Kopyala</translation>
+ </message>
+</context>
+<context>
+ <name>MessageDialogWrapper</name>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="7"/>
+ <source>Remove %1</source>
+ <translation>%1 Çıkar</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="9"/>
+ <source>Do you want to permanently remove %1?</source>
+ <extracomment>%1 nickname</extracomment>
+ <translation>Kalıcı olarak %1 çıkarmak istiyor musunuz?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="10"/>
+ <source>This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request.</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>NetworkSetupWizard</name>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="100"/>
+ <source>This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network.</source>
+ <translation>Bu bilgisayarın internet bağlantısı engellerden arındırılmıştır. Tor ağına doğrudan bağlanmak istiyorum.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="107"/>
+ <source>Connect</source>
+ <translation>Bağlan</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="124"/>
+ <source>This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings.</source>
+ <translation>Bu bilgisayarın internet bağlantısı sarsürlü, filtreli veya proxylidir. Ağ ayarlarını yapılandırmam gerekir.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="131"/>
+ <source>Configure</source>
+ <translation>Yapılandır</translation>
+ </message>
+</context>
+<context>
+ <name>OfflineStateItem</name>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="107"/>
+ <source>Configure</source>
+ <translation>Yapılandır</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="115"/>
+ <source>Details</source>
+ <translation>Ayrıntılar</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="143"/>
+ <source>Connection failed</source>
+ <translation>Bağlantı başarısız</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="169"/>
+ <source>Connecting…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Bağlanıyor...</translation>
+ </message>
+</context>
+<context>
+ <name>OpenBrowserDialog</name>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="39"/>
+ <source><b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead.</source>
+ <translation><b>Uyarı!</b> Varsayılan tarayıcı ile bağlantıları açmak, güvenlik ve anonimliğinize zarar verecektir.<br><br>Bunun yerine <a href='.'>panoya kopyala</a>yabilirsiniz.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="58"/>
+ <source>Don't ask again for links from %1</source>
+ <translation>%1 gelen bağlantıları için tekrar sorma</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="64"/>
+ <source>Don't ask again for any links (not recommended!)</source>
+ <translation>Bundan sonra bağlantılar için bir daha sorma (tavsiye edilmez!)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="71"/>
+ <source>Open Browser</source>
+ <translation>Tarayıcı Aç</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="83"/>
+ <source>Cancel</source>
+ <translation>İptal</translation>
+ </message>
+</context>
+<context>
+ <name>PreferencesDialog</name>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="12"/>
+ <source>Ricochet Preferences</source>
+ <translation>Ricochet Tercihler</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="41"/>
+ <source>General</source>
+ <translation>Genel</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="46"/>
+ <source>Language</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="51"/>
+ <source>Contacts</source>
+ <translation>Kişiler</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="56"/>
+ <source>Tor</source>
+ <translation>Tor</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="61"/>
+ <source>About</source>
+ <translation>Hakkında</translation>
+ </message>
+</context>
+<context>
+ <name>QCocoaMenuItem</name>
+ <message>
+ <location filename="../src/ui/qml/MainWindow.qml" line="29"/>
+ <source>Preference</source>
+ <translation>Tercih</translation>
+ </message>
+</context>
+<context>
+ <name>StartupStatusPage</name>
+ <message>
+ <location filename="../src/ui/qml/StartupStatusPage.qml" line="18"/>
+ <source>The Tor process was not started successfully. This is most likely an installation or system error.</source>
+ <translation>Tor başlatılamadı. Büyük olasılıkla kurulum veya sistem hatası.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/StartupStatusPage.qml" line="49"/>
+ <source>Quit</source>
+ <translation>Çık</translation>
+ </message>
+</context>
+<context>
+ <name>TorBootstrapStatus</name>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="17"/>
+ <source>Connecting to the Tor network…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Tor ağına bağlanıyor...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="50"/>
+ <source>Back</source>
+ <translation>Geri</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="57"/>
+ <source>Hide details</source>
+ <translation>Ayrıntıları gizle</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="57"/>
+ <source>Show details</source>
+ <translation>Ayrıntıları göster</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="69"/>
+ <source>Done</source>
+ <translation>Tamamla</translation>
+ </message>
+</context>
+<context>
+ <name>TorConfigurationPage</name>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="72"/>
+ <source>Does this computer need a proxy to access the internet?</source>
+ <translation>Bu bilgisayar internete erişmek için bir proxye ihtiyacı mı?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="84"/>
+ <source>Proxy type:</source>
+ <translation>Proxy türü:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="89"/>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="91"/>
+ <source>None</source>
+ <translation>Hiçbiri</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="106"/>
+ <source>Address:</source>
+ <translation>Adres:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="115"/>
+ <source>IP address or hostname</source>
+ <translation>IP adresi veya host adı</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="118"/>
+ <source>Port:</source>
+ <translation>Port:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="129"/>
+ <source>Username:</source>
+ <translation>Kullanıcı adı:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="139"/>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="149"/>
+ <source>Optional</source>
+ <translation>İsteğe bağlı</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="142"/>
+ <source>Password:</source>
+ <translation>Parola:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="159"/>
+ <source>Does this computer's Internet connection go through a firewall that only allows connections to certain ports?</source>
+ <translation>Bu bilgisayarın internet bağlantısı sadece belli portlara bağlantı izni veren bir güvenlik duvarı üzerinden gidiyor mu?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="170"/>
+ <source>Allowed ports:</source>
+ <translation>İzin verilen portlar:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="177"/>
+ <source>Example: 80,443</source>
+ <translation>Örnek: 80,443</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="188"/>
+ <source>If this computer's Internet connection is censored, you will need to obtain and use bridge relays.</source>
+ <translation>Bu bilgisayarın internet bağlantısı sansürlü ise, bridge relays adresleri bulmanız ve kullanmanız gerekir.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="197"/>
+ <source>Enter one or more bridge relays (one per line):</source>
+ <translation>Bir veya daha fazla bridge relay adresi girin (satır başına bir tane):</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="212"/>
+ <source>Back</source>
+ <translation>Geri</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="219"/>
+ <source>Connect</source>
+ <translation>Bağlan</translation>
+ </message>
+</context>
+<context>
+ <name>TorPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="24"/>
+ <source>Running:</source>
+ <translation>Çalışıyor:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="27"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="29"/>
+ <source>Yes</source>
+ <translation>Evet</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="27"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="29"/>
+ <source>No</source>
+ <translation>Hayır</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <source>External</source>
+ <translation>Harici</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="26"/>
+ <source>Control connected:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="28"/>
+ <source>Circuits established:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="30"/>
+ <source>Hidden service:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="31"/>
+ <source>Online</source>
+ <translation>Çevrimiçi</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="31"/>
+ <source>Offline</source>
+ <translation>Çevrimdışı</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="32"/>
+ <source>Version:</source>
+ <translation>Sürüm:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="59"/>
+ <source>Error: <b>%1</b></source>
+ <extracomment>%1 is error message</extracomment>
+ <translation>Hata: <b>%1</b></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="75"/>
+ <source>Configure</source>
+ <translation>Yapılandır</translation>
+ </message>
+</context>
+<context>
+ <name>TorStateWidget</name>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="9"/>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="20"/>
+ <source>Connection failed</source>
+ <translation>Bağlantı başarısız</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="12"/>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="22"/>
+ <source>Connecting…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>Bağlanıyor...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="25"/>
+ <source>Connecting… (%1%)</source>
+ <extracomment>%1 is progress percentage, e.g. 100</extracomment>
+ <translation>Bağlanıyor... (%1%)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="32"/>
+ <source>Online</source>
+ <translation>Çevrimiçi</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="34"/>
+ <source>Connected</source>
+ <translation>Bağlı</translation>
+ </message>
+</context>
+</TS>
diff --git a/translation/ricochet_uk.ts b/translation/ricochet_uk.ts
new file mode 100644
index 0000000..1603766
--- /dev/null
+++ b/translation/ricochet_uk.ts
@@ -0,0 +1,694 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="uk">
+<context>
+ <name>AboutPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/AboutPreferences.qml" line="14"/>
+ <source>Ricochet %1</source>
+ <extracomment>%1 version, e.g. 1.0.0</extracomment>
+ <translation>Ricochet %1</translation>
+ </message>
+</context>
+<context>
+ <name>AddContactDialog</name>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="51"/>
+ <source>Share your Ricochet ID to allow connection requests</source>
+ <translation>Поділитись вашим Ricochet ID для запитів на підключення</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="107"/>
+ <source>Cancel</source>
+ <translation>Відмінити</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/AddContactDialog.qml" line="112"/>
+ <source>Add</source>
+ <translation>Додати</translation>
+ </message>
+</context>
+<context>
+ <name>ContactActions</name>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="40"/>
+ <source>Open Window</source>
+ <translation>Відкрити вікно</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="44"/>
+ <source>Details...</source>
+ <translation>Детальніше...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="48"/>
+ <source>Rename</source>
+ <translation>Перейменувати</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactActions.qml" line="53"/>
+ <source>Remove</source>
+ <translation>Видалити</translation>
+ </message>
+</context>
+<context>
+ <name>ContactIDField</name>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="40"/>
+ <source><b>%1</b> is already your contact</source>
+ <translation><b>%1</b> це вже ваш контакт</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="42"/>
+ <source>You can't add yourself as a contact</source>
+ <translation>Ви не можете додати самі себе як контакт</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="44"/>
+ <source>Enter an ID starting with <b>ricochet:</b></source>
+ <translation>Введіть ID який починається з <b>ricochet:</b></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="79"/>
+ <source>Copied to clipboard</source>
+ <translation>Скопійовано до буферу обміну</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactIDField.qml" line="91"/>
+ <source>Copy</source>
+ <translation>Копіювати</translation>
+ </message>
+</context>
+<context>
+ <name>ContactList</name>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="75"/>
+ <source>Online</source>
+ <translation>В мережі</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="76"/>
+ <source>Offline</source>
+ <translation>Не в мережі</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="77"/>
+ <source>Requests</source>
+ <translation>Запити</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="78"/>
+ <source>Rejected</source>
+ <translation>Відхилено</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactList.qml" line="79"/>
+ <source>Outdated</source>
+ <translation>Застарілий</translation>
+ </message>
+</context>
+<context>
+ <name>ContactPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="97"/>
+ <source>Date added:</source>
+ <translation>Дата додавання:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="104"/>
+ <source>Last seen:</source>
+ <translation>Останній раз бачили:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="113"/>
+ <source>Request:</source>
+ <translation>Запит:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="122"/>
+ <source>Pending connection</source>
+ <translation>Встановлення з'єднання</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="123"/>
+ <source>Delivered</source>
+ <translation>Доставлено</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="124"/>
+ <source>Accepted</source>
+ <translation>Прийнято</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="125"/>
+ <source>Error</source>
+ <translation>Помилка</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="126"/>
+ <source>Rejected</source>
+ <translation>Відхилено</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="130"/>
+ <source>%1 (Connected)</source>
+ <extracomment>%1 status, e.g. "Accepted"</extracomment>
+ <translation>%1 (Підключено)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="136"/>
+ <source>Response:</source>
+ <translation>Відповідь:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="158"/>
+ <source>Rename</source>
+ <translation>Перейменувати</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactPreferences.qml" line="165"/>
+ <source>Remove</source>
+ <translation>Видалити</translation>
+ </message>
+</context>
+<context>
+ <name>ContactRequestDialog</name>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="55"/>
+ <source>Someone new is asking to connect to you</source>
+ <translation>Хтось новий запрошує підключення до вас</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="102"/>
+ <source>Reject</source>
+ <translation>Відхилити</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestDialog.qml" line="107"/>
+ <source>Accept</source>
+ <translation>Прийняти</translation>
+ </message>
+</context>
+<context>
+ <name>ContactRequestFields</name>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="17"/>
+ <source>ID:</source>
+ <translation>ID:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="29"/>
+ <source>Name:</source>
+ <translation>Ім'я:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/ContactRequestFields.qml" line="40"/>
+ <source>Message:</source>
+ <translation>Повідомлення:</translation>
+ </message>
+</context>
+<context>
+ <name>GeneralPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="12"/>
+ <source>Use a single window for conversations</source>
+ <translation>Використовувати одне вікно для розмов</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="20"/>
+ <source>Open links in default browser without prompting</source>
+ <translation>Відкривати посилання в браузері без запитань</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="28"/>
+ <source>Play audio notifications</source>
+ <translation>Програти аудіо повідомлення</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/GeneralPreferences.qml" line="37"/>
+ <source>Volume</source>
+ <translation>Гучність</translation>
+ </message>
+</context>
+<context>
+ <name>LanguagePreferences</name>
+ <message>
+ <location filename="../src/ui/qml/LanguagePreferences.qml" line="22"/>
+ <source>Select Language</source>
+ <translation>Вибір мови</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/LanguagePreferences.qml" line="52"/>
+ <source>Restart Ricochet to apply changes</source>
+ <translation>Перезавантажити Ricochet для збереження змін</translation>
+ </message>
+</context>
+<context>
+ <name>LanguagesModel</name>
+ <message>
+ <location filename="../src/ui/LanguagesModel.cpp" line="43"/>
+ <source>System default</source>
+ <translation>Системні налаштування</translation>
+ </message>
+</context>
+<context>
+ <name>Main</name>
+ <message>
+ <location filename="../src/main.cpp" line="73"/>
+ <source>Ricochet Error</source>
+ <translation>Помилка Ricochet</translation>
+ </message>
+</context>
+<context>
+ <name>MainToolBar</name>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="19"/>
+ <source>Add Contact</source>
+ <translation>Додати контакт</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="28"/>
+ <source>Preferences</source>
+ <translation>Налаштування</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MainToolBar.qml" line="75"/>
+ <source>Click to add contacts</source>
+ <translation>Натисніть щоб додати контакти</translation>
+ </message>
+</context>
+<context>
+ <name>MainWindow</name>
+ <message>
+ <location filename="../src/ui/MainWindow.cpp" line="149"/>
+ <source>Remove %1</source>
+ <translation>Видалити %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/MainWindow.cpp" line="150"/>
+ <source>Do you want to permanently remove %1?</source>
+ <translation>Бажаєте назавжди видалити %1?</translation>
+ </message>
+</context>
+<context>
+ <name>MessageDelegate</name>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="26"/>
+ <source>%1 is offline</source>
+ <extracomment>%1 nickname</extracomment>
+ <translation>%1 не в мережі</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="134"/>
+ <source>Copy ID</source>
+ <translation>Копіювати ID</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="134"/>
+ <source>Copy Link</source>
+ <translation>Копіювати посилання</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="139"/>
+ <source>Open with Browser</source>
+ <translation>Відкрити браузер</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="153"/>
+ <source>Add as Contact</source>
+ <translation>Додати як контакт</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="165"/>
+ <source>Copy Message</source>
+ <translation>Скопіювати повідомлення</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDelegate.qml" line="172"/>
+ <source>Copy Selection</source>
+ <translation>Скопіювати вибране</translation>
+ </message>
+</context>
+<context>
+ <name>MessageDialogWrapper</name>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="7"/>
+ <source>Remove %1</source>
+ <translation>Видалити %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="9"/>
+ <source>Do you want to permanently remove %1?</source>
+ <extracomment>%1 nickname</extracomment>
+ <translation>Бажаєте назавжди видалити %1?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/MessageDialogWrapper.qml" line="10"/>
+ <source>This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request.</source>
+ <translation>Цей контакт більше не зможе вам писати і буде проінформований про видалення. Він може послати запит на нове з'єднання. </translation>
+ </message>
+</context>
+<context>
+ <name>NetworkSetupWizard</name>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="100"/>
+ <source>This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network.</source>
+ <translation>Це інтернет з'єднання не блокується провайдером. Я б хотів підключитись напряму до мережі Tor.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="107"/>
+ <source>Connect</source>
+ <translation>Підключення</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="124"/>
+ <source>This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings.</source>
+ <translation>Це інтернет з'єднання цензурується провайдером або фільтрується. Мені треба налаштувати параметри мережі.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/NetworkSetupWizard.qml" line="131"/>
+ <source>Configure</source>
+ <translation>Налаштувати</translation>
+ </message>
+</context>
+<context>
+ <name>OfflineStateItem</name>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="107"/>
+ <source>Configure</source>
+ <translation>Налаштувати</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="115"/>
+ <source>Details</source>
+ <translation>Детальніше</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="143"/>
+ <source>Connection failed</source>
+ <translation>З'єднання не вдалося.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OfflineStateItem.qml" line="169"/>
+ <source>Connecting…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>З'єднання...</translation>
+ </message>
+</context>
+<context>
+ <name>OpenBrowserDialog</name>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="39"/>
+ <source><b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead.</source>
+ <translation><b>Попередження!</b> Відкриття посилань у браузері за замовчуванням може нашкодити вашій безпеці та анонімності. <br><br>Замість цього ви можете їх <a href='.'>скопіювати у буфер обміну</a> .</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="58"/>
+ <source>Don't ask again for links from %1</source>
+ <translation>Не запитувати знову про посилання від %1</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="64"/>
+ <source>Don't ask again for any links (not recommended!)</source>
+ <translation>Не запитувати знову про будь-які посилання (не рекомендовано!)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="71"/>
+ <source>Open Browser</source>
+ <translation>Відкрити браузер</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/OpenBrowserDialog.qml" line="83"/>
+ <source>Cancel</source>
+ <translation>Відмінити</translation>
+ </message>
+</context>
+<context>
+ <name>PreferencesDialog</name>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="12"/>
+ <source>Ricochet Preferences</source>
+ <translation>Налаштування Ricochet</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="41"/>
+ <source>General</source>
+ <translation>Основні</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="46"/>
+ <source>Language</source>
+ <translation>Мова</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="51"/>
+ <source>Contacts</source>
+ <translation>Контакти</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="56"/>
+ <source>Tor</source>
+ <translation>Tor</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/PreferencesDialog.qml" line="61"/>
+ <source>About</source>
+ <translation>Про </translation>
+ </message>
+</context>
+<context>
+ <name>QCocoaMenuItem</name>
+ <message>
+ <location filename="../src/ui/qml/MainWindow.qml" line="29"/>
+ <source>Preference</source>
+ <translation>Налаштування</translation>
+ </message>
+</context>
+<context>
+ <name>StartupStatusPage</name>
+ <message>
+ <location filename="../src/ui/qml/StartupStatusPage.qml" line="18"/>
+ <source>The Tor process was not started successfully. This is most likely an installation or system error.</source>
+ <translation>Процес Tor не був вдало запущений. Скоріш за все це помилка при установці або системна.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/StartupStatusPage.qml" line="49"/>
+ <source>Quit</source>
+ <translation>Вийти</translation>
+ </message>
+</context>
+<context>
+ <name>TorBootstrapStatus</name>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="17"/>
+ <source>Connecting to the Tor network…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>З'єднання з мережею Tor...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="50"/>
+ <source>Back</source>
+ <translation>Назад</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="57"/>
+ <source>Hide details</source>
+ <translation>Сховати деталі</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="57"/>
+ <source>Show details</source>
+ <translation>Показати деталі</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorBootstrapStatus.qml" line="69"/>
+ <source>Done</source>
+ <translation>Виконано</translation>
+ </message>
+</context>
+<context>
+ <name>TorConfigurationPage</name>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="72"/>
+ <source>Does this computer need a proxy to access the internet?</source>
+ <translation>Чи потребує цей комп'ютер проксі для доступу до інтернету?</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="84"/>
+ <source>Proxy type:</source>
+ <translation>Тип проксі:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="89"/>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="91"/>
+ <source>None</source>
+ <translation>Ніякого</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="106"/>
+ <source>Address:</source>
+ <translation>Адреса:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="115"/>
+ <source>IP address or hostname</source>
+ <translation>IP адреса чи ім'я сервера</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="118"/>
+ <source>Port:</source>
+ <translation>Порт:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="129"/>
+ <source>Username:</source>
+ <translation>Ім'я:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="139"/>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="149"/>
+ <source>Optional</source>
+ <translation>Опції</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="142"/>
+ <source>Password:</source>
+ <translation>Пароль:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="159"/>
+ <source>Does this computer's Internet connection go through a firewall that only allows connections to certain ports?</source>
+ <translation>З'єднання цього комп'ютеру проходять через firewall який дозволяй з'єднання тільки з певними портами? </translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="170"/>
+ <source>Allowed ports:</source>
+ <translation>Дозволені порти:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="177"/>
+ <source>Example: 80,443</source>
+ <translation>Наприклад: 80,443</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="188"/>
+ <source>If this computer's Internet connection is censored, you will need to obtain and use bridge relays.</source>
+ <translation>Якщо інтернет з'єднання цього комп'ютеру цензуруються, то вам потрібно отримати і використовувати ретранслятори. </translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="197"/>
+ <source>Enter one or more bridge relays (one per line):</source>
+ <translation>Введіть один або декілька адрес ретрансляторів (один на лінію)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="212"/>
+ <source>Back</source>
+ <translation>Назад</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorConfigurationPage.qml" line="219"/>
+ <source>Connect</source>
+ <translation>Підключення</translation>
+ </message>
+</context>
+<context>
+ <name>TorPreferences</name>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="24"/>
+ <source>Running:</source>
+ <translation>Запущено:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="27"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="29"/>
+ <source>Yes</source>
+ <translation>Так</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="27"/>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="29"/>
+ <source>No</source>
+ <translation>Ні</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="25"/>
+ <source>External</source>
+ <translation>Зовнішній</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="26"/>
+ <source>Control connected:</source>
+ <translation>Контрольне з'єднання:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="28"/>
+ <source>Circuits established:</source>
+ <translation>Схема побудована:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="30"/>
+ <source>Hidden service:</source>
+ <translation>Прихований сервіс:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="31"/>
+ <source>Online</source>
+ <translation>В мережі</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="31"/>
+ <source>Offline</source>
+ <translation>Не в мережі</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="32"/>
+ <source>Version:</source>
+ <translation>Версія:</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="59"/>
+ <source>Error: <b>%1</b></source>
+ <extracomment>%1 is error message</extracomment>
+ <translation>Помилка: <b>%1</b></translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorPreferences.qml" line="75"/>
+ <source>Configure</source>
+ <translation>Налаштувати</translation>
+ </message>
+</context>
+<context>
+ <name>TorStateWidget</name>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="9"/>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="20"/>
+ <source>Connection failed</source>
+ <translation>З'єднання не вдалося.</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="12"/>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="22"/>
+ <source>Connecting…</source>
+ <extracomment>\u2026 is ellipsis</extracomment>
+ <translation>З'єднання...</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="25"/>
+ <source>Connecting… (%1%)</source>
+ <extracomment>%1 is progress percentage, e.g. 100</extracomment>
+ <translation>З'єднання... (%1%)</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="32"/>
+ <source>Online</source>
+ <translation>В мережі</translation>
+ </message>
+ <message>
+ <location filename="../src/ui/qml/TorStateWidget.qml" line="34"/>
+ <source>Connected</source>
+ <translation>Підключено</translation>
+ </message>
+</context>
+</TS>
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-privacy/packages/ricochet-im.git
More information about the Pkg-privacy-commits
mailing list