1
0
mirror of https://github.com/qTox/qTox.git synced 2024-03-22 14:00:36 +08:00

Merge pull request #5932

Mick Sayson (6):
      feat(messages): Multipacket message support
      feat(messages): History and offline message support for extended messages
      feat(extensions): UI updates for extension support
      fix(extensions): Add toxext to CI scripts
      feat(extensions): Update documentation
      feat(extensions): Split messages on extended messages
This commit is contained in:
Anthony Bilinski 2021-01-30 23:47:20 -08:00
commit 59038088cb
No known key found for this signature in database
GPG Key ID: 2AA8E0DA1B31FB3C
63 changed files with 2160 additions and 524 deletions

View File

@ -126,16 +126,18 @@ CC="ccache $CC" CXX="ccache $CXX" make -j$(nproc)
sudo checkinstall --install --pkgname libsodium --pkgversion 1.0.8 --nodoc -y
sudo ldconfig
cd ..
# toxcore
git clone --branch v0.2.12 --depth=1 https://github.com/toktok/c-toxcore.git toxcore
cd toxcore
autoreconf -if
CC="ccache $CC" CXX="ccache $CXX" ./configure
CC="ccache $CC" CXX="ccache $CXX" make -j$(nproc) > /dev/null
mkdir build-cmake
cd build-cmake
CC="ccache $CC" CXX="ccache $CXX" cmake ..
make -j$(nproc) > /dev/null
sudo make install
echo '/usr/local/lib/' | sudo tee -a /etc/ld.so.conf.d/locallib.conf
sudo ldconfig
cd ..
cd ../..
# filteraudio
git clone --branch v0.0.1 --depth=1 https://github.com/irungentoo/filter_audio filteraudio
@ -144,12 +146,32 @@ CC="ccache $CC" CXX="ccache $CXX" sudo make install -j$(nproc)
sudo ldconfig
cd ..
$CC --version
$CXX --version
# toxext
git clone --branch v0.0.2 --depth=1 https://github.com/toxext/toxext toxext
cd toxext
mkdir build
cd build
cmake ..
make -j$(nproc)
sudo make install
cd ../../
# toxext_messages
git clone --branch v0.0.2 --depth=1 https://github.com/toxext/tox_extension_messages tox_extension_messages
cd tox_extension_messages
mkdir build
cd build
cmake ..
make -j$(nproc)
sudo make install
cd ../../
# needed, otherwise ffmpeg doesn't get detected
export PKG_CONFIG_PATH="$PWD/libs/lib/pkgconfig"
$CC --version
$CXX --version
build_qtox() {
bdir() {
cd $BUILDDIR

View File

@ -218,6 +218,8 @@ set(${PROJECT_NAME}_SOURCES
src/chatlog/textformatter.h
src/core/coreav.cpp
src/core/coreav.h
src/core/coreext.cpp
src/core/coreext.h
src/core/core.cpp
src/core/corefile.cpp
src/core/corefile.h
@ -360,6 +362,8 @@ set(${PROJECT_NAME}_SOURCES
src/widget/contentlayout.h
src/widget/emoticonswidget.cpp
src/widget/emoticonswidget.h
src/widget/extensionstatus.cpp
src/widget/extensionstatus.h
src/widget/flowlayout.cpp
src/widget/flowlayout.h
src/widget/searchform.cpp

View File

@ -35,7 +35,7 @@
## Dependencies
| Name | Version | Modules |
|---------------|-------------|----------------------------------------------------------|
|--------------------------|-------------|----------------------------------------------------------|
| [Qt] | >= 5.5.0 | concurrent, core, gui, network, opengl, svg, widget, xml |
| [GCC]/[MinGW] | >= 4.8 | C++11 enabled |
| [toxcore] | >= 0.2.10 | core, av |
@ -46,6 +46,8 @@
| [sqlcipher] | >= 3.2.0 | |
| [pkg-config] | >= 0.28 | |
| [snorenotify] | >= 0.7.0 | optional dependency |
| [toxext] | >= 0.0.1 | |
| [tox_extension_messages] | >= 0.0.1 | |
## Optional dependencies
@ -424,6 +426,30 @@ echo '/usr/local/lib/' | sudo tee -a /etc/ld.so.conf.d/locallib.conf
sudo ldconfig
```
### Compile extensions
qTox uses the toxext library and some of the extensions that go with it.
You will likely have to compile these yourself
```bash
git clone https://github.com/toxext/toxext.git toxext
cd toxext
git checkout v0.0.2
cmake .
make -j$(nproc)
sudo make install
```
```bash
git clone https://github.com/toxext/tox_extension_messages.git tox_extension_messages
cd tox_extension_messages
git checkout v0.0.2
cmake .
make -j$(nproc)
sudo make install
```
### Compile qTox
**Make sure that all the dependencies are installed.** If you experience
@ -756,3 +782,5 @@ Switches:
[toxcore]: https://github.com/TokTok/c-toxcore/
[sonnet]: https://github.com/KDE/sonnet
[snorenotify]: https://techbase.kde.org/Projects/Snorenotify
[toxext]: https://github.com/toxext/toxext
[tox_extension_messages]: https://github.com/toxext/tox_extension_messages

View File

@ -52,7 +52,7 @@ copy_libs() {
echo Copying libraries…
for lib in "${libs[@]}"
do
cp -v "$lib" "$dest"
cp -v -r "$lib" "$dest"
done
}

View File

@ -49,14 +49,20 @@ readonly BASE_DIR="${SCRIPT_DIR}/${INSTALL_DIR}"
# versions of libs to checkout
readonly TOXCORE_VERSION="v0.2.12"
readonly TOXEXT_VERSION="v0.0.2"
readonly TOX_EXT_MESSAGES_VERSION="v0.0.2"
readonly SQLCIPHER_VERSION="v4.3.0"
# directory names of cloned repositories
readonly TOXCORE_DIR="libtoxcore-$TOXCORE_VERSION"
readonly TOXEXT_DIR="toxext-$TOXEXT_VERSION"
readonly TOX_EXT_MESSAGES_DIR="tox_ext_messages-$TOXEXT_VERSION"
readonly SQLCIPHER_DIR="sqlcipher-$SQLCIPHER_VERSION"
# default values for user given parameters
INSTALL_TOX=true
INSTALL_TOXEXT=true
INSTALL_TOX_EXT_MESSAGES=true
INSTALL_SQLCIPHER=false
SYSTEM_WIDE=true
KEEP_BUILD_FILES=false
@ -128,6 +134,57 @@ install_toxcore() {
fi
}
install_toxext() {
if [[ $INSTALL_TOXEXT = "true" ]]
then
git clone https://github.com/toxext/toxext.git \
--branch $TOXEXT_VERSION \
"${BASE_DIR}/${TOXEXT_DIR}"
pushd ${BASE_DIR}/${TOXEXT_DIR}
# compile and install
if [[ $SYSTEM_WIDE = "false" ]]
then
cmake . -DCMAKE_INSTALL_PREFIX=${BASE_DIR}
make -j $(nproc)
make install
else
cmake .
make -j $(nproc)
sudo make install
sudo ldconfig
fi
popd
fi
}
install_tox_ext_messages() {
if [[ $INSTALL_TOX_EXT_MESSAGES = "true" ]]
then
git clone https://github.com/toxext/tox_extension_messages.git \
--branch $TOX_EXT_MESSAGES_VERSION \
"${BASE_DIR}/${TOX_EXT_MESSAGES_DIR}"
pushd ${BASE_DIR}/${TOX_EXT_MESSAGES_DIR}
# compile and install
if [[ $SYSTEM_WIDE = "false" ]]
then
cmake . -DCMAKE_INSTALL_PREFIX=${BASE_DIR}
make -j $(nproc)
make install
else
cmake .
make -j $(nproc)
sudo make install
sudo ldconfig
fi
popd
fi
}
install_sqlcipher() {
if [[ $INSTALL_SQLCIPHER = "true" ]]
@ -178,6 +235,22 @@ main() {
then
INSTALL_TOX=false
shift
elif [ ${1} = "--with-toxext" ]
then
INSTALL_TOXEXT=true
shift
elif [ ${1} = "--without-toxext" ]
then
INSTALL_TOXEXT=false
shift
elif [ ${1} = "--with-toxext-messages" ]
then
INSTALL_TOX_EXT_MESSAGES=true
shift
elif [ ${1} = "--without-toxext-messages" ]
then
INSTALL_TOX_EXT_MESSAGES=false
shift
elif [ ${1} = "--with-sqlcipher" ]
then
INSTALL_SQLCIPHER=true
@ -221,6 +294,8 @@ main() {
############### install step ###############
install_toxcore
install_toxext
install_tox_ext_messages
install_sqlcipher
############### cleanup step ###############

View File

@ -33,6 +33,9 @@ find_package(Qt5Test REQUIRED)
find_package(Qt5Widgets REQUIRED)
find_package(Qt5Xml REQUIRED)
find_package(ToxExt REQUIRED)
find_package(ToxExtensionMessages REQUIRED)
function(add_dependency)
set(ALL_LIBRARIES ${ALL_LIBRARIES} ${ARGN} PARENT_SCOPE)
endfunction()
@ -47,6 +50,10 @@ add_dependency(
Qt5::Widgets
Qt5::Xml)
add_dependency(
ToxExt::ToxExt
ToxExtensionMessages::ToxExtensionMessages)
include(CMakeParseArguments)
function(search_dependency pkg)

View File

@ -489,3 +489,11 @@ public key, and which changes on every start of a client, so it's best to use a
[ToxMe service]: #register-on-toxme
[user profile]: #user-profile
[profile corner]: #profile-corner
# Extensions
qTox supports extra features through the use of extensions to the tox protocol. Not all contacts are going to support these extensions.
For most cases you won't have to do anything, but you may wonder why behavior of chats is different for some friends. There is a puzzle piece icon to the left of your contact's name in the top of a chat. If it's green that means that they support all the features qTox cares about. If it's yellow it means some of the features are supported. If it's red it means that they don't support any extensions.
You can hover over the icon to see which extensions they support. qTox should dynamically enable/disable features based on the extension set of your friend.

View File

@ -117,6 +117,30 @@
}
]
},
{
"name": "toxext",
"buildsystem": "cmake-ninja",
"sources": [
{
"type": "git",
"url": "https://github.com/toxext/toxext",
"tag": "v0.0.2",
"commit": "0280357a0dded4dd46d0ff29f52875687136472d"
}
]
},
{
"name": "tox_extension_messages",
"buildsystem": "cmake-ninja",
"sources": [
{
"type": "git",
"url": "https://github.com/toxext/tox_extension_messages",
"tag": "v0.0.2",
"commit": "f1f4539cf1aeed0bcc0ad476fbae74cb5bd0cf66"
}
]
},
{
"name": "qTox",
"buildsystem": "cmake-ninja",

View File

@ -0,0 +1 @@
<?xml version="1.0" ?><svg height="23px" version="1.1" viewBox="0 0 23 23" width="23px" xmlns="http://www.w3.org/2000/svg" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns" xmlns:xlink="http://www.w3.org/1999/xlink"><title/><desc/><defs/><g fill="none" fill-rule="evenodd" id="Page-1" stroke="none" stroke-width="1"><g fill="#6FC062" id="Core" transform="translate(-253.000000, -211.000000)"><g id="extension" transform="translate(253.500000, 211.500000)"><path d="M18.5,10 L17,10 L17,6 C17,4.9 16.1,4 15,4 L11,4 L11,2.5 C11,1.1 9.9,0 8.5,0 C7.1,0 6,1.1 6,2.5 L6,4 L2,4 C0.9,4 0,4.9 0,6 L0,9.8 L1.5,9.8 C3,9.8 4.2,11 4.2,12.5 C4.2,14 3,15.2 1.5,15.2 L0,15.2 L0,19 C0,20.1 0.9,21 2,21 L5.8,21 L5.8,19.5 C5.8,18 7,16.8 8.5,16.8 C10,16.8 11.2,18 11.2,19.5 L11.2,21 L15,21 C16.1,21 17,20.1 17,19 L17,15 L18.5,15 C19.9,15 21,13.9 21,12.5 C21,11.1 19.9,10 18.5,10 L18.5,10 Z" id="Shape"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 906 B

View File

@ -0,0 +1 @@
<?xml version="1.0" ?><svg height="23px" version="1.1" viewBox="0 0 23 23" width="23px" xmlns="http://www.w3.org/2000/svg" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns" xmlns:xlink="http://www.w3.org/1999/xlink"><title/><desc/><defs/><g fill="none" fill-rule="evenodd" id="Page-1" stroke="none" stroke-width="1"><g fill="#CDBE41" id="Core" transform="translate(-253.000000, -211.000000)"><g id="extension" transform="translate(253.500000, 211.500000)"><path d="M18.5,10 L17,10 L17,6 C17,4.9 16.1,4 15,4 L11,4 L11,2.5 C11,1.1 9.9,0 8.5,0 C7.1,0 6,1.1 6,2.5 L6,4 L2,4 C0.9,4 0,4.9 0,6 L0,9.8 L1.5,9.8 C3,9.8 4.2,11 4.2,12.5 C4.2,14 3,15.2 1.5,15.2 L0,15.2 L0,19 C0,20.1 0.9,21 2,21 L5.8,21 L5.8,19.5 C5.8,18 7,16.8 8.5,16.8 C10,16.8 11.2,18 11.2,19.5 L11.2,21 L15,21 C16.1,21 17,20.1 17,19 L17,15 L18.5,15 C19.9,15 21,13.9 21,12.5 C21,11.1 19.9,10 18.5,10 L18.5,10 Z" id="Shape"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 906 B

View File

@ -0,0 +1 @@
<?xml version="1.0" ?><svg height="23px" version="1.1" viewBox="0 0 23 23" width="23px" xmlns="http://www.w3.org/2000/svg" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns" xmlns:xlink="http://www.w3.org/1999/xlink"><title/><desc/><defs/><g fill="none" fill-rule="evenodd" id="Page-1" stroke="none" stroke-width="1"><g fill="#C94F50" id="Core" transform="translate(-253.000000, -211.000000)"><g id="extension" transform="translate(253.500000, 211.500000)"><path d="M18.5,10 L17,10 L17,6 C17,4.9 16.1,4 15,4 L11,4 L11,2.5 C11,1.1 9.9,0 8.5,0 C7.1,0 6,1.1 6,2.5 L6,4 L2,4 C0.9,4 0,4.9 0,6 L0,9.8 L1.5,9.8 C3,9.8 4.2,11 4.2,12.5 C4.2,14 3,15.2 1.5,15.2 L0,15.2 L0,19 C0,20.1 0.9,21 2,21 L5.8,21 L5.8,19.5 C5.8,18 7,16.8 8.5,16.8 C10,16.8 11.2,18 11.2,19.5 L11.2,21 L15,21 C16.1,21 17,20.1 17,19 L17,15 L18.5,15 C19.9,15 21,13.9 21,12.5 C21,11.1 19.9,10 18.5,10 L18.5,10 Z" id="Shape"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 906 B

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 10 10" enable-background="new 0 0 10 10" xml:space="preserve">
<g>
<path fill="#C94F50" d="M5,1.5c1.9,0,3.5,1.6,3.5,3.5S6.9,8.5,5,8.5C3.1,8.5,1.5,6.9,1.5,5S3.1,1.5,5,1.5 M5,0C2.2,0,0,2.2,0,5
s2.2,5,5,5c2.8,0,5-2.2,5-5S7.8,0,5,0z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 621 B

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<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"
height="14"
width="14"
xml:space="preserve"
enable-background="new 0 0 10 10"
viewBox="0 0 14 14"
y="0px"
x="0px"
id="Layer_1"
version="1.1"><metadata
id="metadata9"><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></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs7" /><g
style="stroke-width:0.36135802;stroke-miterlimit:4;stroke-dasharray:none;stroke:#c94f50"
id="layer1"
transform="matrix(3.0289233,0,0,3.0289233,0.18380903,-887.48895)"><path
style="fill:#000000;stroke-width:0.36135802;stroke-miterlimit:4;stroke-dasharray:none;stroke:#c94f50"
d=""
id="path4500" /><path
style="fill:#000000;stroke-width:0.36135802;stroke-miterlimit:4;stroke-dasharray:none;stroke:#c94f50"
d=""
id="path4498" /><path
id="path4528"
style="fill:none;stroke:#c94f50;stroke-width:0.36135802;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 0.10133195,293.90792 1.88406635,1.94794 H 2.5570778 L 4.3764717,293.9374 M 0.13990242,293.77165 H 4.375461 v 2.99779 H 0.13310726 l 9.0535e-4,-3.17789" /></g></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -41,6 +41,8 @@ QT_VER=($(ls ${QT_DIR} | sed -n -e 's/^\([0-9]*\.([0-9]*\.([0-9]*\).*/\1/' -e '1
QT_DIR_VER="${QT_DIR}/${QT_VER[1]}"
TOXCORE_DIR="${MAIN_DIR}/toxcore" # Change to Git location
TOX_EXT_DIR="${MAIN_DIR}/toxext"
TOX_EXT_MESSAGES_DIR="${MAIN_DIR}/tox_extension_messages"
LIB_INSTALL_PREFIX="${QTOX_DIR}/libs"
@ -80,6 +82,43 @@ build_toxcore() {
make install > /dev/null || exit 1
}
build_toxext() {
echo "Starting Toxext build and install"
cd $TOX_EXT_DIR
echo "Now working in: ${PWD}"
[[ $TRAVIS != true ]] \
&& sleep 3
mkdir _build && cd _build
fcho "Starting cmake ..."
PKG_CONFIG_PATH="${LIB_INSTALL_PREFIX}"/lib/pkgconfig cmake -DCMAKE_INSTALL_PREFIX="${LIB_INSTALL_PREFIX}" ..
make clean &> /dev/null
fcho "Compiling toxext."
make > /dev/null || exit 1
fcho "Installing toxext."
make install > /dev/null || exit 1
}
build_tox_extension_messages() {
echo "Starting tox_extension_messages build and install"
cd $TOX_EXT_MESSAGES_DIR
echo "Now working in: ${PWD}"
[[ $TRAVIS != true ]] \
&& sleep 3
mkdir _build && cd _build
fcho "Starting cmake ..."
PKG_CONFIG_PATH="${LIB_INSTALL_PREFIX}"/lib/pkgconfig cmake -DCMAKE_INSTALL_PREFIX="${LIB_INSTALL_PREFIX}" ..
make clean &> /dev/null
fcho "Compiling tox_extension_messages."
make > /dev/null || exit 1
fcho "Installing tox_extension_messages."
make install > /dev/null || exit 1
}
install() {
fcho "=============================="
fcho "This script will install the necessary applications and libraries needed to compile qTox properly."
@ -145,6 +184,26 @@ install() {
fcho "Cloning Toxcore git ... "
git clone --branch v0.2.12 --depth=1 https://github.com/toktok/c-toxcore "$TOXCORE_DIR"
fi
# toxext
if [[ -e $TOX_EXT_DIR/.git/index ]]
then
fcho "ToxExt git repo already in place !"
cd $TOX_EXT_DIR
git pull
else
fcho "Cloning ToxExt git ... "
git clone --branch v0.0.2 --depth=1 https://github.com/toxext/toxext "$TOX_EXT_DIR"
fi
# tox_extension_messages
if [[ -e $TOX_EXT_MESSAGES_DIR/.git/index ]]
then
fcho "ToxExt git repo already in place !"
cd $TOX_EXT_MESSAGES_DIR
git pul
else
fcho "Cloning tox_extension_messages git ... "
git clone --branch v0.0.2 --depth=1 https://github.com/toxext/tox_extension_messages "$TOX_EXT_MESSAGES_DIR"
fi
# qTox
if [[ $TRAVIS = true ]]
then
@ -161,12 +220,16 @@ install() {
fi
fi
if [[ $TRAVIS != true ]]
then
fcho "If all went well you should now have all the tools needed to compile qTox!"
fi
# toxcore build
if [[ $TRAVIS = true ]]
then
build_toxcore
else
fcho "If all went well you should now have all the tools needed to compile qTox!"
read -r -p "Would you like to install toxcore now? [y/N] " response
if [[ $response =~ ^([yY][eE][sS]|[yY])$ ]]
then
@ -176,6 +239,34 @@ install() {
fi
fi
# toxext build
if [[ $TRAVIS = true ]]
then
build_toxext
else
read -r -p "Would you like to install toxext now? [y/N] " response
if [[ $response =~ ^([yY][eE][sS]|[yY])$ ]]
then
build_toxext
else
fcho "You can simply use the -u command and say [Yes/n] when prompted"
fi
fi
# tox_extension_messages build
if [[ $TRAVIS = true ]]
then
build_tox_extension_messages
else
read -r -p "Would you like to install tox_extension_messages now? [y/N] " response
if [[ $response =~ ^([yY][eE][sS]|[yY])$ ]]
then
build_tox_extension_messages
else
fcho "You can simply use the -u command and say [Yes/n] when prompted"
fi
fi
QT_VER=($(ls ${QT_DIR} | sed -n -e 's/^\([0-9]*\.([0-9]*\.([0-9]*\).*/\1/' -e '1p;$p'))
QT_DIR_VER="${QT_DIR}/${QT_VER[1]}"
@ -230,7 +321,6 @@ build() {
cd $BUILD_DIR
fcho "Now working in ${PWD}"
fcho "Starting cmake ..."
export CMAKE_PREFIX_PATH=$(brew --prefix qt5)
if [[ $TRAVIS = true ]]
then
@ -238,7 +328,11 @@ build() {
else
STRICT_OPTIONS="OFF"
fi
cmake -H$QTOX_DIR -B. -DUPDATE_CHECK=ON -DSPELL_CHECK=OFF -DSTRICT_OPTIONS="${STRICT_OPTIONS}"
cmake -H$QTOX_DIR -B. \
-DUPDATE_CHECK=ON \
-DSPELL_CHECK=OFF \
-DSTRICT_OPTIONS="${STRICT_OPTIONS}" \
-DCMAKE_PREFIX_PATH="$(brew --prefix qt5);${LIB_INSTALL_PREFIX}"
make -j$(sysctl -n hw.ncpu)
}
@ -262,6 +356,8 @@ bootstrap() {
#Toxcore
build_toxcore
build_toxext
build_tox_extension_messages
#Boot Strap
fcho "Running: sudo ${QTOX_DIR}/bootstrap-osx.sh"

View File

@ -22,10 +22,15 @@
<file>img/status/away_notification.svg</file>
<file>img/status/busy.svg</file>
<file>img/status/busy_notification.svg</file>
<file>img/status/negotiating.svg</file>
<file>img/status/negotiating_notification.svg</file>
<file>img/status/offline.svg</file>
<file>img/status/offline_notification.svg</file>
<file>img/status/online.svg</file>
<file>img/status/online_notification.svg</file>
<file>img/status/extensions_available.svg</file>
<file>img/status/extensions_partial.svg</file>
<file>img/status/extensions_unavailable.svg</file>
<file>img/taskbar/dark/taskbar_online.svg</file>
<file>img/taskbar/dark/taskbar_online_event.svg</file>
<file>img/taskbar/dark/taskbar_away.svg</file>

View File

@ -228,6 +228,11 @@ void ChatMessage::markAsDelivered(const QDateTime& time)
replaceContent(2, new Timestamp(time, Settings::getInstance().getTimestampFormat(), baseFont));
}
void ChatMessage::markAsBroken()
{
replaceContent(2, new Broken(Style::getImagePath("chatArea/error.svg"), QSize(16, 16)));
}
QString ChatMessage::toString() const
{
ChatLineContent* c = getContent(1);

View File

@ -60,6 +60,7 @@ public:
static ChatMessage::Ptr createBusyNotification();
void markAsDelivered(const QDateTime& time);
void markAsBroken();
QString toString() const;
bool isAction() const;
void setAsAction();

View File

@ -21,6 +21,8 @@
#include "core.h"
#include "coreav.h"
#include "corefile.h"
#include "src/core/coreext.h"
#include "src/core/dhtserver.h"
#include "src/core/icoresettings.h"
#include "src/core/toxlogger.h"
@ -515,6 +517,7 @@ void Core::registerCallbacks(Tox* tox)
tox_callback_conference_peer_list_changed(tox, onGroupPeerListChange);
tox_callback_conference_peer_name(tox, onGroupPeerNameChange);
tox_callback_conference_title(tox, onGroupTitleChange);
tox_callback_friend_lossless_packet(tox, onLosslessPacket);
}
/**
@ -639,6 +642,9 @@ ToxCorePtr Core::makeToxCore(const QByteArray& savedata, const ICoreSettings* co
return {};
}
core->ext = CoreExt::makeCoreExt(core->tox.get());
connect(core.get(), &Core::friendStatusChanged, core->ext.get(), &CoreExt::onFriendStatusChanged);
registerCallbacks(core->tox.get());
// connect the thread with the Core
@ -714,6 +720,16 @@ QMutex &Core::getCoreLoopLock() const
return coreLoopLock;
}
const CoreExt* Core::getExt() const
{
return ext.get();
}
CoreExt* Core::getExt()
{
return ext.get();
}
/* Using the now commented out statements in checkConnection(), I watched how
* many ticks disconnects-after-initial-connect lasted. Out of roughly 15 trials,
* 5 disconnected; 4 were DCd for less than 20 ticks, while the 5th was ~50 ticks.
@ -734,6 +750,7 @@ void Core::process()
static int tolerance = CORE_DISCONNECT_TOLERANCE;
tox_iterate(tox.get(), this);
ext->process();
#ifdef DEBUG
// we want to see the debug messages immediately
@ -988,6 +1005,16 @@ void Core::onGroupTitleChange(Tox*, uint32_t groupId, uint32_t peerId, const uin
emit core->groupTitleChanged(groupId, author, ToxString(cTitle, length).getQString());
}
/**
* @brief Handling of custom lossless packets received by toxcore. Currently only used to forward toxext packets to CoreExt
*/
void Core::onLosslessPacket(Tox*, uint32_t friendId,
const uint8_t* data, size_t length, void* vCore)
{
Core* core = static_cast<Core*>(vCore);
core->ext->onLosslessPacket(friendId, data, length);
}
void Core::onReadReceiptCallback(Tox*, uint32_t friendId, uint32_t receipt, void* core)
{
emit static_cast<Core*>(core)->receiptRecieved(friendId, ReceiptNum{receipt});
@ -1067,8 +1094,9 @@ bool Core::sendMessageWithType(uint32_t friendId, const QString& message, Tox_Me
ReceiptNum& receipt)
{
int size = message.toUtf8().size();
auto maxSize = static_cast<int>(tox_max_message_length());
auto maxSize = static_cast<int>(getMaxMessageSize());
if (size > maxSize) {
assert(false);
qCritical() << "Core::sendMessageWithType called with message of size:" << size
<< "when max is:" << maxSize << ". Ignoring.";
return false;
@ -1112,7 +1140,7 @@ void Core::sendGroupMessageWithType(int groupId, const QString& message, Tox_Mes
QMutexLocker ml{&coreLoopLock};
int size = message.toUtf8().size();
auto maxSize = static_cast<int>(tox_max_message_length());
auto maxSize = static_cast<int>(getMaxMessageSize());
if (size > maxSize) {
qCritical() << "Core::sendMessageWithType called with message of size:" << size
<< "when max is:" << maxSize << ". Ignoring.";
@ -1734,11 +1762,8 @@ QString Core::getFriendUsername(uint32_t friendnumber) const
return ToxString(nameBuf.data(), nameSize).getQString();
}
QStringList Core::splitMessage(const QString& message)
uint64_t Core::getMaxMessageSize() const
{
QStringList splittedMsgs;
QByteArray ba_message{message.toUtf8()};
/*
* TODO: Remove this hack; the reported max message length we receive from c-toxcore
* as of 08-02-2019 is inaccurate, causing us to generate too large messages when splitting
@ -1749,33 +1774,7 @@ QStringList Core::splitMessage(const QString& message)
*
* (uint32_t tox_max_message_length(void); declared in tox.h, unable to see explicit definition)
*/
const auto maxLen = static_cast<int>(tox_max_message_length()) - 50;
while (ba_message.size() > maxLen) {
int splitPos = ba_message.lastIndexOf('\n', maxLen - 1);
if (splitPos <= 0) {
splitPos = ba_message.lastIndexOf(' ', maxLen - 1);
}
if (splitPos <= 0) {
constexpr uint8_t firstOfMultiByteMask = 0xC0;
constexpr uint8_t multiByteMask = 0x80;
splitPos = maxLen;
// don't split a utf8 character
if ((ba_message[splitPos] & multiByteMask) == multiByteMask) {
while ((ba_message[splitPos] & firstOfMultiByteMask) != firstOfMultiByteMask) {
--splitPos;
}
}
--splitPos;
}
splittedMsgs.append(QString{ba_message.left(splitPos + 1)});
ba_message = ba_message.mid(splitPos + 1);
}
splittedMsgs.append(QString{ba_message});
return splittedMsgs;
return tox_max_message_length() - 50;
}
QString Core::getPeerName(const ToxPk& id) const

View File

@ -44,6 +44,7 @@
class CoreAV;
class CoreFile;
class CoreExt;
class IAudioControl;
class ICoreSettings;
class GroupInvite;
@ -79,10 +80,12 @@ public:
Tox* getTox() const;
QMutex& getCoreLoopLock() const;
const CoreExt* getExt() const;
CoreExt* getExt();
~Core();
static const QString TOX_EXT;
static QStringList splitMessage(const QString& message);
uint64_t getMaxMessageSize() const;
QString getPeerName(const ToxPk& id) const;
QVector<uint32_t> getFriendList() const;
GroupId getGroupPersistentId(uint32_t groupNumber) const override;
@ -215,6 +218,9 @@ private:
size_t length, void* core);
static void onGroupTitleChange(Tox* tox, uint32_t groupId, uint32_t peerId,
const uint8_t* cTitle, size_t length, void* vCore);
static void onLosslessPacket(Tox* tox, uint32_t friendId,
const uint8_t* data, size_t length, void* core);
static void onReadReceiptCallback(Tox* tox, uint32_t friendId, uint32_t receipt, void* core);
void sendGroupMessageWithType(int groupId, const QString& message, Tox_Message_Type type);
@ -249,6 +255,7 @@ private:
std::unique_ptr<CoreFile> file;
CoreAV* av = nullptr;
std::unique_ptr<CoreExt> ext;
QTimer* toxTimer = nullptr;
// recursive, since we might call our own functions
mutable QMutex coreLoopLock{QMutex::Recursive};

191
src/core/coreext.cpp Normal file
View File

@ -0,0 +1,191 @@
/*
Copyright © 2019-2020 by The qTox Project Contributors
This file is part of qTox, a Qt-based graphical interface for Tox.
qTox is libre software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
qTox 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with qTox. If not, see <http://www.gnu.org/licenses/>.
*/
#include "coreext.h"
#include "toxstring.h"
#include <QDateTime>
#include <QTimeZone>
#include <QtCore>
#include <memory>
#include <cassert>
extern "C" {
#include <toxext/toxext.h>
#include <tox_extension_messages.h>
}
std::unique_ptr<CoreExt> CoreExt::makeCoreExt(Tox* core) {
auto toxExtPtr = toxext_init(core);
if (!toxExtPtr) {
return nullptr;
}
auto toxExt = ExtensionPtr<ToxExt>(toxExtPtr, toxext_free);
return std::unique_ptr<CoreExt>(new CoreExt(std::move(toxExt)));
}
CoreExt::CoreExt(ExtensionPtr<ToxExt> toxExt_)
: toxExt(std::move(toxExt_))
, toxExtMessages(nullptr, nullptr)
{
toxExtMessages = ExtensionPtr<ToxExtensionMessages>(
tox_extension_messages_register(
toxExt.get(),
CoreExt::onExtendedMessageReceived,
CoreExt::onExtendedMessageReceipt,
CoreExt::onExtendedMessageNegotiation,
this,
TOX_EXTENSION_MESSAGES_DEFAULT_MAX_RECEIVING_MESSAGE_SIZE),
tox_extension_messages_free);
}
void CoreExt::process()
{
toxext_iterate(toxExt.get());
}
void CoreExt::onLosslessPacket(uint32_t friendId, const uint8_t* data, size_t length)
{
if (is_toxext_packet(data, length)) {
toxext_handle_lossless_custom_packet(toxExt.get(), friendId, data, length);
}
}
CoreExt::Packet::Packet(
ToxExtPacketList* packetList,
ToxExtensionMessages* toxExtMessages,
uint32_t friendId,
PacketPassKey)
: toxExtMessages(toxExtMessages)
, packetList(packetList)
, friendId(friendId)
{}
std::unique_ptr<ICoreExtPacket> CoreExt::getPacket(uint32_t friendId)
{
return std::unique_ptr<Packet>(new Packet(
toxext_packet_list_create(toxExt.get(), friendId),
toxExtMessages.get(),
friendId,
PacketPassKey{}));
}
uint64_t CoreExt::Packet::addExtendedMessage(QString message)
{
if (hasBeenSent) {
assert(false);
qWarning() << "Invalid use of CoreExt::Packet";
// Hope that UINT64_MAX will never collide with an actual receipt num
// that we care about
return UINT64_MAX;
}
int size = message.toUtf8().size();
enum Tox_Extension_Messages_Error err;
auto maxSize = static_cast<int>(tox_extension_messages_get_max_sending_size(
toxExtMessages,
friendId,
&err));
if (size > maxSize) {
assert(false);
qCritical() << "addExtendedMessage called with message of size:" << size
<< "when max is:" << maxSize << ". Ignoring.";
return false;
}
ToxString toxString(message);
const auto receipt = tox_extension_messages_append(
toxExtMessages,
packetList,
toxString.data(),
toxString.size(),
friendId,
&err);
if (err != TOX_EXTENSION_MESSAGES_SUCCESS) {
qWarning() << "Error sending extension message";
}
return receipt;
}
bool CoreExt::Packet::send()
{
auto ret = toxext_send(packetList);
if (ret != TOXEXT_SUCCESS) {
qWarning() << "Failed to send packet";
}
// Indicate we've sent the packet even on failure since our packetlist will
// be invalid no matter what
hasBeenSent = true;
return ret == TOXEXT_SUCCESS;
}
uint64_t CoreExt::getMaxExtendedMessageSize()
{
return TOX_EXTENSION_MESSAGES_DEFAULT_MAX_RECEIVING_MESSAGE_SIZE;
}
void CoreExt::onFriendStatusChanged(uint32_t friendId, Status::Status status)
{
const auto prevStatusIt = currentStatuses.find(friendId);
const auto prevStatus = prevStatusIt == currentStatuses.end()
? Status::Status::Offline : prevStatusIt->second;
currentStatuses[friendId] = status;
// Does not depend on prevStatus since prevStatus could be newly
// constructed. In this case we should still ensure the rest of the system
// knows there is no extension support
if (status == Status::Status::Offline) {
emit extendedMessageSupport(friendId, false);
} else if (prevStatus == Status::Status::Offline) {
tox_extension_messages_negotiate(toxExtMessages.get(), friendId);
}
}
void CoreExt::onExtendedMessageReceived(uint32_t friendId, const uint8_t* data, size_t size, void* userData)
{
QString msg = ToxString(data, size).getQString();
emit static_cast<CoreExt*>(userData)->extendedMessageReceived(friendId, msg);
}
void CoreExt::onExtendedMessageReceipt(uint32_t friendId, uint64_t receiptId, void* userData)
{
emit static_cast<CoreExt*>(userData)->extendedReceiptReceived(friendId, receiptId);
}
void CoreExt::onExtendedMessageNegotiation(uint32_t friendId, bool compatible, uint64_t maxMessageSize, void* userData)
{
auto coreExt = static_cast<CoreExt*>(userData);
// HACK: handling configurable max message size per-friend is not trivial.
// For now the upper layers just assume that the max size for extended
// messages is the same for all friends. If a friend has a max message size
// lower than this value we just pretend they do not have the extension since
// we will not split correctly for this friend.
if (maxMessageSize < coreExt->getMaxExtendedMessageSize())
compatible = false;
emit coreExt->extendedMessageSupport(friendId, compatible);
}

147
src/core/coreext.h Normal file
View File

@ -0,0 +1,147 @@
/*
Copyright © 2019-2020 by The qTox Project Contributors
This file is part of qTox, a Qt-based graphical interface for Tox.
qTox is libre software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
qTox 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with qTox. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "src/model/status.h"
#include "icoreextpacket.h"
#include <QObject>
#include <QMap>
#include <bitset>
#include <memory>
#include <unordered_map>
struct Tox;
struct ToxExt;
struct ToxExtensionMessages;
struct ToxExtPacketList;
/**
* Bridge between the toxext library and the rest of qTox.
*/
class CoreExt : public QObject, public ICoreExtPacketAllocator
{
Q_OBJECT
private:
// PassKey idiom to prevent others from making PacketBuilders
struct PacketPassKey {};
public:
/**
* @brief Creates a CoreExt instance. Using a pointer here makes our
* registrations with extensions significantly easier to manage
*
* @param[in] pointer to core tox instance
* @return CoreExt on success, nullptr on failure
*/
static std::unique_ptr<CoreExt> makeCoreExt(Tox* core);
// We do registration with our own pointer, need to ensure we're in a stable location
CoreExt(CoreExt const& other) = delete;
CoreExt(CoreExt&& other) = delete;
CoreExt& operator=(CoreExt const& other) = delete;
CoreExt& operator=(CoreExt&& other) = delete;
/**
* @brief Periodic service function
*/
void process();
/**
* @brief Handles extension related lossless packets
* @param[in] friendId Core id of friend
* @param[in] data Packet data
* @param[in] length Length of packet data
*/
void onLosslessPacket(uint32_t friendId, const uint8_t* data, size_t length);
/**
* See documentation of ICoreExtPacket
*/
class Packet : public ICoreExtPacket
{
public:
/**
* @brief Internal constructor for a packet.
*/
Packet(
ToxExtPacketList* packetList,
ToxExtensionMessages* toxExtMessages,
uint32_t friendId,
PacketPassKey);
// Delete copy constructor, we shouldn't be able to copy
Packet(Packet const& other) = delete;
Packet(Packet&& other)
{
toxExtMessages = other.toxExtMessages;
packetList = other.packetList;
friendId = other.friendId;
hasBeenSent = other.hasBeenSent;
other.toxExtMessages = nullptr;
other.packetList = nullptr;
other.friendId = 0;
other.hasBeenSent = false;
}
uint64_t addExtendedMessage(QString message) override;
bool send() override;
private:
bool hasBeenSent = false;
// Note: non-owning pointer
ToxExtensionMessages* toxExtMessages;
// Note: packetList is freed on send() call
ToxExtPacketList* packetList;
uint32_t friendId;
};
std::unique_ptr<ICoreExtPacket> getPacket(uint32_t friendId) override;
uint64_t getMaxExtendedMessageSize();
signals:
void extendedMessageReceived(uint32_t friendId, const QString& message);
void extendedReceiptReceived(uint32_t friendId, uint64_t receiptId);
void extendedMessageSupport(uint32_t friendId, bool supported);
public slots:
void onFriendStatusChanged(uint32_t friendId, Status::Status status);
private:
static void onExtendedMessageReceived(uint32_t friendId, const uint8_t* data, size_t size, void* userData);
static void onExtendedMessageReceipt(uint32_t friendId, uint64_t receiptId, void* userData);
static void onExtendedMessageNegotiation(uint32_t friendId, bool compatible, uint64_t maxMessageSize, void* userData);
// A little extra cost to hide the deleters, but this lets us fwd declare
// and prevent any extension headers from leaking out to the rest of the
// system
template <class T>
using ExtensionPtr = std::unique_ptr<T, void(*)(T*)>;
CoreExt(ExtensionPtr<ToxExt> toxExt);
std::unordered_map<uint32_t, Status::Status> currentStatuses;
ExtensionPtr<ToxExt> toxExt;
ExtensionPtr<ToxExtensionMessages> toxExtMessages;
};

32
src/core/extension.h Normal file
View File

@ -0,0 +1,32 @@
/*
Copyright © 2019 by The qTox Project Contributors
This file is part of qTox, a Qt-based graphical interface for Tox.
qTox is libre software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
qTox 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with qTox. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <bitset>
// Do not use enum class because we use these as indexes frequently (see ExtensionSet)
struct ExtensionType
{
enum {
messages,
max
};
};
using ExtensionSet = std::bitset<ExtensionType::max>;

68
src/core/icoreextpacket.h Normal file
View File

@ -0,0 +1,68 @@
/*
Copyright © 2019 by The qTox Project Contributors
This file is part of qTox, a Qt-based graphical interface for Tox.
qTox is libre software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
qTox 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with qTox. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <QDateTime>
#include <cstdint>
#include <memory>
/**
* Abstraction around the toxext packet. The toxext flow is to allow several extensions
* to tack onto the same packet before sending it to avoid needing the toxext overhead
* for every single extension. This abstraction models a toxext packet list.
*
* Intent is to retrieve a ICoreExtPacket from an ICoreExtPacketAllocator, append all
* relevant extension data, and then finally send the packet. After sending the packet
* is no longer guaranteed to be valid.
*/
class ICoreExtPacket
{
public:
virtual ~ICoreExtPacket() = default;
/**
* @brief Adds message to packet
* @return Extended message receipt, UINT64_MAX on failure
* @note Any other extensions related to this message have to be added
* _before_ the message itself
*/
virtual uint64_t addExtendedMessage(QString message) = 0;
/**
* @brief Consumes the packet constructed with PacketBuilder packet and
* sends it to toxext
*/
virtual bool send() = 0;
};
/**
* Provider of toxext packets
*/
class ICoreExtPacketAllocator
{
public:
virtual ~ICoreExtPacketAllocator() = default;
/**
* @brief Gets a new packet builder for friend with core friend id friendId
*/
virtual std::unique_ptr<ICoreExtPacket> getPacket(uint32_t friendId) = 0;
};

View File

@ -26,3 +26,6 @@
using ReceiptNum = NamedType<uint32_t, struct ReceiptNumTag, Orderable>;
Q_DECLARE_METATYPE(ReceiptNum)
using ExtendedReceiptNum = NamedType<uint32_t, struct ExtendedReceiptNumTag, Orderable>;
Q_DECLARE_METATYPE(ExtendedReceiptNum);

View File

@ -0,0 +1,27 @@
/*
Copyright © 2015-2019 by The qTox Project Contributors
This file is part of qTox, a Qt-based graphical interface for Tox.
qTox is libre software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
qTox 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with qTox. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
// NOTE: Numbers are important here as this is cast to an int and persisted in the DB
enum class BrokenMessageReason : int
{
unknown = 0,
unsupportedExtensions = 1
};

View File

@ -82,6 +82,8 @@ ChatHistory::ChatHistory(Friend& f_, History* history_, const ICoreIdHandler& co
&ChatHistory::onMessageComplete);
connect(&messageDispatcher, &IMessageDispatcher::messageReceived, this,
&ChatHistory::onMessageReceived);
connect(&messageDispatcher, &IMessageDispatcher::messageBroken, this,
&ChatHistory::onMessageBroken);
if (canUseHistory()) {
// Defer messageSent callback until we finish firing off all our unsent messages.
@ -266,7 +268,7 @@ void ChatHistory::onMessageReceived(const ToxPk& sender, const Message& message)
content = ChatForm::ACTION_PREFIX + content;
}
history->addNewMessage(friendPk, content, friendPk, message.timestamp, true, displayName);
history->addNewMessage(friendPk, content, friendPk, message.timestamp, true, message.extensionSet, displayName);
}
sessionChatLog.onMessageReceived(sender, message);
@ -287,7 +289,7 @@ void ChatHistory::onMessageSent(DispatchedMessageId id, const Message& message)
auto onInsertion = [this, id](RowId historyId) { handleDispatchedMessage(id, historyId); };
history->addNewMessage(friendPk, content, selfPk, message.timestamp, false, username,
history->addNewMessage(friendPk, content, selfPk, message.timestamp, false, message.extensionSet, username,
onInsertion);
}
@ -303,6 +305,15 @@ void ChatHistory::onMessageComplete(DispatchedMessageId id)
sessionChatLog.onMessageComplete(id);
}
void ChatHistory::onMessageBroken(DispatchedMessageId id, BrokenMessageReason reason)
{
if (canUseHistory()) {
breakMessage(id, reason);
}
sessionChatLog.onMessageBroken(id, reason);
}
/**
* @brief Forces the given index and all future indexes to be in the chatlog
* @param[in] idx
@ -405,6 +416,13 @@ void ChatHistory::loadHistoryIntoSessionChatLog(ChatLogIdx start) const
void ChatHistory::dispatchUnsentMessages(IMessageDispatcher& messageDispatcher)
{
auto unsentMessages = history->getUndeliveredMessagesForFriend(f.getPublicKey());
auto requiredExtensions = std::accumulate(
unsentMessages.begin(), unsentMessages.end(),
ExtensionSet(), [] (const ExtensionSet& a, const History::HistMessage& b) {
return a | b.extensionSet;
});
for (auto& message : unsentMessages) {
// We should only store messages as unsent, if this changes in the
// future we need to extend this logic
@ -418,12 +436,14 @@ void ChatHistory::dispatchUnsentMessages(IMessageDispatcher& messageDispatcher)
// with the new timestamp. This is intentional as everywhere else we use
// attempted send time (which is whenever the it was initially inserted
// into history
auto dispatchIds = messageDispatcher.sendMessage(isAction, messageContent);
auto dispatchId = requiredExtensions.none()
// We should only send a single message, but in the odd case where we end
// up having to split more than when we added the message to history we'll
// just associate the last dispatched id with the history message
handleDispatchedMessage(dispatchIds.second, message.id);
? messageDispatcher.sendMessage(isAction, messageContent).second
: messageDispatcher.sendExtendedMessage(messageContent, requiredExtensions).second;
handleDispatchedMessage(dispatchId, message.id);
// We don't add the messages to the underlying chatlog since
// 1. We don't even know the ChatLogIdx of this message
@ -435,11 +455,20 @@ void ChatHistory::dispatchUnsentMessages(IMessageDispatcher& messageDispatcher)
void ChatHistory::handleDispatchedMessage(DispatchedMessageId dispatchId, RowId historyId)
{
auto completedMessageIt = completedMessages.find(dispatchId);
if (completedMessageIt == completedMessages.end()) {
dispatchedMessageRowIdMap.insert(dispatchId, historyId);
} else {
auto brokenMessageIt = brokenMessages.find(dispatchId);
const auto isCompleted = completedMessageIt != completedMessages.end();
const auto isBroken = brokenMessageIt != brokenMessages.end();
assert(!(isCompleted && isBroken));
if (isCompleted) {
history->markAsDelivered(historyId);
completedMessages.erase(completedMessageIt);
} else if (isBroken) {
history->markAsBroken(historyId, brokenMessageIt.value());
brokenMessages.erase(brokenMessageIt);
} else {
dispatchedMessageRowIdMap.insert(dispatchId, historyId);
}
}
@ -455,6 +484,18 @@ void ChatHistory::completeMessage(DispatchedMessageId id)
}
}
void ChatHistory::breakMessage(DispatchedMessageId id, BrokenMessageReason reason)
{
auto dispatchedMessageIt = dispatchedMessageRowIdMap.find(id);
if (dispatchedMessageIt == dispatchedMessageRowIdMap.end()) {
brokenMessages.insert(id, reason);
} else {
history->markAsBroken(*dispatchedMessageIt, reason);
dispatchedMessageRowIdMap.erase(dispatchedMessageIt);
}
}
bool ChatHistory::canUseHistory() const
{
return history && settings.getEnableLogging();

View File

@ -21,6 +21,7 @@
#include "ichatlog.h"
#include "sessionchatlog.h"
#include "src/model/brokenmessagereason.h"
#include "src/persistence/history.h"
#include <QSet>
@ -51,6 +52,7 @@ private slots:
void onMessageReceived(const ToxPk& sender, const Message& message);
void onMessageSent(DispatchedMessageId id, const Message& message);
void onMessageComplete(DispatchedMessageId id);
void onMessageBroken(DispatchedMessageId id, BrokenMessageReason reason);
private:
void ensureIdxInSessionChatLog(ChatLogIdx idx) const;
@ -58,6 +60,7 @@ private:
void dispatchUnsentMessages(IMessageDispatcher& messageDispatcher);
void handleDispatchedMessage(DispatchedMessageId dispatchId, RowId historyId);
void completeMessage(DispatchedMessageId id);
void breakMessage(DispatchedMessageId id, BrokenMessageReason reason);
bool canUseHistory() const;
ChatLogIdx getInitialChatLogIdx() const;
@ -70,6 +73,9 @@ private:
// If a message completes before it's inserted into history it will end up
// in this set
QSet<DispatchedMessageId> completedMessages;
// If a message breaks before it's inserted into history it will end up
// in this set
QMap<DispatchedMessageId, BrokenMessageReason> brokenMessages;
// If a message is inserted into history before it gets a completion
// callback it will end up in this map

View File

@ -23,6 +23,9 @@
#include "src/persistence/profile.h"
#include "src/widget/form/chatform.h"
#include <QDebug>
#include <memory>
Friend::Friend(uint32_t friendId, const ToxPk& friendPk, const QString& userAlias, const QString& userName)
: userName{userName}
, userAlias{userAlias}
@ -30,6 +33,7 @@ Friend::Friend(uint32_t friendId, const ToxPk& friendPk, const QString& userAlia
, friendId{friendId}
, hasNewEvents{false}
, friendStatus{Status::Status::Offline}
, isNegotiating{false}
{
if (userName.isEmpty()) {
this->userName = friendPk.toString();
@ -151,25 +155,75 @@ bool Friend::getEventFlag() const
void Friend::setStatus(Status::Status s)
{
if (friendStatus != s) {
auto oldStatus = friendStatus;
friendStatus = s;
emit statusChanged(friendPk, friendStatus);
if (!Status::isOnline(oldStatus) && Status::isOnline(friendStatus)) {
emit onlineOfflineChanged(friendPk, true);
} else if (Status::isOnline(oldStatus) && !Status::isOnline(friendStatus)) {
emit onlineOfflineChanged(friendPk, false);
// Internal status should never be negotiating. We only expose this externally through the use of isNegotiating
assert(s != Status::Status::Negotiating);
const bool wasOnline = Status::isOnline(getStatus());
if (friendStatus == s) {
return;
}
// When a friend goes online we want to give them some time to negotiate
// extension support
const auto startNegotating = friendStatus == Status::Status::Offline;
if (startNegotating) {
qDebug() << "Starting negotiation with friend " << friendId;
isNegotiating = true;
}
friendStatus = s;
const bool nowOnline = Status::isOnline(getStatus());
const auto emitStatusChange = startNegotating || !isNegotiating;
if (emitStatusChange) {
const auto statusToEmit = isNegotiating ? Status::Status::Negotiating : friendStatus;
emit statusChanged(friendPk, statusToEmit);
if (wasOnline && !nowOnline) {
emit onlineOfflineChanged(friendPk, false);
} else if (!wasOnline && nowOnline) {
emit onlineOfflineChanged(friendPk, true);
}
}
}
Status::Status Friend::getStatus() const
{
return friendStatus;
return isNegotiating ? Status::Status::Negotiating : friendStatus;
}
bool Friend::useHistory() const
{
return true;
}
void Friend::setExtendedMessageSupport(bool supported)
{
supportedExtensions[ExtensionType::messages] = supported;
emit extensionSupportChanged(supportedExtensions);
// If all extensions are supported we can exit early
if (supportedExtensions.all()) {
onNegotiationComplete();
}
}
ExtensionSet Friend::getSupportedExtensions() const
{
return supportedExtensions;
}
void Friend::onNegotiationComplete() {
if (!isNegotiating) {
return;
}
qDebug() << "Negotiation complete for friend " << friendId;
isNegotiating = false;
emit statusChanged(friendPk, friendStatus);
if (Status::isOnline(getStatus())) {
emit onlineOfflineChanged(friendPk, true);
}
}

View File

@ -21,6 +21,7 @@
#include "contact.h"
#include "src/core/core.h"
#include "src/core/extension.h"
#include "src/core/toxid.h"
#include "src/core/contactid.h"
#include "src/model/status.h"
@ -50,20 +51,25 @@ public:
uint32_t getId() const override;
const ContactId& getPersistentId() const override;
void finishNegotiation();
void setStatus(Status::Status s);
Status::Status getStatus() const;
bool useHistory() const final;
void setExtendedMessageSupport(bool supported);
ExtensionSet getSupportedExtensions() const;
signals:
void nameChanged(const ToxPk& friendId, const QString& name);
void aliasChanged(const ToxPk& friendId, QString alias);
void statusChanged(const ToxPk& friendId, Status::Status status);
void onlineOfflineChanged(const ToxPk& friendId, bool isOnline);
void statusMessageChanged(const ToxPk& friendId, const QString& message);
void extensionSupportChanged(ExtensionSet extensions);
void loadChatHistory();
public slots:
void onNegotiationComplete();
private:
QString userName;
QString userAlias;
@ -72,4 +78,6 @@ private:
uint32_t friendId;
bool hasNewEvents;
Status::Status friendStatus;
bool isNegotiating;
ExtensionSet supportedExtensions;
};

View File

@ -21,64 +21,52 @@
#include "src/persistence/settings.h"
#include "src/model/status.h"
namespace {
/**
* @brief Sends message to friend using messageSender
* @param[in] messageSender
* @param[in] f
* @param[in] message
* @param[out] receipt
*/
bool sendMessageToCore(ICoreFriendMessageSender& messageSender, const Friend& f,
const Message& message, ReceiptNum& receipt)
{
uint32_t friendId = f.getId();
auto sendFn = message.isAction ? std::mem_fn(&ICoreFriendMessageSender::sendAction)
: std::mem_fn(&ICoreFriendMessageSender::sendMessage);
return sendFn(messageSender, friendId, message.content, receipt);
}
} // namespace
FriendMessageDispatcher::FriendMessageDispatcher(Friend& f_, MessageProcessor processor_,
ICoreFriendMessageSender& messageSender_)
ICoreFriendMessageSender& messageSender_,
ICoreExtPacketAllocator& coreExtPacketAllocator_)
: f(f_)
, messageSender(messageSender_)
, offlineMsgEngine(&f_, &messageSender_)
, processor(std::move(processor_))
, coreExtPacketAllocator(coreExtPacketAllocator_)
{
connect(&f, &Friend::onlineOfflineChanged, this, &FriendMessageDispatcher::onFriendOnlineOfflineChanged);
}
/**
* @see IMessageSender::sendMessage
* @see IMessageDispatcher::sendMessage
*/
std::pair<DispatchedMessageId, DispatchedMessageId>
FriendMessageDispatcher::sendMessage(bool isAction, const QString& content)
{
const auto firstId = nextMessageId;
auto lastId = nextMessageId;
for (const auto& message : processor.processOutgoingMessage(isAction, content)) {
for (const auto& message : processor.processOutgoingMessage(isAction, content, f.getSupportedExtensions())) {
auto messageId = nextMessageId++;
lastId = messageId;
auto onOfflineMsgComplete = [this, messageId] { emit this->messageComplete(messageId); };
ReceiptNum receipt;
auto onOfflineMsgComplete = getCompletionFn(messageId);
sendProcessedMessage(message, onOfflineMsgComplete);
bool messageSent = false;
if (Status::isOnline(f.getStatus())) {
messageSent = sendMessageToCore(messageSender, f, message, receipt);
emit this->messageSent(messageId, message);
}
return std::make_pair(firstId, lastId);
}
if (!messageSent) {
offlineMsgEngine.addUnsentMessage(message, onOfflineMsgComplete);
} else {
offlineMsgEngine.addSentMessage(receipt, message, onOfflineMsgComplete);
}
/**
* @see IMessageDispatcher::sendExtendedMessage
*/
std::pair<DispatchedMessageId, DispatchedMessageId>
FriendMessageDispatcher::sendExtendedMessage(const QString& content, ExtensionSet extensions)
{
const auto firstId = nextMessageId;
auto lastId = nextMessageId;
for (const auto& message : processor.processOutgoingMessage(false, content, extensions)) {
auto messageId = nextMessageId++;
lastId = messageId;
auto onOfflineMsgComplete = getCompletionFn(messageId);
sendProcessedMessage(message, onOfflineMsgComplete);
emit this->messageSent(messageId, message);
}
@ -92,7 +80,7 @@ FriendMessageDispatcher::sendMessage(bool isAction, const QString& content)
*/
void FriendMessageDispatcher::onMessageReceived(bool isAction, const QString& content)
{
emit this->messageReceived(f.getPublicKey(), processor.processIncomingMessage(isAction, content));
emit this->messageReceived(f.getPublicKey(), processor.processIncomingCoreMessage(isAction, content));
}
/**
@ -104,6 +92,17 @@ void FriendMessageDispatcher::onReceiptReceived(ReceiptNum receipt)
offlineMsgEngine.onReceiptReceived(receipt);
}
void FriendMessageDispatcher::onExtMessageReceived(const QString& content)
{
auto message = processor.processIncomingExtMessage(content);
emit this->messageReceived(f.getPublicKey(), message);
}
void FriendMessageDispatcher::onExtReceiptReceived(uint64_t receiptId)
{
offlineMsgEngine.onExtendedReceiptReceived(ExtendedReceiptNum(receiptId));
}
/**
* @brief Handles status change for friend
* @note Parameters just to fit slot api
@ -111,7 +110,10 @@ void FriendMessageDispatcher::onReceiptReceived(ReceiptNum receipt)
void FriendMessageDispatcher::onFriendOnlineOfflineChanged(const ToxPk&, bool isOnline)
{
if (isOnline) {
offlineMsgEngine.deliverOfflineMsgs();
auto messagesToResend = offlineMsgEngine.removeAllMessages();
for (auto const& message : messagesToResend) {
sendProcessedMessage(message.message, message.callback);
}
}
}
@ -122,3 +124,78 @@ void FriendMessageDispatcher::clearOutgoingMessages()
{
offlineMsgEngine.removeAllMessages();
}
void FriendMessageDispatcher::sendProcessedMessage(Message const& message, OfflineMsgEngine::CompletionFn onOfflineMsgComplete)
{
if (!Status::isOnline(f.getStatus())) {
offlineMsgEngine.addUnsentMessage(message, onOfflineMsgComplete);
return;
}
if (message.extensionSet[ExtensionType::messages] && !message.isAction) {
sendExtendedProcessedMessage(message, onOfflineMsgComplete);
} else {
sendCoreProcessedMessage(message, onOfflineMsgComplete);
}
}
void FriendMessageDispatcher::sendExtendedProcessedMessage(Message const& message, OfflineMsgEngine::CompletionFn onOfflineMsgComplete)
{
assert(!message.isAction); // Actions not supported with extensions
if ((f.getSupportedExtensions() & message.extensionSet) != message.extensionSet) {
onOfflineMsgComplete(false);
return;
}
auto receipt = ExtendedReceiptNum();
const auto friendId = f.getId();
auto packet = coreExtPacketAllocator.getPacket(friendId);
if (message.extensionSet[ExtensionType::messages]) {
receipt.get() = packet->addExtendedMessage(message.content);
}
const auto messageSent = packet->send();
if (messageSent) {
offlineMsgEngine.addSentExtendedMessage(receipt, message, onOfflineMsgComplete);
} else {
offlineMsgEngine.addUnsentMessage(message, onOfflineMsgComplete);
}
}
void FriendMessageDispatcher::sendCoreProcessedMessage(Message const& message, OfflineMsgEngine::CompletionFn onOfflineMsgComplete)
{
auto receipt = ReceiptNum();
uint32_t friendId = f.getId();
auto sendFn = message.isAction ? std::mem_fn(&ICoreFriendMessageSender::sendAction)
: std::mem_fn(&ICoreFriendMessageSender::sendMessage);
const auto messageSent = sendFn(messageSender, friendId, message.content, receipt);
if (messageSent) {
offlineMsgEngine.addSentCoreMessage(receipt, message, onOfflineMsgComplete);
} else {
offlineMsgEngine.addUnsentMessage(message, onOfflineMsgComplete);
}
}
OfflineMsgEngine::CompletionFn FriendMessageDispatcher::getCompletionFn(DispatchedMessageId messageId)
{
return [this, messageId] (bool success) {
if (success) {
emit this->messageComplete(messageId);
} else {
// For now we know the only reason we can fail after giving to the
// offline message engine is due to a reduced extension set
emit this->messageBroken(messageId, BrokenMessageReason::unsupportedExtensions);
}
};
}

View File

@ -35,18 +35,29 @@ class FriendMessageDispatcher : public IMessageDispatcher
Q_OBJECT
public:
FriendMessageDispatcher(Friend& f, MessageProcessor processor,
ICoreFriendMessageSender& messageSender);
ICoreFriendMessageSender& messageSender,
ICoreExtPacketAllocator& coreExt);
std::pair<DispatchedMessageId, DispatchedMessageId> sendMessage(bool isAction,
const QString& content) override;
std::pair<DispatchedMessageId, DispatchedMessageId> sendExtendedMessage(const QString& content, ExtensionSet extensions) override;
void onMessageReceived(bool isAction, const QString& content);
void onReceiptReceived(ReceiptNum receipt);
void onExtMessageReceived(const QString& message);
void onExtReceiptReceived(uint64_t receiptId);
void clearOutgoingMessages();
private slots:
void onFriendOnlineOfflineChanged(const ToxPk& key, bool isOnline);
private:
void sendProcessedMessage(Message const& msg, OfflineMsgEngine::CompletionFn fn);
void sendExtendedProcessedMessage(Message const& msg, OfflineMsgEngine::CompletionFn fn);
void sendCoreProcessedMessage(Message const& msg, OfflineMsgEngine::CompletionFn fn);
OfflineMsgEngine::CompletionFn getCompletionFn(DispatchedMessageId messageId);
Friend& f;
ICoreExtPacketAllocator& coreExtPacketAllocator;
DispatchedMessageId nextMessageId = DispatchedMessageId(0);
ICoreFriendMessageSender& messageSender;

View File

@ -41,7 +41,7 @@ GroupMessageDispatcher::sendMessage(bool isAction, QString const& content)
const auto firstMessageId = nextMessageId;
auto lastMessageId = firstMessageId;
for (auto const& message : processor.processOutgoingMessage(isAction, content)) {
for (auto const& message : processor.processOutgoingMessage(isAction, content, ExtensionSet())) {
auto messageId = nextMessageId++;
lastMessageId = messageId;
if (group.getPeersCount() != 1) {
@ -65,6 +65,17 @@ GroupMessageDispatcher::sendMessage(bool isAction, QString const& content)
return std::make_pair(firstMessageId, lastMessageId);
}
std::pair<DispatchedMessageId, DispatchedMessageId>
GroupMessageDispatcher::sendExtendedMessage(const QString& content, ExtensionSet extensions)
{
// Stub this api to immediately fail
auto messageId = nextMessageId++;
auto messages = processor.processOutgoingMessage(false, content, ExtensionSet());
emit this->messageSent(messageId, messages[0]);
emit this->messageBroken(messageId, BrokenMessageReason::unsupportedExtensions);
return {messageId, messageId};
}
/**
* @brief Processes and dispatches received message from toxcore
* @param[in] sender
@ -84,5 +95,5 @@ void GroupMessageDispatcher::onMessageReceived(const ToxPk& sender, bool isActio
return;
}
emit messageReceived(sender, processor.processIncomingMessage(isAction, content));
emit messageReceived(sender, processor.processIncomingCoreMessage(isAction, content));
}

View File

@ -42,6 +42,9 @@ public:
std::pair<DispatchedMessageId, DispatchedMessageId> sendMessage(bool isAction,
QString const& content) override;
std::pair<DispatchedMessageId, DispatchedMessageId> sendExtendedMessage(const QString& content,
ExtensionSet extensions) override;
void onMessageReceived(ToxPk const& sender, bool isAction, QString const& content);
private:

View File

@ -21,6 +21,7 @@
#include "src/model/friend.h"
#include "src/model/message.h"
#include "src/model/brokenmessagereason.h"
#include <QObject>
#include <QString>
@ -44,6 +45,18 @@ public:
*/
virtual std::pair<DispatchedMessageId, DispatchedMessageId>
sendMessage(bool isAction, const QString& content) = 0;
/**
* @brief Sends message to associated chat ensuring that extensions are available
* @param[in] content Message content
* @param[in] extensions extensions required for given message
* @return Pair of first and last dispatched message IDs
* @note If the provided extensions are not supported the message will be flagged
* as broken
*/
virtual std::pair<DispatchedMessageId, DispatchedMessageId>
sendExtendedMessage(const QString& content, ExtensionSet extensions) = 0;
signals:
/**
* @brief Emitted when a message is received and processed
@ -62,4 +75,6 @@ signals:
* @param id Id of message that is completed
*/
void messageComplete(DispatchedMessageId id);
void messageBroken(DispatchedMessageId id, BrokenMessageReason reason);
};

View File

@ -21,6 +21,40 @@
#include "friend.h"
#include "src/core/core.h"
#include <cassert>
namespace {
QStringList splitMessage(const QString& message, uint64_t maxLength)
{
QStringList splittedMsgs;
QByteArray ba_message{message.toUtf8()};
while (static_cast<uint64_t>(ba_message.size()) > maxLength) {
int splitPos = ba_message.lastIndexOf('\n', maxLength - 1);
if (splitPos <= 0) {
splitPos = ba_message.lastIndexOf(' ', maxLength - 1);
}
if (splitPos <= 0) {
constexpr uint8_t firstOfMultiByteMask = 0xC0;
constexpr uint8_t multiByteMask = 0x80;
splitPos = maxLength;
// don't split a utf8 character
if ((ba_message[splitPos] & multiByteMask) == multiByteMask) {
while ((ba_message[splitPos] & firstOfMultiByteMask) != firstOfMultiByteMask) {
--splitPos;
}
}
--splitPos;
}
splittedMsgs.append(QString{ba_message.left(splitPos + 1)});
ba_message = ba_message.mid(splitPos + 1);
}
splittedMsgs.append(QString{ba_message});
return splittedMsgs;
}
}
void MessageProcessor::SharedParams::onUserNameSet(const QString& username)
{
QString sanename = username;
@ -49,11 +83,16 @@ MessageProcessor::MessageProcessor(const MessageProcessor::SharedParams& sharedP
/**
* @brief Converts an outgoing message into one (or many) sanitized Message(s)
*/
std::vector<Message> MessageProcessor::processOutgoingMessage(bool isAction, QString const& content)
std::vector<Message> MessageProcessor::processOutgoingMessage(bool isAction, QString const& content, ExtensionSet extensions)
{
std::vector<Message> ret;
QStringList splitMsgs = Core::splitMessage(content);
const auto maxSendingSize = extensions[ExtensionType::messages]
? sharedParams.getMaxExtendedMessageSize()
: sharedParams.getMaxCoreMessageSize();
const auto splitMsgs = splitMessage(content, maxSendingSize);
ret.reserve(splitMsgs.size());
QDateTime timestamp = QDateTime::currentDateTime();
@ -63,17 +102,20 @@ std::vector<Message> MessageProcessor::processOutgoingMessage(bool isAction, QSt
message.isAction = isAction;
message.content = part;
message.timestamp = timestamp;
// In theory we could limit this only to the extensions
// required but since Core owns the splitting logic it
// isn't trivial to do that now
message.extensionSet = extensions;
return message;
});
return ret;
}
/**
* @brief Converts an incoming message into a sanitized Message
*/
Message MessageProcessor::processIncomingMessage(bool isAction, QString const& message)
Message MessageProcessor::processIncomingCoreMessage(bool isAction, QString const& message)
{
QDateTime timestamp = QDateTime::currentDateTime();
auto ret = Message{};
@ -82,9 +124,9 @@ Message MessageProcessor::processIncomingMessage(bool isAction, QString const& m
ret.timestamp = timestamp;
if (detectingMentions) {
auto nameMention = sharedParams.GetNameMention();
auto sanitizedNameMention = sharedParams.GetSanitizedNameMention();
auto pubKeyMention = sharedParams.GetPublicKeyMention();
auto nameMention = sharedParams.getNameMention();
auto sanitizedNameMention = sharedParams.getSanitizedNameMention();
auto pubKeyMention = sharedParams.getPublicKeyMention();
for (auto const& mention : {nameMention, sanitizedNameMention, pubKeyMention}) {
auto matchIt = mention.globalMatch(ret.content);
@ -109,3 +151,18 @@ Message MessageProcessor::processIncomingMessage(bool isAction, QString const& m
return ret;
}
Message MessageProcessor::processIncomingExtMessage(const QString& content)
{
// Note: detectingMentions not implemented here since mentions are only
// currently useful in group messages which do not support extensions. If we
// were to support mentions we would probably want to do something more
// intelligent anyways
assert(detectingMentions == false);
auto message = Message();
message.timestamp = QDateTime::currentDateTime();
message.content = content;
message.extensionSet |= ExtensionType::messages;
return message;
}

View File

@ -19,6 +19,9 @@
#pragma once
#include "src/core/coreext.h"
#include "src/core/extension.h"
#include <QDateTime>
#include <QRegularExpression>
#include <QString>
@ -26,6 +29,7 @@
#include <vector>
class Friend;
class CoreExt;
// NOTE: This could be extended in the future to handle all text processing (see
// ChatMessage::createChatMessage)
@ -50,6 +54,7 @@ struct Message
bool isAction;
QString content;
QDateTime timestamp;
ExtensionSet extensionSet;
std::vector<MessageMetadata> metadata;
};
@ -66,22 +71,39 @@ public:
{
public:
QRegularExpression GetNameMention() const
SharedParams(uint64_t maxCoreMessageSize_, uint64_t maxExtendedMessageSize_)
: maxCoreMessageSize(maxCoreMessageSize_)
, maxExtendedMessageSize(maxExtendedMessageSize_)
{}
QRegularExpression getNameMention() const
{
return nameMention;
}
QRegularExpression GetSanitizedNameMention() const
QRegularExpression getSanitizedNameMention() const
{
return sanitizedNameMention;
}
QRegularExpression GetPublicKeyMention() const
QRegularExpression getPublicKeyMention() const
{
return pubKeyMention;
}
void onUserNameSet(const QString& username);
void setPublicKey(const QString& pk);
uint64_t getMaxCoreMessageSize() const
{
return maxCoreMessageSize;
}
uint64_t getMaxExtendedMessageSize() const
{
return maxExtendedMessageSize;
}
private:
uint64_t maxCoreMessageSize;
uint64_t maxExtendedMessageSize;
QRegularExpression nameMention;
QRegularExpression sanitizedNameMention;
QRegularExpression pubKeyMention;
@ -89,9 +111,9 @@ public:
MessageProcessor(const SharedParams& sharedParams);
std::vector<Message> processOutgoingMessage(bool isAction, QString const& content);
Message processIncomingMessage(bool isAction, QString const& message);
std::vector<Message> processOutgoingMessage(bool isAction, const QString& content, ExtensionSet extensions);
Message processIncomingCoreMessage(bool isAction, const QString& content);
Message processIncomingExtMessage(const QString& content);
/**
* @brief Enables mention detection in the processor

View File

@ -420,6 +420,29 @@ void SessionChatLog::onMessageComplete(DispatchedMessageId id)
emit this->itemUpdated(messageIt->first);
}
void SessionChatLog::onMessageBroken(DispatchedMessageId id, BrokenMessageReason)
{
auto chatLogIdxIt = outgoingMessages.find(id);
if (chatLogIdxIt == outgoingMessages.end()) {
qWarning() << "Failed to find outgoing message";
return;
}
const auto& chatLogIdx = *chatLogIdxIt;
auto messageIt = items.find(chatLogIdx);
if (messageIt == items.end()) {
qWarning() << "Failed to look up message in chat log";
return;
}
// NOTE: Reason for broken message not currently shown in UI, but it could be
messageIt->second.getContentAsMessage().state = MessageState::broken;
emit this->itemUpdated(messageIt->first);
}
/**
* @brief Updates file state in the chatlog
* @note The files need to be pre-filtered for the current chat since we do no validation

View File

@ -57,6 +57,7 @@ public slots:
void onMessageReceived(const ToxPk& sender, const Message& message);
void onMessageSent(DispatchedMessageId id, const Message& message);
void onMessageComplete(DispatchedMessageId id);
void onMessageBroken(DispatchedMessageId id, BrokenMessageReason reason);
void onFileUpdated(const ToxPk& sender, const ToxFile& file);
void onFileTransferRemotePausedUnpaused(const ToxPk& sender, const ToxFile& file, bool paused);

View File

@ -41,6 +41,8 @@ namespace Status
return QObject::tr("offline", "contact status");
case Status::Blocked:
return QObject::tr("blocked", "contact status");
case Status::Negotiating:
return QObject::tr("negotitating", "contact status");
}
assert(false);
@ -60,6 +62,8 @@ namespace Status
return "offline";
case Status::Blocked:
return "blocked";
case Status::Negotiating:
return "negotiating";
}
assert(false);
return QStringLiteral("");
@ -78,6 +82,9 @@ namespace Status
bool isOnline(Status status)
{
return status != Status::Offline && status != Status::Blocked;
return status != Status::Offline
&& status != Status::Blocked
// We don't want to treat a friend as online unless we know their feature set
&& status != Status::Negotiating;
}
} // namespace Status

View File

@ -31,7 +31,8 @@ namespace Status
Away,
Busy,
Offline,
Blocked
Blocked,
Negotiating,
};
QString getIconPath(Status status, bool event = false);

View File

@ -108,6 +108,8 @@ void Nexus::start()
qRegisterMetaType<GroupInvite>("GroupInvite");
qRegisterMetaType<ReceiptNum>("ReceiptNum");
qRegisterMetaType<RowId>("RowId");
qRegisterMetaType<uint64_t>("uint64_t");
qRegisterMetaType<ExtensionSet>("ExtensionSet");
qApp->setQuitOnLastWindowClosed(false);

View File

@ -27,7 +27,7 @@
#include "src/core/toxpk.h"
namespace {
static constexpr int SCHEMA_VERSION = 5;
static constexpr int SCHEMA_VERSION = 6;
bool createCurrentSchema(RawDatabase& db)
{
@ -67,8 +67,10 @@ bool createCurrentSchema(RawDatabase& db)
"direction INTEGER NOT NULL, "
"file_state INTEGER NOT NULL);"
"CREATE TABLE faux_offline_pending (id INTEGER PRIMARY KEY, "
"required_extensions INTEGER NOT NULL DEFAULT 0, "
"FOREIGN KEY (id) REFERENCES history(id));"
"CREATE TABLE broken_messages (id INTEGER PRIMARY KEY, "
"reason INTEGER NOT NULL DEFAULT 0, "
"FOREIGN KEY (id) REFERENCES history(id));"));
// sqlite doesn't support including the index as part of the CREATE TABLE statement, so add a second query
queries += RawDatabase::Query(
@ -95,9 +97,7 @@ bool isNewDb(std::shared_ptr<RawDatabase>& db, bool& success)
bool dbSchema0to1(RawDatabase& db)
{
QVector<RawDatabase::Query> queries;
queries +=
RawDatabase::Query(QStringLiteral(
"CREATE TABLE file_transfers "
queries += RawDatabase::Query(QStringLiteral("CREATE TABLE file_transfers "
"(id INTEGER PRIMARY KEY, "
"chat_id INTEGER NOT NULL, "
"file_restart_id BLOB NOT NULL, "
@ -107,8 +107,7 @@ bool dbSchema0to1(RawDatabase& db)
"file_size INTEGER NOT NULL, "
"direction INTEGER NOT NULL, "
"file_state INTEGER NOT NULL);"));
queries +=
RawDatabase::Query(QStringLiteral("ALTER TABLE history ADD file_id INTEGER;"));
queries += RawDatabase::Query(QStringLiteral("ALTER TABLE history ADD file_id INTEGER;"));
queries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = 1;"));
return db.execNow(queries);
}
@ -120,17 +119,15 @@ bool dbSchema1to2(RawDatabase& db)
// faux_offline_pending to broken_messages
// the last non-pending message in each chat
QString lastDeliveredQuery = QString(
"SELECT chat_id, MAX(history.id) FROM "
QString lastDeliveredQuery =
QString("SELECT chat_id, MAX(history.id) FROM "
"history JOIN peers chat ON chat_id = chat.id "
"LEFT JOIN faux_offline_pending ON history.id = faux_offline_pending.id "
"WHERE faux_offline_pending.id IS NULL "
"GROUP BY chat_id;");
QVector<RawDatabase::Query> upgradeQueries;
upgradeQueries +=
RawDatabase::Query(QStringLiteral(
"CREATE TABLE broken_messages "
upgradeQueries += RawDatabase::Query(QStringLiteral("CREATE TABLE broken_messages "
"(id INTEGER PRIMARY KEY);"));
auto rowCallback = [&upgradeQueries](const QVector<QVariant>& row) {
@ -142,7 +139,9 @@ bool dbSchema1to2(RawDatabase& db)
"history JOIN faux_offline_pending "
"ON faux_offline_pending.id = history.id "
"WHERE history.chat_id=%1 "
"AND history.id < %2;").arg(chatId).arg(lastDeliveredHistoryId);
"AND history.id < %2;")
.arg(chatId)
.arg(lastDeliveredHistoryId);
};
// note this doesn't modify the db, just generate new queries, so is safe
// to run outside of our upgrade transaction
@ -150,8 +149,7 @@ bool dbSchema1to2(RawDatabase& db)
return false;
}
upgradeQueries += QString(
"DELETE FROM faux_offline_pending "
upgradeQueries += QString("DELETE FROM faux_offline_pending "
"WHERE id in ("
"SELECT id FROM broken_messages);");
@ -178,8 +176,7 @@ bool dbSchema2to3(RawDatabase& db)
"WHERE history.message = ?;"),
{emptyActionMessageString.toUtf8()}};
upgradeQueries += QString(
"DELETE FROM faux_offline_pending "
upgradeQueries += QString("DELETE FROM faux_offline_pending "
"WHERE id in ("
"SELECT id FROM broken_messages);");
@ -277,14 +274,48 @@ bool dbSchema4to5(RawDatabase& db)
return transactionPass;
}
bool dbSchema5to6(RawDatabase& db)
{
QVector<RawDatabase::Query> upgradeQueries;
upgradeQueries += RawDatabase::Query{QString("ALTER TABLE faux_offline_pending "
"ADD COLUMN required_extensions INTEGER NOT NULL "
"DEFAULT 0;")};
upgradeQueries += RawDatabase::Query{QString("ALTER TABLE broken_messages "
"ADD COLUMN reason INTEGER NOT NULL "
"DEFAULT 0;")};
upgradeQueries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = 6;"));
return db.execNow(upgradeQueries);
}
/**
* @brief Upgrade the db schema
* @return True if the schema upgrade succeded, false otherwise
* @note On future alterations of the database all you have to do is bump the SCHEMA_VERSION
* variable and add another case to the switch statement below. Make sure to fall through on each case.
*/
* @brief Upgrade the db schema
* @note On future alterations of the database all you have to do is bump the SCHEMA_VERSION
* variable and add another case to the switch statement below. Make sure to fall through on each case.
*/
bool dbSchemaUpgrade(std::shared_ptr<RawDatabase>& db)
{
// If we're a new dB we can just make a new one and call it a day
bool success = false;
const bool newDb = isNewDb(db, success);
if (!success) {
qCritical() << "Failed to create current db schema";
return false;
}
if (newDb) {
if (!createCurrentSchema(*db)) {
qCritical() << "Failed to create current db schema";
return false;
}
qDebug() << "Database created at schema version" << SCHEMA_VERSION;
return true;
}
// Otherwise we have to do upgrades from our current version to the latest version
int64_t databaseSchemaVersion;
if (!db->execNow(RawDatabase::Query("PRAGMA user_version", [&](const QVector<QVariant>& row) {
@ -295,8 +326,9 @@ bool dbSchemaUpgrade(std::shared_ptr<RawDatabase>& db)
}
if (databaseSchemaVersion > SCHEMA_VERSION) {
qWarning().nospace() << "Database version (" << databaseSchemaVersion <<
") is newer than we currently support (" << SCHEMA_VERSION << "). Please upgrade qTox";
qWarning().nospace() << "Database version (" << databaseSchemaVersion
<< ") is newer than we currently support (" << SCHEMA_VERSION
<< "). Please upgrade qTox";
// We don't know what future versions have done, we have to disable db access until we re-upgrade
return false;
} else if (databaseSchemaVersion == SCHEMA_VERSION) {
@ -304,66 +336,24 @@ bool dbSchemaUpgrade(std::shared_ptr<RawDatabase>& db)
return true;
}
switch (databaseSchemaVersion) {
case 0: {
// Note: 0 is a special version that is actually two versions.
// possibility 1) it is a newly created database and it neesds the current schema to be created.
// possibility 2) it is a old existing database, before version 1 and before we saved schema version,
// and needs to be updated.
bool success = false;
const bool newDb = isNewDb(db, success);
if (!success) {
qCritical() << "Failed to create current db schema";
using DbSchemaUpgradeFn = bool (*)(RawDatabase&);
std::vector<DbSchemaUpgradeFn> upgradeFns = {dbSchema0to1, dbSchema1to2, dbSchema2to3,
dbSchema3to4, dbSchema4to5, dbSchema5to6};
assert(databaseSchemaVersion < static_cast<int>(upgradeFns.size()));
assert(upgradeFns.size() == SCHEMA_VERSION);
for (int64_t i = databaseSchemaVersion; i < static_cast<int>(upgradeFns.size()); ++i) {
auto const newDbVersion = i + 1;
if (!upgradeFns[i](*db)) {
qCritical() << "Failed to upgrade db to schema version " << newDbVersion << " aborting";
return false;
}
if (newDb) {
if (!createCurrentSchema(*db)) {
qCritical() << "Failed to create current db schema";
return false;
}
qDebug() << "Database created at schema version" << SCHEMA_VERSION;
break; // new db is the only case where we don't incrementally upgrade through each version
} else {
if (!dbSchema0to1(*db)) {
qCritical() << "Failed to upgrade db to schema version 1, aborting";
return false;
}
qDebug() << "Database upgraded incrementally to schema version 1";
}
}
// fallthrough
case 1:
if (!dbSchema1to2(*db)) {
qCritical() << "Failed to upgrade db to schema version 2, aborting";
return false;
}
qDebug() << "Database upgraded incrementally to schema version 2";
//fallthrough
case 2:
if (!dbSchema2to3(*db)) {
qCritical() << "Failed to upgrade db to schema version 3, aborting";
return false;
}
qDebug() << "Database upgraded incrementally to schema version 3";
case 3:
if (!dbSchema3to4(*db)) {
qCritical() << "Failed to upgrade db to schema version 4, aborting";
return false;
}
qDebug() << "Database upgraded incrementally to schema version 4";
//fallthrough
case 4:
if (!dbSchema4to5(*db)) {
qCritical() << "Failed to upgrade db to schema version 5, aborting";
return false;
}
qDebug() << "Database upgraded incrementally to schema version 5";
// etc.
default:
qInfo() << "Database upgrade finished (databaseSchemaVersion" << databaseSchemaVersion
<< "->" << SCHEMA_VERSION << ")";
qDebug() << "Database upgraded incrementally to schema version " << newDbVersion;
}
qInfo() << "Database upgrade finished (databaseSchemaVersion" << databaseSchemaVersion << "->"
<< SCHEMA_VERSION << ")";
return true;
}
@ -371,6 +361,7 @@ MessageState getMessageState(bool isPending, bool isBroken)
{
assert(!(isPending && isBroken));
MessageState messageState;
if (isPending) {
messageState = MessageState::pending;
} else if (isBroken) {
@ -544,7 +535,8 @@ void History::removeFriendHistory(const ToxPk& friendPk)
QVector<RawDatabase::Query>
History::generateNewMessageQueries(const ToxPk& friendPk, const QString& message,
const ToxPk& sender, const QDateTime& time, bool isDelivered,
QString dispName, std::function<void(RowId)> insertIdCallback)
ExtensionSet extensionSet, QString dispName,
std::function<void(RowId)> insertIdCallback)
{
QVector<RawDatabase::Query> queries;
@ -565,9 +557,10 @@ History::generateNewMessageQueries(const ToxPk& friendPk, const QString& message
{message.toUtf8(), dispName.toUtf8()}, insertIdCallback);
if (!isDelivered) {
queries += RawDatabase::Query{"INSERT INTO faux_offline_pending (id) VALUES ("
" last_insert_rowid()"
");"};
queries += RawDatabase::Query{QString("INSERT INTO faux_offline_pending (id, required_extensions) VALUES ("
" last_insert_rowid(), %1"
");")
.arg(extensionSet.to_ulong())};
}
return queries;
@ -590,7 +583,8 @@ void History::onFileInsertionReady(FileDbInsertionData data)
.arg(data.size)
.arg(static_cast<int>(data.direction))
.arg(ToxFile::CANCELED),
{data.fileId.toUtf8(), data.filePath.toUtf8(), data.fileName.toUtf8(), QByteArray()},
{data.fileId.toUtf8(), data.filePath.toUtf8(), data.fileName.toUtf8(),
QByteArray()},
[weakThis, fileId](RowId id) {
auto pThis = weakThis.lock();
if (pThis) {
@ -687,7 +681,7 @@ void History::addNewFileMessage(const ToxPk& friendPk, const QString& fileId,
emit thisPtr->fileInsertionReady(std::move(insertionDataRw));
};
addNewMessage(friendPk, "", sender, time, true, dispName, insertFileTransferFn);
addNewMessage(friendPk, "", sender, time, true, ExtensionSet(), dispName, insertFileTransferFn);
}
/**
@ -701,15 +695,15 @@ void History::addNewFileMessage(const ToxPk& friendPk, const QString& fileId,
* @param insertIdCallback Function, called after query execution.
*/
void History::addNewMessage(const ToxPk& friendPk, const QString& message, const ToxPk& sender,
const QDateTime& time, bool isDelivered, QString dispName,
const std::function<void(RowId)>& insertIdCallback)
const QDateTime& time, bool isDelivered, ExtensionSet extensionSet,
QString dispName, const std::function<void(RowId)>& insertIdCallback)
{
if (historyAccessBlocked()) {
return;
}
db->execLater(generateNewMessageQueries(friendPk, message, sender, time, isDelivered, dispName,
insertIdCallback));
db->execLater(generateNewMessageQueries(friendPk, message, sender, time, isDelivered,
extensionSet, dispName, insertIdCallback));
}
void History::setFileFinished(const QString& fileId, bool success, const QString& filePath,
@ -785,7 +779,8 @@ QList<History::HistMessage> History::getMessagesForFriend(const ToxPk& friendPk,
"message, file_transfers.file_restart_id, "
"file_transfers.file_path, file_transfers.file_name, "
"file_transfers.file_size, file_transfers.direction, "
"file_transfers.file_state, broken_messages.id FROM history "
"file_transfers.file_state, broken_messages.id, "
"faux_offline_pending.required_extensions FROM history "
"LEFT JOIN faux_offline_pending ON history.id = faux_offline_pending.id "
"JOIN peers chat ON history.chat_id = chat.id "
"JOIN aliases ON sender_alias = aliases.id "
@ -808,11 +803,12 @@ QList<History::HistMessage> History::getMessagesForFriend(const ToxPk& friendPk,
auto display_name = QString::fromUtf8(row[4].toByteArray().replace('\0', ""));
auto sender_key = row[5].toString();
auto isBroken = !row[13].isNull();
auto requiredExtensions = ExtensionSet(row[14].toLongLong());
MessageState messageState = getMessageState(isPending, isBroken);
if (row[7].isNull()) {
messages += {id, messageState, timestamp, friend_key,
messages += {id, messageState, requiredExtensions, timestamp, friend_key,
display_name, sender_key, row[6].toString()};
} else {
ToxFile file;
@ -823,8 +819,7 @@ QList<History::HistMessage> History::getMessagesForFriend(const ToxPk& friendPk,
file.filesize = row[10].toLongLong();
file.direction = static_cast<ToxFile::FileDirection>(row[11].toLongLong());
file.status = static_cast<ToxFile::FileStatus>(row[12].toInt());
messages +=
{id, messageState, timestamp, friend_key, display_name, sender_key, file};
messages += {id, messageState, timestamp, friend_key, display_name, sender_key, file};
}
};
@ -841,7 +836,8 @@ QList<History::HistMessage> History::getUndeliveredMessagesForFriend(const ToxPk
auto queryText =
QString("SELECT history.id, faux_offline_pending.id, timestamp, chat.public_key, "
"aliases.display_name, sender.public_key, message, broken_messages.id "
"aliases.display_name, sender.public_key, message, broken_messages.id, "
"faux_offline_pending.required_extensions "
"FROM history "
"JOIN faux_offline_pending ON history.id = faux_offline_pending.id "
"JOIN peers chat on history.chat_id = chat.id "
@ -862,11 +858,12 @@ QList<History::HistMessage> History::getUndeliveredMessagesForFriend(const ToxPk
auto display_name = QString::fromUtf8(row[4].toByteArray().replace('\0', ""));
auto sender_key = row[5].toString();
auto isBroken = !row[7].isNull();
auto extensionSet = ExtensionSet(row[8].toLongLong());
MessageState messageState = getMessageState(isPending, isBroken);
ret += {id, messageState, timestamp, friend_key,
display_name, sender_key, row[6].toString()};
ret +=
{id, messageState, extensionSet, timestamp, friend_key, display_name, sender_key, row[6].toString()};
};
db->execNow({queryText, rowCallback});
@ -1066,5 +1063,20 @@ bool History::historyAccessBlocked()
}
return false;
}
void History::markAsBroken(RowId messageId, BrokenMessageReason reason)
{
if (!isValid()) {
return;
}
QVector<RawDatabase::Query> queries;
queries += RawDatabase::Query(QString("DELETE FROM faux_offline_pending WHERE id=%1;").arg(messageId.get()));
queries += RawDatabase::Query(QString("INSERT INTO broken_messages (id, reason) "
"VALUES (%1, %2);")
.arg(messageId.get())
.arg(static_cast<int64_t>(reason)));
db->execLater(queries);
}

View File

@ -30,6 +30,8 @@
#include "src/core/toxfile.h"
#include "src/core/toxpk.h"
#include "src/core/extension.h"
#include "src/model/brokenmessagereason.h"
#include "src/persistence/db/rawdatabase.h"
#include "src/widget/searchtypes.h"
@ -117,7 +119,7 @@ class History : public QObject, public std::enable_shared_from_this<History>
public:
struct HistMessage
{
HistMessage(RowId id, MessageState state, QDateTime timestamp, QString chat, QString dispName,
HistMessage(RowId id, MessageState state, ExtensionSet extensionSet, QDateTime timestamp, QString chat, QString dispName,
QString sender, QString message)
: chat{chat}
, sender{sender}
@ -125,6 +127,7 @@ public:
, timestamp{timestamp}
, id{id}
, state{state}
, extensionSet(extensionSet)
, content(std::move(message))
{}
@ -146,6 +149,7 @@ public:
QDateTime timestamp;
RowId id;
MessageState state;
ExtensionSet extensionSet;
HistMessageContent content;
};
@ -166,8 +170,8 @@ public:
void eraseHistory();
void removeFriendHistory(const ToxPk& friendPk);
void addNewMessage(const ToxPk& friendPk, const QString& message, const ToxPk& sender,
const QDateTime& time, bool isDelivered, QString dispName,
const std::function<void(RowId)>& insertIdCallback = {});
const QDateTime& time, bool isDelivered, ExtensionSet extensions,
QString dispName, const std::function<void(RowId)>& insertIdCallback = {});
void addNewFileMessage(const ToxPk& friendPk, const QString& fileId,
const QString& fileName, const QString& filePath, int64_t size,
@ -184,12 +188,13 @@ public:
const QDate& from, size_t maxNum);
void markAsDelivered(RowId messageId);
void markAsBroken(RowId messageId, BrokenMessageReason reason);
protected:
QVector<RawDatabase::Query>
generateNewMessageQueries(const ToxPk& friendPk, const QString& message,
const ToxPk& sender, const QDateTime& time, bool isDelivered,
QString dispName, std::function<void(RowId)> insertIdCallback = {});
ExtensionSet extensionSet, QString dispName, std::function<void(RowId)> insertIdCallback = {});
signals:
void fileInsertionReady(FileDbInsertionData data);

View File

@ -29,10 +29,8 @@
#include <QCoreApplication>
#include <chrono>
OfflineMsgEngine::OfflineMsgEngine(Friend* frnd, ICoreFriendMessageSender* messageSender)
OfflineMsgEngine::OfflineMsgEngine()
: mutex(QMutex::Recursive)
, f(frnd)
, messageSender(messageSender)
{}
/**
@ -43,12 +41,13 @@ OfflineMsgEngine::OfflineMsgEngine(Friend* frnd, ICoreFriendMessageSender* messa
void OfflineMsgEngine::onReceiptReceived(ReceiptNum receipt)
{
QMutexLocker ml(&mutex);
if (receivedReceipts.contains(receipt)) {
qWarning() << "Receievd duplicate receipt" << receipt.get() << "from friend" << f->getId();
return;
}
receivedReceipts.append(receipt);
checkForCompleteMessages(receipt);
receiptResolver.notifyReceiptReceived(receipt);
}
void OfflineMsgEngine::onExtendedReceiptReceived(ExtendedReceiptNum receipt)
{
QMutexLocker ml(&mutex);
extendedReceiptResolver.notifyReceiptReceived(receipt);
}
/**
@ -63,7 +62,7 @@ void OfflineMsgEngine::onReceiptReceived(ReceiptNum receipt)
void OfflineMsgEngine::addUnsentMessage(Message const& message, CompletionFn completionCallback)
{
QMutexLocker ml(&mutex);
unsentMessages.append(OfflineMessage{message, std::chrono::steady_clock::now(), completionCallback});
unsentMessages.push_back(OfflineMessage{message, std::chrono::steady_clock::now(), completionCallback});
}
/**
@ -76,79 +75,51 @@ void OfflineMsgEngine::addUnsentMessage(Message const& message, CompletionFn com
* @param[in] messageID database RowId of the message, used to eventually mark messages as received in history
* @param[in] msg chat message line in the chatlog, used to eventually set the message's receieved timestamp
*/
void OfflineMsgEngine::addSentMessage(ReceiptNum receipt, Message const& message,
void OfflineMsgEngine::addSentCoreMessage(ReceiptNum receipt, Message const& message,
CompletionFn completionCallback)
{
QMutexLocker ml(&mutex);
assert(!sentMessages.contains(receipt));
sentMessages.insert(receipt, {message, std::chrono::steady_clock::now(), completionCallback});
checkForCompleteMessages(receipt);
receiptResolver.notifyMessageSent(receipt, {message, std::chrono::steady_clock::now(), completionCallback});
}
/**
* @brief Deliver all messages, used when a friend comes online.
*/
void OfflineMsgEngine::deliverOfflineMsgs()
void OfflineMsgEngine::addSentExtendedMessage(ExtendedReceiptNum receipt, Message const& message,
CompletionFn completionCallback)
{
QMutexLocker ml(&mutex);
if (!Status::isOnline(f->getStatus())) {
return;
}
if (sentMessages.empty() && unsentMessages.empty()) {
return;
}
QVector<OfflineMessage> messages = sentMessages.values().toVector() + unsentMessages;
// order messages by authorship time to resend in same order as they were written
std::sort(messages.begin(), messages.end(), [](const OfflineMessage& lhs, const OfflineMessage& rhs) {
return lhs.authorshipTime < rhs.authorshipTime;
});
removeAllMessages();
for (const auto& message : messages) {
QString messageText = message.message.content;
ReceiptNum receipt;
bool messageSent{false};
if (message.message.isAction) {
messageSent = messageSender->sendAction(f->getId(), messageText, receipt);
} else {
messageSent = messageSender->sendMessage(f->getId(), messageText, receipt);
}
if (messageSent) {
addSentMessage(receipt, message.message, message.completionFn);
} else {
qCritical() << "deliverOfflineMsgs failed to send message";
addUnsentMessage(message.message, message.completionFn);
}
}
extendedReceiptResolver.notifyMessageSent(receipt, {message, std::chrono::steady_clock::now(), completionCallback});
}
/**
* @brief Removes all messages which are being tracked.
*/
void OfflineMsgEngine::removeAllMessages()
std::vector<OfflineMsgEngine::RemovedMessage> OfflineMsgEngine::removeAllMessages()
{
QMutexLocker ml(&mutex);
receivedReceipts.clear();
sentMessages.clear();
auto messages = receiptResolver.clear();
auto extendedMessages = extendedReceiptResolver.clear();
messages.insert(
messages.end(),
std::make_move_iterator(extendedMessages.begin()),
std::make_move_iterator(extendedMessages.end()));
messages.insert(
messages.end(),
std::make_move_iterator(unsentMessages.begin()),
std::make_move_iterator(unsentMessages.end()));
unsentMessages.clear();
}
void OfflineMsgEngine::completeMessage(QMap<ReceiptNum, OfflineMessage>::iterator msgIt)
{
msgIt->completionFn();
receivedReceipts.removeOne(msgIt.key());
sentMessages.erase(msgIt);
}
std::sort(messages.begin(), messages.end(), [] (const OfflineMessage& a, const OfflineMessage& b) {
return a.authorshipTime < b.authorshipTime;
});
void OfflineMsgEngine::checkForCompleteMessages(ReceiptNum receipt)
{
auto msgIt = sentMessages.find(receipt);
const bool receiptReceived = receivedReceipts.contains(receipt);
if (!receiptReceived || msgIt == sentMessages.end()) {
return;
}
completeMessage(msgIt);
auto ret = std::vector<RemovedMessage>();
ret.reserve(messages.size());
std::transform(messages.begin(), messages.end(), std::back_inserter(ret), [](const OfflineMessage& msg) {
return RemovedMessage{msg.message, msg.completionFn};
});
return ret;
}

View File

@ -30,23 +30,27 @@
#include <QSet>
#include <chrono>
class Friend;
class ICoreFriendMessageSender;
class OfflineMsgEngine : public QObject
{
Q_OBJECT
public:
explicit OfflineMsgEngine(Friend* f, ICoreFriendMessageSender* messageSender);
using CompletionFn = std::function<void()>;
using CompletionFn = std::function<void(bool)>;
OfflineMsgEngine();
void addUnsentMessage(Message const& message, CompletionFn completionCallback);
void addSentMessage(ReceiptNum receipt, Message const& message, CompletionFn completionCallback);
void deliverOfflineMsgs();
void addSentCoreMessage(ReceiptNum receipt, Message const& message, CompletionFn completionCallback);
void addSentExtendedMessage(ExtendedReceiptNum receipt, Message const& message, CompletionFn completionCallback);
struct RemovedMessage
{
Message message;
CompletionFn callback;
};
std::vector<RemovedMessage> removeAllMessages();
public slots:
void removeAllMessages();
void onReceiptReceived(ReceiptNum receipt);
void onExtendedReceiptReceived(ExtendedReceiptNum receipt);
private:
struct OfflineMessage
@ -56,16 +60,58 @@ private:
CompletionFn completionFn;
};
private slots:
void completeMessage(QMap<ReceiptNum, OfflineMessage>::iterator msgIt);
private:
void checkForCompleteMessages(ReceiptNum receipt);
QMutex mutex;
const Friend* f;
ICoreFriendMessageSender* messageSender;
QVector<ReceiptNum> receivedReceipts;
QMap<ReceiptNum, OfflineMessage> sentMessages;
QVector<OfflineMessage> unsentMessages;
template <class ReceiptT>
class ReceiptResolver
{
public:
void notifyMessageSent(ReceiptT receipt, OfflineMessage const& message)
{
auto receivedReceiptIt = std::find(
receivedReceipts.begin(), receivedReceipts.end(), receipt);
if (receivedReceiptIt != receivedReceipts.end()) {
receivedReceipts.erase(receivedReceiptIt);
message.completionFn(true);
return;
}
unAckedMessages[receipt] = message;
}
void notifyReceiptReceived(ReceiptT receipt)
{
auto unackedMessageIt = unAckedMessages.find(receipt);
if (unackedMessageIt != unAckedMessages.end()) {
unackedMessageIt->second.completionFn(true);
unAckedMessages.erase(unackedMessageIt);
return;
}
receivedReceipts.push_back(receipt);
}
std::vector<OfflineMessage> clear()
{
auto ret = std::vector<OfflineMessage>();
std::transform(
std::make_move_iterator(unAckedMessages.begin()), std::make_move_iterator(unAckedMessages.end()),
std::back_inserter(ret),
[] (const std::pair<ReceiptT, OfflineMessage>& pair) {
return std::move(pair.second);
});
receivedReceipts.clear();
unAckedMessages.clear();
return ret;
}
std::vector<ReceiptT> receivedReceipts;
std::map<ReceiptT, OfflineMessage> unAckedMessages;
};
ReceiptResolver<ReceiptNum> receiptResolver;
ReceiptResolver<ExtendedReceiptNum> extendedReceiptResolver;
std::vector<OfflineMessage> unsentMessages;
};

View File

@ -712,7 +712,7 @@ void Profile::onRequestSent(const ToxPk& friendPk, const QString& message)
const ToxPk selfPk = core->getSelfPublicKey();
const QDateTime datetime = QDateTime::currentDateTime();
const QString name = core->getUsername();
history->addNewMessage(friendPk, inviteStr, selfPk, datetime, true, name);
history->addNewMessage(friendPk, inviteStr, selfPk, datetime, true, ExtensionSet(), name);
}
/**

View File

@ -18,6 +18,9 @@
*/
#include "chatformheader.h"
#include "extensionstatus.h"
#include "src/model/status.h"
#include "src/widget/gui.h"
#include "src/widget/maskablepixmapwidget.h"
@ -117,6 +120,11 @@ ChatFormHeader::ChatFormHeader(QWidget* parent)
avatar = new MaskablePixmapWidget(this, AVATAR_SIZE, ":/img/avatar_mask.svg");
avatar->setObjectName("avatar");
nameLine = new QHBoxLayout();
nameLine->setSpacing(3);
extensionStatus = new ExtensionStatus();
nameLabel = new CroppingLabel();
nameLabel->setObjectName("nameLabel");
nameLabel->setMinimumHeight(Style::getFont(Style::Medium).pixelSize());
@ -124,9 +132,12 @@ ChatFormHeader::ChatFormHeader(QWidget* parent)
nameLabel->setTextFormat(Qt::PlainText);
connect(nameLabel, &CroppingLabel::editFinished, this, &ChatFormHeader::nameChanged);
nameLine->addWidget(extensionStatus);
nameLine->addWidget(nameLabel);
headTextLayout = new QVBoxLayout();
headTextLayout->addStretch();
headTextLayout->addWidget(nameLabel);
headTextLayout->addLayout(nameLine);
headTextLayout->addStretch();
micButton = createButton("micButton", this, &ChatFormHeader::micMuteToggle);
@ -223,6 +234,11 @@ void ChatFormHeader::removeCallConfirm()
callConfirm.reset(nullptr);
}
void ChatFormHeader::updateExtensionSupport(ExtensionSet extensions)
{
extensionStatus->onExtensionSetUpdate(extensions);
}
void ChatFormHeader::updateCallButtons(bool online, bool audio, bool video)
{
const bool audioAvaliable = online && (mode & Mode::Audio);

View File

@ -21,14 +21,19 @@
#include <QWidget>
#include "src/core/extension.h"
#include <memory>
class MaskablePixmapWidget;
class QVBoxLayout;
class QHBoxLayout;
class CroppingLabel;
class QPushButton;
class QToolButton;
class CallConfirmWidget;
class QLabel;
class ExtensionStatus;
class ChatFormHeader : public QWidget
{
@ -64,6 +69,7 @@ public:
void showCallConfirm();
void removeCallConfirm();
void updateExtensionSupport(ExtensionSet extensions);
void updateCallButtons(bool online, bool audio, bool video = false);
void updateMuteMicButton(bool active, bool inputMuted);
void updateMuteVolButton(bool active, bool outputMuted);
@ -98,6 +104,8 @@ private:
Mode mode;
MaskablePixmapWidget* avatar;
QVBoxLayout* headTextLayout;
QHBoxLayout* nameLine;
ExtensionStatus* extensionStatus;
CroppingLabel* nameLabel;
QPushButton* callButton;

View File

@ -0,0 +1,54 @@
/*
Copyright © 2019-2020 by The qTox Project Contributors
This file is part of qTox, a Qt-based graphical interface for Tox.
qTox is libre software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
qTox 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with qTox. If not, see <http://www.gnu.org/licenses/>.
*/
#include "extensionstatus.h"
#include <QIcon>
ExtensionStatus::ExtensionStatus(QWidget* parent)
: QLabel(parent)
{
// Initialize with 0 extensions
onExtensionSetUpdate(ExtensionSet());
}
void ExtensionStatus::onExtensionSetUpdate(ExtensionSet extensionSet)
{
QString iconName;
QString hoverText;
if (extensionSet.all()) {
iconName = ":/img/status/extensions_available.svg";
hoverText = tr("All extensions supported");
} else if (extensionSet.none()) {
iconName = ":/img/status/extensions_unavailable.svg";
hoverText = tr("No extensions supported");
} else {
iconName = ":/img/status/extensions_partial.svg";
hoverText = tr("Not all extensions supported");
}
hoverText += "\n";
hoverText += tr("Multipart Messages: ");
hoverText += extensionSet[ExtensionType::messages] ? "" : "";
auto pixmap = QIcon(iconName).pixmap(QSize(16, 16));
setPixmap(pixmap);
setToolTip(hoverText);
}

View File

@ -0,0 +1,34 @@
/*
Copyright © 2019-2020 by The qTox Project Contributors
This file is part of qTox, a Qt-based graphical interface for Tox.
qTox is libre software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
qTox 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with qTox. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "src/core/extension.h"
#include <QLabel>
class ExtensionStatus : public QLabel
{
Q_OBJECT
public:
ExtensionStatus(QWidget* parent = nullptr);
public slots:
void onExtensionSetUpdate(ExtensionSet extensionSet);
};

View File

@ -142,9 +142,10 @@ ChatForm::ChatForm(Profile& profile, Friend* chatFriend, IChatLog& chatLog, IMes
connect(coreFile, &CoreFile::fileReceiveRequested, this, &ChatForm::updateFriendActivityForFile);
connect(coreFile, &CoreFile::fileSendStarted, this, &ChatForm::updateFriendActivityForFile);
connect(&core, &Core::friendTypingChanged, this, &ChatForm::onFriendTypingChanged);
connect(&core, &Core::friendStatusChanged, this, &ChatForm::onFriendStatusChanged);
connect(coreFile, &CoreFile::fileNameChanged, this, &ChatForm::onFileNameChanged);
connect(chatFriend, &Friend::statusChanged, this, &ChatForm::onFriendStatusChanged);
const CoreAV* av = core.getAv();
connect(av, &CoreAV::avInvite, this, &ChatForm::onAvInvite);
connect(av, &CoreAV::avStart, this, &ChatForm::onAvStart);
@ -180,6 +181,8 @@ ChatForm::ChatForm(Profile& profile, Friend* chatFriend, IChatLog& chatLog, IMes
connect(bodySplitter, &QSplitter::splitterMoved, this, &ChatForm::onSplitterMoved);
connect(f, &Friend::extensionSupportChanged, this, &ChatForm::onExtensionSupportChanged);
updateCallButtons();
setAcceptDrops(true);
@ -223,6 +226,11 @@ void ChatForm::onFileNameChanged(const ToxPk& friendPk)
"so you can save the file on Windows."));
}
void ChatForm::onExtensionSupportChanged(ExtensionSet extensions)
{
headWidget->updateExtensionSupport(extensions);
}
void ChatForm::onTextEditChanged()
{
if (!Settings::getInstance().getTypingNotification()) {
@ -423,12 +431,10 @@ void ChatForm::onVolMuteToggle()
updateMuteVolButton();
}
void ChatForm::onFriendStatusChanged(uint32_t friendId, Status::Status status)
void ChatForm::onFriendStatusChanged(const ToxPk& friendPk, Status::Status status)
{
// Disable call buttons if friend is offline
if (friendId != f->getId()) {
return;
}
assert(friendPk == f->getPublicKey());
if (!Status::isOnline(f->getStatus())) {
// Hide the "is typing" message when a friend goes offline

View File

@ -72,6 +72,7 @@ public slots:
void onAvEnd(uint32_t friendId, bool error);
void onAvatarChanged(const ToxPk& friendPk, const QPixmap& pic);
void onFileNameChanged(const ToxPk& friendPk);
void onExtensionSupportChanged(ExtensionSet extensions);
void clearChatArea();
void onShowMessagesClicked();
void onSplitterMoved(int pos, int index);
@ -90,7 +91,7 @@ private slots:
void onMicMuteToggle();
void onVolMuteToggle();
void onFriendStatusChanged(quint32 friendId, Status::Status status);
void onFriendStatusChanged(const ToxPk& friendPk, Status::Status status);
void onFriendTypingChanged(quint32 friendId, bool isTyping);
void onFriendNameChanged(const QString& name);
void onStatusMessage(const QString& message);

View File

@ -190,6 +190,8 @@ void renderMessageRaw(const QString& displayName, bool isSelf, bool colorizeName
if (chatMessage) {
if (chatLogMessage.state == MessageState::complete) {
chatMessage->markAsDelivered(chatLogMessage.message.timestamp);
} else if (chatLogMessage.state == MessageState::broken) {
chatMessage->markAsBroken();
}
} else {
chatMessage = createMessage(displayName, isSelf, colorizeNames, chatLogMessage);

View File

@ -343,6 +343,8 @@ QString FriendWidget::getStatusString() const
tr("Away"),
tr("Busy"),
tr("Offline"),
tr("Blocked"),
tr("Negotiating")
};
return event ? tr("New message") : names.value(status);

View File

@ -248,6 +248,9 @@ void Widget::init()
ui->searchContactFilterBox->setMenu(filterMenu);
core = &profile.getCore();
auto coreExt = core->getExt();
sharedMessageProcessorParams.reset(new MessageProcessor::SharedParams(core->getMaxMessageSize(), coreExt->getMaxExtendedMessageSize()));
contactListWidget = new FriendListWidget(*core, this, settings.getGroupchatPosition());
connect(contactListWidget, &FriendListWidget::searchCircle, this, &Widget::searchCircle);
@ -679,7 +682,7 @@ void Widget::onCoreChanged(Core& core)
connect(&core, &Core::friendAdded, this, &Widget::addFriend);
connect(&core, &Core::failedToAddFriend, this, &Widget::addFriendFailed);
connect(&core, &Core::friendUsernameChanged, this, &Widget::onFriendUsernameChanged);
connect(&core, &Core::friendStatusChanged, this, &Widget::onFriendStatusChanged);
connect(&core, &Core::friendStatusChanged, this, &Widget::onCoreFriendStatusChanged);
connect(&core, &Core::friendStatusMessageChanged, this, &Widget::onFriendStatusMessageChanged);
connect(&core, &Core::friendRequestReceived, this, &Widget::onFriendRequestReceived);
connect(&core, &Core::friendMessageReceived, this, &Widget::onFriendMessageReceived);
@ -695,12 +698,19 @@ void Widget::onCoreChanged(Core& core)
connect(&core, &Core::friendTypingChanged, this, &Widget::onFriendTypingChanged);
connect(&core, &Core::groupSentFailed, this, &Widget::onGroupSendFailed);
connect(&core, &Core::usernameSet, this, &Widget::refreshPeerListsLocal);
auto coreExt = core.getExt();
connect(coreExt, &CoreExt::extendedMessageReceived, this, &Widget::onFriendExtMessageReceived);
connect(coreExt, &CoreExt::extendedReceiptReceived, this, &Widget::onExtReceiptReceived);
connect(coreExt, &CoreExt::extendedMessageSupport, this, &Widget::onExtendedMessageSupport);
connect(this, &Widget::statusSet, &core, &Core::setStatus);
connect(this, &Widget::friendRequested, &core, &Core::requestFriendship);
connect(this, &Widget::friendRequestAccepted, &core, &Core::acceptFriendRequest);
connect(this, &Widget::changeGroupTitle, &core, &Core::changeGroupTitle);
sharedMessageProcessorParams.setPublicKey(core.getSelfPublicKey().toString());
sharedMessageProcessorParams->setPublicKey(core.getSelfPublicKey().toString());
}
void Widget::onConnected()
@ -992,7 +1002,7 @@ void Widget::setUsername(const QString& username)
Qt::convertFromPlainText(username, Qt::WhiteSpaceNormal)); // for overlength names
}
sharedMessageProcessorParams.onUserNameSet(username);
sharedMessageProcessorParams->onUserNameSet(username);
}
void Widget::onStatusMessageChanged(const QString& newStatusMessage)
@ -1144,9 +1154,9 @@ void Widget::addFriend(uint32_t friendId, const ToxPk& friendPk)
connectFriendWidget(*widget);
auto history = profile.getHistory();
auto messageProcessor = MessageProcessor(sharedMessageProcessorParams);
auto messageProcessor = MessageProcessor(*sharedMessageProcessorParams);
auto friendMessageDispatcher =
std::make_shared<FriendMessageDispatcher>(*newfriend, std::move(messageProcessor), *core);
std::make_shared<FriendMessageDispatcher>(*newfriend, std::move(messageProcessor), *core, *core->getExt());
// Note: We do not have to connect the message dispatcher signals since
// ChatHistory hooks them up in a very specific order
@ -1183,6 +1193,7 @@ void Widget::addFriend(uint32_t friendId, const ToxPk& friendPk)
friendAlertConnections.insert(friendPk, notifyReceivedConnection);
connect(newfriend, &Friend::aliasChanged, this, &Widget::onFriendAliasChanged);
connect(newfriend, &Friend::displayedNameChanged, this, &Widget::onFriendDisplayedNameChanged);
connect(newfriend, &Friend::statusChanged, this, &Widget::onFriendStatusChanged);
connect(friendForm, &ChatForm::incomingNotification, this, &Widget::incomingNotification);
connect(friendForm, &ChatForm::outgoingNotification, this, &Widget::outgoingNotification);
@ -1222,7 +1233,7 @@ void Widget::addFriendFailed(const ToxPk&, const QString& errorInfo)
QMessageBox::critical(nullptr, "Error", info);
}
void Widget::onFriendStatusChanged(int friendId, Status::Status status)
void Widget::onCoreFriendStatusChanged(int friendId, Status::Status status)
{
const auto& friendPk = FriendList::id2Key(friendId);
Friend* f = FriendList::findFriend(friendPk);
@ -1230,18 +1241,35 @@ void Widget::onFriendStatusChanged(int friendId, Status::Status status)
return;
}
bool isActualChange = f->getStatus() != status;
auto const oldStatus = f->getStatus();
f->setStatus(status);
auto const newStatus = f->getStatus();
FriendWidget* widget = friendWidgets[f->getPublicKey()];
if (isActualChange) {
if (!Status::isOnline(f->getStatus())) {
auto const startedNegotiating = (newStatus == Status::Status::Negotiating && oldStatus != newStatus);
if (startedNegotiating) {
constexpr auto negotiationTimeoutMs = 1000;
auto timer = std::unique_ptr<QTimer>(new QTimer);
timer->setSingleShot(true);
timer->setInterval(negotiationTimeoutMs);
connect(timer.get(), &QTimer::timeout, f, &Friend::onNegotiationComplete);
timer->start();
negotiateTimers[friendPk] = std::move(timer);
}
// Any widget behavior will be triggered based off of the status
// transformations done by the Friend class
}
void Widget::onFriendStatusChanged(const ToxPk& friendPk, Status::Status status)
{
FriendWidget* widget = friendWidgets[friendPk];
if (Status::isOnline(status)) {
contactListWidget->moveWidget(widget, Status::Status::Online);
} else if (status == Status::Status::Offline) {
} else {
contactListWidget->moveWidget(widget, Status::Status::Offline);
}
}
f->setStatus(status);
widget->updateStatusLight();
if (widget->isActive()) {
setWindowTitle(widget->getTitle());
@ -1401,6 +1429,29 @@ void Widget::onReceiptReceived(int friendId, ReceiptNum receipt)
friendMessageDispatchers[f->getPublicKey()]->onReceiptReceived(receipt);
}
void Widget::onExtendedMessageSupport(uint32_t friendNumber, bool compatible)
{
const auto& friendKey = FriendList::id2Key(friendNumber);
Friend* f = FriendList::findFriend(friendKey);
if (!f) {
return;
}
f->setExtendedMessageSupport(compatible);
}
void Widget::onFriendExtMessageReceived(uint32_t friendNumber, const QString& message)
{
const auto& friendKey = FriendList::id2Key(friendNumber);
friendMessageDispatchers[friendKey]->onExtMessageReceived(message);
}
void Widget::onExtReceiptReceived(uint32_t friendNumber, uint64_t receiptId)
{
const auto& friendKey = FriendList::id2Key(friendNumber);
friendMessageDispatchers[friendKey]->onExtReceiptReceived(receiptId);
}
void Widget::addFriendDialog(const Friend* frnd, ContentDialog* dialog)
{
const ToxPk& friendPk = frnd->getPublicKey();
@ -2079,7 +2130,7 @@ Group* Widget::createGroup(uint32_t groupnumber, const GroupId& groupId)
const auto compact = settings.getCompactLayout();
auto widget = new GroupWidget(chatroom, compact);
auto messageProcessor = MessageProcessor(sharedMessageProcessorParams);
auto messageProcessor = MessageProcessor(*sharedMessageProcessorParams);
auto messageDispatcher =
std::make_shared<GroupMessageDispatcher>(*newgroup, std::move(messageProcessor), *core,
*core, settings);
@ -2091,6 +2142,8 @@ Group* Widget::createGroup(uint32_t groupnumber, const GroupId& groupId)
&SessionChatLog::onMessageSent);
connect(messageDispatcher.get(), &IMessageDispatcher::messageComplete, groupChatLog.get(),
&SessionChatLog::onMessageComplete);
connect(messageDispatcher.get(), &IMessageDispatcher::messageBroken, groupChatLog.get(),
&SessionChatLog::onMessageBroken);
auto notifyReceivedCallback = [this, groupId](const ToxPk& author, const Message& message) {
auto isTargeted = std::any_of(message.metadata.begin(), message.metadata.end(),

View File

@ -165,13 +165,17 @@ public slots:
void setStatusMessage(const QString& statusMessage);
void addFriend(uint32_t friendId, const ToxPk& friendPk);
void addFriendFailed(const ToxPk& userId, const QString& errorInfo = QString());
void onFriendStatusChanged(int friendId, Status::Status status);
void onCoreFriendStatusChanged(int friendId, Status::Status status);
void onFriendStatusChanged(const ToxPk& friendPk, Status::Status status);
void onFriendStatusMessageChanged(int friendId, const QString& message);
void onFriendDisplayedNameChanged(const QString& displayed);
void onFriendUsernameChanged(int friendId, const QString& username);
void onFriendAliasChanged(const ToxPk& friendId, const QString& alias);
void onFriendMessageReceived(uint32_t friendnumber, const QString& message, bool isAction);
void onReceiptReceived(int friendId, ReceiptNum receipt);
void onExtendedMessageSupport(uint32_t friendNumber, bool supported);
void onFriendExtMessageReceived(uint32_t friendNumber, const QString& message);
void onExtReceiptReceived(uint32_t friendNumber, uint64_t receiptId);
void onFriendRequestReceived(const ToxPk& friendPk, const QString& message);
void onFileReceiveRequested(const ToxFile& file);
void onEmptyGroupCreated(uint32_t groupnumber, const GroupId& groupId, const QString& title);
@ -344,6 +348,7 @@ private:
QMap<ToxPk, std::shared_ptr<ChatHistory>> friendChatLogs;
QMap<ToxPk, std::shared_ptr<FriendChatroom>> friendChatrooms;
QMap<ToxPk, ChatForm*> chatForms;
std::map<ToxPk, std::unique_ptr<QTimer>> negotiateTimers;
QMap<GroupId, GroupWidget*> groupWidgets;
QMap<GroupId, std::shared_ptr<GroupMessageDispatcher>> groupMessageDispatchers;
@ -359,7 +364,7 @@ private:
Core* core = nullptr;
MessageProcessor::SharedParams sharedMessageProcessorParams;
std::unique_ptr<MessageProcessor::SharedParams> sharedMessageProcessorParams;
#if DESKTOP_NOTIFICATIONS
std::unique_ptr<NotificationGenerator> notificationGenerator;
DesktopNotify notifier;

View File

@ -25,8 +25,50 @@
#include <QObject>
#include <QtTest/QtTest>
#include <set>
#include <deque>
static constexpr uint64_t testMaxExtendedMessageSize = 10 * 1024 * 1024;
class MockCoreExtPacket : public ICoreExtPacket
{
public:
MockCoreExtPacket(uint64_t& numSentMessages, uint64_t& currentReceiptId)
: numSentMessages(numSentMessages)
, currentReceiptId(currentReceiptId)
{}
uint64_t addExtendedMessage(QString message) override
{
this->message = message;
return currentReceiptId++;
}
bool send() override
{
numSentMessages++;
return true;
}
uint64_t& numSentMessages;
uint64_t& currentReceiptId;
QDateTime senderTimestamp;
QString message;
};
class MockCoreExtPacketAllocator : public ICoreExtPacketAllocator
{
public:
std::unique_ptr<ICoreExtPacket> getPacket(uint32_t friendId) override
{
return std::unique_ptr<MockCoreExtPacket>(new MockCoreExtPacket(numSentMessages, currentReceiptId));
}
uint64_t numSentMessages;
uint64_t currentReceiptId;
};
class MockFriendMessageSender : public ICoreFriendMessageSender
{
@ -69,6 +111,11 @@ private slots:
void testMessageSending();
void testOfflineMessages();
void testFailedMessage();
void testNegotiationFailure();
void testNegotiationSuccess();
void testOfflineExtensionMessages();
void testSentMessageExtensionSetReduced();
void testActionMessagesSplitWithExtensions();
void onMessageSent(DispatchedMessageId id, Message message)
{
@ -89,14 +136,21 @@ private slots:
receivedMessages.push_back(std::move(message));
}
void onMessageBroken(DispatchedMessageId id, BrokenMessageReason)
{
brokenMessages.insert(id);
}
private:
// All unique_ptrs to make construction/init() easier to manage
std::unique_ptr<Friend> f;
std::unique_ptr<MockFriendMessageSender> messageSender;
std::unique_ptr<MockCoreExtPacketAllocator> coreExtPacketAllocator;
std::unique_ptr<MessageProcessor::SharedParams> sharedProcessorParams;
std::unique_ptr<MessageProcessor> messageProcessor;
std::unique_ptr<FriendMessageDispatcher> friendMessageDispatcher;
std::map<DispatchedMessageId, Message> outgoingMessages;
std::set<DispatchedMessageId> brokenMessages;
std::deque<Message> receivedMessages;
};
@ -109,12 +163,15 @@ void TestFriendMessageDispatcher::init()
{
f = std::unique_ptr<Friend>(new Friend(0, ToxPk()));
f->setStatus(Status::Status::Online);
f->onNegotiationComplete();
messageSender = std::unique_ptr<MockFriendMessageSender>(new MockFriendMessageSender());
coreExtPacketAllocator = std::unique_ptr<MockCoreExtPacketAllocator>(new MockCoreExtPacketAllocator());
sharedProcessorParams =
std::unique_ptr<MessageProcessor::SharedParams>(new MessageProcessor::SharedParams());
std::unique_ptr<MessageProcessor::SharedParams>(new MessageProcessor::SharedParams(tox_max_message_length(), testMaxExtendedMessageSize));
messageProcessor = std::unique_ptr<MessageProcessor>(new MessageProcessor(*sharedProcessorParams));
friendMessageDispatcher = std::unique_ptr<FriendMessageDispatcher>(
new FriendMessageDispatcher(*f, *messageProcessor, *messageSender));
new FriendMessageDispatcher(*f, *messageProcessor, *messageSender, *coreExtPacketAllocator));
connect(friendMessageDispatcher.get(), &FriendMessageDispatcher::messageSent, this,
&TestFriendMessageDispatcher::onMessageSent);
@ -122,9 +179,12 @@ void TestFriendMessageDispatcher::init()
&TestFriendMessageDispatcher::onMessageComplete);
connect(friendMessageDispatcher.get(), &FriendMessageDispatcher::messageReceived, this,
&TestFriendMessageDispatcher::onMessageReceived);
connect(friendMessageDispatcher.get(), &FriendMessageDispatcher::messageBroken, this,
&TestFriendMessageDispatcher::onMessageBroken);
outgoingMessages = std::map<DispatchedMessageId, Message>();
receivedMessages = std::deque<Message>();
brokenMessages = std::set<DispatchedMessageId>();
}
/**
@ -194,6 +254,7 @@ void TestFriendMessageDispatcher::testOfflineMessages()
QVERIFY(outgoingMessages.size() == 3);
f->setStatus(Status::Status::Online);
f->onNegotiationComplete();
QVERIFY(messageSender->numSentActions == 1);
QVERIFY(messageSender->numSentMessages == 2);
@ -223,9 +284,112 @@ void TestFriendMessageDispatcher::testFailedMessage()
messageSender->canSend = true;
f->setStatus(Status::Status::Offline);
f->setStatus(Status::Status::Online);
f->onNegotiationComplete();
QVERIFY(messageSender->numSentMessages == 1);
}
void TestFriendMessageDispatcher::testNegotiationFailure()
{
f->setStatus(Status::Status::Offline);
f->setStatus(Status::Status::Online);
QVERIFY(f->getStatus() == Status::Status::Negotiating);
friendMessageDispatcher->sendMessage(false, "test");
QVERIFY(messageSender->numSentMessages == 0);
f->onNegotiationComplete();
QVERIFY(messageSender->numSentMessages == 1);
}
void TestFriendMessageDispatcher::testNegotiationSuccess()
{
f->setStatus(Status::Status::Offline);
f->setStatus(Status::Status::Online);
f->setExtendedMessageSupport(true);
f->onNegotiationComplete();
friendMessageDispatcher->sendMessage(false, "test");
QVERIFY(coreExtPacketAllocator->numSentMessages == 1);
friendMessageDispatcher->sendMessage(false, "test");
QVERIFY(coreExtPacketAllocator->numSentMessages == 2);
QVERIFY(messageSender->numSentMessages == 0);
}
void TestFriendMessageDispatcher::testOfflineExtensionMessages()
{
f->setStatus(Status::Status::Offline);
auto requiredExtensions = ExtensionSet();
requiredExtensions[ExtensionType::messages] = true;
friendMessageDispatcher->sendExtendedMessage("Test", requiredExtensions);
f->setStatus(Status::Status::Online);
f->setExtendedMessageSupport(true);
f->onNegotiationComplete();
// Ensure that when our friend came online with the desired extensions we
// were able to send them our message over the extended message path
QVERIFY(coreExtPacketAllocator->numSentMessages == 1);
f->setStatus(Status::Status::Offline);
friendMessageDispatcher->sendExtendedMessage("Test", requiredExtensions);
f->setStatus(Status::Status::Online);
f->setExtendedMessageSupport(false);
f->onNegotiationComplete();
// Here we want to make sure that when they do _not_ support extensions
// we discard the message instead of attempting to send it over either
// channel
QVERIFY(coreExtPacketAllocator->numSentMessages == 1);
QVERIFY(messageSender->numSentMessages == 0);
}
void TestFriendMessageDispatcher::testSentMessageExtensionSetReduced()
{
f->setStatus(Status::Status::Online);
f->setExtendedMessageSupport(true);
f->onNegotiationComplete();
friendMessageDispatcher->sendMessage(false, "Test");
f->setStatus(Status::Status::Offline);
f->setStatus(Status::Status::Online);
f->setExtendedMessageSupport(false);
f->onNegotiationComplete();
// Ensure that when we reduce our extension set we correctly emit the
// "messageBroken" signal
QVERIFY(brokenMessages.size() == 1);
}
void TestFriendMessageDispatcher::testActionMessagesSplitWithExtensions()
{
f->setStatus(Status::Status::Online);
f->setExtendedMessageSupport(true);
f->onNegotiationComplete();
auto reallyLongMessage = QString("a");
for (uint64_t i = 0; i < testMaxExtendedMessageSize + 50; ++i) {
reallyLongMessage += i;
}
friendMessageDispatcher->sendMessage(true, reallyLongMessage);
QVERIFY(coreExtPacketAllocator->numSentMessages == 0);
QVERIFY(messageSender->numSentMessages == 0);
QVERIFY(messageSender->numSentActions > 1);
}
QTEST_GUILESS_MAIN(TestFriendMessageDispatcher)
#include "friendmessagedispatcher_test.moc"

View File

@ -131,7 +131,7 @@ void TestGroupMessageDispatcher::init()
new Group(0, GroupId(), "TestGroup", false, "me", *groupQuery, *coreIdHandler));
messageSender = std::unique_ptr<MockGroupMessageSender>(new MockGroupMessageSender());
sharedProcessorParams =
std::unique_ptr<MessageProcessor::SharedParams>(new MessageProcessor::SharedParams());
std::unique_ptr<MessageProcessor::SharedParams>(new MessageProcessor::SharedParams(tox_max_message_length(), 10 * 1024 * 1024));
messageProcessor = std::unique_ptr<MessageProcessor>(new MessageProcessor(*sharedProcessorParams));
groupMessageDispatcher = std::unique_ptr<GroupMessageDispatcher>(
new GroupMessageDispatcher(*g, *messageProcessor, *coreIdHandler, *messageSender,

View File

@ -53,7 +53,7 @@ private slots:
*/
void TestMessageProcessor::testSelfMention()
{
MessageProcessor::SharedParams sharedParams;
MessageProcessor::SharedParams sharedParams(tox_max_message_length(), 10 * 1024 * 1024);;
const QLatin1String testUserName{"MyUserName"};
const QLatin1String testToxPk{
"0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF"};
@ -66,55 +66,55 @@ void TestMessageProcessor::testSelfMention()
for (const auto& str : {testUserName, testToxPk}) {
// Using my name or public key should match
auto processedMessage = messageProcessor.processIncomingMessage(false, str % " hi");
auto processedMessage = messageProcessor.processIncomingCoreMessage(false, str % " hi");
QVERIFY(messageHasSelfMention(processedMessage));
// Action messages should match too
processedMessage = messageProcessor.processIncomingMessage(true, str % " hi");
processedMessage = messageProcessor.processIncomingCoreMessage(true, str % " hi");
QVERIFY(messageHasSelfMention(processedMessage));
// Too much text shouldn't match
processedMessage = messageProcessor.processIncomingMessage(false, str % "2");
processedMessage = messageProcessor.processIncomingCoreMessage(false, str % "2");
QVERIFY(!messageHasSelfMention(processedMessage));
// Unless it's a colon
processedMessage = messageProcessor.processIncomingMessage(false, str % ": test");
processedMessage = messageProcessor.processIncomingCoreMessage(false, str % ": test");
QVERIFY(messageHasSelfMention(processedMessage));
// remove last character
QString chopped = str;
chopped.chop(1);
// Too little text shouldn't match
processedMessage = messageProcessor.processIncomingMessage(false, chopped);
processedMessage = messageProcessor.processIncomingCoreMessage(false, chopped);
QVERIFY(!messageHasSelfMention(processedMessage));
// make lower case
QString lower = QString(str).toLower();
// The regex should be case insensitive
processedMessage = messageProcessor.processIncomingMessage(false, lower % " hi");
processedMessage = messageProcessor.processIncomingCoreMessage(false, lower % " hi");
QVERIFY(messageHasSelfMention(processedMessage));
}
// New user name changes should be detected
sharedParams.onUserNameSet("NewUserName");
auto processedMessage = messageProcessor.processIncomingMessage(false, "NewUserName: hi");
auto processedMessage = messageProcessor.processIncomingCoreMessage(false, "NewUserName: hi");
QVERIFY(messageHasSelfMention(processedMessage));
// Special characters should be removed
sharedParams.onUserNameSet("New\nUserName");
processedMessage = messageProcessor.processIncomingMessage(false, "NewUserName: hi");
processedMessage = messageProcessor.processIncomingCoreMessage(false, "NewUserName: hi");
QVERIFY(messageHasSelfMention(processedMessage));
// Regression tests for: https://github.com/qTox/qTox/issues/2119
{
// Empty usernames should not match
sharedParams.onUserNameSet("");
processedMessage = messageProcessor.processIncomingMessage(false, "");
processedMessage = messageProcessor.processIncomingCoreMessage(false, "");
QVERIFY(!messageHasSelfMention(processedMessage));
// Empty usernames matched on everything, ensure this is not the case
processedMessage = messageProcessor.processIncomingMessage(false, "a");
processedMessage = messageProcessor.processIncomingCoreMessage(false, "a");
QVERIFY(!messageHasSelfMention(processedMessage));
}
}
@ -124,7 +124,7 @@ void TestMessageProcessor::testSelfMention()
*/
void TestMessageProcessor::testOutgoingMessage()
{
auto sharedParams = MessageProcessor::SharedParams();
auto sharedParams = MessageProcessor::SharedParams(tox_max_message_length(), 10 * 1024 * 1024);
auto messageProcessor = MessageProcessor(sharedParams);
QString testStr;
@ -133,10 +133,17 @@ void TestMessageProcessor::testOutgoingMessage()
testStr += "a";
}
auto messages = messageProcessor.processOutgoingMessage(false, testStr);
auto messages = messageProcessor.processOutgoingMessage(false, testStr, ExtensionSet());
// The message processor should split our messages
QVERIFY(messages.size() == 2);
auto extensionSet = ExtensionSet();
extensionSet[ExtensionType::messages] = true;
messages = messageProcessor.processOutgoingMessage(false, testStr, extensionSet);
// If we have multipart messages we shouldn't split our messages
QVERIFY(messages.size() == 1);
}
/**
@ -145,9 +152,9 @@ void TestMessageProcessor::testOutgoingMessage()
void TestMessageProcessor::testIncomingMessage()
{
// Nothing too special happening on the incoming side if we aren't looking for self mentions
auto sharedParams = MessageProcessor::SharedParams();
auto sharedParams = MessageProcessor::SharedParams(tox_max_message_length(), 10 * 1024 * 1024);
auto messageProcessor = MessageProcessor(sharedParams);
auto message = messageProcessor.processIncomingMessage(false, "test");
auto message = messageProcessor.processIncomingCoreMessage(false, "test");
QVERIFY(message.isAction == false);
QVERIFY(message.content == "test");

View File

@ -51,6 +51,7 @@ private slots:
void test2to3();
void test3to4();
void test4to5();
void test5to6();
void cleanupTestCase();
private:
bool initSucess{false};
@ -66,7 +67,8 @@ const QString testFileList[] = {
"test1to2.db",
"test2to3.db",
"test3to4.db",
"test4to5.db"
"test4to5.db",
"test5to6.db"
};
// db schemas can be select with "SELECT name, sql FROM sqlite_master;" on the database.
@ -122,6 +124,17 @@ const std::vector<SqliteMasterEntry> schema5 {
{"chat_id_idx", "CREATE INDEX chat_id_idx on history (chat_id)"}
};
// added toxext extensions
const std::vector<SqliteMasterEntry> schema6 {
{"aliases", "CREATE TABLE aliases (id INTEGER PRIMARY KEY, owner INTEGER, display_name BLOB NOT NULL, UNIQUE(owner, display_name), FOREIGN KEY (owner) REFERENCES peers(id))"},
{"faux_offline_pending", "CREATE TABLE faux_offline_pending (id INTEGER PRIMARY KEY, required_extensions INTEGER NOT NULL DEFAULT 0, FOREIGN KEY (id) REFERENCES history(id))"},
{"file_transfers", "CREATE TABLE file_transfers (id INTEGER PRIMARY KEY, chat_id INTEGER NOT NULL, file_restart_id BLOB NOT NULL, file_name BLOB NOT NULL, file_path BLOB NOT NULL, file_hash BLOB NOT NULL, file_size INTEGER NOT NULL, direction INTEGER NOT NULL, file_state INTEGER NOT NULL)"},
{"history", "CREATE TABLE history (id INTEGER PRIMARY KEY, timestamp INTEGER NOT NULL, chat_id INTEGER NOT NULL, sender_alias INTEGER NOT NULL, message BLOB NOT NULL, file_id INTEGER, FOREIGN KEY (file_id) REFERENCES file_transfers(id), FOREIGN KEY (chat_id) REFERENCES peers(id), FOREIGN KEY (sender_alias) REFERENCES aliases(id))"},
{"peers", "CREATE TABLE peers (id INTEGER PRIMARY KEY, public_key TEXT NOT NULL UNIQUE)"},
{"broken_messages", "CREATE TABLE broken_messages (id INTEGER PRIMARY KEY, reason INTEGER NOT NULL DEFAULT 0, FOREIGN KEY (id) REFERENCES history(id))"},
{"chat_id_idx", "CREATE INDEX chat_id_idx on history (chat_id)"}
};
void TestDbSchema::initTestCase()
{
for (const auto& path : testFileList) {
@ -176,7 +189,7 @@ void TestDbSchema::testCreation()
QVector<RawDatabase::Query> queries;
auto db = std::shared_ptr<RawDatabase>{new RawDatabase{"testCreation.db", {}, {}}};
QVERIFY(createCurrentSchema(*db));
verifyDb(db, schema5);
verifyDb(db, schema6);
}
void TestDbSchema::testIsNewDb()
@ -374,5 +387,13 @@ void TestDbSchema::test4to5()
verifyDb(db, schema5);
}
void TestDbSchema::test5to6()
{
auto db = std::shared_ptr<RawDatabase>{new RawDatabase{"test5to6.db", {}, {}}};
createSchemaAtVersion(db, schema5);
QVERIFY(dbSchema5to6(*db));
verifyDb(db, schema6);
}
QTEST_GUILESS_MAIN(TestDbSchema)
#include "dbschema_test.moc"

View File

@ -24,171 +24,164 @@
#include <QtTest/QtTest>
struct MockFriendMessageSender : public QObject, public ICoreFriendMessageSender
{
Q_OBJECT
public:
MockFriendMessageSender(Friend* f)
: f(f){}
bool sendAction(uint32_t friendId, const QString& action, ReceiptNum& receipt) override
{
return false;
}
bool sendMessage(uint32_t friendId, const QString& message, ReceiptNum& receipt) override
{
if (Status::isOnline(f->getStatus())) {
receipt.get() = receiptNum++;
if (!dropReceipts) {
msgs.push_back(message);
emit receiptReceived(receipt);
}
numMessagesSent++;
} else {
numMessagesFailed++;
}
return Status::isOnline(f->getStatus());
}
signals:
void receiptReceived(ReceiptNum receipt);
public:
Friend* f;
bool dropReceipts = false;
size_t numMessagesSent = 0;
size_t numMessagesFailed = 0;
int receiptNum = 0;
std::vector<QString> msgs;
};
class TestOfflineMsgEngine : public QObject
{
Q_OBJECT
private slots:
void testReceiptResolution();
void testOfflineFriend();
void testSentUnsentCoordination();
void testReceiptBeforeMessage();
void testReceiptAfterMessage();
void testResendWorkflow();
void testTypeCoordination();
void testCallback();
void testExtendedMessageCoordination();
};
class OfflineMsgEngineFixture
{
public:
OfflineMsgEngineFixture()
: f(0, ToxPk(QByteArray(32, 0)))
, friendMessageSender(&f)
, offlineMsgEngine(&f, &friendMessageSender)
{
f.setStatus(Status::Status::Online);
QObject::connect(&friendMessageSender, &MockFriendMessageSender::receiptReceived,
&offlineMsgEngine, &OfflineMsgEngine::onReceiptReceived);
}
void completionFn(bool) {}
Friend f;
MockFriendMessageSender friendMessageSender;
void TestOfflineMsgEngine::testReceiptBeforeMessage()
{
OfflineMsgEngine offlineMsgEngine;
};
void completionFn() {}
void TestOfflineMsgEngine::testReceiptResolution()
{
OfflineMsgEngineFixture fixture;
Message msg{false, QString(), QDateTime()};
ReceiptNum receipt;
fixture.friendMessageSender.sendMessage(0, msg.content, receipt);
fixture.offlineMsgEngine.addSentMessage(receipt, msg, completionFn);
auto const receipt = ReceiptNum(0);
offlineMsgEngine.onReceiptReceived(receipt);
offlineMsgEngine.addSentCoreMessage(receipt, Message(), completionFn);
// We should have no offline messages to deliver if we resolved our receipt
// correctly
fixture.offlineMsgEngine.deliverOfflineMsgs();
fixture.offlineMsgEngine.deliverOfflineMsgs();
fixture.offlineMsgEngine.deliverOfflineMsgs();
auto const removedMessages = offlineMsgEngine.removeAllMessages();
QVERIFY(fixture.friendMessageSender.numMessagesSent == 1);
// If we drop receipts we should keep trying to send messages every time we
// "deliverOfflineMsgs"
fixture.friendMessageSender.dropReceipts = true;
fixture.friendMessageSender.sendMessage(0, msg.content, receipt);
fixture.offlineMsgEngine.addSentMessage(receipt, msg, completionFn);
fixture.offlineMsgEngine.deliverOfflineMsgs();
fixture.offlineMsgEngine.deliverOfflineMsgs();
fixture.offlineMsgEngine.deliverOfflineMsgs();
QVERIFY(fixture.friendMessageSender.numMessagesSent == 5);
// And once we stop dropping and try one more time we should run out of
// messages to send again
fixture.friendMessageSender.dropReceipts = false;
fixture.offlineMsgEngine.deliverOfflineMsgs();
fixture.offlineMsgEngine.deliverOfflineMsgs();
fixture.offlineMsgEngine.deliverOfflineMsgs();
QVERIFY(fixture.friendMessageSender.numMessagesSent == 6);
QVERIFY(removedMessages.empty());
}
void TestOfflineMsgEngine::testOfflineFriend()
void TestOfflineMsgEngine::testReceiptAfterMessage()
{
OfflineMsgEngineFixture fixture;
OfflineMsgEngine offlineMsgEngine;
Message msg{false, QString(), QDateTime()};
auto const receipt = ReceiptNum(0);
offlineMsgEngine.addSentCoreMessage(receipt, Message(), completionFn);
offlineMsgEngine.onReceiptReceived(receipt);
fixture.f.setStatus(Status::Status::Offline);
auto const removedMessages = offlineMsgEngine.removeAllMessages();
fixture.offlineMsgEngine.addUnsentMessage(msg, completionFn);
fixture.offlineMsgEngine.addUnsentMessage(msg, completionFn);
fixture.offlineMsgEngine.addUnsentMessage(msg, completionFn);
fixture.offlineMsgEngine.addUnsentMessage(msg, completionFn);
fixture.offlineMsgEngine.addUnsentMessage(msg, completionFn);
fixture.f.setStatus(Status::Status::Online);
fixture.offlineMsgEngine.deliverOfflineMsgs();
QVERIFY(fixture.friendMessageSender.numMessagesFailed == 0);
QVERIFY(fixture.friendMessageSender.numMessagesSent == 5);
QVERIFY(removedMessages.empty());
}
void TestOfflineMsgEngine::testSentUnsentCoordination()
void TestOfflineMsgEngine::testResendWorkflow()
{
OfflineMsgEngineFixture fixture;
Message msg{false, QString("a"), QDateTime()};
ReceiptNum receipt;
OfflineMsgEngine offlineMsgEngine;
fixture.offlineMsgEngine.addUnsentMessage(msg, completionFn);
msg.content = "b";
fixture.friendMessageSender.dropReceipts = true;
fixture.friendMessageSender.sendMessage(0, msg.content, receipt);
fixture.friendMessageSender.dropReceipts = false;
fixture.offlineMsgEngine.addSentMessage(receipt, msg, completionFn);
msg.content = "c";
fixture.offlineMsgEngine.addUnsentMessage(msg, completionFn);
auto const receipt = ReceiptNum(0);
offlineMsgEngine.addSentCoreMessage(receipt, Message(), completionFn);
auto messagesToResend = offlineMsgEngine.removeAllMessages();
fixture.offlineMsgEngine.deliverOfflineMsgs();
QVERIFY(messagesToResend.size() == 1);
auto expectedResponseOrder = std::vector<QString>{"a", "b", "c"};
QVERIFY(fixture.friendMessageSender.msgs == expectedResponseOrder);
offlineMsgEngine.addSentCoreMessage(receipt, Message(), completionFn);
offlineMsgEngine.onReceiptReceived(receipt);
messagesToResend = offlineMsgEngine.removeAllMessages();
QVERIFY(messagesToResend.size() == 0);
auto const nullMsg = Message();
auto msg2 = Message();
auto msg3 = Message();
msg2.content = "msg2";
msg3.content = "msg3";
offlineMsgEngine.addSentCoreMessage(ReceiptNum(0), nullMsg, completionFn);
offlineMsgEngine.addSentCoreMessage(ReceiptNum(1), nullMsg, completionFn);
offlineMsgEngine.addSentCoreMessage(ReceiptNum(2), msg2, completionFn);
offlineMsgEngine.addSentCoreMessage(ReceiptNum(3), msg3, completionFn);
offlineMsgEngine.onReceiptReceived(ReceiptNum(0));
offlineMsgEngine.onReceiptReceived(ReceiptNum(1));
offlineMsgEngine.onReceiptReceived(ReceiptNum(3));
messagesToResend = offlineMsgEngine.removeAllMessages();
QVERIFY(messagesToResend.size() == 1);
QVERIFY(messagesToResend[0].message.content == "msg2");
}
void TestOfflineMsgEngine::testTypeCoordination()
{
OfflineMsgEngine offlineMsgEngine;
auto msg1 = Message();
auto msg2 = Message();
auto msg3 = Message();
auto msg4 = Message();
auto msg5 = Message();
msg1.content = "msg1";
msg2.content = "msg2";
msg3.content = "msg3";
msg4.content = "msg4";
msg5.content = "msg5";
offlineMsgEngine.addSentCoreMessage(ReceiptNum(1), msg1, completionFn);
offlineMsgEngine.addUnsentMessage(msg2, completionFn);
offlineMsgEngine.addSentExtendedMessage(ExtendedReceiptNum(1), msg3, completionFn);
offlineMsgEngine.addSentCoreMessage(ReceiptNum(2), msg4, completionFn);
offlineMsgEngine.addSentCoreMessage(ReceiptNum(3), msg5, completionFn);
const auto messagesToResend = offlineMsgEngine.removeAllMessages();
QVERIFY(messagesToResend[0].message.content == "msg1");
QVERIFY(messagesToResend[1].message.content == "msg2");
QVERIFY(messagesToResend[2].message.content == "msg3");
QVERIFY(messagesToResend[3].message.content == "msg4");
QVERIFY(messagesToResend[4].message.content == "msg5");
}
void TestOfflineMsgEngine::testCallback()
{
OfflineMsgEngineFixture fixture;
OfflineMsgEngine offlineMsgEngine;
size_t numCallbacks = 0;
auto callback = [&numCallbacks] { numCallbacks++; };
auto callback = [&numCallbacks] (bool) { numCallbacks++; };
Message msg{false, QString(), QDateTime()};
ReceiptNum receipt;
fixture.friendMessageSender.sendMessage(0, msg.content, receipt);
fixture.offlineMsgEngine.addSentMessage(receipt, msg, callback);
fixture.offlineMsgEngine.addUnsentMessage(msg, callback);
offlineMsgEngine.addSentCoreMessage(ReceiptNum(1), Message(), callback);
offlineMsgEngine.addSentCoreMessage(ReceiptNum(2), Message(), callback);
offlineMsgEngine.onReceiptReceived(ReceiptNum(1));
offlineMsgEngine.onReceiptReceived(ReceiptNum(2));
fixture.offlineMsgEngine.deliverOfflineMsgs();
QVERIFY(numCallbacks == 2);
}
void TestOfflineMsgEngine::testExtendedMessageCoordination()
{
OfflineMsgEngine offlineMsgEngine;
size_t numCallbacks = 0;
auto callback = [&numCallbacks] (bool) { numCallbacks++; };
auto msg1 = Message();
auto msg2 = Message();
auto msg3 = Message();
offlineMsgEngine.addSentCoreMessage(ReceiptNum(1), msg1, callback);
offlineMsgEngine.addSentExtendedMessage(ExtendedReceiptNum(1), msg1, callback);
offlineMsgEngine.addSentCoreMessage(ReceiptNum(2), msg1, callback);
offlineMsgEngine.onExtendedReceiptReceived(ExtendedReceiptNum(2));
QVERIFY(numCallbacks == 0);
offlineMsgEngine.onReceiptReceived(ReceiptNum(1));
QVERIFY(numCallbacks == 1);
offlineMsgEngine.onReceiptReceived(ReceiptNum(1));
QVERIFY(numCallbacks == 1);
offlineMsgEngine.onExtendedReceiptReceived(ExtendedReceiptNum(1));
QVERIFY(numCallbacks == 2);
offlineMsgEngine.onReceiptReceived(ReceiptNum(2));
QVERIFY(numCallbacks == 3);
}
QTEST_GUILESS_MAIN(TestOfflineMsgEngine)
#include "offlinemsgengine_test.moc"

View File

@ -1030,6 +1030,109 @@ else
echo "Using cached build of Toxcore `cat $TOXCORE_PREFIX_DIR/done`"
fi
# toxext
TOXEXT_PREFIX_DIR="$DEP_DIR/toxext"
TOXEXT_VERSION=0.0.2
TOXEXT_HASH="047093eeed396ea9b4a3f0cd0a6bc4e0e09b339e2b03ba4b676e30888fe6acde"
TOXEXT_FILENAME="toxext-$TOXEXT_VERSION.tar.gz"
if [ ! -f "$TOXEXT_PREFIX_DIR/done" ]
then
rm -rf "$TOXEXT_PREFIX_DIR"
mkdir -p "$TOXEXT_PREFIX_DIR"
curl $CURL_OPTIONS https://github.com/toxext/toxext/archive/v$TOXEXT_VERSION.tar.gz -o $TOXEXT_FILENAME
check_sha256 "$TOXEXT_HASH" "$TOXEXT_FILENAME"
bsdtar --no-same-owner --no-same-permissions -xf "$TOXEXT_FILENAME"
rm "$TOXEXT_FILENAME"
cd toxext*
mkdir -p build
cd build
export PKG_CONFIG_PATH="$OPUS_PREFIX_DIR/lib/pkgconfig:$SODIUM_PREFIX_DIR/lib/pkgconfig:$VPX_PREFIX_DIR/lib/pkgconfig:$TOXCORE_PREFIX_DIR/lib/pkgconfig"
export PKG_CONFIG_LIBDIR="/usr/$ARCH-w64-mingw32"
echo "
SET(CMAKE_SYSTEM_NAME Windows)
SET(CMAKE_C_COMPILER $ARCH-w64-mingw32-gcc)
SET(CMAKE_CXX_COMPILER $ARCH-w64-mingw32-g++)
SET(CMAKE_RC_COMPILER $ARCH-w64-mingw32-windres)
SET(CMAKE_FIND_ROOT_PATH /usr/$ARCH-w64-mingw32 $OPUS_PREFIX_DIR $SODIUM_PREFIX_DIR $VPX_PREFIX_DIR $TOXCORE_PREFIX_DIR)
" > toolchain.cmake
cmake -DCMAKE_INSTALL_PREFIX=$TOXEXT_PREFIX_DIR \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_TOOLCHAIN_FILE=toolchain.cmake \
..
make
make install
echo -n $TOXEXT_VERSION > $TOXEXT_PREFIX_DIR/done
unset PKG_CONFIG_PATH
unset PKG_CONFIG_LIBDIR
cd ..
cd ..
rm -rf ./toxext*
else
echo "Using cached build of ToxExt `cat $TOXEXT_PREFIX_DIR/done`"
fi
# tox_extension_messages
TOX_EXTENSION_MESSAGES_PREFIX_DIR="$DEP_DIR/tox_extension_messages"
TOX_EXTENSION_MESSAGES_VERSION=0.0.2
TOX_EXTENSION_MESSAGES_HASH="95e8cdd1de6cc7ba561620716f340e9606a06b3c2ff9c9020af4784c22fd0d7f"
TOX_EXTENSION_MESSAGES_FILENAME="tox_extension_messages-$TOX_EXTENSION_MESSAGES_VERSION.tar.gz"
if [ ! -f "$TOX_EXTENSION_MESSAGES_PREFIX_DIR/done" ]
then
rm -rf "$TOX_EXTENSION_MESSAGES_PREFIX_DIR"
mkdir -p "$TOX_EXTENSION_MESSAGES_PREFIX_DIR"
curl $CURL_OPTIONS https://github.com/toxext/tox_extension_messages/archive/v$TOX_EXTENSION_MESSAGES_VERSION.tar.gz -o $TOX_EXTENSION_MESSAGES_FILENAME
check_sha256 "$TOX_EXTENSION_MESSAGES_HASH" "$TOX_EXTENSION_MESSAGES_FILENAME"
bsdtar --no-same-owner --no-same-permissions -xf "$TOX_EXTENSION_MESSAGES_FILENAME"
rm "$TOX_EXTENSION_MESSAGES_FILENAME"
cd tox_extension_messages*
mkdir -p build
cd build
export PKG_CONFIG_PATH="$OPUS_PREFIX_DIR/lib/pkgconfig:$SODIUM_PREFIX_DIR/lib/pkgconfig:$VPX_PREFIX_DIR/lib/pkgconfig:$TOXCORE_PREFIX_DIR/lib/pkgconfig"
export PKG_CONFIG_LIBDIR="/usr/$ARCH-w64-mingw32"
echo "
SET(CMAKE_SYSTEM_NAME Windows)
SET(CMAKE_C_COMPILER $ARCH-w64-mingw32-gcc)
SET(CMAKE_CXX_COMPILER $ARCH-w64-mingw32-g++)
SET(CMAKE_RC_COMPILER $ARCH-w64-mingw32-windres)
SET(CMAKE_FIND_ROOT_PATH /usr/$ARCH-w64-mingw32 $OPUS_PREFIX_DIR $SODIUM_PREFIX_DIR $VPX_PREFIX_DIR $TOXCORE_PREFIX_DIR $TOXEXT_PREFIX_DIR)
" > toolchain.cmake
cmake -DCMAKE_INSTALL_PREFIX=$TOX_EXTENSION_MESSAGES_PREFIX_DIR \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_TOOLCHAIN_FILE=toolchain.cmake \
..
make
make install
echo -n $TOX_EXTENSION_MESSAGES_VERSION > $TOX_EXTENSION_MESSAGES_PREFIX_DIR/done
unset PKG_CONFIG_PATH
unset PKG_CONFIG_LIBDIR
cd ..
cd ..
rm -rf ./tox_extension_messages*
else
echo "Using cached build of tox_extension_messages `cat $TOX_EXTENSION_MESSAGES_PREFIX_DIR/done`"
fi
set +u
if [[ -n "$TRAVIS_CI_STAGE" ]] || [[ "$BUILD_TYPE" == "debug" ]]