diff --git a/.buildbot/android/Dockerfile b/.buildbot/android/Dockerfile new file mode 100755 index 00000000..2d722834 --- /dev/null +++ b/.buildbot/android/Dockerfile @@ -0,0 +1,102 @@ +# A container for buildbot + +FROM ubuntu:focal AS android + +ENV DEBIAN_FRONTEND=noninteractive +ENV ANDROID_HOME="/opt/android" + +RUN apt-get update -qq > /dev/null \ + && apt-get -y install -qq --no-install-recommends locales \ + && locale-gen en_US.UTF-8 +ENV LANG="en_US.UTF-8" \ + LANGUAGE="en_US.UTF-8" \ + LC_ALL="en_US.UTF-8" + +# install system/build dependencies +RUN apt-get -y update -qq \ + && apt-get -y install -qq --no-install-recommends \ + curl autoconf automake build-essential cmake git nano libtool \ + libltdl-dev libffi-dev libssl-dev \ + patch pkg-config python-is-python3 python3-dev python3-pip unzip zip + +RUN apt-get -y install -qq --no-install-recommends openjdk-17-jdk \ + && apt-get -y autoremove + +RUN pip install pip install buildozer cython virtualenv + + +ENV ANDROID_NDK_HOME="${ANDROID_HOME}/android-ndk" +ENV ANDROID_NDK_VERSION="25b" +ENV ANDROID_NDK_HOME_V="${ANDROID_NDK_HOME}-r${ANDROID_NDK_VERSION}" + +# get the latest version from https://developer.android.com/ndk/downloads/index.html +ENV ANDROID_NDK_ARCHIVE="android-ndk-r${ANDROID_NDK_VERSION}-linux.zip" +ENV ANDROID_NDK_DL_URL="https://dl.google.com/android/repository/${ANDROID_NDK_ARCHIVE}" +# download and install Android NDK +RUN curl "${ANDROID_NDK_DL_URL}" --output "${ANDROID_NDK_ARCHIVE}" \ + && mkdir -p "${ANDROID_NDK_HOME_V}" \ + && unzip -q "${ANDROID_NDK_ARCHIVE}" -d "${ANDROID_HOME}" \ + && ln -sfn "${ANDROID_NDK_HOME_V}" "${ANDROID_NDK_HOME}" \ + && rm -rf "${ANDROID_NDK_ARCHIVE}" + +ENV ANDROID_SDK_HOME="${ANDROID_HOME}/android-sdk" + +# get the latest version from https://developer.android.com/studio/index.html +ENV ANDROID_SDK_TOOLS_VERSION="11076708" +ENV ANDROID_SDK_BUILD_TOOLS_VERSION="34.0.0" +ENV ANDROID_SDK_CMDLINE_TOOLS_VERSION="12.0" +ENV ANDROID_SDK_TOOLS_ARCHIVE="commandlinetools-linux-${ANDROID_SDK_TOOLS_VERSION}_latest.zip" +ENV ANDROID_SDK_TOOLS_DL_URL="https://dl.google.com/android/repository/${ANDROID_SDK_TOOLS_ARCHIVE}" +ENV ANDROID_CMDLINE_TOOLS_DIR="${ANDROID_SDK_HOME}/cmdline-tools/${ANDROID_SDK_CMDLINE_TOOLS_VERSION}" +ENV ANDROID_SDK_MANAGER="${ANDROID_CMDLINE_TOOLS_DIR}/bin/sdkmanager --sdk_root=${ANDROID_SDK_HOME}" + +# download and install Android SDK +RUN curl "${ANDROID_SDK_TOOLS_DL_URL}" --output "${ANDROID_SDK_TOOLS_ARCHIVE}" \ + && mkdir -p "${ANDROID_SDK_HOME}/cmdline-tools" \ + && unzip -q "${ANDROID_SDK_TOOLS_ARCHIVE}" \ + -d "${ANDROID_SDK_HOME}/cmdline-tools" \ + && mv "${ANDROID_SDK_HOME}/cmdline-tools/cmdline-tools" \ + ${ANDROID_CMDLINE_TOOLS_DIR} \ + && ln -sfn ${ANDROID_CMDLINE_TOOLS_DIR} "${ANDROID_SDK_HOME}/tools" \ + && rm -rf "${ANDROID_SDK_TOOLS_ARCHIVE}" + +# update Android SDK, install Android API, Build Tools... +RUN mkdir -p "${ANDROID_SDK_HOME}/.android/" \ + && echo '### User Sources for Android SDK Manager' \ + > "${ANDROID_SDK_HOME}/.android/repositories.cfg" + +# accept Android licenses (JDK necessary!) +RUN yes | ${ANDROID_SDK_MANAGER} --licenses > /dev/null + +# download platforms, API, build tools +RUN ${ANDROID_SDK_MANAGER} "platforms;android-30" > /dev/null \ + && ${ANDROID_SDK_MANAGER} "platforms;android-28" > /dev/null \ + && ${ANDROID_SDK_MANAGER} "platform-tools" > /dev/null \ + && ${ANDROID_SDK_MANAGER} "build-tools;${ANDROID_SDK_BUILD_TOOLS_VERSION}" \ + > /dev/null \ + && ${ANDROID_SDK_MANAGER} "extras;android;m2repository" > /dev/null \ + && chmod +x "${ANDROID_CMDLINE_TOOLS_DIR}/bin/avdmanager" + +# download ANT +ENV APACHE_ANT_VERSION="1.9.4" +ENV APACHE_ANT_ARCHIVE="apache-ant-${APACHE_ANT_VERSION}-bin.tar.gz" +ENV APACHE_ANT_DL_URL="https://archive.apache.org/dist/ant/binaries/${APACHE_ANT_ARCHIVE}" +ENV APACHE_ANT_HOME="${ANDROID_HOME}/apache-ant" +ENV APACHE_ANT_HOME_V="${APACHE_ANT_HOME}-${APACHE_ANT_VERSION}" + +RUN curl "${APACHE_ANT_DL_URL}" --output "${APACHE_ANT_ARCHIVE}" \ + && tar -xf "${APACHE_ANT_ARCHIVE}" -C "${ANDROID_HOME}" \ + && ln -sfn "${APACHE_ANT_HOME_V}" "${APACHE_ANT_HOME}" \ + && rm -rf "${APACHE_ANT_ARCHIVE}" + + +RUN useradd -m -U builder && mkdir /android + +WORKDIR /android + +RUN chown -R builder.builder /android "${ANDROID_SDK_HOME}" \ + && chmod -R go+w "${ANDROID_SDK_HOME}" + +USER builder + +ADD . . diff --git a/.buildbot/android/build.sh b/.buildbot/android/build.sh new file mode 100755 index 00000000..10176ef3 --- /dev/null +++ b/.buildbot/android/build.sh @@ -0,0 +1,9 @@ +#!/bin/bash +export LC_ALL=en_US.UTF-8 +export LANG=en_US.UTF-8 +pushd packages/android +buildozer android debug || exit $? +popd + +mkdir -p ../out +cp packages/android/bin/*.apk ../out diff --git a/.buildbot/android/test.sh b/.buildbot/android/test.sh new file mode 100755 index 00000000..b61fac85 --- /dev/null +++ b/.buildbot/android/test.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +unzip -p packages/android/bin/*.apk assets/private.tar \ + | tar --list -z > package.list +cat package.list +cat package.list | grep '\.sql$' || exit 1 diff --git a/.buildbot/appimage/Dockerfile b/.buildbot/appimage/Dockerfile new file mode 100644 index 00000000..6c2b87d5 --- /dev/null +++ b/.buildbot/appimage/Dockerfile @@ -0,0 +1,26 @@ +FROM ubuntu:bionic + +RUN apt-get update + +RUN apt-get install -yq --no-install-suggests --no-install-recommends \ + ca-certificates software-properties-common \ + build-essential libcap-dev libssl-dev \ + python-all-dev python-setuptools wget \ + git gtk-update-icon-cache \ + binutils-multiarch crossbuild-essential-armhf crossbuild-essential-arm64 + +RUN dpkg --add-architecture armhf +RUN dpkg --add-architecture arm64 + +RUN sed -iE "s|deb |deb [arch=amd64] |g" /etc/apt/sources.list \ + && echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic main universe" >> /etc/apt/sources.list \ + && echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic-updates main universe" >> /etc/apt/sources.list + +RUN apt-get update | true + +RUN apt-get install -yq libssl-dev:armhf libssl-dev:arm64 + +RUN wget -qO appimage-builder-x86_64.AppImage \ + https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage + +ADD . . diff --git a/.buildbot/appimage/build.sh b/.buildbot/appimage/build.sh new file mode 100755 index 00000000..c8dc4f56 --- /dev/null +++ b/.buildbot/appimage/build.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +export APPIMAGE_EXTRACT_AND_RUN=1 +BUILDER=appimage-builder-x86_64.AppImage +RECIPE=packages/AppImage/AppImageBuilder.yml + +export APP_VERSION=$(git describe --tags | cut -d- -f1,3 | tr -d v) + +function set_sourceline { + if [ ${ARCH} == amd64 ]; then + export SOURCELINE="deb http://archive.ubuntu.com/ubuntu/ bionic main universe" + else + export SOURCELINE="deb [arch=${ARCH}] http://ports.ubuntu.com/ubuntu-ports/ bionic main universe" + fi +} + +function build_appimage { + set_sourceline + ./${BUILDER} --recipe ${RECIPE} || exit 1 + rm -rf build +} + +[ -f ${BUILDER} ] || wget -qO ${BUILDER} \ + https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage \ + && chmod +x ${BUILDER} + +chmod 1777 /tmp + +export ARCH=amd64 +export APPIMAGE_ARCH=x86_64 +export RUNTIME=${APPIMAGE_ARCH} + +build_appimage + +export ARCH=armhf +export APPIMAGE_ARCH=${ARCH} +export RUNTIME=gnueabihf +export CC=arm-linux-gnueabihf-gcc +export CXX=${CC} + +build_appimage + +export ARCH=arm64 +export APPIMAGE_ARCH=aarch64 +export RUNTIME=${APPIMAGE_ARCH} +export CC=aarch64-linux-gnu-gcc +export CXX=${CC} + +build_appimage + +EXISTING_OWNER=$(stat -c %u ../out) || mkdir -p ../out + +sha256sum PyBitmessage*.AppImage >> ../out/SHA256SUMS +cp PyBitmessage*.AppImage ../out + +if [ ${EXISTING_OWNER} ]; then + chown ${EXISTING_OWNER} ../out/PyBitmessage*.AppImage ../out/SHA256SUMS +fi diff --git a/.buildbot/appimage/test.sh b/.buildbot/appimage/test.sh new file mode 100755 index 00000000..871fc83a --- /dev/null +++ b/.buildbot/appimage/test.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +export APPIMAGE_EXTRACT_AND_RUN=1 + +chmod +x PyBitmessage-*-x86_64.AppImage +./PyBitmessage-*-x86_64.AppImage -t diff --git a/.buildbot/kivy/Dockerfile b/.buildbot/kivy/Dockerfile new file mode 100644 index 00000000..10b1e569 --- /dev/null +++ b/.buildbot/kivy/Dockerfile @@ -0,0 +1,18 @@ +# A container for buildbot +FROM ubuntu:focal AS kivy + +ENV DEBIAN_FRONTEND=noninteractive + +ENV SKIPCACHE=2022-08-29 + +RUN apt-get update + +RUN apt-get install -yq \ + build-essential libcap-dev libssl-dev \ + libmtdev-dev libpq-dev \ + python3-dev python3-pip python3-virtualenv \ + xvfb ffmpeg xclip xsel + +RUN ln -sf /usr/bin/python3 /usr/bin/python + +RUN pip3 install --upgrade setuptools pip diff --git a/.buildbot/kivy/build.sh b/.buildbot/kivy/build.sh new file mode 100755 index 00000000..87aae8f7 --- /dev/null +++ b/.buildbot/kivy/build.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +pip3 install -r kivy-requirements.txt + +export INSTALL_TESTS=True + +pip3 install . diff --git a/.buildbot/kivy/test.sh b/.buildbot/kivy/test.sh new file mode 100755 index 00000000..3231f250 --- /dev/null +++ b/.buildbot/kivy/test.sh @@ -0,0 +1,4 @@ +#!/bin/bash +export INSTALL_TESTS=True + +xvfb-run --server-args="-screen 0, 720x1280x24" python3 tests-kivy.py diff --git a/.buildbot/snap/Dockerfile b/.buildbot/snap/Dockerfile new file mode 100644 index 00000000..7fde093d --- /dev/null +++ b/.buildbot/snap/Dockerfile @@ -0,0 +1,7 @@ +FROM ubuntu:bionic + +ENV SKIPCACHE=2022-07-17 + +RUN apt-get update + +RUN apt-get install -yq --no-install-suggests --no-install-recommends snapcraft diff --git a/.buildbot/snap/build.sh b/.buildbot/snap/build.sh new file mode 100755 index 00000000..3a83ade7 --- /dev/null +++ b/.buildbot/snap/build.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +git remote add -f upstream https://github.com/Bitmessage/PyBitmessage.git +HEAD="$(git rev-parse HEAD)" +UPSTREAM="$(git merge-base --fork-point upstream/v0.6)" +SNAP_DIFF="$(git diff upstream/v0.6 -- packages/snap .buildbot/snap)" + +[ -z "${SNAP_DIFF}" ] && [ $HEAD != $UPSTREAM ] && exit 0 + +pushd packages && snapcraft || exit 1 + +popd +mkdir -p ../out +mv packages/pybitmessage*.snap ../out +cd ../out +sha256sum pybitmessage*.snap > SHA256SUMS diff --git a/.buildbot/tox-bionic/Dockerfile b/.buildbot/tox-bionic/Dockerfile new file mode 100644 index 00000000..5cc36b7f --- /dev/null +++ b/.buildbot/tox-bionic/Dockerfile @@ -0,0 +1,22 @@ +FROM ubuntu:bionic + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update + +RUN apt-get install -yq --no-install-suggests --no-install-recommends \ + software-properties-common build-essential libcap-dev libffi-dev \ + libssl-dev python-all-dev python-setuptools \ + python3-dev python3-pip python3.8 python3.8-dev python3.8-venv \ + python-msgpack python-qt4 language-pack-en qt5dxcb-plugin tor xvfb + +RUN apt-get install -yq sudo + +RUN echo 'builder ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers + +RUN python3.8 -m pip install setuptools wheel +RUN python3.8 -m pip install --upgrade pip tox virtualenv + +ENV LANG en_US.UTF-8 +ENV LANGUAGE en_US:en +ENV LC_ALL en_US.UTF-8 diff --git a/.buildbot/tox-bionic/build.sh b/.buildbot/tox-bionic/build.sh new file mode 100755 index 00000000..87f670ce --- /dev/null +++ b/.buildbot/tox-bionic/build.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +sudo service tor start diff --git a/.buildbot/tox-bionic/test.sh b/.buildbot/tox-bionic/test.sh new file mode 100755 index 00000000..b280953a --- /dev/null +++ b/.buildbot/tox-bionic/test.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +tox -e lint-basic || exit 1 +tox diff --git a/.buildbot/tox-focal/Dockerfile b/.buildbot/tox-focal/Dockerfile new file mode 100644 index 00000000..c2ba0ffc --- /dev/null +++ b/.buildbot/tox-focal/Dockerfile @@ -0,0 +1,15 @@ +FROM ubuntu:focal + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update + +RUN apt-get install -yq --no-install-suggests --no-install-recommends \ + software-properties-common build-essential libcap-dev libffi-dev \ + libssl-dev python-all-dev python-setuptools \ + python3-dev python3-pip python3.9 python3.9-dev python3.9-venv \ + language-pack-en qt5dxcb-plugin tor xvfb + +RUN python3.9 -m pip install --upgrade pip tox virtualenv + +ADD . . diff --git a/.buildbot/tox-focal/test.sh b/.buildbot/tox-focal/test.sh new file mode 120000 index 00000000..a9f8525c --- /dev/null +++ b/.buildbot/tox-focal/test.sh @@ -0,0 +1 @@ +../tox-bionic/test.sh \ No newline at end of file diff --git a/.buildbot/tox-jammy/Dockerfile b/.buildbot/tox-jammy/Dockerfile new file mode 100644 index 00000000..8ca63aa0 --- /dev/null +++ b/.buildbot/tox-jammy/Dockerfile @@ -0,0 +1,12 @@ +FROM ubuntu:jammy + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update + +RUN apt-get install -yq --no-install-suggests --no-install-recommends \ + software-properties-common build-essential libcap-dev libffi-dev \ + libssl-dev python-all-dev python-is-python3 python-setuptools \ + python3-dev python3-pip language-pack-en qt5dxcb-plugin tor xvfb + +RUN pip install tox diff --git a/.buildbot/tox-jammy/test.sh b/.buildbot/tox-jammy/test.sh new file mode 100755 index 00000000..ab6134c4 --- /dev/null +++ b/.buildbot/tox-jammy/test.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +tox -e lint || exit 1 +tox -e py310 diff --git a/.buildbot/winebuild/Dockerfile b/.buildbot/winebuild/Dockerfile new file mode 100644 index 00000000..9b687f8f --- /dev/null +++ b/.buildbot/winebuild/Dockerfile @@ -0,0 +1,14 @@ +FROM ubuntu:bionic + +ENV DEBIAN_FRONTEND=noninteractive + +RUN dpkg --add-architecture i386 + +RUN apt-get update + +RUN apt-get install -yq --no-install-suggests --no-install-recommends \ + software-properties-common build-essential libcap-dev libffi-dev \ + libssl-dev python-all-dev python-setuptools xvfb \ + mingw-w64 wine-stable winetricks wine32 wine64 + +ADD . . diff --git a/.buildbot/winebuild/build.sh b/.buildbot/winebuild/build.sh new file mode 100755 index 00000000..fdf5bedc --- /dev/null +++ b/.buildbot/winebuild/build.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +xvfb-run -a buildscripts/winbuild.sh || exit 1 + +mkdir -p ../out +mv packages/pyinstaller/dist/Bitmessage*.exe ../out +cd ../out +sha256sum Bitmessage*.exe > SHA256SUMS diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..ed4f2b89 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,41 @@ +FROM ubuntu:jammy + +ARG USERNAME=user +ARG USER_UID=1000 +ARG USER_GID=$USER_UID + +RUN apt-get update +RUN DEBIAN_FRONTEND=noninteractive apt-get install -y \ + curl \ + flake8 \ + gh \ + git \ + gnupg2 \ + jq \ + libcap-dev \ + libssl-dev \ + pylint \ + python-setuptools \ + python2.7 \ + python2.7-dev \ + python3 \ + python3-dev \ + python3-flake8 \ + python3-pip \ + python3-pycodestyle \ + software-properties-common \ + sudo \ + zsh + +RUN apt-add-repository ppa:deadsnakes/ppa + +RUN pip install 'tox<4' 'virtualenv<20.22.0' + +RUN groupadd --gid $USER_GID $USERNAME \ + && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \ + && chsh -s /usr/bin/zsh user \ + && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \ + && chmod 0440 /etc/sudoers.d/$USERNAME + +USER $USERNAME +WORKDIR /home/$USERNAME diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..91a470aa --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,35 @@ +{ + "name": "Codespaces Python3", + "extensions": [ + "cschleiden.vscode-github-actions", + "eamodio.gitlens", + "github.vscode-pull-request-github", + "ms-azuretools.vscode-docker", + "ms-python.flake8", + "ms-python.pylint", + "ms-python.python", + "ms-vsliveshare.vsliveshare", + "nwgh.bandit", + "the-compiler.python-tox", + "vscode-icons-team.vscode-icons", + "visualstudioexptteam.vscodeintellicode" + ], + "dockerFile": "Dockerfile", + "postCreateCommand": "pip3 install -r requirements.txt", + "updateContentCommand": "python2.7 setup.py install --user", + "remoteEnv": { + "PATH": "${containerEnv:PATH}:/home/user/.local/bin" + }, + "settings": { + "flake8.args": ["--config=setup.cfg"], + "pylint.args": ["--rcfile=setup.cfg"], + "terminal.integrated.shell.linux": "/usr/bin/zsh", + "terminal.integrated.defaultProfile.linux": "zsh", + "terminal.integrated.fontFamily": "'SourceCodePro+Powerline+Awesome Regular'", + "terminal.integrated.fontSize": 14, + "files.exclude": { + "**/CODE_OF_CONDUCT.md": true, + "**/LICENSE": true + } + } +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..77d72c64 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +bin +build +dist +__pycache__ +.buildozer +.tox +mprofile_* diff --git a/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md similarity index 73% rename from PULL_REQUEST_TEMPLATE.md rename to .github/PULL_REQUEST_TEMPLATE.md index c820c50d..fb735a84 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,21 +1,18 @@ ## Repository contributions to the PyBitmessage project -- You can get paid for merged commits if you register at [Tip4Commit](https://tip4commit.com/github/Bitmessage/PyBitmessage) - ### Code - Try to refer to github issue tracker or other permanent sources of discussion about the issue. -- It is clear from the diff *what* you have done, it may be less clear *why* you have done it so explain why this change is necessary rather than what it does +- It is clear from the diff *what* you have done, it may be less clear *why* you have done it so explain why this change is necessary rather than what it does. ### Documentation -- If there has been a change to the code, there's a good possibility there should be a corresponding change to the documentation -- If you can't run `fab build_docs` successfully, ask for someone to run it against your branch +Use `tox -e py27-doc` to build a local copy of the documentation. ### Tests - If there has been a change to the code, there's a good possibility there should be a corresponding change to the tests -- If you can't run `fab tests` successfully, ask for someone to run it against your branch +- To run tests locally use `tox` or `./run-tests-in-docker.sh` ## Translations diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..415583a9 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +# Basic dependabot.yml for kivymd +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "daily" diff --git a/.gitignore b/.gitignore index 72e5700f..fc331499 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ src/**/*.so src/**/a.out build/lib.* build/temp.* +bin dist *.egg-info docs/_*/* @@ -19,6 +20,8 @@ docs/autodoc/ build pyan/ **.coverage +coverage.xml **htmlcov* **coverage.json - +.buildozer +.tox diff --git a/.readthedocs.yml b/.readthedocs.yml index 2c7b9839..136ef6e9 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,9 +1,12 @@ version: 2 +build: + os: ubuntu-20.04 + tools: + python: "2.7" + python: - version: 2.7 install: - requirements: docs/requirements.txt - - method: setuptools + - method: pip path: . - system_packages: false diff --git a/.travis-kivy.yml b/.travis-kivy.yml deleted file mode 100644 index 46ef7963..00000000 --- a/.travis-kivy.yml +++ /dev/null @@ -1,18 +0,0 @@ -language: python3.7 -cache: pip3 -dist: bionic -python: - - "3.7" -addons: - apt: - packages: - - build-essential - - libcap-dev - - libmtdev-dev - - xvfb -install: - - pip3 install -r kivy-requirements.txt - - python3 setup.py install - - export PYTHONWARNINGS=all -script: - - xvfb-run python3 tests-kivy.py diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e13d9e33..00000000 --- a/.travis.yml +++ /dev/null @@ -1,22 +0,0 @@ -language: python -cache: pip -dist: bionic -python: - - "2.7_with_system_site_packages" - - "3.7" -addons: - apt: - packages: - - build-essential - - libcap-dev - - python-pyqt5 - - tor - - xvfb -install: - - pip install -r requirements.txt - - python setup.py install - - export PYTHONWARNINGS=all -script: - - python checkdeps.py - - python src/bitmessagemain.py -t - - python -bm tests diff --git a/COPYING b/COPYING index 078bf213..279cef2a 100644 --- a/COPYING +++ b/COPYING @@ -1,5 +1,5 @@ Copyright (c) 2012-2016 Jonathan Warren -Copyright (c) 2012-2020 The Bitmessage Developers +Copyright (c) 2012-2022 The Bitmessage Developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Dockerfile b/Dockerfile index 6e665ff6..b409d27a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # A container for PyBitmessage daemon -FROM ubuntu:xenial +FROM ubuntu:bionic RUN apt-get update @@ -9,8 +9,6 @@ RUN apt-get install -yq --no-install-suggests --no-install-recommends \ build-essential libcap-dev libssl-dev \ python-all-dev python-msgpack python-pip python-setuptools -RUN pip2 install --upgrade pip - EXPOSE 8444 8442 ENV HOME /home/bitmessage @@ -18,26 +16,22 @@ ENV BITMESSAGE_HOME ${HOME} WORKDIR ${HOME} ADD . ${HOME} +COPY packages/docker/launcher.sh /usr/bin/ -# Install tests dependencies -RUN pip2 install -r requirements.txt # Install -RUN python2 setup.py install +RUN pip2 install jsonrpclib . + +# Cleanup +RUN rm -rf /var/lib/apt/lists/* +RUN rm -rf ${HOME} # Create a user -RUN useradd bitmessage && chown -R bitmessage ${HOME} +RUN useradd -r bitmessage && chown -R bitmessage ${HOME} USER bitmessage -# Clean HOME -RUN rm -rf ${HOME}/* - # Generate default config RUN pybitmessage -t -# Setup environment -RUN APIPASS=$(tr -dc a-zA-Z0-9 < /dev/urandom | head -c32 && echo) \ - && echo "\napiusername: api\napipassword: $APIPASS" \ - && echo "apienabled = true\napiinterface = 0.0.0.0\napiusername = api\napipassword = $APIPASS" >> keys.dat - -CMD ["pybitmessage", "-d"] +ENTRYPOINT ["launcher.sh"] +CMD ["-d"] diff --git a/INSTALL.md b/INSTALL.md index f4aefbed..7942a957 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,40 +1,46 @@ # PyBitmessage Installation Instructions -- Binary (no separate installation of dependencies required) - - windows (32bit only): https://download.bitmessage.org/snapshots/ - - linux (64bit): https://appimage.bitmessage.org/releases/ - - mac (64bit, not up to date): https://github.com/Bitmessage/PyBitmessage/releases/tag/v0.6.3 +- Binary (64bit, no separate installation of dependencies required) + - Windows: https://download.bitmessage.org/snapshots/ + - Linux AppImages: https://artifacts.bitmessage.at/appimage/ + - Linux snaps: https://artifacts.bitmessage.at/snap/ + - Mac (not up to date): https://github.com/Bitmessage/PyBitmessage/releases/tag/v0.6.1 - Source - git clone git://github.com/Bitmessage/PyBitmessage.git + `git clone git://github.com/Bitmessage/PyBitmessage.git` ## Helper Script for building from source Go to the directory with PyBitmessage source code and run: ``` python checkdeps.py ``` -If there are missing dependencies, it will explain you what is missing and for many Unix-like systems also what you have to do to resolve it. You need to repeat calling the script until you get nothing mandatory missing. How you then run setuptools depends on whether you want to install it to user's directory or system. +If there are missing dependencies, it will explain you what is missing +and for many Unix-like systems also what you have to do to resolve it. You need +to repeat calling the script until you get nothing mandatory missing. How you +then run setuptools depends on whether you want to install it to +user's directory or system. ### If checkdeps fails, then verify manually which dependencies are missing from below Before running PyBitmessage, make sure you have all the necessary dependencies installed on your system. -These dependencies may not be available on a recent OS and PyBitmessage may not build on such systems. -Here's a list of dependencies needed for PyBitmessage based on operating system +These dependencies may not be available on a recent OS and PyBitmessage may not +build on such systems. Here's a list of dependencies needed for PyBitmessage +based on operating system For Debian-based (Ubuntu, Raspbian, PiBang, others) ``` -python2.7 openssl libssl-dev git python-msgpack python-qt4 python-six +python2.7 openssl libssl-dev python-msgpack python-qt4 python-six ``` For Arch Linux ``` -python2 openssl git python2-pyqt4 python-six +python2 openssl python2-pyqt4 python-six ``` For Fedora ``` -python python-qt4 git openssl-compat-bitcoin-libs python-six +python python-qt4 openssl-compat-bitcoin-libs python-six ``` For Red Hat Enterprise Linux (RHEL) ``` -python python-qt4 git openssl-compat-bitcoin-libs python-six +python python-qt4 openssl-compat-bitcoin-libs python-six ``` For GNU Guix ``` @@ -42,9 +48,10 @@ python2-msgpack python2-pyqt@4.11.4 python2-sip openssl python-six ``` ## setuptools -This is now the recommended and in most cases the easiest procedure for installing PyBitmessage. +This is now the recommended and in most cases the easiest way for +installing PyBitmessage. -There are 3 options for running setuptools: root, user, venv +There are 2 options for installing with setuptools: root and user. ### as root: ``` @@ -58,7 +65,7 @@ python setup.py install --user ~/.local/bin/pybitmessage ``` -### as venv: +## pip venv (daemon): Create virtualenv with Python 2.x version ``` virtualenv -p python2 env @@ -69,19 +76,11 @@ Activate env source env/bin/activate ``` -Install requirements.txt -``` -pip install -r requirements.txt -``` - Build & run pybitmessage ``` -python setup.py install -pybitmessage +pip install . +pybitmessage -d ``` ## Alternative way to run PyBitmessage, without setuptools (this isn't recommended) -run `src/bitmessagemain.py`. -``` -cd PyBitmessage/ && python src/bitmessagemain.py -``` +run `./start.sh`. diff --git a/LICENSE b/LICENSE index c2eeff82..fd772201 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) Copyright (c) 2012-2016 Jonathan Warren -Copyright (c) 2012-2020 The Bitmessage Developers +Copyright (c) 2012-2022 The Bitmessage Developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in @@ -91,4 +91,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in index 3cbc72cd..15a6bf81 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,3 +2,4 @@ include COPYING include README.md include requirements.txt recursive-include desktop * +recursive-include packages/apparmor * diff --git a/README.md b/README.md index 17049e7a..06c97c01 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Feel welcome to join chan "bitmessage", BM-2cWy7cvHoq3f1rYMerRJp8PT653jjSuEdY References ---------- * [Project Website](https://bitmessage.org) -* [Protocol Specification](https://bitmessage.org/wiki/Protocol_specification) +* [Protocol Specification](https://pybitmessage.rtfd.io/en/v0.6/protocol.html) * [Whitepaper](https://bitmessage.org/bitmessage.pdf) * [Installation](https://bitmessage.org/wiki/Compiling_instructions) * [Discuss on Reddit](https://www.reddit.com/r/bitmessage) diff --git a/buildscripts/androiddev.sh b/buildscripts/androiddev.sh old mode 100644 new mode 100755 index c035fea0..1634d4c0 --- a/buildscripts/androiddev.sh +++ b/buildscripts/androiddev.sh @@ -58,23 +58,14 @@ install_ndk() } # INSTALL SDK -function install_sdk() +install_sdk() { - if [[ "$get_python_version" -eq " 2 " ]]; - then - ANDROID_SDK_BUILD_TOOLS_VERSION="28.0.3" - elif [[ "$get_python_version" -eq " 3 " ]]; - then - ANDROID_SDK_BUILD_TOOLS_VERSION="29.0.2" - else - exit - fi + ANDROID_SDK_BUILD_TOOLS_VERSION="29.0.2" ANDROID_SDK_HOME="${ANDROID_HOME}/android-sdk" # get the latest version from https://developer.android.com/studio/index.html ANDROID_SDK_TOOLS_VERSION="4333796" ANDROID_SDK_TOOLS_ARCHIVE="sdk-tools-linux-${ANDROID_SDK_TOOLS_VERSION}.zip" ANDROID_SDK_TOOLS_DL_URL="https://dl.google.com/android/repository/${ANDROID_SDK_TOOLS_ARCHIVE}" - echo "Downloading sdk.........................................................................." wget -nc ${ANDROID_SDK_TOOLS_DL_URL} mkdir --parents "${ANDROID_SDK_HOME}" unzip -q "${ANDROID_SDK_TOOLS_ARCHIVE}" -d "${ANDROID_SDK_HOME}" @@ -84,7 +75,7 @@ function install_sdk() echo '### Sources for Android SDK Manager' > "${ANDROID_SDK_HOME}/.android/repositories.cfg" # accept Android licenses (JDK necessary!) apt -y update -qq - apt -y install -qq --no-install-recommends openjdk-8-jdk + apt -y install -qq --no-install-recommends openjdk-11-jdk apt -y autoremove yes | "${ANDROID_SDK_HOME}/tools/bin/sdkmanager" "build-tools;${ANDROID_SDK_BUILD_TOOLS_VERSION}" > /dev/null # download platforms, API, build tools @@ -98,23 +89,14 @@ function install_sdk() } # INSTALL APACHE-ANT -function install_ant() +install_ant() { - if [[ "$get_python_version" -eq " 2 " ]]; - then - APACHE_ANT_VERSION="1.9.4" - elif [[ "$get_python_version" -eq " 3 " ]]; - then - APACHE_ANT_VERSION="1.10.7" - else - exit - fi + APACHE_ANT_VERSION="1.10.12" APACHE_ANT_ARCHIVE="apache-ant-${APACHE_ANT_VERSION}-bin.tar.gz" APACHE_ANT_DL_URL="http://archive.apache.org/dist/ant/binaries/${APACHE_ANT_ARCHIVE}" APACHE_ANT_HOME="${ANDROID_HOME}/apache-ant" APACHE_ANT_HOME_V="${APACHE_ANT_HOME}-${APACHE_ANT_VERSION}" - echo "Downloading ant.........................................................................." wget -nc ${APACHE_ANT_DL_URL} tar -xf "${APACHE_ANT_ARCHIVE}" -C "${ANDROID_HOME}" ln -sfn "${APACHE_ANT_HOME_V}" "${APACHE_ANT_HOME}" diff --git a/buildscripts/appimage.sh b/buildscripts/appimage.sh index 1b190de0..a5691783 100755 --- a/buildscripts/appimage.sh +++ b/buildscripts/appimage.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash # Cleanup rm -rf PyBitmessage @@ -18,9 +18,19 @@ fi ./pkg2appimage packages/AppImage/PyBitmessage.yml -if [ -f "out/PyBitmessage-${VERSION}.glibc2.15-x86_64.AppImage" ]; then +./pkg2appimage --appimage-extract + +. ./squashfs-root/usr/share/pkg2appimage/functions.sh + +GLIBC=$(glibc_needed) + +VERSION_EXPANDED=${VERSION}.glibc${GLIBC}-${SYSTEM_ARCH} + +if [ -f "out/PyBitmessage-${VERSION_EXPANDED}.AppImage" ]; then echo "Build Successful"; - echo "Run out/PyBitmessage-${VERSION}.glibc2.15-x86_64.AppImage" + echo "Run out/PyBitmessage-${VERSION_EXPANDED}.AppImage"; + out/PyBitmessage-${VERSION_EXPANDED}.AppImage -t else - echo "Build Failed" + echo "Build Failed"; + exit 1 fi diff --git a/buildscripts/update_translation_source.sh b/buildscripts/update_translation_source.sh new file mode 100644 index 00000000..205767cb --- /dev/null +++ b/buildscripts/update_translation_source.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +xgettext -Lpython --output=src/translations/messages.pot \ +src/bitmessagekivy/mpybit.py src/bitmessagekivy/main.kv \ +src/bitmessagekivy/baseclass/*.py src/bitmessagekivy/kv/*.kv diff --git a/buildscripts/winbuild.sh b/buildscripts/winbuild.sh index e809089a..e1dac103 100755 --- a/buildscripts/winbuild.sh +++ b/buildscripts/winbuild.sh @@ -100,16 +100,18 @@ function install_openssl(){ function install_pyinstaller() { - cd "${BASE_DIR}" || exit 1 - echo "Installing PyInstaller" - if [ "${MACHINE_TYPE}" == 'x86_64' ]; then - # 3.6 is the last version to support python 2.7 - wine python -m pip install -I pyinstaller==3.6 - else - # 3.2.1 is the last version to work on XP - # see https://github.com/pyinstaller/pyinstaller/issues/2931 - wine python -m pip install -I pyinstaller==3.2.1 - fi + cd "${BASE_DIR}" || exit 1 + echo "Installing PyInstaller" + if [ "${MACHINE_TYPE}" == 'x86_64' ]; then + # 3.6 is the last version to support python 2.7 + # but the resulting executable cannot run in wine + # see https://github.com/pyinstaller/pyinstaller/issues/4628 + wine python -m pip install -I pyinstaller==3.5 + else + # 3.2.1 is the last version to work on XP + # see https://github.com/pyinstaller/pyinstaller/issues/2931 + wine python -m pip install -I pyinstaller==3.2.1 + fi } function install_pip_depends() @@ -138,13 +140,13 @@ function build_dll(){ cd src/bitmsghash || exit 1 if [ "${MACHINE_TYPE}" == 'x86_64' ]; then echo "Create dll" - x86_64-w64-mingw32-g++ -D_WIN32 -Wall -O3 -march=native \ + x86_64-w64-mingw32-g++ -D_WIN32 -Wall -O3 -march=x86-64 \ "-I$HOME/.wine64/drive_c/OpenSSL-Win64/include" \ -I/usr/x86_64-w64-mingw32/include \ "-L$HOME/.wine64/drive_c/OpenSSL-Win64/lib" \ -c bitmsghash.cpp x86_64-w64-mingw32-g++ -static-libgcc -shared bitmsghash.o \ - -D_WIN32 -O3 -march=native \ + -D_WIN32 -O3 -march=x86-64 \ "-I$HOME/.wine64/drive_c/OpenSSL-Win64/include" \ "-L$HOME/.wine64/drive_c/OpenSSL-Win64" \ -L/usr/lib/x86_64-linux-gnu/wine \ @@ -152,13 +154,13 @@ function build_dll(){ -o bitmsghash64.dll -Wl,--out-implib,bitmsghash.a else echo "Create dll" - i686-w64-mingw32-g++ -D_WIN32 -Wall -m32 -O3 -march=native \ + i686-w64-mingw32-g++ -D_WIN32 -Wall -m32 -O3 -march=i686 \ "-I$HOME/.wine32/drive_c/OpenSSL-Win32/include" \ -I/usr/i686-w64-mingw32/include \ "-L$HOME/.wine32/drive_c/OpenSSL-Win32/lib" \ -c bitmsghash.cpp i686-w64-mingw32-g++ -static-libgcc -shared bitmsghash.o \ - -D_WIN32 -O3 -march=native \ + -D_WIN32 -O3 -march=i686 \ "-I$HOME/.wine32/drive_c/OpenSSL-Win32/include" \ "-L$HOME/.wine32/drive_c/OpenSSL-Win32/lib/MinGW" \ -fPIC -shared -lcrypt32 -leay32 -lwsock32 \ @@ -173,11 +175,14 @@ function build_exe(){ } function dryrun_exe(){ - cd "${BASE_DIR}" || exit 1 - if [ ! "${MACHINE_TYPE}" == 'x86_64' ]; then - local VERSION=$(python setup.py --version) - wine packages/pyinstaller/dist/Bitmessage_x86_$VERSION.exe -t - fi + cd "${BASE_DIR}" || exit 1 + local VERSION=$(python setup.py --version) + if [ "${MACHINE_TYPE}" == 'x86_64' ]; then + EXE=Bitmessage_x64_$VERSION.exe + else + EXE=Bitmessage_x86_$VERSION.exe + fi + wine packages/pyinstaller/dist/$EXE -t } # prepare on ubuntu diff --git a/docs/address.rst b/docs/address.rst new file mode 100644 index 00000000..eec4bd2c --- /dev/null +++ b/docs/address.rst @@ -0,0 +1,106 @@ +Address +======= + +Bitmessage adresses are Base58 encoded public key hashes. An address looks like +``BM-BcbRqcFFSQUUmXFKsPJgVQPSiFA3Xash``. All Addresses start with ``BM-``, +however clients should accept addresses without the prefix. PyBitmessage does +this. The reason behind this idea is the fact, that when double clicking on an +address for copy and paste, the prefix is usually not selected due to the dash +being a common separator. + +Public Key usage +---------------- + +Addresses may look complicated but they fulfill the purpose of verifying the +sender. A Message claiming to be from a specific address can simply be checked by +decoding a special field in the data packet with the public key, that represents +the address. If the decryption succeeds, the message is from the address it +claims to be. + +Length +------ + +Without the ``BM-`` prefix, an address is usually 32-34 chars long. Since an +address is a hash it can be calculated by the client in a way, that the first +bytes are zero (``\0``) and bitmessage strips these. This causes the client to do +much more work to be lucky and find such an address. This is an optional checkbox +in address generation dialog. + +Versions +-------- + + * v1 addresses used a single RSA key pair + * v2 addresses use 2 ECC key pairs + * v3 addresses extends v2 addresses to allow specifying the proof of work + requirements. The pubkey object is signed to mitigate against + forgery/tampering. + * v4 addresses protect against harvesting addresses from getpubkey and pubkey + objects + +Address Types +------------- + +There are two address types the user can generate in PyBitmessage. The resulting +addresses have no difference, but the method how they are created differs. + +Random Address +^^^^^^^^^^^^^^ + +Random addresses are generated from a randomly chosen number. The resulting +address cannot be regenerated without knowledge of the number and therefore the +keys.dat should be backed up. Generating random addresses takes slightly longer +due to the POW required for the public key broadcast. + +Usage +""""" + + * Generate unique addresses + * Generate one time addresses. + + +Deterministic Address +^^^^^^^^^^^^^^^^^^^^^ + +For this type of Address a passphrase is required, that is used to seed the +random generator. Using the same passphrase creates the same addresses. +Using deterministic addresses should be done with caution, using a word from a +dictionary or a common number can lead to others generating the same address and +thus being able to receive messages not intended for them. Generating a +deterministic address will not publish the public key. The key is sent in case +somebody requests it. This saves :doc:`pow` time, when generating a bunch of +addresses. + +Usage +""""" + + * Create the same address on multiple systems without the need of copying + keys.dat or an Address Block. + * create a Channel. (Use the *Join/create chan* option in the file menu instead) + * Being able to restore the address in case of address database corruption or + deletation. + +Address generation +------------------ + + 1. Create a private and a public key for encryption and signing (resulting in + 4 keys) + 2. Merge the public part of the signing key and the encryption key together. + (encoded in uncompressed X9.62 format) (A) + 3. Take the SHA512 hash of A. (B) + 4. Take the RIPEMD160 of B. (C) + 5. Repeat step 1-4 until you have a result that starts with a zero + (Or two zeros, if you want a short address). (D) + 6. Remove the zeros at the beginning of D. (E) + 7. Put the stream number (as a var_int) in front of E. (F) + 8. Put the address version (as a var_int) in front of F. (G) + 9. Take a double SHA512 (hash of a hash) of G and use the first four bytes as a + checksum, that you append to the end. (H) + 10. base58 encode H. (J) + 11. Put "BM-" in front J. (K) + +K is your full address + + .. note:: Bitmessage's base58 encoding uses the following sequence + (the same as Bitcoin's): + "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz". + Many existing libraries for base58 do not use this ordering. diff --git a/docs/conf.py b/docs/conf.py index e3eef6b3..b0cfef7b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -19,7 +19,7 @@ import version # noqa:E402 # -- Project information ----------------------------------------------------- project = u'PyBitmessage' -copyright = u'2019, The Bitmessage Team' # pylint: disable=redefined-builtin +copyright = u'2019-2022, The Bitmessage Team' # pylint: disable=redefined-builtin author = u'The Bitmessage Team' # The short X.Y version @@ -203,7 +203,7 @@ autodoc_mock_imports = [ 'pybitmessage.bitmessagekivy', 'pybitmessage.bitmessageqt.foldertree', 'pybitmessage.helper_startup', - 'pybitmessage.mock', + 'pybitmessage.mockbm', 'pybitmessage.network.httpd', 'pybitmessage.network.https', 'ctypes', @@ -232,7 +232,7 @@ apidoc_excluded_paths = [ 'bitmessageqt/addressvalidator.py', 'bitmessageqt/foldertree.py', 'bitmessageqt/migrationwizard.py', 'bitmessageqt/newaddresswizard.py', 'helper_startup.py', - 'kivymd', 'mock', 'main.py', 'navigationdrawer', 'network/http*', + 'kivymd', 'mockbm', 'main.py', 'navigationdrawer', 'network/http*', 'src', 'tests', 'version.py' ] apidoc_module_first = True diff --git a/docs/encrypted_payload.rst b/docs/encrypted_payload.rst new file mode 100644 index 00000000..346d370d --- /dev/null +++ b/docs/encrypted_payload.rst @@ -0,0 +1,19 @@ ++------------+-------------+-----------+--------------------------------------------+ +| Field Size | Description | Data type | Comments | ++============+=============+===========+============================================+ +| 16 | IV | uchar[] | Initialization Vector used for AES-256-CBC | ++------------+-------------+-----------+--------------------------------------------+ +| 2 | Curve type | uint16_t | Elliptic Curve type 0x02CA (714) | ++------------+-------------+-----------+--------------------------------------------+ +| 2 | X length | uint16_t | Length of X component of public key R | ++------------+-------------+-----------+--------------------------------------------+ +| X length | X | uchar[] | X component of public key R | ++------------+-------------+-----------+--------------------------------------------+ +| 2 | Y length | uint16_t | Length of Y component of public key R | ++------------+-------------+-----------+--------------------------------------------+ +| Y length | Y | uchar[] | Y component of public key R | ++------------+-------------+-----------+--------------------------------------------+ +| ? | encrypted | uchar[] | Cipher text | ++------------+-------------+-----------+--------------------------------------------+ +| 32 | MAC | uchar[] | HMACSHA256 Message Authentication Code | ++------------+-------------+-----------+--------------------------------------------+ diff --git a/docs/encryption.rst b/docs/encryption.rst new file mode 100644 index 00000000..61c7fb3e --- /dev/null +++ b/docs/encryption.rst @@ -0,0 +1,257 @@ +Encryption +========== + +Bitmessage uses the Elliptic Curve Integrated Encryption Scheme +`(ECIES) `_ +to encrypt the payload of the Message and Broadcast objects. + +The scheme uses Elliptic Curve Diffie-Hellman +`(ECDH) `_ to generate a shared secret used +to generate the encryption parameters for Advanced Encryption Standard with +256bit key and Cipher-Block Chaining +`(AES-256-CBC) `_. +The encrypted data will be padded to a 16 byte boundary in accordance to +`PKCS7 `_. This +means that the data is padded with N bytes of value N. + +The Key Derivation Function +`(KDF) `_ used to +generate the key material for AES is +`SHA512 `_. The Message Authentication +Code (MAC) scheme used is `HMACSHA256 `_. + +Format +------ + +(See also: :doc:`protocol`) + +.. include:: encrypted_payload.rst + +In order to reconstitute a usable (65 byte) public key (starting with 0x04), +the X and Y components need to be expanded by prepending them with 0x00 bytes +until the individual component lengths are 32 bytes. + +Encryption +---------- + + 1. The destination public key is called K. + 2. Generate 16 random bytes using a secure random number generator. + Call them IV. + 3. Generate a new random EC key pair with private key called r and public key + called R. + 4. Do an EC point multiply with public key K and private key r. This gives you + public key P. + 5. Use the X component of public key P and calculate the SHA512 hash H. + 6. The first 32 bytes of H are called key_e and the last 32 bytes are called + key_m. + 7. Pad the input text to a multiple of 16 bytes, in accordance to PKCS7. [#f1]_ + 8. Encrypt the data with AES-256-CBC, using IV as initialization vector, + key_e as encryption key and the padded input text as payload. Call the + output cipher text. + 9. Calculate a 32 byte MAC with HMACSHA256, using key_m as salt and + IV + R [#f2]_ + cipher text as data. Call the output MAC. + +The resulting data is: IV + R + cipher text + MAC + +Decryption +---------- + + 1. The private key used to decrypt is called k. + 2. Do an EC point multiply with private key k and public key R. This gives you + public key P. + 3. Use the X component of public key P and calculate the SHA512 hash H. + 4. The first 32 bytes of H are called key_e and the last 32 bytes are called + key_m. + 5. Calculate MAC' with HMACSHA256, using key_m as salt and + IV + R + cipher text as data. + 6. Compare MAC with MAC'. If not equal, decryption will fail. + 7. Decrypt the cipher text with AES-256-CBC, using IV as initialization + vector, key_e as decryption key and the cipher text as payload. The output + is the padded input text. + +.. highlight:: nasm + +Partial Example +--------------- + +.. list-table:: Public key K: + :header-rows: 1 + :widths: auto + + * - Data + - Comments + * - + + :: + + 04 + 09 d4 e5 c0 ab 3d 25 fe + 04 8c 64 c9 da 1a 24 2c + 7f 19 41 7e 95 17 cd 26 + 69 50 d7 2c 75 57 13 58 + 5c 61 78 e9 7f e0 92 fc + 89 7c 9a 1f 17 20 d5 77 + 0a e8 ea ad 2f a8 fc bd + 08 e9 32 4a 5d de 18 57 + - Public key, 0x04 prefix, then 32 bytes X and 32 bytes Y. + + +.. list-table:: Initialization Vector IV: + :header-rows: 1 + :widths: auto + + * - Data + - Comments + * - + + :: + + bd db 7c 28 29 b0 80 38 + 75 30 84 a2 f3 99 16 81 + - 16 bytes generated with a secure random number generator. + +.. list-table:: Randomly generated key pair with private key r and public key R: + :header-rows: 1 + :widths: auto + + * - Data + - Comments + * - + + :: + + 5b e6 fa cd 94 1b 76 e9 + d3 ea d0 30 29 fb db 6b + 6e 08 09 29 3f 7f b1 97 + d0 c5 1f 84 e9 6b 8b a4 + - Private key r + * - + + :: + + 02 ca 00 20 + 02 93 21 3d cf 13 88 b6 + 1c 2a e5 cf 80 fe e6 ff + ff c0 49 a2 f9 fe 73 65 + fe 38 67 81 3c a8 12 92 + 00 20 + df 94 68 6c 6a fb 56 5a + c6 14 9b 15 3d 61 b3 b2 + 87 ee 2c 7f 99 7c 14 23 + 87 96 c1 2b 43 a3 86 5a + - Public key R + +.. list-table:: Derived public key P (point multiply r with K): + :header-rows: 1 + :widths: auto + + * - Data + - Comments + * - + + :: + + 04 + 0d b8 e3 ad 8c 0c d7 3f + a2 b3 46 71 b7 b2 47 72 + 9b 10 11 41 57 9d 19 9e + 0d c0 bd 02 4e ae fd 89 + ca c8 f5 28 dc 90 b6 68 + 11 ab ac 51 7d 74 97 be + 52 92 93 12 29 be 0b 74 + 3e 05 03 f4 43 c3 d2 96 + - Public key P + * - + + :: + + 0d b8 e3 ad 8c 0c d7 3f + a2 b3 46 71 b7 b2 47 72 + 9b 10 11 41 57 9d 19 9e + 0d c0 bd 02 4e ae fd 89 + - X component of public key P + +.. list-table:: SHA512 of public key P X component (H): + :header-rows: 1 + :widths: auto + + * - Data + - Comments + * - + + :: + + 17 05 43 82 82 67 86 71 + 05 26 3d 48 28 ef ff 82 + d9 d5 9c bf 08 74 3b 69 + 6b cc 5d 69 fa 18 97 b4 + - First 32 bytes of H called key_e + * - + + :: + + f8 3f 1e 9c c5 d6 b8 44 + 8d 39 dc 6a 9d 5f 5b 7f + 46 0e 4a 78 e9 28 6e e8 + d9 1c e1 66 0a 53 ea cd + - Last 32 bytes of H called key_m + +.. list-table:: Padded input: + :header-rows: 1 + :widths: auto + + * - Data + - Comments + * - + + :: + + 54 68 65 20 71 75 69 63 + 6b 20 62 72 6f 77 6e 20 + 66 6f 78 20 6a 75 6d 70 + 73 20 6f 76 65 72 20 74 + 68 65 20 6c 61 7a 79 20 + 64 6f 67 2e 04 04 04 04 + - The quick brown fox jumps over the lazy dog.0x04,0x04,0x04,0x04 + +.. list-table:: Cipher text: + :header-rows: 1 + :widths: auto + + * - Data + - Comments + * - + + :: + + 64 20 3d 5b 24 68 8e 25 + 47 bb a3 45 fa 13 9a 5a + 1d 96 22 20 d4 d4 8a 0c + f3 b1 57 2c 0d 95 b6 16 + 43 a6 f9 a0 d7 5a f7 ea + cc 1b d9 57 14 7b f7 23 + - 3 blocks of 16 bytes of encrypted data. + +.. list-table:: MAC: + :header-rows: 1 + :widths: auto + + * - Data + - Comments + * - + + :: + + f2 52 6d 61 b4 85 1f b2 + 34 09 86 38 26 fd 20 61 + 65 ed c0 21 36 8c 79 46 + 57 1c ea d6 90 46 e6 19 + - 32 bytes hash + + +.. rubric:: Footnotes + +.. [#f1] The pyelliptic implementation used in PyBitmessage takes unpadded data, + see :obj:`.pyelliptic.Cipher.ciphering`. +.. [#f2] The pyelliptic encodes the pubkey with curve and length, + see :obj:`.pyelliptic.ECC.get_pubkey` diff --git a/docs/extended_encoding.rst b/docs/extended_encoding.rst new file mode 100644 index 00000000..25539ad4 --- /dev/null +++ b/docs/extended_encoding.rst @@ -0,0 +1,55 @@ +Extended encoding +================= + +Extended encoding is an attempt to create a standard for transmitting structured +data. The goals are flexibility, wide platform support and extensibility. It is +currently available in the v0.6 branch and can be enabled by holding "Shift" +while clicking on Send. It is planned that v5 addresses will have to support +this. It's a work in progress, the basic plain text message works but don't +expect anthing else at this time. + +The data structure is in msgpack, then compressed with zlib. The top level is +a key/value store, and the "" key (empty string) contains the value of the type +of object, which can then have its individual format and standards. + +Text fields are encoded using UTF-8. + +Types +----- + +You can find the implementations in the ``src/messagetypes`` directory of +PyBitmessage. Each type has its own file which includes one class, and they are +dynamically loaded on startup. It's planned that this will also contain +initialisation, rendering and so on, so that developers can simply add a new +object type by adding a single file in the messagetypes directory and not have +to change any other part of the code. + +message +^^^^^^^ + +The replacement for the old messages. Mandatory keys are ``body`` and +``subject``, others are currently not implemented and not mandatory. Proposed +other keys: + +``parents``: + array of msgids referring to messages that logically precede it in a + conversation. Allows to create a threaded conversation view + +``files``: + array of files (which is a key/value pair): + + ``name``: + file name, mandatory + ``data``: + the binary data of the file + ``type``: + MIME content type + ``disposition``: + MIME content disposition, possible values are "inline" and "attachment" + +vote +^^^^ + +Dummy code available in the repository. Supposed to serve voting in a chan +(thumbs up/down) for decentralised moderation. Does not actually do anything at +the moment and specification can change. diff --git a/docs/index.rst b/docs/index.rst index 5e8a1c1a..6edb0313 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,7 +1,18 @@ .. mdinclude:: ../README.md + :end-line: 20 -Documentation -------------- +Protocol documentation +---------------------- +.. toctree:: + :maxdepth: 2 + + protocol + address + encryption + pow + +Code documentation +------------------ .. toctree:: :maxdepth: 3 @@ -14,3 +25,6 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` * :ref:`search` + +.. mdinclude:: ../README.md + :start-line: 21 diff --git a/docs/pow.rst b/docs/pow.rst new file mode 100644 index 00000000..3786b075 --- /dev/null +++ b/docs/pow.rst @@ -0,0 +1,77 @@ +Proof of work +============= + +This page describes Bitmessage's Proof of work ("POW") mechanism as it exists in +Protocol Version 3. In this document, hash() means SHA512(). SHA512 was chosen +as it is widely supported and so that Bitcoin POW hardware cannot trivially be +used for Bitmessage POWs. The author acknowledges that they are essentially the +same algorithm with a different key size. + +Both ``averageProofOfWorkNonceTrialsPerByte`` and ``payloadLengthExtraBytes`` +are set by the owner of a Bitmessage address. The default and minimum for each +is 1000. (This is the same as difficulty 1. If the difficulty is 2, then this +value is 2000). The purpose of ``payloadLengthExtraBytes`` is to add some extra +weight to small messages. + +Do a POW +-------- + +Let us use a ``msg`` message as an example:: + + payload = embeddedTime + encodedObjectVersion + encodedStreamNumber + encrypted + +``payloadLength`` + the length of payload, in bytes, + 8 + (to account for the nonce which we will append later) +``TTL`` + the number of seconds in between now and the object expiresTime. + +.. include:: pow_formula.rst + +:: + + initialHash = hash(payload) + +start with ``trialValue = 99999999999999999999`` + +also start with ``nonce = 0`` where nonce is 8 bytes in length and can be +hashed as if it is a string. + +:: + + while trialValue > target: + nonce = nonce + 1 + resultHash = hash(hash( nonce || initialHash )) + trialValue = the first 8 bytes of resultHash, converted to an integer + +When this loop finishes, you will have your 8 byte nonce value which you can +prepend onto the front of the payload. The message is then ready to send. + +Check a POW +----------- + +Let us assume that ``payload`` contains the payload for a msg message (the nonce +down through the encrypted message data). + +``nonce`` + the first 8 bytes of payload +``dataToCheck`` + the ninth byte of payload on down (thus it is everything except the nonce) + +:: + + initialHash = hash(dataToCheck) + + resultHash = hash(hash( nonce || initialHash )) + +``POWValue`` + the first eight bytes of resultHash converted to an integer +``TTL`` + the number of seconds in between now and the object ``expiresTime``. + +.. include:: pow_formula.rst + +If ``POWValue`` is less than or equal to ``target``, then the POW check passes. + + + diff --git a/docs/pow_formula.rst b/docs/pow_formula.rst new file mode 100644 index 00000000..16c3f174 --- /dev/null +++ b/docs/pow_formula.rst @@ -0,0 +1,7 @@ + +.. math:: + + target = \frac{2^{64}}{{\displaystyle + nonceTrialsPerByte (payloadLength + payloadLengthExtraBytes + \frac{ + TTL (payloadLength + payloadLengthExtraBytes)}{2^{16}}) + }} diff --git a/docs/protocol.rst b/docs/protocol.rst new file mode 100644 index 00000000..17a13dd9 --- /dev/null +++ b/docs/protocol.rst @@ -0,0 +1,997 @@ +Protocol specification +====================== + +.. warning:: All objects sent on the network should support protocol v3 + starting on Sun, 16 Nov 2014 22:00:00 GMT. + +.. toctree:: + :maxdepth: 2 + +Common standards +---------------- + +Hashes +^^^^^^ + +Most of the time `SHA-512 `_ hashes are +used, however `RIPEMD-160 `_ is also used +when creating an address. + +A double-round of SHA-512 is used for the Proof Of Work. Example of +double-SHA-512 encoding of string "hello": + +.. highlight:: nasm + +:: + + hello + 9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043(first round of sha-512) + 0592a10584ffabf96539f3d780d776828c67da1ab5b169e9e8aed838aaecc9ed36d49ff1423c55f019e050c66c6324f53588be88894fef4dcffdb74b98e2b200(second round of sha-512) + +For Bitmessage addresses (RIPEMD-160) this would give: + +:: + + hello + 9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043(first round is sha-512) + 79a324faeebcbf9849f310545ed531556882487e (with ripemd-160) + + +Common structures +----------------- + +All integers are encoded in big endian. (This is different from Bitcoin). + +.. list-table:: Message structure + :header-rows: 1 + :widths: auto + + * - Field Size + - Description + - Data type + - Comments + * - 4 + - magic + - uint32_t + - Magic value indicating message origin network, and used to seek to next + message when stream state is unknown + * - 12 + - command + - char[12] + - ASCII string identifying the packet content, NULL padded (non-NULL + padding results in packet rejected) + * - 4 + - length + - uint32_t + - Length of payload in number of bytes. Because of other restrictions, + there is no reason why this length would ever be larger than 1600003 + bytes. Some clients include a sanity-check to avoid processing messages + which are larger than this. + * - 4 + - checksum + - uint32_t + - First 4 bytes of sha512(payload) + * - ? + - message_payload + - uchar[] + - The actual data, a :ref:`message ` or an object_. + Not to be confused with objectPayload. + +Known magic values: + ++-------------+-------------------+ +| Magic value | Sent over wire as | ++=============+===================+ +| 0xE9BEB4D9 | E9 BE B4 D9 | ++-------------+-------------------+ + +.. _varint: + +Variable length integer +^^^^^^^^^^^^^^^^^^^^^^^ + +Integer can be encoded depending on the represented value to save space. +Variable length integers always precede an array/vector of a type of data that +may vary in length. Varints **must** use the minimum possible number of bytes to +encode a value. For example, the value 6 can be encoded with one byte therefore +a varint that uses three bytes to encode the value 6 is malformed and the +decoding task must be aborted. + ++---------------+----------------+------------------------------------------+ +| Value | Storage length | Format | ++===============+================+==========================================+ +| < 0xfd | 1 | uint8_t | ++---------------+----------------+------------------------------------------+ +| <= 0xffff | 3 | 0xfd followed by the integer as uint16_t | ++---------------+----------------+------------------------------------------+ +| <= 0xffffffff | 5 | 0xfe followed by the integer as uint32_t | ++---------------+----------------+------------------------------------------+ +| - | 9 | 0xff followed by the integer as uint64_t | ++---------------+----------------+------------------------------------------+ + +Variable length string +^^^^^^^^^^^^^^^^^^^^^^ + +Variable length string can be stored using a variable length integer followed by +the string itself. + ++------------+-------------+------------+----------------------------------+ +| Field Size | Description | Data type | Comments | ++============+=============+============+==================================+ +| 1+ | length | |var_int| | Length of the string | ++------------+-------------+------------+----------------------------------+ +| ? | string | char[] | The string itself (can be empty) | ++------------+-------------+------------+----------------------------------+ + +Variable length list of integers +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +n integers can be stored using n+1 :ref:`variable length integers ` +where the first var_int equals n. + ++------------+-------------+-----------+----------------------------+ +| Field Size | Description | Data type | Comments | ++============+=============+===========+============================+ +| 1+ | count | |var_int| | Number of var_ints below | ++------------+-------------+-----------+----------------------------+ +| 1+ | | var_int | The first value stored | ++------------+-------------+-----------+----------------------------+ +| 1+ | | var_int | The second value stored... | ++------------+-------------+-----------+----------------------------+ +| 1+ | | var_int | etc... | ++------------+-------------+-----------+----------------------------+ + +.. |var_int| replace:: :ref:`var_int ` + +Network address +^^^^^^^^^^^^^^^ + +When a network address is needed somewhere, this structure is used. Network +addresses are not prefixed with a timestamp or stream in the version_ message. + +.. list-table:: + :header-rows: 1 + :widths: auto + + * - Field Size + - Description + - Data type + - Comments + * - 8 + - time + - uint64 + - the Time. + * - 4 + - stream + - uint32 + - Stream number for this node + * - 8 + - services + - uint64_t + - same service(s) listed in version_ + * - 16 + - IPv6/4 + - char[16] + - IPv6 address. IPv4 addresses are written into the message as a 16 byte + `IPv4-mapped IPv6 address `_ + (12 bytes 00 00 00 00 00 00 00 00 00 00 FF FF, followed by the 4 bytes of + the IPv4 address). + * - 2 + - port + - uint16_t + - port number + +Inventory Vectors +^^^^^^^^^^^^^^^^^ + +Inventory vectors are used for notifying other nodes about objects they have or +data which is being requested. Two rounds of SHA-512 are used, resulting in a +64 byte hash. Only the first 32 bytes are used; the later 32 bytes are ignored. + +Inventory vectors consist of the following data format: + ++------------+-------------+-----------+--------------------+ +| Field Size | Description | Data type | Comments | ++============+=============+===========+====================+ +| 32 | hash | char[32] | Hash of the object | ++------------+-------------+-----------+--------------------+ + +Encrypted payload +^^^^^^^^^^^^^^^^^ + +Bitmessage uses `ECIES `_ to encrypt its messages. For more information see :doc:`encryption` + +.. include:: encrypted_payload.rst + +Unencrypted Message Data +^^^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: auto + + * - Field Size + - Description + - Data type + - Comments + * - 1+ + - msg_version + - var_int + - Message format version. **This field is not included after the + protocol v3 upgrade period**. + * - 1+ + - address_version + - var_int + - Sender's address version number. This is needed in order to calculate + the sender's address to show in the UI, and also to allow for forwards + compatible changes to the public-key data included below. + * - 1+ + - stream + - var_int + - Sender's stream number + * - 4 + - behavior bitfield + - uint32_t + - A bitfield of optional behaviors and features that can be expected from + the node with this pubkey included in this msg message (the sender's + pubkey). + * - 64 + - public signing key + - uchar[] + - The ECC public key used for signing (uncompressed format; + normally prepended with \x04) + * - 64 + - public encryption key + - uchar[] + - The ECC public key used for encryption (uncompressed format; + normally prepended with \x04) + * - 1+ + - nonce_trials_per_byte + - var_int + - Used to calculate the difficulty target of messages accepted by this + node. The higher this value, the more difficult the Proof of Work must + be before this individual will accept the message. This number is the + average number of nonce trials a node will have to perform to meet the + Proof of Work requirement. 1000 is the network minimum so any lower + values will be automatically raised to 1000. **This field is new and is + only included when the address_version >= 3**. + * - 1+ + - extra_bytes + - var_int + - Used to calculate the difficulty target of messages accepted by this + node. The higher this value, the more difficult the Proof of Work must + be before this individual will accept the message. This number is added + to the data length to make sending small messages more difficult. + 1000 is the network minimum so any lower values will be automatically + raised to 1000. **This field is new and is only included when the + address_version >= 3**. + * - 20 + - destination ripe + - uchar[] + - The ripe hash of the public key of the receiver of the message + * - 1+ + - encoding + - var_int + - :ref:`Message Encoding ` type + * - 1+ + - message_length + - var_int + - Message Length + * - message_length + - message + - uchar[] + - The message. + * - 1+ + - ack_length + - var_int + - Length of the acknowledgement data + * - ack_length + - ack_data + - uchar[] + - The acknowledgement data to be transmitted. This takes the form of a + Bitmessage protocol message, like another msg message. The POW therein + must already be completed. + * - 1+ + - sig_length + - var_int + - Length of the signature + * - sig_length + - signature + - uchar[] + - The ECDSA signature which covers the object header starting with the + time, appended with the data described in this table down to the + ack_data. + +.. _msg-encodings: + +Message Encodings +""""""""""""""""" + +.. list-table:: + :header-rows: 1 + :widths: auto + + * - Value + - Name + - Description + * - 0 + - IGNORE + - Any data with this number may be ignored. The sending node might simply + be sharing its public key with you. + * - 1 + - TRIVIAL + - UTF-8. No 'Subject' or 'Body' sections. Useful for simple strings + of data, like URIs or magnet links. + * - 2 + - SIMPLE + - UTF-8. Uses 'Subject' and 'Body' sections. No MIME is used. + :: + messageToTransmit = 'Subject:' + subject + '\n' + 'Body:' + message + * - 3 + - EXTENDED + - See :doc:`extended_encoding` + +Further values for the message encodings can be decided upon by the community. +Any MIME or MIME-like encoding format, should they be used, should make use of +Bitmessage's 8-bit bytes. + +.. _behavior-bitfield: + +Pubkey bitfield features +"""""""""""""""""""""""" + +.. list-table:: + :header-rows: 1 + :widths: auto + + * - Bit + - Name + - Description + * - 0 + - undefined + - The most significant bit at the beginning of the structure. Undefined + * - 1 + - undefined + - The next most significant bit. Undefined + * - ... + - ... + - ... + * - 27 + - onion_router + - (**Proposal**) Node can be used to onion-route messages. In theory any + node can onion route, but since it requires more resources, they may have + the functionality disabled. This field will be used to indicate that the + node is willing to do this. + * - 28 + - forward_secrecy + - (**Proposal**) Receiving node supports a forward secrecy encryption + extension. The exact design is pending. + * - 29 + - chat + - (**Proposal**) Address if for chatting rather than messaging. + * - 30 + - include_destination + - (**Proposal**) Receiving node expects that the RIPE hash encoded in their + address preceedes the encrypted message data of msg messages bound for + them. + + .. note:: since hardly anyone implements this, this will be redesigned as + `simple recipient verification `_ + * - 31 + - does_ack + - If true, the receiving node does send acknowledgements (rather than + dropping them). + +.. _msg-types: + +Message types +------------- + +Undefined messages received on the wire must be ignored. + +version +^^^^^^^ + +When a node creates an outgoing connection, it will immediately advertise its +version. The remote node will respond with its version. No futher communication +is possible until both peers have exchanged their version. + +.. list-table:: Payload + :header-rows: 1 + :widths: auto + + * - Field Size + - Description + - Data type + - Comments + * - 4 + - version + - int32_t + - Identifies protocol version being used by the node. Should equal 3. + Nodes should disconnect if the remote node's version is lower but + continue with the connection if it is higher. + * - 8 + - services + - uint64_t + - bitfield of features to be enabled for this connection + * - 8 + - timestamp + - int64_t + - standard UNIX timestamp in seconds + * - 26 + - addr_recv + - net_addr + - The network address of the node receiving this message (not including the + time or stream number) + * - 26 + - addr_from + - net_addr + - The network address of the node emitting this message (not including the + time or stream number and the ip itself is ignored by the receiver) + * - 8 + - nonce + - uint64_t + - Random nonce used to detect connections to self. + * - 1+ + - user_agent + - var_str + - :doc:`useragent` (0x00 if string is 0 bytes long). Sending nodes must not + include a user_agent longer than 5000 bytes. + * - 1+ + - stream_numbers + - var_int_list + - The stream numbers that the emitting node is interested in. Sending nodes + must not include more than 160000 stream numbers. + +A "verack" packet shall be sent if the version packet was accepted. Once you +have sent and received a verack messages with the remote node, send an addr +message advertising up to 1000 peers of which you are aware, and one or more +inv messages advertising all of the valid objects of which you are aware. + +.. list-table:: The following services are currently assigned + :header-rows: 1 + :widths: auto + + * - Value + - Name + - Description + * - 1 + - NODE_NETWORK + - This is a normal network node. + * - 2 + - NODE_SSL + - This node supports SSL/TLS in the current connect (python < 2.7.9 only + supports a SSL client, so in that case it would only have this on when + the connection is a client). + * - 3 + - NODE_POW + - (**Proposal**) This node may do PoW on behalf of some its peers (PoW + offloading/delegating), but it doesn't have to. Clients may have to meet + additional requirements (e.g. TLS authentication) + * - 4 + - NODE_DANDELION + - Node supports `dandelion `_ + +verack +^^^^^^ + +The *verack* message is sent in reply to *version*. This message consists of +only a :ref:`message header ` with the command string +"verack". The TCP timeout starts out at 20 seconds; after verack messages are +exchanged, the timeout is raised to 10 minutes. + +If both sides announce that they support SSL, they **must** perform an SSL +handshake immediately after they both send and receive verack. During this SSL +handshake, the TCP client acts as an SSL client, and the TCP server acts as an +SSL server. The current implementation (v0.5.4 or later) requires the +AECDH-AES256-SHA cipher over TLSv1 protocol, and prefers the secp256k1 curve +(but other curves may be accepted, depending on the version of python and +OpenSSL used). + +addr +^^^^ + +Provide information on known nodes of the network. Non-advertised nodes should +be forgotten after typically 3 hours + +Payload: + ++------------+-------------+-----------+---------------------------------------+ +| Field Size | Description | Data type | Comments | ++============+=============+===========+=======================================+ +| 1+ | count | |var_int| | Number of address entries (max: 1000) | ++------------+-------------+-----------+---------------------------------------+ +| 38 | addr_list | net_addr | Address of other nodes on the network.| ++------------+-------------+-----------+---------------------------------------+ + +inv +^^^ + +Allows a node to advertise its knowledge of one or more objects. Payload +(maximum payload length: 50000 items): + ++------------+-------------+------------+-----------------------------+ +| Field Size | Description | Data type | Comments | ++============+=============+============+=============================+ +| ? | count | |var_int| | Number of inventory entries | ++------------+-------------+------------+-----------------------------+ +| 32x? | inventory | inv_vect[] | Inventory vectors | ++------------+-------------+------------+-----------------------------+ + +getdata +^^^^^^^ + +getdata is used in response to an inv message to retrieve the content of a +specific object after filtering known elements. + +Payload (maximum payload length: 50000 entries): + ++------------+-------------+------------+-----------------------------+ +| Field Size | Description | Data type | Comments | ++============+=============+============+=============================+ +| ? | count | |var_int| | Number of inventory entries | ++------------+-------------+------------+-----------------------------+ +| 32x? | inventory | inv_vect[] | Inventory vectors | ++------------+-------------+------------+-----------------------------+ + +error +^^^^^ +.. note:: New in version 3 + +This message may be silently ignored (and therefor handled like any other +"unknown" message). + +The message is intended to inform the other node about protocol errors and +can be used for debugging and improving code. + +.. list-table:: + :header-rows: 1 + :widths: auto + + * - Field Size + - Description + - Data type + - Comments + * - 1+ + - fatal + - |var_int| + - This qualifies the error. If set to 0, than its just a "warning". + You can expect, everything still worked fine. If set to 1, than + it's an error, so you may expect, something was going wrong + (e.g. an object got lost). If set to 2, it's a fatal error. The node + will drop the line for that error and maybe ban you for some time. + * - 1+ + - ban time + - var_int + - If the error is fatal, you can specify the ban time in seconds, here. + You inform the other node, that you will not accept further connections + for this number of seconds. For non fatal errors this field has + no meaning and should be zero. + * - 1+ + - inventory vector + - var_str + - If the error is related to an object, this Variable length string + contains the inventory vector of that object. If the error is not + related to an object, this string is empty. + * - 1+ + - error text + - var_str + - A human readable string in English, which describes the error. + + +object +^^^^^^ + +An object is a message which is shared throughout a stream. It is the only +message which propagates; all others are only between two nodes. Objects have a +type, like 'msg', or 'broadcast'. To be a valid object, the +:doc:`pow` must be done. The maximum allowable length of an object +(not to be confused with the ``objectPayload``) is |2^18| bytes. + +.. |2^18| replace:: 2\ :sup:`18`\ + +.. list-table:: Message structure + :header-rows: 1 + :widths: auto + + * - Field Size + - Description + - Data type + - Comments + * - 8 + - nonce + - uint64_t + - Random nonce used for the :doc:`pow` + * - 8 + - expiresTime + - uint64_t + - The "end of life" time of this object (be aware, in version 2 of the + protocol this was the generation time). Objects shall be shared with + peers until its end-of-life time has been reached. The node should store + the inventory vector of that object for some extra period of time to + avoid reloading it from another node with a small time delay. The time + may be no further than 28 days + 3 hours in the future. + * - 4 + - objectType + - uint32_t + - Four values are currently defined: 0-"getpubkey", 1-"pubkey", 2-"msg", + 3-"broadcast". All other values are reserved. Nodes should relay objects + even if they use an undefined object type. + * - 1+ + - version + - var_int + - The object's version. Note that msg objects won't contain a version + until Sun, 16 Nov 2014 22:00:00 GMT. + * - 1+ + - stream number + - var_int + - The stream number in which this object may propagate + * - ? + - objectPayload + - uchar[] + - This field varies depending on the object type; see below. + + +Unsupported messages +^^^^^^^^^^^^^^^^^^^^ + +If a node receives an unknown message it **must** silently ignore it. This is +for further extensions of the protocol with other messages. Nodes that don't +understand such a new message type shall be able to work correct with the +message types they understand. + +Maybe some version 2 nodes did already implement it that way, but in version 3 +it is **part of the protocol specification**, that a node **must** +silently ignore unsupported messages. + + +Object types +------------ + +Here are the payloads for various object types. + +getpubkey +^^^^^^^^^ + +When a node has the hash of a public key (from an address) but not the public +key itself, it must send out a request for the public key. + +.. list-table:: + :header-rows: 1 + :widths: auto + + * - Field Size + - Description + - Data type + - Comments + * - 20 + - ripe + - uchar[] + - The ripemd hash of the public key. This field is only included when the + address version is <= 3. + * - 32 + - tag + - uchar[] + - The tag derived from the address version, stream number, and ripe. This + field is only included when the address version is >= 4. + +pubkey +^^^^^^ + +A version 2 pubkey. This is still in use and supported by current clients but +*new* v2 addresses are not generated by clients. + +.. list-table:: + :header-rows: 1 + :widths: auto + + * - Field Size + - Description + - Data type + - Comments + * - 4 + - |behavior_bitfield| + - uint32_t + - A bitfield of optional behaviors and features that can be expected from + the node receiving the message. + * - 64 + - public signing key + - uchar[] + - The ECC public key used for signing (uncompressed format; + normally prepended with \x04 ) + * - 64 + - public encryption key + - uchar[] + - The ECC public key used for encryption (uncompressed format; + normally prepended with \x04 ) + +.. list-table:: A version 3 pubkey + :header-rows: 1 + :widths: auto + + * - Field Size + - Description + - Data type + - Comments + * - 4 + - |behavior_bitfield| + - uint32_t + - A bitfield of optional behaviors and features that can be expected from + the node receiving the message. + * - 64 + - public signing key + - uchar[] + - The ECC public key used for signing (uncompressed format; + normally prepended with \x04 ) + * - 64 + - public encryption key + - uchar[] + - The ECC public key used for encryption (uncompressed format; + normally prepended with \x04 ) + * - 1+ + - nonce_trials_per_byte + - var_int + - Used to calculate the difficulty target of messages accepted by this + node. The higher this value, the more difficult the Proof of Work must + be before this individual will accept the message. This number is the + average number of nonce trials a node will have to perform to meet the + Proof of Work requirement. 1000 is the network minimum so any lower + values will be automatically raised to 1000. + * - 1+ + - extra_bytes + - var_int + - Used to calculate the difficulty target of messages accepted by this + node. The higher this value, the more difficult the Proof of Work must + be before this individual will accept the message. This number is added + to the data length to make sending small messages more difficult. + 1000 is the network minimum so any lower values will be automatically + raised to 1000. + * - 1+ + - sig_length + - var_int + - Length of the signature + * - sig_length + - signature + - uchar[] + - The ECDSA signature which, as of protocol v3, covers the object + header starting with the time, appended with the data described in this + table down to the extra_bytes. + +.. list-table:: A version 4 pubkey + :header-rows: 1 + :widths: auto + + * - Field Size + - Description + - Data type + - Comments + * - 32 + - tag + - uchar[] + - The tag, made up of bytes 32-64 of the double hash of the address data + (see example python code below) + * - ? + - encrypted + - uchar[] + - Encrypted pubkey data. + +When version 4 pubkeys are created, most of the data in the pubkey is encrypted. +This is done in such a way that only someone who has the Bitmessage address +which corresponds to a pubkey can decrypt and use that pubkey. This prevents +people from gathering pubkeys sent around the network and using the data from +them to create messages to be used in spam or in flooding attacks. + +In order to encrypt the pubkey data, a double SHA-512 hash is calculated from +the address version number, stream number, and ripe hash of the Bitmessage +address that the pubkey corresponds to. The first 32 bytes of this hash are used +to create a public and private key pair with which to encrypt and decrypt the +pubkey data, using the same algorithm as message encryption +(see :doc:`encryption`). The remaining 32 bytes of this hash are added to the +unencrypted part of the pubkey and used as a tag, as above. This allows nodes to +determine which pubkey to decrypt when they wish to send a message. + +In PyBitmessage, the double hash of the address data is calculated using the +python code below: + +.. code-block:: python + + doubleHashOfAddressData = hashlib.sha512(hashlib.sha512( + encodeVarint(addressVersionNumber) + encodeVarint(streamNumber) + hash + ).digest()).digest() + + +.. list-table:: Encrypted data in version 4 pubkeys: + :header-rows: 1 + :widths: auto + + * - Field Size + - Description + - Data type + - Comments + * - 4 + - |behavior_bitfield| + - uint32_t + - A bitfield of optional behaviors and features that can be expected from + the node receiving the message. + * - 64 + - public signing key + - uchar[] + - The ECC public key used for signing (uncompressed format; + normally prepended with \x04 ) + * - 64 + - public encryption key + - uchar[] + - The ECC public key used for encryption (uncompressed format; + normally prepended with \x04 ) + * - 1+ + - nonce_trials_per_byte + - var_int + - Used to calculate the difficulty target of messages accepted by this + node. The higher this value, the more difficult the Proof of Work must + be before this individual will accept the message. This number is the + average number of nonce trials a node will have to perform to meet the + Proof of Work requirement. 1000 is the network minimum so any lower + values will be automatically raised to 1000. + * - 1+ + - extra_bytes + - var_int + - Used to calculate the difficulty target of messages accepted by this + node. The higher this value, the more difficult the Proof of Work must + be before this individual will accept the message. This number is added + to the data length to make sending small messages more difficult. + 1000 is the network minimum so any lower values will be automatically + raised to 1000. + * - 1+ + - sig_length + - var_int + - Length of the signature + * - sig_length + - signature + - uchar[] + - The ECDSA signature which covers everything from the object header + starting with the time, then appended with the decrypted data down to + the extra_bytes. This was changed in protocol v3. + +msg +^^^ + +Used for person-to-person messages. Note that msg objects won't contain a +version in the object header until Sun, 16 Nov 2014 22:00:00 GMT. + +.. list-table:: + :header-rows: 1 + :widths: auto + + * - Field Size + - Description + - Data type + - Comments + * - ? + - encrypted + - uchar[] + - Encrypted data. See `Encrypted payload`_. + See also `Unencrypted Message Data`_ + +broadcast +^^^^^^^^^ + +Users who are subscribed to the sending address will see the message appear in +their inbox. Broadcasts are version 4 or 5. + +Pubkey objects and v5 broadcast objects are encrypted the same way: The data +encoded in the sender's Bitmessage address is hashed twice. The first 32 bytes +of the resulting hash constitutes the "private" encryption key and the last +32 bytes constitute a **tag** so that anyone listening can easily decide if +this particular message is interesting. The sender calculates the public key +from the private key and then encrypts the object with this public key. Thus +anyone who knows the Bitmessage address of the sender of a broadcast or pubkey +object can decrypt it. + +The version of broadcast objects was previously 2 or 3 but was changed to 4 or +5 for protocol v3. Having a broadcast version of 5 indicates that a tag is used +which, in turn, is used when the sender's address version is >=4. + +.. list-table:: + :header-rows: 1 + :widths: auto + + * - Field Size + - Description + - Data type + - Comments + * - 32 + - tag + - uchar[] + - The tag. This field is new and only included when the broadcast version + is >= 5. Changed in protocol v3 + * - ? + - encrypted + - uchar[] + - Encrypted broadcast data. The keys are derived as described in the + paragraph above. See Encrypted payload for details about the encryption + algorithm itself. + +Unencrypted data format: + +.. list-table:: + :header-rows: 1 + :widths: auto + + * - Field Size + - Description + - Data type + - Comments + * - 1+ + - broadcast version + - var_int + - The version number of this broadcast protocol message which is equal + to 2 or 3. This is included here so that it can be signed. This is + no longer included in protocol v3 + * - 1+ + - address version + - var_int + - The sender's address version + * - 1+ + - stream number + - var_int + - The sender's stream number + * - 4 + - |behavior_bitfield| + - uint32_t + - A bitfield of optional behaviors and features that can be expected from + the owner of this pubkey. + * - 64 + - public signing key + - uchar[] + - The ECC public key used for signing (uncompressed format; + normally prepended with \x04) + * - 64 + - public encryption key + - uchar[] + - The ECC public key used for encryption (uncompressed format; + normally prepended with \x04) + * - 1+ + - nonce_trials_per_byte + - var_int + - Used to calculate the difficulty target of messages accepted by this + node. The higher this value, the more difficult the Proof of Work must + be before this individual will accept the message. This number is the + average number of nonce trials a node will have to perform to meet the + Proof of Work requirement. 1000 is the network minimum so any lower + values will be automatically raised to 1000. This field is new and is + only included when the address_version >= 3. + * - 1+ + - extra_bytes + - var_int + - Used to calculate the difficulty target of messages accepted by this + node. The higher this value, the more difficult the Proof of Work must + be before this individual will accept the message. This number is added + to the data length to make sending small messages more difficult. + 1000 is the network minimum so any lower values will be automatically + raised to 1000. This field is new and is only included when the + address_version >= 3. + * - 1+ + - encoding + - var_int + - The encoding type of the message + * - 1+ + - messageLength + - var_int + - The message length in bytes + * - messageLength + - message + - uchar[] + - The message + * - 1+ + - sig_length + - var_int + - Length of the signature + * - sig_length + - signature + - uchar[] + - The signature which did cover the unencrypted data from the broadcast + version down through the message. In protocol v3, it covers the + unencrypted object header starting with the time, all appended with + the decrypted data. + +.. |behavior_bitfield| replace:: :ref:`behavior bitfield ` diff --git a/docs/requirements.txt b/docs/requirements.txt index a62bf415..f8b4b17c 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,5 @@ -m2r +mistune<=0.8.4 +m2r<=0.2.1 +sphinx_rtd_theme sphinxcontrib-apidoc docutils<=0.17.1 diff --git a/docs/useragent.rst b/docs/useragent.rst new file mode 100644 index 00000000..3523a274 --- /dev/null +++ b/docs/useragent.rst @@ -0,0 +1,53 @@ +User Agent +========== + +Bitmessage user agents are a modified browser user agent with more structure +to aid parsers and provide some coherence. The user agent strings are arranged +in a stack with the most underlying software listed first. + +Basic format:: + + /Name:Version/Name:Version/.../ + +Example:: + + /PyBitmessage:0.2.2/Corporate Mail System:0.8/ + /Surdo:5.64/surdo-qt:0.4/ + +The version numbers are not defined to any strict format, although this guide +recommends: + + * Version numbers in the form of Major.Minor.Revision (2.6.41) + * Repository builds using a date in the format of YYYYMMDD (20110128) + +For git repository builds, implementations are free to use the git commitish. +However the issue lies in that it is not immediately obvious without the +repository which version preceeds another. For this reason, we lightly +recommend dates in the format specified above, although this is by no means +a requirement. + +Optional ``-r1``, ``-r2``, ... can be appended to user agent version numbers. +This is another light recommendation, but not a requirement. Implementations +are free to specify version numbers in whatever format needed insofar as it +does not include ``(``, ``)``, ``:`` or ``/`` to interfere with the user agent +syntax. + +An optional comments field after the version number is also allowed. Comments +should be delimited by parenthesis ``(...)``. The contents of comments is +entirely implementation defined although this document recommends the use of +semi-colons ``;`` as a delimiter between pieces of information. + +Example:: + + /cBitmessage:0.2(iPad; U; CPU OS 3_2_1)/AndroidBuild:0.8/ + +Reserved symbols are therefore: ``/ : ( )`` + +They should not be misused beyond what is specified in this section. + +``/`` + separates the code-stack +``:`` + specifies the implementation version of the particular stack +``( and )`` + delimits a comment which optionally separates data using ``;`` diff --git a/kivy-requirements.txt b/kivy-requirements.txt index 8d506a5d..185a3ae7 100644 --- a/kivy-requirements.txt +++ b/kivy-requirements.txt @@ -1,5 +1,11 @@ kivy-garden.qrcode --e git+https://github.com/kivymd/KivyMD#egg=kivymd +kivymd==1.0.2 +kivy==2.1.0 opencv-python pyzbar -telenium \ No newline at end of file +git+https://github.com/tito/telenium@9b54ff1#egg=telenium +Pillow==9.4.0 +jaraco.collections==3.8.0 +jaraco.classes==3.2.3 +pytz==2022.7.1 +pydantic==1.10.6 diff --git a/packages/AppImage/AppImageBuilder.yml b/packages/AppImage/AppImageBuilder.yml new file mode 100644 index 00000000..2c5890d2 --- /dev/null +++ b/packages/AppImage/AppImageBuilder.yml @@ -0,0 +1,81 @@ +version: 1 +script: + # Remove any previous build + - rm -rf AppDir | true + - python setup.py install --prefix=/usr --root=AppDir + +AppDir: + path: ./AppDir + + app_info: + id: pybitmessage + name: PyBitmessage + icon: pybitmessage + version: !ENV ${APP_VERSION} + # Set the python executable as entry point + exec: usr/bin/python + # Set the application main script path as argument. + # Use '$@' to forward CLI parameters + exec_args: "$APPDIR/usr/bin/pybitmessage $@" + + after_runtime: + - sed -i "s|GTK_.*||g" AppDir/AppRun.env + - cp packages/AppImage/qt.conf AppDir/usr/bin/ + + apt: + arch: !ENV '${ARCH}' + sources: + - sourceline: !ENV '${SOURCELINE}' + key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32' + + include: + - python-defusedxml + - python-jsonrpclib + - python-msgpack + - python-qrcode + - python-qt4 + - python-setuptools + - python-sip + - python-six + - python-xdg + - sni-qt + exclude: + - libdb5.3 + - libdbus-1-3 + - libfontconfig1 + - libfreetype6 + - libglib2.0-0 + - libice6 + - libmng2 + - libncursesw5 + - libqt4-declarative + - libqt4-designer + - libqt4-help + - libqt4-script + - libqt4-scripttools + - libqt4-sql + - libqt4-test + - libqt4-xmlpatterns + - libqtassistantclient4 + - libsm6 + - libsystemd0 + - libreadline7 + + files: + exclude: + - usr/lib/x86_64-linux-gnu/gconv + - usr/share/man + - usr/share/doc + + runtime: + arch: [ !ENV '${RUNTIME}' ] + env: + # Set python home + # See https://docs.python.org/3/using/cmdline.html#envvar-PYTHONHOME + PYTHONHOME: '${APPDIR}/usr' + # Path to the site-packages dir or other modules dirs + # See https://docs.python.org/3/using/cmdline.html#envvar-PYTHONPATH + PYTHONPATH: '${APPDIR}/usr/lib/python2.7/site-packages' + +AppImage: + arch: !ENV '${APPIMAGE_ARCH}' diff --git a/packages/AppImage/PyBitmessage.yml b/packages/AppImage/PyBitmessage.yml index 1738da08..e9d23a4b 100644 --- a/packages/AppImage/PyBitmessage.yml +++ b/packages/AppImage/PyBitmessage.yml @@ -15,9 +15,12 @@ ingredients: - python-setuptools - python-sip - python-six + - python-xdg - sni-qt - xkb-data exclude: + - libdb5.3 + - libglib2.0-0 - libmng2 - libncursesw5 - libqt4-declarative @@ -26,6 +29,7 @@ ingredients: - libqt4-script - libqt4-scripttools - libqt4-sql + - libqt4-test - libqt4-xmlpatterns - libqtassistantclient4 - libreadline7 @@ -33,5 +37,6 @@ ingredients: - ../deb_dist/pybitmessage_*_amd64.deb script: + - rm -rf usr/share/glib-2.0/schemas - cp usr/share/icons/hicolor/scalable/apps/pybitmessage.svg . - mv usr/bin/python2.7 usr/bin/python2 diff --git a/packages/AppImage/qt.conf b/packages/AppImage/qt.conf new file mode 100644 index 00000000..0e343236 --- /dev/null +++ b/packages/AppImage/qt.conf @@ -0,0 +1,2 @@ +[Paths] +Prefix = ../lib/x86_64-linux-gnu/qt4 diff --git a/packages/android/buildozer.spec b/packages/android/buildozer.spec new file mode 100644 index 00000000..c98717b4 --- /dev/null +++ b/packages/android/buildozer.spec @@ -0,0 +1,357 @@ +[app] + +# (str) Title of your application +title = mockone + +# (str) Package name +package.name = mock + +# (str) Package domain (needed for android/ios packaging) +package.domain = org.mock + +# (str) Source code where the main.py live +source.dir = ../../src + +# (list) Source files to include (let empty to include all the files) +source.include_exts = py,png,jpg,kv,atlas,tflite,sql + +# (list) List of inclusions using pattern matching +#source.include_patterns = assets/*,images/*.png + +# (list) Source files to exclude (let empty to not exclude anything) +#source.exclude_exts = spec + +# (list) List of directory to exclude (let empty to not exclude anything) +#source.exclude_dirs = tests, bin, venv + +# (list) List of exclusions using pattern matching +#source.exclude_patterns = license,images/*/*.jpg + +# (str) Application versioning (method 1) +version = 0.1 + +# (str) Application versioning (method 2) +# version.regex = __version__ = ['"](.*)['"] +# version.filename = %(source.dir)s/main.py + +# (list) Application requirements +# comma separated e.g. requirements = sqlite3,kivy +requirements = python3,kivy + +# (str) Custom source folders for requirements +# Sets custom source for any requirements with recipes +# requirements.source.kivy = ../../kivy + +# (str) Presplash of the application +#presplash.filename = %(source.dir)s/data/presplash.png + +# (str) Icon of the application +#icon.filename = %(source.dir)s/data/icon.png + +# (str) Supported orientation (one of landscape, sensorLandscape, portrait or all) +orientation = portrait + +# (list) List of service to declare +#services = NAME:ENTRYPOINT_TO_PY,NAME2:ENTRYPOINT2_TO_PY + +# +# OSX Specific +# + +# +# author = © Copyright Info + +# change the major version of python used by the app +osx.python_version = 3 + +# Kivy version to use +osx.kivy_version = 1.9.1 + +# +# Android specific +# + +# (bool) Indicate if the application should be fullscreen or not +fullscreen = 0 + +# (string) Presplash background color (for android toolchain) +# Supported formats are: #RRGGBB #AARRGGBB or one of the following names: +# red, blue, green, black, white, gray, cyan, magenta, yellow, lightgray, +# darkgray, grey, lightgrey, darkgrey, aqua, fuchsia, lime, maroon, navy, +# olive, purple, silver, teal. +#android.presplash_color = #FFFFFF + +# (string) Presplash animation using Lottie format. +# see https://lottiefiles.com/ for examples and https://airbnb.design/lottie/ +# for general documentation. +# Lottie files can be created using various tools, like Adobe After Effect or Synfig. +#android.presplash_lottie = "path/to/lottie/file.json" + +# (list) Permissions +#android.permissions = INTERNET + +# (int) Android API to use (targetSdkVersion AND compileSdkVersion) +# note: when changing, Dockerfile also needs to be changed to install corresponding build tools +android.api = 28 + +# (int) Minimum API required. You will need to set the android.ndk_api to be as low as this value. +android.minapi = 21 + +# (str) Android NDK version to use +android.ndk = 25b + +# (int) Android NDK API to use (optional). This is the minimum API your app will support. +android.ndk_api = 21 + +# (bool) Use --private data storage (True) or --dir public storage (False) +android.private_storage = True + +# (str) Android NDK directory (if empty, it will be automatically downloaded.) +android.ndk_path = /opt/android/android-ndk + +# (str) Android SDK directory (if empty, it will be automatically downloaded.) +android.sdk_path = /opt/android/android-sdk + +# (str) ANT directory (if empty, it will be automatically downloaded.) +android.ant_path = /opt/android/apache-ant + +# (bool) If True, then skip trying to update the Android sdk +# This can be useful to avoid excess Internet downloads or save time +# when an update is due and you just want to test/build your package +# android.skip_update = False + +# (bool) If True, then automatically accept SDK license +# agreements. This is intended for automation only. If set to False, +# the default, you will be shown the license when first running +# buildozer. +# android.accept_sdk_license = False + +# (str) Android entry point, default is ok for Kivy-based app +#android.entrypoint = org.renpy.android.PythonActivity + +# (str) Android app theme, default is ok for Kivy-based app +# android.apptheme = "@android:style/Theme.NoTitleBar" + +# (list) Pattern to whitelist for the whole project +#android.whitelist = + +# (str) Path to a custom whitelist file +#android.whitelist_src = + +# (str) Path to a custom blacklist file +#android.blacklist_src = + +# (list) List of Java .jar files to add to the libs so that pyjnius can access +# their classes. Don't add jars that you do not need, since extra jars can slow +# down the build process. Allows.build wildcards matching, for example: +# OUYA-ODK/libs/*.jar +#android.add_jars = foo.jar,bar.jar,path/to/more/*.jar + +# (list) List of Java files to add to the android project (can be java or a +# directory containing the files) +#android.add_src = + +# (list) Android AAR archives to add +#android.add_aars = + +# (list) Gradle dependencies to add +#android.gradle_dependencies = + +# (list) add java compile options +# this can for example be necessary when importing certain java libraries using the 'android.gradle_dependencies' option +# see https://developer.android.com/studio/write/java8-support for further information +#android.add_compile_options = "sourceCompatibility = 1.8", "targetCompatibility = 1.8" + +# (list) Gradle repositories to add {can be necessary for some android.gradle_dependencies} +# please enclose in double quotes +# e.g. android.gradle_repositories = "maven { url 'https://kotlin.bintray.com/ktor' }" +#android.add_gradle_repositories = +#android.gradle_dependencies = "org.tensorflow:tensorflow-lite:+","org.tensorflow:tensorflow-lite-support:0.0.0-nightly" + +# (list) packaging options to add +# see https://google.github.io/android-gradle-dsl/current/com.android.build.gradle.internal.dsl.PackagingOptions.html +# can be necessary to solve conflicts in gradle_dependencies +# please enclose in double quotes +# e.g. android.add_packaging_options = "exclude 'META-INF/common.kotlin_module'", "exclude 'META-INF/*.kotlin_module'" +#android.add_packaging_options = + +# (list) Java classes to add as activities to the manifest. +#android.add_activities = com.example.ExampleActivity + +# (str) OUYA Console category. Should be one of GAME or APP +# If you leave this blank, OUYA support will not be enabled +#android.ouya.category = GAME + +# (str) Filename of OUYA Console icon. It must be a 732x412 png image. +#android.ouya.icon.filename = %(source.dir)s/data/ouya_icon.png + +# (str) XML file to include as an intent filters in tag +#android.manifest.intent_filters = + +# (str) launchMode to set for the main activity +#android.manifest.launch_mode = standard + +# (list) Android additional libraries to copy into libs/armeabi +#android.add_libs_armeabi = libs/android/*.so +#android.add_libs_armeabi_v7a = libs/android-v7/*.so +#android.add_libs_arm64_v8a = libs/android-v8/*.so +#android.add_libs_x86 = libs/android-x86/*.so +#android.add_libs_mips = libs/android-mips/*.so + +# (bool) Indicate whether the screen should stay on +# Don't forget to add the WAKE_LOCK permission if you set this to True +#android.wakelock = False + +# (list) Android application meta-data to set (key=value format) +#android.meta_data = + +# (list) Android library project to add (will be added in the +# project.properties automatically.) +#android.library_references = + +# (list) Android shared libraries which will be added to AndroidManifest.xml using tag +#android.uses_library = + +# (str) Android logcat filters to use +#android.logcat_filters = *:S python:D + +# (bool) Android logcat only display log for activity's pid +#android.logcat_pid_only = False + +# (str) Android additional adb arguments +#android.adb_args = -H host.docker.internal + +# (bool) Copy library instead of making a libpymodules.so +#android.copy_libs = 1 + +# (str) The Android arch to build for, choices: armeabi-v7a, arm64-v8a, x86, x86_64 +android.archs = armeabi-v7a, arm64-v8a, x86_64 + +# (int) overrides automatic versionCode computation (used in build.gradle) +# this is not the same as app version and should only be edited if you know what you're doing +# android.numeric_version = 1 + +# (bool) enables Android auto backup feature (Android API >=23) +android.allow_backup = True + +# (str) XML file for custom backup rules (see official auto backup documentation) +# android.backup_rules = + +# (str) If you need to insert variables into your AndroidManifest.xml file, +# you can do so with the manifestPlaceholders property. +# This property takes a map of key-value pairs. (via a string) +# Usage example : android.manifest_placeholders = [myCustomUrl:\"org.kivy.customurl\"] +# android.manifest_placeholders = [:] + +# +# Python for android (p4a) specific +# + +# (str) python-for-android fork to use, defaults to upstream (kivy) +#p4a.fork = kivy + +# (str) python-for-android branch to use, defaults to master +#p4a.branch = master + +# (str) python-for-android git clone directory (if empty, it will be automatically cloned from github) +#p4a.source_dir = + +# (str) The directory in which python-for-android should look for your own build recipes (if any) +#p4a.local_recipes = + +# (str) Filename to the hook for p4a +#p4a.hook = + +# (str) Bootstrap to use for android builds +# p4a.bootstrap = sdl2 + +# (int) port number to specify an explicit --port= p4a argument (eg for bootstrap flask) +#p4a.port = + +# Control passing the --use-setup-py vs --ignore-setup-py to p4a +# "in the future" --use-setup-py is going to be the default behaviour in p4a, right now it is not +# Setting this to false will pass --ignore-setup-py, true will pass --use-setup-py +# NOTE: this is general setuptools integration, having pyproject.toml is enough, no need to generate +# setup.py if you're using Poetry, but you need to add "toml" to source.include_exts. +#p4a.setup_py = false + + +# +# iOS specific +# + +# (str) Path to a custom kivy-ios folder +#ios.kivy_ios_dir = ../kivy-ios +# Alternately, specify the URL and branch of a git checkout: +ios.kivy_ios_url = https://github.com/kivy/kivy-ios +ios.kivy_ios_branch = master + +# Another platform dependency: ios-deploy +# Uncomment to use a custom checkout +#ios.ios_deploy_dir = ../ios_deploy +# Or specify URL and branch +ios.ios_deploy_url = https://github.com/phonegap/ios-deploy +ios.ios_deploy_branch = 1.10.0 + +# (bool) Whether or not to sign the code +ios.codesign.allowed = false + +# (str) Name of the certificate to use for signing the debug version +# Get a list of available identities: buildozer ios list_identities +#ios.codesign.debug = "iPhone Developer: ()" + +# (str) Name of the certificate to use for signing the release version +#ios.codesign.release = %(ios.codesign.debug)s + + +[buildozer] + +# (int) Log level (0 = error only, 1 = info, 2 = debug (with command output)) +log_level = 2 + +# (int) Display warning if buildozer is run as root (0 = False, 1 = True) +warn_on_root = 1 + +# (str) Path to build artifact storage, absolute or relative to spec file +# build_dir = ./.buildozer + +# (str) Path to build output (i.e. .apk, .ipa) storage +# bin_dir = ./bin + +# ----------------------------------------------------------------------------- +# List as sections +# +# You can define all the "list" as [section:key]. +# Each line will be considered as a option to the list. +# Let's take [app] / source.exclude_patterns. +# Instead of doing: +# +#[app] +#source.exclude_patterns = license,data/audio/*.wav,data/images/original/* +# +# This can be translated into: +# +#[app:source.exclude_patterns] +#license +#data/audio/*.wav +#data/images/original/* +# + + +# ----------------------------------------------------------------------------- +# Profiles +# +# You can extend section / key with a profile +# For example, you want to deploy a demo version of your application without +# HD content. You could first change the title to add "(demo)" in the name +# and extend the excluded directories to remove the HD content. +# +#[app@demo] +#title = My Application (demo) +# +#[app:source.exclude_patterns@demo] +#images/hd/* +# +# Then, invoke the command line with the "demo" profile: +# +#buildozer --profile demo android debug diff --git a/packages/docker/Dockerfile.bionic b/packages/docker/Dockerfile.bionic index e2b7288c..ff53e4e7 100644 --- a/packages/docker/Dockerfile.bionic +++ b/packages/docker/Dockerfile.bionic @@ -1,62 +1,45 @@ FROM ubuntu:bionic AS base ENV DEBIAN_FRONTEND noninteractive -ENV TRAVIS_SKIP_APT_UPDATE 1 - -ENV LANG en_US.UTF-8 -ENV LANGUAGE en_US:en -ENV LC_ALL en_US.UTF-8 RUN apt-get update +# Common apt packages RUN apt-get install -yq --no-install-suggests --no-install-recommends \ - software-properties-common + software-properties-common build-essential libcap-dev libssl-dev \ + python-all-dev python-setuptools wget xvfb -RUN dpkg --add-architecture i386 +############################################################################### -RUN add-apt-repository ppa:deadsnakes/ppa - -RUN apt-get -y install sudo +FROM base AS appimage RUN apt-get install -yq --no-install-suggests --no-install-recommends \ - # travis xenial bionic - python-setuptools libssl-dev python-prctl \ - python-dev python-virtualenv python-pip virtualenv \ - # dpkg - python-minimal python-all python openssl libssl-dev \ - dh-apparmor debhelper dh-python python-msgpack python-qt4 git python-stdeb \ - python-all-dev python-crypto python-psutil \ - fakeroot python-pytest python3-wheel \ - libglib2.0-dev \ - # Code quality - pylint python-pycodestyle python3-pycodestyle pycodestyle python-flake8 \ - python3-flake8 flake8 python-pyflakes python3-pyflakes pyflakes pyflakes3 \ - curl \ - # Wine - python python-pip wget wine-stable winetricks mingw-w64 wine32 wine64 xvfb \ - # Buildbot - python3-dev libffi-dev python3-setuptools \ - python3-pip \ - # python 3.7 - python3.7 python3.7-dev \ - # .travis.yml - build-essential libcap-dev tor \ - language-pack-en + debhelper dh-apparmor dh-python python-stdeb fakeroot +COPY . /home/builder/src -# cleanup -RUN rm -rf /var/lib/apt/lists/* +WORKDIR /home/builder/src -##################################################################################################### +CMD python setup.py sdist \ + && python setup.py --command-packages=stdeb.command bdist_deb \ + && dpkg-deb -I deb_dist/*.deb \ + && cp deb_dist/*.deb /dist/ \ + && ln -s /dist out \ + && buildscripts/appimage.sh -FROM base AS travis +############################################################################### -# travis2bash -RUN wget -O /usr/local/bin/travis2bash.sh https://git.bitmessage.org/Bitmessage/buildbot-scripts/raw/branch/master/travis2bash.sh -RUN chmod +x /usr/local/bin/travis2bash.sh +FROM base AS tox + +RUN apt-get install -yq --no-install-suggests --no-install-recommends \ + language-pack-en \ + libffi-dev python3-dev python3-pip python3.8 python3.8-dev python3.8-venv \ + python-msgpack python-pip python-qt4 python-six qt5dxcb-plugin tor + +RUN python3.8 -m pip install setuptools wheel +RUN python3.8 -m pip install --upgrade pip tox virtualenv RUN useradd -m -U builder -RUN echo 'builder ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers # copy sources COPY . /home/builder/src @@ -64,14 +47,51 @@ RUN chown -R builder.builder /home/builder/src USER builder +ENV LANG en_US.UTF-8 +ENV LANGUAGE en_US:en +ENV LC_ALL en_US.UTF-8 + WORKDIR /home/builder/src -ENTRYPOINT /usr/local/bin/travis2bash.sh +ENTRYPOINT ["tox"] -##################################################################################################### +############################################################################### + +FROM base AS snap + +RUN apt-get install -yq --no-install-suggests --no-install-recommends snapcraft + +COPY . /home/builder/src + +WORKDIR /home/builder/src + +CMD cd packages && snapcraft && cp *.snap /dist/ + +############################################################################### + +FROM base AS winebuild + +RUN dpkg --add-architecture i386 +RUN apt-get update + +RUN apt-get install -yq --no-install-suggests --no-install-recommends \ + mingw-w64 wine-stable winetricks wine32 wine64 + +COPY . /home/builder/src + +WORKDIR /home/builder/src + +# xvfb-run -a buildscripts/winbuild.sh +CMD xvfb-run -a i386 buildscripts/winbuild.sh \ + && cp packages/pyinstaller/dist/*.exe /dist/ + +############################################################################### FROM base AS buildbot +# cleanup +RUN rm -rf /var/lib/apt/lists/* + # travis2bash RUN wget -O /usr/local/bin/travis2bash.sh https://git.bitmessage.org/Bitmessage/buildbot-scripts/raw/branch/master/travis2bash.sh RUN chmod +x /usr/local/bin/travis2bash.sh @@ -87,22 +107,7 @@ USER buildbot ENTRYPOINT /entrypoint.sh "$BUILDMASTER" "$WORKERNAME" "$WORKERPASS" -################################################################################################# - -FROM base AS appimage - -COPY . /home/builder/src - -WORKDIR /home/builder/src - -RUN VERSION=$(python setup.py -V) \ - && python setup.py sdist \ - && python setup.py --command-packages=stdeb.command bdist_deb \ - && dpkg-deb -I deb_dist/pybitmessage_${VERSION}-1_amd64.deb - -RUN buildscripts/appimage.sh -RUN VERSION=$(python setup.py -V) \ - && out/PyBitmessage-${VERSION}.glibc2.15-x86_64.AppImage --appimage-extract-and-run -t +############################################################################### FROM base AS appandroid diff --git a/packages/docker/launcher.sh b/packages/docker/launcher.sh new file mode 100755 index 00000000..c0e48855 --- /dev/null +++ b/packages/docker/launcher.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# Setup the environment for docker container +APIUSER=${USER:-api} +APIPASS=${PASSWORD:-$(tr -dc a-zA-Z0-9 < /dev/urandom | head -c32 && echo)} + +echo "\napiusername: $APIUSER\napipassword: $APIPASS" + +sed -i -e "s|\(apiinterface = \).*|\10\.0\.0\.0|g" \ + -e "s|\(apivariant = \).*|\1json|g" \ + -e "s|\(apiusername = \).*|\1$APIUSER|g" \ + -e "s|\(apipassword = \).*|\1$APIPASS|g" \ + -e "s|apinotifypath = .*||g" ${BITMESSAGE_HOME}/keys.dat + +# Run +exec pybitmessage "$@" diff --git a/packages/pyinstaller/bitmessagemain.spec b/packages/pyinstaller/bitmessagemain.spec index 739d0424..1c564ffe 100644 --- a/packages/pyinstaller/bitmessagemain.spec +++ b/packages/pyinstaller/bitmessagemain.spec @@ -6,6 +6,8 @@ import time from PyInstaller.utils.hooks import copy_metadata + +DEBUG = False site_root = os.path.abspath(HOMEPATH) spec_root = os.path.abspath(SPECPATH) arch = 32 if ctypes.sizeof(ctypes.c_voidp) == 4 else 64 @@ -22,13 +24,17 @@ os.chdir(srcPath) snapshot = False -hookspath=os.path.join(spec_root, 'hooks') +hookspath = os.path.join(spec_root, 'hooks') + +excludes = ['bsddb', 'bz2', 'tcl', 'tk', 'Tkinter', 'tests'] +if not DEBUG: + excludes += ['pybitmessage.tests', 'pyelliptic.tests'] a = Analysis( [os.path.join(srcPath, 'bitmessagemain.py')], - datas = [ + datas=[ (os.path.join(spec_root[:-20], 'pybitmessage.egg-info') + '/*', - 'pybitmessage.egg-info') + 'pybitmessage.egg-info') ] + copy_metadata('msgpack-python') + copy_metadata('qrcode') + copy_metadata('six') + copy_metadata('stem'), pathex=[outPath], @@ -38,16 +44,15 @@ a = Analysis( 'plugins.menu_qrcode', 'plugins.proxyconfig_stem' ], # https://github.com/pyinstaller/pyinstaller/wiki/Recipe-PyQt4-API-Version - runtime_hooks=[ + runtime_hooks = [ os.path.join(hookspath, hook) for hook in ( 'pyinstaller_rthook_pyqt4.py', 'pyinstaller_rthook_plugins.py' )], - excludes=[ - 'bsddb', 'bz2', - 'PyQt4.QtOpenGL', 'PyQt4.QtOpenGL', 'PyQt4.QtSql', + excludes += [ + 'PyQt4.QtOpenGL','PyQt4.QtSql', 'PyQt4.QtSvg', 'PyQt4.QtTest', 'PyQt4.QtWebKit', 'PyQt4.QtXml', - 'tcl', 'tk', 'Tkinter', 'win32ui', 'tests'] + 'win32ui'] ) @@ -81,9 +86,16 @@ a.datas += [ for file_ in os.listdir(dir_append) if file_.endswith('.ui') ] +sql_dir = os.path.join(srcPath, 'sql') + +a.datas += [ + (os.path.join('sql', file_), os.path.join(sql_dir, file_), 'DATA') + for file_ in os.listdir(sql_dir) if file_.endswith('.sql') +] + # append the translations directory a.datas += addTranslations() - +a.datas += [('default.ini', os.path.join(srcPath, 'default.ini'), 'DATA')] excluded_binaries = [ 'QtOpenGL4.dll', 'QtSql4.dll', 'QtSvg4.dll', 'QtTest4.dll', @@ -96,16 +108,15 @@ a.binaries += [ ('libeay32.dll', os.path.join(openSSLPath, 'libeay32.dll'), 'BINARY'), (os.path.join('bitmsghash', 'bitmsghash%i.dll' % arch), os.path.join(srcPath, 'bitmsghash', 'bitmsghash%i.dll' % arch), - 'BINARY'), + 'BINARY'), (os.path.join('bitmsghash', 'bitmsghash.cl'), - os.path.join(srcPath, 'bitmsghash', 'bitmsghash.cl'), 'BINARY'), + os.path.join(srcPath, 'bitmsghash', 'bitmsghash.cl'), 'BINARY'), (os.path.join('sslkeys', 'cert.pem'), - os.path.join(srcPath, 'sslkeys', 'cert.pem'), 'BINARY'), + os.path.join(srcPath, 'sslkeys', 'cert.pem'), 'BINARY'), (os.path.join('sslkeys', 'key.pem'), - os.path.join(srcPath, 'sslkeys', 'key.pem'), 'BINARY') + os.path.join(srcPath, 'sslkeys', 'key.pem'), 'BINARY') ] - from version import softwareVersion today = time.strftime("%Y%m%d") @@ -123,10 +134,10 @@ exe = EXE( a.zipfiles, a.datas, name=fname, - debug=False, + debug=DEBUG, strip=None, upx=False, - console=False, icon=os.path.join(srcPath, 'images', 'can-icon.ico') + console=DEBUG, icon=os.path.join(srcPath, 'images', 'can-icon.ico') ) coll = COLLECT( diff --git a/packages/pyinstaller/hooks/pyinstaller_rthook_pyqt4.py b/packages/pyinstaller/hooks/pyinstaller_rthook_pyqt4.py index b30fb1cc..a72584ca 100644 --- a/packages/pyinstaller/hooks/pyinstaller_rthook_pyqt4.py +++ b/packages/pyinstaller/hooks/pyinstaller_rthook_pyqt4.py @@ -1,10 +1,10 @@ -# https://github.com/pyinstaller/pyinstaller/wiki/Recipe-PyQt4-API-Version -import sip - -sip.setapi(u'QDate', 2) -sip.setapi(u'QDateTime', 2) -sip.setapi(u'QString', 2) -sip.setapi(u'QTextStream', 2) -sip.setapi(u'QTime', 2) -sip.setapi(u'QUrl', 2) -sip.setapi(u'QVariant', 2) +# https://github.com/pyinstaller/pyinstaller/wiki/Recipe-PyQt4-API-Version +import sip + +sip.setapi(u'QDate', 2) +sip.setapi(u'QDateTime', 2) +sip.setapi(u'QString', 2) +sip.setapi(u'QTextStream', 2) +sip.setapi(u'QTime', 2) +sip.setapi(u'QUrl', 2) +sip.setapi(u'QVariant', 2) diff --git a/packages/snap/snapcraft.yaml b/packages/snap/snapcraft.yaml new file mode 100644 index 00000000..47c27936 --- /dev/null +++ b/packages/snap/snapcraft.yaml @@ -0,0 +1,76 @@ +name: pybitmessage +base: core18 +grade: devel +confinement: strict +summary: Reference client for Bitmessage, a P2P communications protocol +description: | + Bitmessage is a P2P communication protocol used to send encrypted messages to + another person or to many subscribers. It is decentralized and trustless, + meaning that you need-not inherently trust any entities like root certificate + authorities. It uses strong authentication, which means that the sender of a + message cannot be spoofed. BM aims to hide metadata from passive + eavesdroppers like those ongoing warrantless wiretapping programs. Hence + the sender and receiver of Bitmessages stay anonymous. +adopt-info: pybitmessage + +apps: + pybitmessage: + command: desktop-launch pybitmessage + plugs: [desktop, home, network-bind, unity7] + desktop: share/applications/pybitmessage.desktop + passthrough: + autostart: pybitmessage.desktop + +parts: + pybitmessage: + # https://wiki.ubuntu.com/snapcraft/parts + after: [qt4conf, desktop-qt4, indicator-qt4, tor] + source: https://github.com/Bitmessage/PyBitmessage.git + override-pull: | + snapcraftctl pull + snapcraftctl set-version $(git describe --tags | cut -d- -f1,3 | tr -d v) + plugin: python + python-version: python2 + build-packages: + - libssl-dev + - python-all-dev + python-packages: + - jsonrpclib + - qrcode + - pyxdg + - stem + stage-packages: + - python-qt4 + - python-sip + # parse-info: [setup.py] + tor: + source: https://dist.torproject.org/tor-0.4.6.9.tar.gz + source-checksum: sha256/c7e93380988ce20b82aa19c06cdb2f10302b72cfebec7c15b5b96bcfc94ca9a9 + source-type: tar + plugin: autotools + build-packages: + - libssl-dev + - zlib1g-dev + after: [libevent] + libevent: + source: https://github.com/libevent/libevent/releases/download/release-2.1.12-stable/libevent-2.1.12-stable.tar.gz + source-checksum: sha256/92e6de1be9ec176428fd2367677e61ceffc2ee1cb119035037a27d346b0403bb + source-type: tar + plugin: autotools + cleanup: + after: [pybitmessage] + plugin: nil + override-prime: | + set -eux + sed -ie \ + 's|.*Icon=.*|Icon=${SNAP}/share/icons/hicolor/scalable/apps/pybitmessage.svg|g' \ + $SNAPCRAFT_PRIME/share/applications/pybitmessage.desktop + rm -rf $SNAPCRAFT_PRIME/lib/python2.7/site-packages/pip + for DIR in doc man icons themes fonts mime; do + rm -rf $SNAPCRAFT_PRIME/usr/share/$DIR/* + done + LIBS="libQtDeclarative libQtDesigner libQtHelp libQtScript libQtSql \ + libQtXmlPatterns libdb-5 libicu libgdk libgio libglib libcairo" + for LIBGLOB in $LIBS; do + rm $SNAPCRAFT_PRIME/usr/lib/$SNAPCRAFT_ARCH_TRIPLET/${LIBGLOB}* + done diff --git a/requirements.txt b/requirements.txt index c7c599d5..6f4a22fb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,8 @@ coverage psutil -pycrypto +pycryptodome PyQt5;python_version>="3.7" +mock;python_version<="2.7" python_prctl;platform_system=="Linux" six xvfbwrapper;platform_system=="Linux" diff --git a/run-tests-in-docker.sh b/run-tests-in-docker.sh index 9fa9bfcc..174cb754 100755 --- a/run-tests-in-docker.sh +++ b/run-tests-in-docker.sh @@ -1,5 +1,13 @@ -#!/bin/bash +#!/bin/sh -docker build --target travis -t pybm -f packages/docker/Dockerfile.bionic . -docker run pybm +DOCKERFILE=packages/docker/Dockerfile.bionic +# explicitly mark appimage stage because it builds in any case +docker build --target appimage -t pybm/appimage -f $DOCKERFILE . + +if [ $? -gt 0 ]; then + docker build --no-cache --target appimage -t pybm/appimage -f $DOCKERFILE . +fi + +docker build --target tox -t pybm/tox -f $DOCKERFILE . +docker run --rm -t pybm/tox diff --git a/setup.cfg b/setup.cfg index a4e0547c..28ceaede 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,6 +8,7 @@ max-line-length = 119 [flake8] max-line-length = 119 +exclude = bitmessagecli.py,bitmessagecurses,bitmessageqt,plugins,tests,umsgpack ignore = E722,F841,W503 # E722: pylint is preferred for bare-except # F841: pylint is preferred for unused-variable diff --git a/setup.py b/setup.py index f43f3d58..30436bec 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ from src.version import softwareVersion EXTRAS_REQUIRE = { - 'docs': ['sphinx', 'sphinx_rtd_theme'], + 'docs': ['sphinx'], 'gir': ['pygobject'], 'json': ['jsonrpclib'], 'notify2': ['notify2'], @@ -72,11 +72,28 @@ if __name__ == "__main__": 'pybitmessage.network', 'pybitmessage.plugins', 'pybitmessage.pyelliptic', - 'pybitmessage.storage', + 'pybitmessage.storage' ] + package_data = {'': [ + 'bitmessageqt/*.ui', 'bitmsghash/*.cl', 'sslkeys/*.pem', + 'translations/*.ts', 'translations/*.qm', 'default.ini', 'sql/*.sql', + 'images/*.png', 'images/*.ico', 'images/*.icns', + 'bitmessagekivy/main.kv', 'bitmessagekivy/screens_data.json', + 'bitmessagekivy/kv/*.kv', 'images/kivy/payment/*.png', 'images/kivy/*.gif', + 'images/kivy/text_images*.png' + ]} if sys.version_info[0] == 3: - packages.append('pybitmessage.bitmessagekivy') + packages.extend( + [ + 'pybitmessage.bitmessagekivy', + 'pybitmessage.bitmessagekivy.baseclass' + ] + ) + + if os.environ.get('INSTALL_TESTS', False): + packages.extend(['pybitmessage.mockbm', 'pybitmessage.backend', 'pybitmessage.bitmessagekivy.tests']) + package_data[''].extend(['bitmessagekivy/tests/sampleData/*.dat']) # this will silently accept alternative providers of msgpack # if they are already installed @@ -134,11 +151,7 @@ if __name__ == "__main__": ], package_dir={'pybitmessage': 'src'}, packages=packages, - package_data={'': [ - 'bitmessageqt/*.ui', 'bitmsghash/*.cl', 'sslkeys/*.pem', - 'translations/*.ts', 'translations/*.qm', - 'images/*.png', 'images/*.ico', 'images/*.icns' - ]}, + package_data=package_data, data_files=data_files, ext_modules=[bitmsghash], zip_safe=False, diff --git a/src/addresses.py b/src/addresses.py index e48873a1..885c1f64 100644 --- a/src/addresses.py +++ b/src/addresses.py @@ -2,11 +2,16 @@ Operations with addresses """ # pylint: disable=inconsistent-return-statements -import hashlib + import logging from binascii import hexlify, unhexlify from struct import pack, unpack +try: + from highlevelcrypto import double_sha512 +except ImportError: + from .highlevelcrypto import double_sha512 + logger = logging.getLogger('default') @@ -134,15 +139,6 @@ def decodeVarint(data): return (encodedValue, 9) -def calculateInventoryHash(data): - """Calculate inventory hash from object data""" - sha = hashlib.new('sha512') - sha2 = hashlib.new('sha512') - sha.update(data) - sha2.update(sha.digest()) - return sha2.digest()[0:32] - - def encodeAddress(version, stream, ripe): """Convert ripe to address""" if version >= 2 and version < 4: @@ -166,12 +162,7 @@ def encodeAddress(version, stream, ripe): storedBinaryData = encodeVarint(version) + encodeVarint(stream) + ripe # Generate the checksum - sha = hashlib.new('sha512') - sha.update(storedBinaryData) - currentHash = sha.digest() - sha = hashlib.new('sha512') - sha.update(currentHash) - checksum = sha.digest()[0:4] + checksum = double_sha512(storedBinaryData)[0:4] # FIXME: encodeBase58 should take binary data, to reduce conversions # encodeBase58(storedBinaryData + checksum) @@ -207,13 +198,7 @@ def decodeAddress(address): data = unhexlify(hexdata) checksum = data[-4:] - sha = hashlib.new('sha512') - sha.update(data[:-4]) - currentHash = sha.digest() - sha = hashlib.new('sha512') - sha.update(currentHash) - - if checksum != sha.digest()[0:4]: + if checksum != double_sha512(data[:-4])[0:4]: status = 'checksumfailed' return status, 0, 0, '' diff --git a/src/api.py b/src/api.py index de220cc4..a4445569 100644 --- a/src/api.py +++ b/src/api.py @@ -1,5 +1,5 @@ # Copyright (c) 2012-2016 Jonathan Warren -# Copyright (c) 2012-2020 The Bitmessage developers +# Copyright (c) 2012-2023 The Bitmessage developers """ This is not what you run to start the Bitmessage API. @@ -39,17 +39,18 @@ To use the API concider such simple example: .. code-block:: python - import jsonrpclib + from jsonrpclib import jsonrpc - from pybitmessage import bmconfigparser, helper_startup + from pybitmessage import helper_startup + from pybitmessage.bmconfigparser import config helper_startup.loadConfig() # find and load local config file - conf = bmconfigparser.BMConfigParser() - api_uri = "http://%s:%s@127.0.0.1:8442/" % ( - conf.safeGet('bitmessagesettings', 'apiusername'), - conf.safeGet('bitmessagesettings', 'apipassword') + api_uri = "http://%s:%s@127.0.0.1:%s/" % ( + config.safeGet('bitmessagesettings', 'apiusername'), + config.safeGet('bitmessagesettings', 'apipassword'), + config.safeGet('bitmessagesettings', 'apiport') ) - api = jsonrpclib.ServerProxy(api_uri) + api = jsonrpc.ServerProxy(api_uri) print(api.clientStatus()) @@ -57,42 +58,49 @@ For further examples please reference `.tests.test_api`. """ import base64 -import ConfigParser import errno import hashlib -import httplib import json -import random # nosec +import random import socket -import subprocess +import subprocess # nosec B404 import time -import xmlrpclib from binascii import hexlify, unhexlify -from SimpleXMLRPCServer import SimpleXMLRPCRequestHandler, SimpleXMLRPCServer -from struct import pack +from struct import pack, unpack + +import six +from six.moves import configparser, http_client, xmlrpc_server -import defaults import helper_inbox import helper_sent -import network.stats +import protocol import proofofwork import queues import shared + import shutdown import state from addresses import ( addBMIfNotPresent, - calculateInventoryHash, decodeAddress, decodeVarint, varintDecodeError ) -from bmconfigparser import BMConfigParser +from bmconfigparser import config from debug import logger -from helper_sql import SqlBulkExecute, sqlExecute, sqlQuery, sqlStoredProcedure, sql_ready -from inventory import Inventory -from network.threads import StoppableThread -from six.moves import queue +from defaults import ( + networkDefaultProofOfWorkNonceTrialsPerByte, + networkDefaultPayloadLengthExtraBytes) +from helper_sql import ( + SqlBulkExecute, sqlExecute, sqlQuery, sqlStoredProcedure, sql_ready) +from highlevelcrypto import calculateInventoryHash + +try: + from network import connectionpool +except ImportError: + connectionpool = None + +from network import stats, StoppableThread from version import softwareVersion try: # TODO: write tests for XML vulnerabilities @@ -154,7 +162,7 @@ class ErrorCodes(type): def __new__(mcs, name, bases, namespace): result = super(ErrorCodes, mcs).__new__(mcs, name, bases, namespace) - for code in mcs._CODES.iteritems(): + for code in six.iteritems(mcs._CODES): # beware: the formatting is adjusted for list-table result.__doc__ += """ * - %04i - %s @@ -162,7 +170,7 @@ class ErrorCodes(type): return result -class APIError(xmlrpclib.Fault): +class APIError(xmlrpc_server.Fault): """ APIError exception class @@ -190,8 +198,8 @@ class singleAPI(StoppableThread): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: s.connect(( - BMConfigParser().get('bitmessagesettings', 'apiinterface'), - BMConfigParser().getint('bitmessagesettings', 'apiport') + config.get('bitmessagesettings', 'apiinterface'), + config.getint('bitmessagesettings', 'apiport') )) s.shutdown(socket.SHUT_RDWR) s.close() @@ -204,15 +212,15 @@ class singleAPI(StoppableThread): :class:`jsonrpclib.SimpleJSONRPCServer` is created and started here with `BMRPCDispatcher` dispatcher. """ - port = BMConfigParser().getint('bitmessagesettings', 'apiport') + port = config.getint('bitmessagesettings', 'apiport') try: getattr(errno, 'WSAEADDRINUSE') except AttributeError: errno.WSAEADDRINUSE = errno.EADDRINUSE - RPCServerBase = SimpleXMLRPCServer + RPCServerBase = xmlrpc_server.SimpleXMLRPCServer ct = 'text/xml' - if BMConfigParser().safeGet( + if config.safeGet( 'bitmessagesettings', 'apivariant') == 'json': try: from jsonrpclib.SimpleJSONRPCServer import ( @@ -240,9 +248,9 @@ class singleAPI(StoppableThread): if attempt > 0: logger.warning( 'Failed to start API listener on port %s', port) - port = random.randint(32767, 65535) + port = random.randint(32767, 65535) # nosec B311 se = StoppableRPCServer( - (BMConfigParser().get( + (config.get( 'bitmessagesettings', 'apiinterface'), port), BMXMLRPCRequestHandler, True, encoding='UTF-8') @@ -252,26 +260,26 @@ class singleAPI(StoppableThread): else: if attempt > 0: logger.warning('Setting apiport to %s', port) - BMConfigParser().set( + config.set( 'bitmessagesettings', 'apiport', str(port)) - BMConfigParser().save() + config.save() break se.register_instance(BMRPCDispatcher()) se.register_introspection_functions() - apiNotifyPath = BMConfigParser().safeGet( + apiNotifyPath = config.safeGet( 'bitmessagesettings', 'apinotifypath') if apiNotifyPath: logger.info('Trying to call %s', apiNotifyPath) try: - subprocess.call([apiNotifyPath, "startingUp"]) + subprocess.call([apiNotifyPath, "startingUp"]) # nosec B603 except OSError: logger.warning( 'Failed to call %s, removing apinotifypath setting', apiNotifyPath) - BMConfigParser().remove_option( + config.remove_option( 'bitmessagesettings', 'apinotifypath') se.serve_forever() @@ -286,7 +294,7 @@ class CommandHandler(type): # pylint: disable=protected-access result = super(CommandHandler, mcs).__new__( mcs, name, bases, namespace) - result.config = BMConfigParser() + result.config = config result._handlers = {} apivariant = result.config.safeGet('bitmessagesettings', 'apivariant') for func in namespace.values(): @@ -325,7 +333,7 @@ class command(object): # pylint: disable=too-few-public-methods def __call__(self, func): - if BMConfigParser().safeGet( + if config.safeGet( 'bitmessagesettings', 'apivariant') == 'legacy': def wrapper(*args): """ @@ -351,7 +359,7 @@ class command(object): # pylint: disable=too-few-public-methods # Modified by Jonathan Warren (Atheros). # Further modified by the Bitmessage developers # http://code.activestate.com/recipes/501148 -class BMXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): +class BMXMLRPCRequestHandler(xmlrpc_server.SimpleXMLRPCRequestHandler): """The main API handler""" # pylint: disable=protected-access @@ -382,17 +390,21 @@ class BMXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): L = [] while size_remaining: chunk_size = min(size_remaining, max_chunk_size) - L.append(self.rfile.read(chunk_size)) + chunk = self.rfile.read(chunk_size) + if not chunk: + break + L.append(chunk) size_remaining -= len(L[-1]) - data = ''.join(L) + data = b''.join(L) + # data = self.decode_request_content(data) # pylint: disable=attribute-defined-outside-init self.cookies = [] validuser = self.APIAuthenticateClient() if not validuser: time.sleep(2) - self.send_response(httplib.UNAUTHORIZED) + self.send_response(http_client.UNAUTHORIZED) self.end_headers() return # "RPC Username or password incorrect or HTTP header" @@ -409,11 +421,11 @@ class BMXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): ) except Exception: # This should only happen if the module is buggy # internal error, report as HTTP server error - self.send_response(httplib.INTERNAL_SERVER_ERROR) + self.send_response(http_client.INTERNAL_SERVER_ERROR) self.end_headers() else: # got a valid XML RPC response - self.send_response(httplib.OK) + self.send_response(http_client.OK) self.send_header("Content-type", self.server.content_type) self.send_header("Content-length", str(len(response))) @@ -442,11 +454,12 @@ class BMXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): if 'Authorization' in self.headers: # handle Basic authentication encstr = self.headers.get('Authorization').split()[1] - emailid, password = encstr.decode('base64').split(':') + emailid, password = base64.b64decode( + encstr).decode('utf-8').split(':') return ( - emailid == BMConfigParser().get( + emailid == config.get( 'bitmessagesettings', 'apiusername' - ) and password == BMConfigParser().get( + ) and password == config.get( 'bitmessagesettings', 'apipassword')) else: logger.warning( @@ -458,9 +471,9 @@ class BMXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): # pylint: disable=no-self-use,no-member,too-many-public-methods +@six.add_metaclass(CommandHandler) class BMRPCDispatcher(object): """This class is used to dispatch API commands""" - __metaclass__ = CommandHandler @staticmethod def _decode(text, decode_type): @@ -645,13 +658,11 @@ class BMRPCDispatcher(object): nonceTrialsPerByte = self.config.get( 'bitmessagesettings', 'defaultnoncetrialsperbyte' ) if not totalDifficulty else int( - defaults.networkDefaultProofOfWorkNonceTrialsPerByte - * totalDifficulty) + networkDefaultProofOfWorkNonceTrialsPerByte * totalDifficulty) payloadLengthExtraBytes = self.config.get( 'bitmessagesettings', 'defaultpayloadlengthextrabytes' ) if not smallMessageDifficulty else int( - defaults.networkDefaultPayloadLengthExtraBytes - * smallMessageDifficulty) + networkDefaultPayloadLengthExtraBytes * smallMessageDifficulty) if not isinstance(eighteenByteRipe, bool): raise APIError( @@ -693,13 +704,11 @@ class BMRPCDispatcher(object): nonceTrialsPerByte = self.config.get( 'bitmessagesettings', 'defaultnoncetrialsperbyte' ) if not totalDifficulty else int( - defaults.networkDefaultProofOfWorkNonceTrialsPerByte - * totalDifficulty) + networkDefaultProofOfWorkNonceTrialsPerByte * totalDifficulty) payloadLengthExtraBytes = self.config.get( 'bitmessagesettings', 'defaultpayloadlengthextrabytes' ) if not smallMessageDifficulty else int( - defaults.networkDefaultPayloadLengthExtraBytes - * smallMessageDifficulty) + networkDefaultPayloadLengthExtraBytes * smallMessageDifficulty) if not passphrase: raise APIError(1, 'The specified passphrase is blank.') @@ -858,7 +867,7 @@ class BMRPCDispatcher(object): ' Use deleteAddress API call instead.') try: self.config.remove_section(address) - except ConfigParser.NoSectionError: + except configparser.NoSectionError: raise APIError( 13, 'Could not find this address in your keys.dat file.') self.config.save() @@ -875,7 +884,7 @@ class BMRPCDispatcher(object): address = addBMIfNotPresent(address) try: self.config.remove_section(address) - except ConfigParser.NoSectionError: + except configparser.NoSectionError: raise APIError( 13, 'Could not find this address in your keys.dat file.') self.config.save() @@ -883,6 +892,16 @@ class BMRPCDispatcher(object): shared.reloadMyAddressHashes() return "success" + @command('enableAddress') + def HandleEnableAddress(self, address, enable=True): + """Enable or disable the address depending on the *enable* value""" + self._verifyAddress(address) + address = addBMIfNotPresent(address) + config.set(address, 'enabled', str(enable)) + self.config.save() + shared.reloadMyAddressHashes() + return "success" + @command('getAllInboxMessages') def HandleGetAllInboxMessages(self): """ @@ -1118,9 +1137,8 @@ class BMRPCDispatcher(object): fromAddress = addBMIfNotPresent(fromAddress) self._verifyAddress(fromAddress) try: - fromAddressEnabled = self.config.getboolean( - fromAddress, 'enabled') - except BaseException: + fromAddressEnabled = self.config.getboolean(fromAddress, 'enabled') + except configparser.NoSectionError: raise APIError( 13, 'Could not find your fromAddress in the keys.dat file.') if not fromAddressEnabled: @@ -1164,10 +1182,13 @@ class BMRPCDispatcher(object): fromAddress = addBMIfNotPresent(fromAddress) self._verifyAddress(fromAddress) try: - self.config.getboolean(fromAddress, 'enabled') - except BaseException: + fromAddressEnabled = self.config.getboolean(fromAddress, 'enabled') + except configparser.NoSectionError: raise APIError( 13, 'Could not find your fromAddress in the keys.dat file.') + if not fromAddressEnabled: + raise APIError(14, 'Your fromAddress is disabled. Cannot send.') + toAddress = str_broadcast_subscribers ackdata = helper_sent.insert( @@ -1260,55 +1281,73 @@ class BMRPCDispatcher(object): }) return {'subscriptions': data} - @command('disseminatePreEncryptedMsg') - def HandleDisseminatePreEncryptedMsg( - self, encryptedPayload, requiredAverageProofOfWorkNonceTrialsPerByte, - requiredPayloadLengthExtraBytes): - """Handle a request to disseminate an encrypted message""" + @command('disseminatePreEncryptedMsg', 'disseminatePreparedObject') + def HandleDisseminatePreparedObject( + self, encryptedPayload, + nonceTrialsPerByte=networkDefaultProofOfWorkNonceTrialsPerByte, + payloadLengthExtraBytes=networkDefaultPayloadLengthExtraBytes + ): + """ + Handle a request to disseminate an encrypted message. - # The device issuing this command to PyBitmessage supplies a msg - # object that has already been encrypted but which still needs the POW - # to be done. PyBitmessage accepts this msg object and sends it out - # to the rest of the Bitmessage network as if it had generated - # the message itself. Please do not yet add this to the api doc. + The device issuing this command to PyBitmessage supplies an object + that has already been encrypted but which may still need the PoW + to be done. PyBitmessage accepts this object and sends it out + to the rest of the Bitmessage network as if it had generated + the message itself. + + *encryptedPayload* is a hex encoded string starting with the nonce, + 8 zero bytes in case of no PoW done. + """ encryptedPayload = self._decode(encryptedPayload, "hex") - # Let us do the POW and attach it to the front - target = 2**64 / ( - ( - len(encryptedPayload) - + requiredPayloadLengthExtraBytes - + 8 - ) * requiredAverageProofOfWorkNonceTrialsPerByte) - logger.info( - '(For msg message via API) Doing proof of work. Total required' - ' difficulty: %s\nRequired small message difficulty: %s', - float(requiredAverageProofOfWorkNonceTrialsPerByte) - / defaults.networkDefaultProofOfWorkNonceTrialsPerByte, - float(requiredPayloadLengthExtraBytes) - / defaults.networkDefaultPayloadLengthExtraBytes, - ) - powStartTime = time.time() - initialHash = hashlib.sha512(encryptedPayload).digest() - trialValue, nonce = proofofwork.run(target, initialHash) - logger.info( - '(For msg message via API) Found proof of work %s\nNonce: %s\n' - 'POW took %s seconds. %s nonce trials per second.', - trialValue, nonce, int(time.time() - powStartTime), - nonce / (time.time() - powStartTime) - ) - encryptedPayload = pack('>Q', nonce) + encryptedPayload - toStreamNumber = decodeVarint(encryptedPayload[16:26])[0] + + nonce, = unpack('>Q', encryptedPayload[:8]) + objectType, toStreamNumber, expiresTime = \ + protocol.decodeObjectParameters(encryptedPayload) + + if nonce == 0: # Let us do the POW and attach it to the front + encryptedPayload = encryptedPayload[8:] + TTL = expiresTime - time.time() + 300 # a bit of extra padding + # Let us do the POW and attach it to the front + logger.debug("expiresTime: %s", expiresTime) + logger.debug("TTL: %s", TTL) + logger.debug("objectType: %s", objectType) + logger.info( + '(For msg message via API) Doing proof of work. Total required' + ' difficulty: %s\nRequired small message difficulty: %s', + float(nonceTrialsPerByte) + / networkDefaultProofOfWorkNonceTrialsPerByte, + float(payloadLengthExtraBytes) + / networkDefaultPayloadLengthExtraBytes, + ) + powStartTime = time.time() + target = 2**64 / ( + nonceTrialsPerByte * ( + len(encryptedPayload) + 8 + payloadLengthExtraBytes + (( + TTL * ( + len(encryptedPayload) + 8 + payloadLengthExtraBytes + )) / (2 ** 16)) + )) + initialHash = hashlib.sha512(encryptedPayload).digest() + trialValue, nonce = proofofwork.run(target, initialHash) + logger.info( + '(For msg message via API) Found proof of work %s\nNonce: %s\n' + 'POW took %s seconds. %s nonce trials per second.', + trialValue, nonce, int(time.time() - powStartTime), + nonce / (time.time() - powStartTime) + ) + encryptedPayload = pack('>Q', nonce) + encryptedPayload + inventoryHash = calculateInventoryHash(encryptedPayload) - objectType = 2 - TTL = 2.5 * 24 * 60 * 60 - Inventory()[inventoryHash] = ( + state.Inventory[inventoryHash] = ( objectType, toStreamNumber, encryptedPayload, - int(time.time()) + TTL, '' + expiresTime, b'' ) logger.info( 'Broadcasting inv for msg(API disseminatePreEncryptedMsg' ' command): %s', hexlify(inventoryHash)) queues.invQueue.put((toStreamNumber, inventoryHash)) + return hexlify(inventoryHash).decode() @command('trashSentMessageByAckData') def HandleTrashSentMessageByAckDAta(self, ackdata): @@ -1331,8 +1370,8 @@ class BMRPCDispatcher(object): # Let us do the POW target = 2 ** 64 / (( - len(payload) + defaults.networkDefaultPayloadLengthExtraBytes + 8 - ) * defaults.networkDefaultProofOfWorkNonceTrialsPerByte) + len(payload) + networkDefaultPayloadLengthExtraBytes + 8 + ) * networkDefaultProofOfWorkNonceTrialsPerByte) logger.info('(For pubkey message via API) Doing proof of work...') initialHash = hashlib.sha512(payload).digest() trialValue, nonce = proofofwork.run(target, initialHash) @@ -1356,7 +1395,7 @@ class BMRPCDispatcher(object): inventoryHash = calculateInventoryHash(payload) objectType = 1 # .. todo::: support v4 pubkeys TTL = 28 * 24 * 60 * 60 - Inventory()[inventoryHash] = ( + state.Inventory[inventoryHash] = ( objectType, pubkeyStreamNumber, payload, int(time.time()) + TTL, '' ) logger.info( @@ -1411,7 +1450,8 @@ class BMRPCDispatcher(object): or "connectedAndReceivingIncomingConnections". """ - connections_num = len(network.stats.connectedHostsList()) + connections_num = len(stats.connectedHostsList()) + if connections_num == 0: networkStatus = 'notConnected' elif state.clientHasReceivedIncomingConnections: @@ -1423,12 +1463,41 @@ class BMRPCDispatcher(object): 'numberOfMessagesProcessed': state.numberOfMessagesProcessed, 'numberOfBroadcastsProcessed': state.numberOfBroadcastsProcessed, 'numberOfPubkeysProcessed': state.numberOfPubkeysProcessed, - 'pendingDownload': network.stats.pendingDownload(), + 'pendingDownload': stats.pendingDownload(), 'networkStatus': networkStatus, 'softwareName': 'PyBitmessage', 'softwareVersion': softwareVersion } + @command('listConnections') + def HandleListConnections(self): + """ + Returns bitmessage connection information as dict with keys *inbound*, + *outbound*. + """ + if connectionpool is None: + raise APIError(21, 'Could not import BMConnectionPool.') + inboundConnections = [] + outboundConnections = [] + for i in connectionpool.pool.inboundConnections.values(): + inboundConnections.append({ + 'host': i.destination.host, + 'port': i.destination.port, + 'fullyEstablished': i.fullyEstablished, + 'userAgent': str(i.userAgent) + }) + for i in connectionpool.pool.outboundConnections.values(): + outboundConnections.append({ + 'host': i.destination.host, + 'port': i.destination.port, + 'fullyEstablished': i.fullyEstablished, + 'userAgent': str(i.userAgent) + }) + return { + 'inbound': inboundConnections, + 'outbound': outboundConnections + } + @command('helloWorld') def HandleHelloWorld(self, a, b): """Test two string params""" @@ -1439,25 +1508,11 @@ class BMRPCDispatcher(object): """Test two numeric params""" return a + b - @testmode('clearUISignalQueue') - def HandleclearUISignalQueue(self): - """clear UISignalQueue""" - queues.UISignalQueue.queue.clear() - return "success" - @command('statusBar') def HandleStatusBar(self, message): """Update GUI statusbar message""" queues.UISignalQueue.put(('updateStatusBar', message)) - - @testmode('getStatusBar') - def HandleGetStatusBar(self): - """Get GUI statusbar message""" - try: - _, data = queues.UISignalQueue.get(block=False) - except queue.Empty: - return None - return data + return "success" @testmode('undeleteMessage') def HandleUndeleteMessage(self, msgid): diff --git a/src/backend/address_generator.py b/src/backend/address_generator.py new file mode 100644 index 00000000..312c313b --- /dev/null +++ b/src/backend/address_generator.py @@ -0,0 +1,49 @@ +""" +Common methods and functions for kivy and qt. +""" + +from pybitmessage import queues +from pybitmessage.bmconfigparser import config +from pybitmessage.defaults import ( + networkDefaultProofOfWorkNonceTrialsPerByte, + networkDefaultPayloadLengthExtraBytes +) + + +class AddressGenerator(object): + """"Base class for address generation and validation""" + def __init__(self): + pass + + @staticmethod + def random_address_generation( + label, streamNumberForAddress=1, eighteenByteRipe=False, + nonceTrialsPerByte=networkDefaultProofOfWorkNonceTrialsPerByte, + payloadLengthExtraBytes=networkDefaultPayloadLengthExtraBytes + ): + """Start address generation and return whether validation was successful""" + + labels = [config.get(obj, 'label') + for obj in config.addresses()] + if label and label not in labels: + queues.addressGeneratorQueue.put(( + 'createRandomAddress', 4, streamNumberForAddress, label, 1, + "", eighteenByteRipe, nonceTrialsPerByte, + payloadLengthExtraBytes)) + return True + return False + + @staticmethod + def address_validation(instance, label): + """Checking address validation while creating""" + labels = [config.get(obj, 'label') for obj in config.addresses()] + if label in labels: + instance.error = True + instance.helper_text = 'it is already exist you'\ + ' can try this Ex. ( {0}_1, {0}_2 )'.format( + label) + elif label: + instance.error = False + else: + instance.error = True + instance.helper_text = 'This field is required' diff --git a/src/bitmessagecli.py b/src/bitmessagecli.py index adcab8b1..84c618af 100644 --- a/src/bitmessagecli.py +++ b/src/bitmessagecli.py @@ -23,7 +23,7 @@ import sys import time import xmlrpclib -from bmconfigparser import BMConfigParser +from bmconfigparser import config api = '' @@ -86,15 +86,15 @@ def lookupAppdataFolder(): def configInit(): """Initialised the configuration""" - BMConfigParser().add_section('bitmessagesettings') + config.add_section('bitmessagesettings') # Sets the bitmessage port to stop the warning about the api not properly # being setup. This is in the event that the keys.dat is in a different # directory or is created locally to connect to a machine remotely. - BMConfigParser().set('bitmessagesettings', 'port', '8444') - BMConfigParser().set('bitmessagesettings', 'apienabled', 'true') # Sets apienabled to true in keys.dat + config.set('bitmessagesettings', 'port', '8444') + config.set('bitmessagesettings', 'apienabled', 'true') # Sets apienabled to true in keys.dat with open(keysName, 'wb') as configfile: - BMConfigParser().write(configfile) + config.write(configfile) print('\n ' + str(keysName) + ' Initalized in the same directory as daemon.py') print(' You will now need to configure the ' + str(keysName) + ' file.\n') @@ -104,15 +104,15 @@ def apiInit(apiEnabled): """Initialise the API""" global usrPrompt - BMConfigParser().read(keysPath) + config.read(keysPath) if apiEnabled is False: # API information there but the api is disabled. uInput = userInput("The API is not enabled. Would you like to do that now, (Y)es or (N)o?").lower() if uInput == "y": - BMConfigParser().set('bitmessagesettings', 'apienabled', 'true') # Sets apienabled to true in keys.dat + config.set('bitmessagesettings', 'apienabled', 'true') # Sets apienabled to true in keys.dat with open(keysPath, 'wb') as configfile: - BMConfigParser().write(configfile) + config.write(configfile) print('Done') restartBmNotify() @@ -158,15 +158,15 @@ def apiInit(apiEnabled): # sets the bitmessage port to stop the warning about the api not properly # being setup. This is in the event that the keys.dat is in a different # directory or is created locally to connect to a machine remotely. - BMConfigParser().set('bitmessagesettings', 'port', '8444') - BMConfigParser().set('bitmessagesettings', 'apienabled', 'true') - BMConfigParser().set('bitmessagesettings', 'apiport', apiPort) - BMConfigParser().set('bitmessagesettings', 'apiinterface', '127.0.0.1') - BMConfigParser().set('bitmessagesettings', 'apiusername', apiUsr) - BMConfigParser().set('bitmessagesettings', 'apipassword', apiPwd) - BMConfigParser().set('bitmessagesettings', 'daemon', daemon) + config.set('bitmessagesettings', 'port', '8444') + config.set('bitmessagesettings', 'apienabled', 'true') + config.set('bitmessagesettings', 'apiport', apiPort) + config.set('bitmessagesettings', 'apiinterface', '127.0.0.1') + config.set('bitmessagesettings', 'apiusername', apiUsr) + config.set('bitmessagesettings', 'apipassword', apiPwd) + config.set('bitmessagesettings', 'daemon', daemon) with open(keysPath, 'wb') as configfile: - BMConfigParser().write(configfile) + config.write(configfile) print('\n Finished configuring the keys.dat file with API information.\n') restartBmNotify() @@ -191,19 +191,19 @@ def apiData(): global keysPath global usrPrompt - BMConfigParser().read(keysPath) # First try to load the config file (the keys.dat file) from the program directory + config.read(keysPath) # First try to load the config file (the keys.dat file) from the program directory try: - BMConfigParser().get('bitmessagesettings', 'port') + config.get('bitmessagesettings', 'port') appDataFolder = '' except: # noqa:E722 # Could not load the keys.dat file in the program directory. Perhaps it is in the appdata directory. appDataFolder = lookupAppdataFolder() keysPath = appDataFolder + keysPath - BMConfigParser().read(keysPath) + config.read(keysPath) try: - BMConfigParser().get('bitmessagesettings', 'port') + config.get('bitmessagesettings', 'port') except: # noqa:E722 # keys.dat was not there either, something is wrong. print('\n ******************************************************************') @@ -230,24 +230,24 @@ def apiData(): main() try: # checks to make sure that everyting is configured correctly. Excluding apiEnabled, it is checked after - BMConfigParser().get('bitmessagesettings', 'apiport') - BMConfigParser().get('bitmessagesettings', 'apiinterface') - BMConfigParser().get('bitmessagesettings', 'apiusername') - BMConfigParser().get('bitmessagesettings', 'apipassword') + config.get('bitmessagesettings', 'apiport') + config.get('bitmessagesettings', 'apiinterface') + config.get('bitmessagesettings', 'apiusername') + config.get('bitmessagesettings', 'apipassword') except: # noqa:E722 apiInit("") # Initalize the keys.dat file with API information # keys.dat file was found or appropriately configured, allow information retrieval # apiEnabled = - # apiInit(BMConfigParser().safeGetBoolean('bitmessagesettings','apienabled')) + # apiInit(config.safeGetBoolean('bitmessagesettings','apienabled')) # #if false it will prompt the user, if true it will return true - BMConfigParser().read(keysPath) # read again since changes have been made - apiPort = int(BMConfigParser().get('bitmessagesettings', 'apiport')) - apiInterface = BMConfigParser().get('bitmessagesettings', 'apiinterface') - apiUsername = BMConfigParser().get('bitmessagesettings', 'apiusername') - apiPassword = BMConfigParser().get('bitmessagesettings', 'apipassword') + config.read(keysPath) # read again since changes have been made + apiPort = int(config.get('bitmessagesettings', 'apiport')) + apiInterface = config.get('bitmessagesettings', 'apiinterface') + apiUsername = config.get('bitmessagesettings', 'apiusername') + apiPassword = config.get('bitmessagesettings', 'apipassword') print('\n API data successfully imported.\n') @@ -277,28 +277,28 @@ def bmSettings(): keysPath = 'keys.dat' - BMConfigParser().read(keysPath) # Read the keys.dat + config.read(keysPath) # Read the keys.dat try: - port = BMConfigParser().get('bitmessagesettings', 'port') + port = config.get('bitmessagesettings', 'port') except: # noqa:E722 print('\n File not found.\n') usrPrompt = 0 main() - startonlogon = BMConfigParser().safeGetBoolean('bitmessagesettings', 'startonlogon') - minimizetotray = BMConfigParser().safeGetBoolean('bitmessagesettings', 'minimizetotray') - showtraynotifications = BMConfigParser().safeGetBoolean('bitmessagesettings', 'showtraynotifications') - startintray = BMConfigParser().safeGetBoolean('bitmessagesettings', 'startintray') - defaultnoncetrialsperbyte = BMConfigParser().get('bitmessagesettings', 'defaultnoncetrialsperbyte') - defaultpayloadlengthextrabytes = BMConfigParser().get('bitmessagesettings', 'defaultpayloadlengthextrabytes') - daemon = BMConfigParser().safeGetBoolean('bitmessagesettings', 'daemon') + startonlogon = config.safeGetBoolean('bitmessagesettings', 'startonlogon') + minimizetotray = config.safeGetBoolean('bitmessagesettings', 'minimizetotray') + showtraynotifications = config.safeGetBoolean('bitmessagesettings', 'showtraynotifications') + startintray = config.safeGetBoolean('bitmessagesettings', 'startintray') + defaultnoncetrialsperbyte = config.get('bitmessagesettings', 'defaultnoncetrialsperbyte') + defaultpayloadlengthextrabytes = config.get('bitmessagesettings', 'defaultpayloadlengthextrabytes') + daemon = config.safeGetBoolean('bitmessagesettings', 'daemon') - socksproxytype = BMConfigParser().get('bitmessagesettings', 'socksproxytype') - sockshostname = BMConfigParser().get('bitmessagesettings', 'sockshostname') - socksport = BMConfigParser().get('bitmessagesettings', 'socksport') - socksauthentication = BMConfigParser().safeGetBoolean('bitmessagesettings', 'socksauthentication') - socksusername = BMConfigParser().get('bitmessagesettings', 'socksusername') - sockspassword = BMConfigParser().get('bitmessagesettings', 'sockspassword') + socksproxytype = config.get('bitmessagesettings', 'socksproxytype') + sockshostname = config.get('bitmessagesettings', 'sockshostname') + socksport = config.get('bitmessagesettings', 'socksport') + socksauthentication = config.safeGetBoolean('bitmessagesettings', 'socksauthentication') + socksusername = config.get('bitmessagesettings', 'socksusername') + sockspassword = config.get('bitmessagesettings', 'sockspassword') print('\n -----------------------------------') print(' | Current Bitmessage Settings |') @@ -333,60 +333,60 @@ def bmSettings(): if uInput == "port": print(' Current port number: ' + port) uInput = userInput("Enter the new port number.") - BMConfigParser().set('bitmessagesettings', 'port', str(uInput)) + config.set('bitmessagesettings', 'port', str(uInput)) elif uInput == "startonlogon": print(' Current status: ' + str(startonlogon)) uInput = userInput("Enter the new status.") - BMConfigParser().set('bitmessagesettings', 'startonlogon', str(uInput)) + config.set('bitmessagesettings', 'startonlogon', str(uInput)) elif uInput == "minimizetotray": print(' Current status: ' + str(minimizetotray)) uInput = userInput("Enter the new status.") - BMConfigParser().set('bitmessagesettings', 'minimizetotray', str(uInput)) + config.set('bitmessagesettings', 'minimizetotray', str(uInput)) elif uInput == "showtraynotifications": print(' Current status: ' + str(showtraynotifications)) uInput = userInput("Enter the new status.") - BMConfigParser().set('bitmessagesettings', 'showtraynotifications', str(uInput)) + config.set('bitmessagesettings', 'showtraynotifications', str(uInput)) elif uInput == "startintray": print(' Current status: ' + str(startintray)) uInput = userInput("Enter the new status.") - BMConfigParser().set('bitmessagesettings', 'startintray', str(uInput)) + config.set('bitmessagesettings', 'startintray', str(uInput)) elif uInput == "defaultnoncetrialsperbyte": print(' Current default nonce trials per byte: ' + defaultnoncetrialsperbyte) uInput = userInput("Enter the new defaultnoncetrialsperbyte.") - BMConfigParser().set('bitmessagesettings', 'defaultnoncetrialsperbyte', str(uInput)) + config.set('bitmessagesettings', 'defaultnoncetrialsperbyte', str(uInput)) elif uInput == "defaultpayloadlengthextrabytes": print(' Current default payload length extra bytes: ' + defaultpayloadlengthextrabytes) uInput = userInput("Enter the new defaultpayloadlengthextrabytes.") - BMConfigParser().set('bitmessagesettings', 'defaultpayloadlengthextrabytes', str(uInput)) + config.set('bitmessagesettings', 'defaultpayloadlengthextrabytes', str(uInput)) elif uInput == "daemon": print(' Current status: ' + str(daemon)) uInput = userInput("Enter the new status.").lower() - BMConfigParser().set('bitmessagesettings', 'daemon', str(uInput)) + config.set('bitmessagesettings', 'daemon', str(uInput)) elif uInput == "socksproxytype": print(' Current socks proxy type: ' + socksproxytype) print("Possibilities: 'none', 'SOCKS4a', 'SOCKS5'.") uInput = userInput("Enter the new socksproxytype.") - BMConfigParser().set('bitmessagesettings', 'socksproxytype', str(uInput)) + config.set('bitmessagesettings', 'socksproxytype', str(uInput)) elif uInput == "sockshostname": print(' Current socks host name: ' + sockshostname) uInput = userInput("Enter the new sockshostname.") - BMConfigParser().set('bitmessagesettings', 'sockshostname', str(uInput)) + config.set('bitmessagesettings', 'sockshostname', str(uInput)) elif uInput == "socksport": print(' Current socks port number: ' + socksport) uInput = userInput("Enter the new socksport.") - BMConfigParser().set('bitmessagesettings', 'socksport', str(uInput)) + config.set('bitmessagesettings', 'socksport', str(uInput)) elif uInput == "socksauthentication": print(' Current status: ' + str(socksauthentication)) uInput = userInput("Enter the new status.") - BMConfigParser().set('bitmessagesettings', 'socksauthentication', str(uInput)) + config.set('bitmessagesettings', 'socksauthentication', str(uInput)) elif uInput == "socksusername": print(' Current socks username: ' + socksusername) uInput = userInput("Enter the new socksusername.") - BMConfigParser().set('bitmessagesettings', 'socksusername', str(uInput)) + config.set('bitmessagesettings', 'socksusername', str(uInput)) elif uInput == "sockspassword": print(' Current socks password: ' + sockspassword) uInput = userInput("Enter the new password.") - BMConfigParser().set('bitmessagesettings', 'sockspassword', str(uInput)) + config.set('bitmessagesettings', 'sockspassword', str(uInput)) else: print("\n Invalid input. Please try again.\n") invalidInput = True @@ -397,7 +397,7 @@ def bmSettings(): if uInput != "y": print('\n Changes Made.\n') with open(keysPath, 'wb') as configfile: - BMConfigParser().write(configfile) + config.write(configfile) restartBmNotify() break diff --git a/src/bitmessagecurses/__init__.py b/src/bitmessagecurses/__init__.py index 7b52457d..64fd735b 100644 --- a/src/bitmessagecurses/__init__.py +++ b/src/bitmessagecurses/__init__.py @@ -28,9 +28,8 @@ import shutdown import state from addresses import addBMIfNotPresent, decodeAddress -from bmconfigparser import BMConfigParser +from bmconfigparser import config from helper_sql import sqlExecute, sqlQuery -from inventory import Inventory # pylint: disable=global-statement @@ -145,8 +144,8 @@ def scrollbox(d, text, height=None, width=None): def resetlookups(): """Reset the Inventory Lookups""" global inventorydata - inventorydata = Inventory().numberOfInventoryLookupsPerformed - Inventory().numberOfInventoryLookupsPerformed = 0 + inventorydata = state.Inventory.numberOfInventoryLookupsPerformed + state.Inventory.numberOfInventoryLookupsPerformed = 0 Timer(1, resetlookups, ()).start() @@ -618,19 +617,19 @@ def handlech(c, stdscr): r, t = d.inputbox("New address label", init=label) if r == d.DIALOG_OK: label = t - BMConfigParser().set(a, "label", label) + config.set(a, "label", label) # Write config - BMConfigParser().save() + config.save() addresses[addrcur][0] = label elif t == "4": # Enable address a = addresses[addrcur][2] - BMConfigParser().set(a, "enabled", "true") # Set config + config.set(a, "enabled", "true") # Set config # Write config - BMConfigParser().save() + config.save() # Change color - if BMConfigParser().safeGetBoolean(a, 'chan'): + if config.safeGetBoolean(a, 'chan'): addresses[addrcur][3] = 9 # orange - elif BMConfigParser().safeGetBoolean(a, 'mailinglist'): + elif config.safeGetBoolean(a, 'mailinglist'): addresses[addrcur][3] = 5 # magenta else: addresses[addrcur][3] = 0 # black @@ -638,26 +637,26 @@ def handlech(c, stdscr): shared.reloadMyAddressHashes() # Reload address hashes elif t == "5": # Disable address a = addresses[addrcur][2] - BMConfigParser().set(a, "enabled", "false") # Set config + config.set(a, "enabled", "false") # Set config addresses[addrcur][3] = 8 # Set color to gray # Write config - BMConfigParser().save() + config.save() addresses[addrcur][1] = False shared.reloadMyAddressHashes() # Reload address hashes elif t == "6": # Delete address r, t = d.inputbox("Type in \"I want to delete this address\"", width=50) if r == d.DIALOG_OK and t == "I want to delete this address": - BMConfigParser().remove_section(addresses[addrcur][2]) - BMConfigParser().save() + config.remove_section(addresses[addrcur][2]) + config.save() del addresses[addrcur] elif t == "7": # Special address behavior a = addresses[addrcur][2] set_background_title(d, "Special address behavior") - if BMConfigParser().safeGetBoolean(a, "chan"): + if config.safeGetBoolean(a, "chan"): scrollbox(d, unicode( "This is a chan address. You cannot use it as a pseudo-mailing list.")) else: - m = BMConfigParser().safeGetBoolean(a, "mailinglist") + m = config.safeGetBoolean(a, "mailinglist") r, t = d.radiolist( "Select address behavior", choices=[ @@ -665,24 +664,24 @@ def handlech(c, stdscr): ("2", "Behave as a pseudo-mailing-list address", m)]) if r == d.DIALOG_OK: if t == "1" and m: - BMConfigParser().set(a, "mailinglist", "false") + config.set(a, "mailinglist", "false") if addresses[addrcur][1]: addresses[addrcur][3] = 0 # Set color to black else: addresses[addrcur][3] = 8 # Set color to gray elif t == "2" and m is False: try: - mn = BMConfigParser().get(a, "mailinglistname") + mn = config.get(a, "mailinglistname") except ConfigParser.NoOptionError: mn = "" r, t = d.inputbox("Mailing list name", init=mn) if r == d.DIALOG_OK: mn = t - BMConfigParser().set(a, "mailinglist", "true") - BMConfigParser().set(a, "mailinglistname", mn) + config.set(a, "mailinglist", "true") + config.set(a, "mailinglistname", mn) addresses[addrcur][3] = 6 # Set color to magenta # Write config - BMConfigParser().save() + config.save() elif menutab == 5: set_background_title(d, "Subscriptions Dialog Box") if len(subscriptions) <= subcur: @@ -1002,7 +1001,7 @@ def loadInbox(): if toaddr == BROADCAST_STR: tolabel = BROADCAST_STR else: - tolabel = BMConfigParser().get(toaddr, "label") + tolabel = config.get(toaddr, "label") except: # noqa:E722 tolabel = "" if tolabel == "": @@ -1011,8 +1010,8 @@ def loadInbox(): # Set label for from address fromlabel = "" - if BMConfigParser().has_section(fromaddr): - fromlabel = BMConfigParser().get(fromaddr, "label") + if config.has_section(fromaddr): + fromlabel = config.get(fromaddr, "label") if fromlabel == "": # Check Address Book qr = sqlQuery("SELECT label FROM addressbook WHERE address=?", fromaddr) if qr != []: @@ -1062,15 +1061,15 @@ def loadSent(): for r in qr: tolabel, = r if tolabel == "": - if BMConfigParser().has_section(toaddr): - tolabel = BMConfigParser().get(toaddr, "label") + if config.has_section(toaddr): + tolabel = config.get(toaddr, "label") if tolabel == "": tolabel = toaddr # Set label for from address fromlabel = "" - if BMConfigParser().has_section(fromaddr): - fromlabel = BMConfigParser().get(fromaddr, "label") + if config.has_section(fromaddr): + fromlabel = config.get(fromaddr, "label") if fromlabel == "": fromlabel = fromaddr @@ -1146,7 +1145,7 @@ def loadSubscriptions(): def loadBlackWhiteList(): """load black/white list""" global bwtype - bwtype = BMConfigParser().get("bitmessagesettings", "blackwhitelist") + bwtype = config.get("bitmessagesettings", "blackwhitelist") if bwtype == "black": ret = sqlQuery("SELECT label, address, enabled FROM blacklist") else: @@ -1205,16 +1204,16 @@ def run(stdscr): curses.init_pair(9, curses.COLOR_YELLOW, curses.COLOR_BLACK) # orangish # Init list of address in 'Your Identities' tab - configSections = BMConfigParser().addresses() + configSections = config.addresses() for addressInKeysFile in configSections: - isEnabled = BMConfigParser().getboolean(addressInKeysFile, "enabled") - addresses.append([BMConfigParser().get(addressInKeysFile, "label"), isEnabled, addressInKeysFile]) + isEnabled = config.getboolean(addressInKeysFile, "enabled") + addresses.append([config.get(addressInKeysFile, "label"), isEnabled, addressInKeysFile]) # Set address color if not isEnabled: addresses[len(addresses) - 1].append(8) # gray - elif BMConfigParser().safeGetBoolean(addressInKeysFile, 'chan'): + elif config.safeGetBoolean(addressInKeysFile, 'chan'): addresses[len(addresses) - 1].append(9) # orange - elif BMConfigParser().safeGetBoolean(addressInKeysFile, 'mailinglist'): + elif config.safeGetBoolean(addressInKeysFile, 'mailinglist'): addresses[len(addresses) - 1].append(5) # magenta else: addresses[len(addresses) - 1].append(0) # black diff --git a/src/bitmessagekivy/base_navigation.py b/src/bitmessagekivy/base_navigation.py new file mode 100644 index 00000000..5f6b1aa5 --- /dev/null +++ b/src/bitmessagekivy/base_navigation.py @@ -0,0 +1,110 @@ +# pylint: disable=unused-argument, no-name-in-module, too-few-public-methods +""" + Base class for Navigation Drawer +""" + +from kivy.lang import Observable + +from kivy.properties import ( + BooleanProperty, + NumericProperty, + StringProperty +) +from kivy.metrics import dp +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.spinner import Spinner + +from kivy.clock import Clock +from kivy.core.window import Window + +from kivymd.uix.list import ( + OneLineAvatarIconListItem, + OneLineListItem +) + +from pybitmessage.bmconfigparser import config + + +class BaseLanguage(Observable): + """UI Language""" + observers = [] + lang = None + + def __init__(self, defaultlang): + super(BaseLanguage, self).__init__() + self.ugettext = None + self.lang = defaultlang + + @staticmethod + def _(text): + return text + + +class BaseNavigationItem(OneLineAvatarIconListItem): + """NavigationItem class for kivy Ui""" + badge_text = StringProperty() + icon = StringProperty() + active = BooleanProperty(False) + + def currentlyActive(self): + """Currenly active""" + for nav_obj in self.parent.children: + nav_obj.active = False + self.active = True + + +class BaseNavigationDrawerDivider(OneLineListItem): + """ + A small full-width divider that can be placed + in the :class:`MDNavigationDrawer` + """ + + disabled = True + divider = None + _txt_top_pad = NumericProperty(dp(8)) + _txt_bot_pad = NumericProperty(dp(8)) + + def __init__(self, **kwargs): + super(BaseNavigationDrawerDivider, self).__init__(**kwargs) + self.height = dp(16) + + +class BaseNavigationDrawerSubheader(OneLineListItem): + """ + A subheader for separating content in :class:`MDNavigationDrawer` + + Works well alongside :class:`NavigationDrawerDivider` + """ + + disabled = True + divider = None + theme_text_color = 'Secondary' + + +class BaseContentNavigationDrawer(BoxLayout): + """ContentNavigationDrawer class for kivy Uir""" + + def __init__(self, *args, **kwargs): + """Method used for contentNavigationDrawer""" + super(BaseContentNavigationDrawer, self).__init__(*args, **kwargs) + Clock.schedule_once(self.init_ui, 0) + + def init_ui(self, dt=0): + """Clock Schdule for class contentNavigationDrawer""" + self.ids.scroll_y.bind(scroll_y=self.check_scroll_y) + + def check_scroll_y(self, instance, somethingelse): + """show data on scroll down""" + if self.ids.identity_dropdown.is_open: + self.ids.identity_dropdown.is_open = False + + +class BaseIdentitySpinner(Spinner): + """Base Class for Identity Spinner(Dropdown)""" + + def __init__(self, *args, **kwargs): + """Method used for setting size of spinner""" + super(BaseIdentitySpinner, self).__init__(*args, **kwargs) + self.dropdown_cls.max_height = Window.size[1] / 3 + self.values = list(addr for addr in config.addresses() + if config.getboolean(str(addr), 'enabled')) diff --git a/src/mock/__init__.py b/src/bitmessagekivy/baseclass/__init__.py similarity index 100% rename from src/mock/__init__.py rename to src/bitmessagekivy/baseclass/__init__.py diff --git a/src/bitmessagekivy/baseclass/addressbook.py b/src/bitmessagekivy/baseclass/addressbook.py new file mode 100644 index 00000000..f18a0142 --- /dev/null +++ b/src/bitmessagekivy/baseclass/addressbook.py @@ -0,0 +1,164 @@ +# pylint: disable=unused-argument, consider-using-f-string, import-error +# pylint: disable=unnecessary-comprehension, no-member, no-name-in-module + +""" +addressbook.py +============== + +All saved addresses are managed in Addressbook + +""" + +import os +import logging +from functools import partial + +from kivy.properties import ( + ListProperty, + StringProperty +) +from kivy.uix.screenmanager import Screen +from kivy.app import App + +from pybitmessage.bitmessagekivy.get_platform import platform +from pybitmessage.bitmessagekivy import kivy_helper_search +from pybitmessage.bitmessagekivy.baseclass.common import ( + avatar_image_first_letter, toast, empty_screen_label, + ThemeClsColor, SwipeToDeleteItem, kivy_state_variables +) +from pybitmessage.bitmessagekivy.baseclass.popup import SavedAddressDetailPopup +from pybitmessage.bitmessagekivy.baseclass.addressbook_widgets import HelperAddressBook +from pybitmessage.helper_sql import sqlExecute + +logger = logging.getLogger('default') + + +class AddressBook(Screen, HelperAddressBook): + """AddressBook Screen class for kivy Ui""" + + queryreturn = ListProperty() + has_refreshed = True + address_label = StringProperty() + address = StringProperty() + label_str = "No contact Address found yet......" + no_search_res_found = "No search result found" + + def __init__(self, *args, **kwargs): + """Getting AddressBook Details""" + super(AddressBook, self).__init__(*args, **kwargs) + self.addbook_popup = None + self.kivy_state = kivy_state_variables() + + def loadAddresslist(self, account, where="", what=""): + """Clock Schdule for method AddressBook""" + if self.kivy_state.searching_text: + self.ids.scroll_y.scroll_y = 1.0 + where = ['label', 'address'] + what = self.kivy_state.searching_text + xAddress = '' + self.ids.tag_label.text = '' + self.queryreturn = kivy_helper_search.search_sql( + xAddress, account, "addressbook", where, what, False) + self.queryreturn = [obj for obj in reversed(self.queryreturn)] + if self.queryreturn: + self.ids.tag_label.text = 'Address Book' + self.has_refreshed = True + self.set_mdList(0, 20) + self.ids.scroll_y.bind(scroll_y=self.check_scroll_y) + else: + self.ids.ml.add_widget(empty_screen_label(self.label_str, self.no_search_res_found)) + + def set_mdList(self, start_index, end_index): + """Creating the mdList""" + for item in self.queryreturn[start_index:end_index]: + message_row = SwipeToDeleteItem( + text=item[0], + ) + listItem = message_row.ids.content + listItem.secondary_text = item[1] + listItem.theme_text_color = "Custom" + listItem.text_color = ThemeClsColor + image = os.path.join( + self.kivy_state.imageDir, "text_images", "{}.png".format(avatar_image_first_letter(item[0].strip())) + ) + message_row.ids.avater_img.source = image + listItem.bind(on_release=partial( + self.addBook_detail, item[1], item[0], message_row)) + message_row.ids.delete_msg.bind(on_press=partial(self.delete_address, item[1])) + self.ids.ml.add_widget(message_row) + + def check_scroll_y(self, instance, somethingelse): + """Load data on scroll""" + if self.ids.scroll_y.scroll_y <= -0.0 and self.has_refreshed: + self.ids.scroll_y.scroll_y = 0.06 + exist_addresses = len(self.ids.ml.children) + if exist_addresses != len(self.queryreturn): + self.update_addressBook_on_scroll(exist_addresses) + self.has_refreshed = ( + True if exist_addresses != len(self.queryreturn) else False + ) + + def update_addressBook_on_scroll(self, exist_addresses): + """Load more data on scroll down""" + self.set_mdList(exist_addresses, exist_addresses + 5) + + @staticmethod + def refreshs(*args): + """Refresh the Widget""" + + # @staticmethod + def addBook_detail(self, address, label, instance, *args): + """Addressbook details""" + if instance.state == 'closed': + instance.ids.delete_msg.disabled = True + if instance.open_progress == 0.0: + obj = SavedAddressDetailPopup() + self.address_label = obj.address_label = label + self.address = obj.address = address + width = .9 if platform == 'android' else .8 + self.addbook_popup = self.address_detail_popup( + obj, self.send_message_to, self.update_addbook_label, + self.close_pop, width) + self.addbook_popup.auto_dismiss = False + self.addbook_popup.open() + else: + instance.ids.delete_msg.disabled = False + + def delete_address(self, address, instance, *args): + """Delete inbox mail from inbox listing""" + self.ids.ml.remove_widget(instance.parent.parent) + # if len(self.ids.ml.children) == 0: + if self.ids.ml.children is not None: + self.ids.tag_label.text = '' + sqlExecute( + "DELETE FROM addressbook WHERE address = ?", address) + toast('Address Deleted') + + def close_pop(self, instance): + """Pop is Canceled""" + self.addbook_popup.dismiss() + toast('Canceled') + + def update_addbook_label(self, instance): + """Updating the label of address book address""" + address_list = kivy_helper_search.search_sql(folder="addressbook") + stored_labels = [labels[0] for labels in address_list] + add_dict = dict(address_list) + label = str(self.addbook_popup.content_cls.ids.add_label.text) + if label in stored_labels and self.address == add_dict[label]: + stored_labels.remove(label) + if label and label not in stored_labels: + sqlExecute(""" + UPDATE addressbook + SET label = ? + WHERE address = ?""", label, self.addbook_popup.content_cls.address) + App.get_running_app().root.ids.id_addressbook.ids.ml.clear_widgets() + App.get_running_app().root.ids.id_addressbook.loadAddresslist(None, 'All', '') + self.addbook_popup.dismiss() + toast('Saved') + + def send_message_to(self, instance): + """Method used to fill to_address of composer autofield""" + App.get_running_app().set_navbar_for_composer() + self.compose_message(None, self.address) + self.addbook_popup.dismiss() diff --git a/src/bitmessagekivy/baseclass/addressbook_widgets.py b/src/bitmessagekivy/baseclass/addressbook_widgets.py new file mode 100644 index 00000000..3654dfa3 --- /dev/null +++ b/src/bitmessagekivy/baseclass/addressbook_widgets.py @@ -0,0 +1,50 @@ +# pylint: disable=no-member, too-many-arguments, too-few-public-methods +""" +Addressbook widgets are here. +""" + +from kivy.app import App +from kivymd.uix.button import MDRaisedButton +from kivymd.uix.dialog import MDDialog + + +class HelperAddressBook(object): + """Widget used in Addressbook are here""" + + @staticmethod + def address_detail_popup(obj, send_message, update_address, close_popup, width): + """This function shows the address's details and opens the popup.""" + show_dialogue = MDDialog( + type="custom", + size_hint=(width, .25), + content_cls=obj, + buttons=[ + MDRaisedButton( + text="Send message to", + on_release=send_message, + ), + MDRaisedButton( + text="Save", + on_release=update_address, + ), + MDRaisedButton( + text="Cancel", + on_release=close_popup, + ), + ], + ) + return show_dialogue + + @staticmethod + def compose_message(from_addr=None, to_addr=None): + """This UI independent method for message sending to reciever""" + window_obj = App.get_runnint_app().root.ids + if to_addr: + window_obj.id_create.children[1].ids.txt_input.text = to_addr + if from_addr: + window_obj.id_create.children[1].ids.txt_input.text = from_addr + window_obj.id_create.children[1].ids.ti.text = '' + window_obj.id_create.children[1].ids.composer_dropdown.text = 'Select' + window_obj.id_create.children[1].ids.subject.text = '' + window_obj.id_create.children[1].ids.body.text = '' + window_obj.scr_mngr.current = 'create' diff --git a/src/bitmessagekivy/baseclass/allmail.py b/src/bitmessagekivy/baseclass/allmail.py new file mode 100644 index 00000000..d30310d8 --- /dev/null +++ b/src/bitmessagekivy/baseclass/allmail.py @@ -0,0 +1,67 @@ +# pylint: disable=import-error, no-name-in-module +# pylint: disable=unused-argument, no-member, attribute-defined-outside-init + +""" +allmail.py +============== + +All mails are managed in allmail screen + +""" + +from kivy.clock import Clock +from kivy.properties import ( + ListProperty, + StringProperty +) +from kivy.uix.screenmanager import Screen +from kivy.app import App + +from pybitmessage.bitmessagekivy.baseclass.common import ( + show_limited_cnt, empty_screen_label, kivy_state_variables, +) + +import logging +logger = logging.getLogger('default') + + +class Allmails(Screen): + """Allmails Screen for kivy Ui""" + data = ListProperty() + has_refreshed = True + all_mails = ListProperty() + account = StringProperty() + label_str = 'yet no message for this account!!!!!!!!!!!!!' + + def __init__(self, *args, **kwargs): + """Method Parsing the address""" + super(Allmails, self).__init__(*args, **kwargs) + self.kivy_state = kivy_state_variables() + if self.kivy_state.selected_address == '': + if App.get_running_app().identity_list: + self.kivy_state.selected_address = App.get_running_app().identity_list[0] + Clock.schedule_once(self.init_ui, 0) + + def init_ui(self, dt=0): + """Clock Schdule for method all mails""" + self.loadMessagelist() + logger.debug(dt) + + def loadMessagelist(self): + """Load Inbox, Sent anf Draft list of messages""" + self.account = self.kivy_state.selected_address + self.ids.tag_label.text = '' + if self.all_mails: + self.ids.tag_label.text = 'All Mails' + self.kivy_state.all_count = str( + int(self.kivy_state.sent_count) + int(self.kivy_state.inbox_count)) + self.set_AllmailCnt(self.kivy_state.all_count) + else: + self.set_AllmailCnt('0') + self.ids.ml.add_widget(empty_screen_label(self.label_str)) + + @staticmethod + def set_AllmailCnt(Count): + """This method is used to set allmails message count""" + allmailCnt_obj = App.get_running_app().root.ids.content_drawer.ids.allmail_cnt + allmailCnt_obj.ids.badge_txt.text = show_limited_cnt(int(Count)) diff --git a/src/bitmessagekivy/baseclass/chat.py b/src/bitmessagekivy/baseclass/chat.py new file mode 100644 index 00000000..c5f94b8a --- /dev/null +++ b/src/bitmessagekivy/baseclass/chat.py @@ -0,0 +1,11 @@ +# pylint: disable=import-error, no-name-in-module, too-few-public-methods, too-many-ancestors + +''' + Chats are managed in this screen +''' + +from kivy.uix.screenmanager import Screen + + +class Chat(Screen): + """Chat Screen class for kivy Ui""" diff --git a/src/bitmessagekivy/baseclass/common.py b/src/bitmessagekivy/baseclass/common.py new file mode 100644 index 00000000..2cf4d76b --- /dev/null +++ b/src/bitmessagekivy/baseclass/common.py @@ -0,0 +1,236 @@ +# pylint: disable=no-name-in-module, attribute-defined-outside-init, import-error, unused-argument +""" + All Common widgets of kivy are managed here. +""" + +import os +from datetime import datetime + +from kivy.core.window import Window +from kivy.metrics import dp +from kivy.uix.image import Image +from kivy.properties import ( + NumericProperty, + StringProperty, + ListProperty +) +from kivy.app import App + +from kivymd.uix.list import ( + ILeftBody, + IRightBodyTouch, +) +from kivymd.uix.label import MDLabel +from kivymd.toast import kivytoast +from kivymd.uix.card import MDCardSwipe +from kivymd.uix.chip import MDChip +from kivymd.uix.dialog import MDDialog +from kivymd.uix.button import MDFlatButton + +from pybitmessage.bitmessagekivy.get_platform import platform +from pybitmessage.bmconfigparser import config + +ThemeClsColor = [0.12, 0.58, 0.95, 1] + + +data_screens = { + "MailDetail": { + "kv_string": "maildetail", + "Factory": "MailDetail()", + "name_screen": "mailDetail", + "object": 0, + "Import": "from pybitmessage.bitmessagekivy.baseclass.maildetail import MailDetail", + }, +} + + +def load_image_path(): + """Return the path of kivy images""" + image_path = os.path.abspath(os.path.join('pybitmessage', 'images', 'kivy')) + return image_path + + +def get_identity_list(): + """Get list of identities and access 'identity_list' variable in .kv file""" + identity_list = ListProperty( + addr for addr in config.addresses() if config.getboolean(str(addr), 'enabled') + ) + return identity_list + + +def kivy_state_variables(): + """Return kivy_state variable""" + kivy_running_app = App.get_running_app() + kivy_state = kivy_running_app.kivy_state_obj + return kivy_state + + +def chip_tag(text): + """Create a new ChipTag""" + obj = MDChip() + # obj.size_hint = (None, None) + obj.size_hint = (0.16 if platform == "android" else 0.08, None) + obj.text = text + obj.icon = "" + obj.pos_hint = { + "center_x": 0.91 if platform == "android" else 0.94, + "center_y": 0.3 + } + obj.height = dp(18) + obj.text_color = (1, 1, 1, 1) + obj.radius = [8] + return obj + + +def toast(text): + """Method will display the toast message""" + kivytoast.toast(text) + + +def show_limited_cnt(total_msg): + """This method set the total count limit in badge_text""" + max_msg_count = '99+' + total_msg_limit = 99 + return max_msg_count if total_msg > total_msg_limit else str(total_msg) + + +def avatar_image_first_letter(letter_string): + """Returns first letter for the avatar image""" + try: + image_letter = letter_string.title()[0] + if image_letter.isalnum(): + return image_letter + return '!' + except IndexError: + return '!' + + +def add_time_widget(time): # pylint: disable=redefined-outer-name, W0201 + """This method is used to create TimeWidget""" + action_time = TimeTagRightSampleWidget( + text=str(show_time_history(time)), + font_style="Caption", + size=[120, 140] if platform == "android" else [64, 80], + ) + action_time.font_size = "11sp" + return action_time + + +def show_time_history(act_time): + """This method is used to return the message sent or receive time""" + action_time = datetime.fromtimestamp(int(act_time)) + crnt_date = datetime.now() + duration = crnt_date - action_time + if duration.days < 1: + return action_time.strftime("%I:%M %p") + if duration.days < 365: + return action_time.strftime("%d %b") + return action_time.strftime("%d/%m/%Y") + + +# pylint: disable=too-few-public-methods +class AvatarSampleWidget(ILeftBody, Image): + """AvatarSampleWidget class for kivy Ui""" + + +class TimeTagRightSampleWidget(IRightBodyTouch, MDLabel): + """TimeTagRightSampleWidget class for Ui""" + + +class SwipeToDeleteItem(MDCardSwipe): + """Swipe delete class for App UI""" + text = StringProperty() + cla = Window.size[0] / 2 + # cla = 800 + swipe_distance = NumericProperty(cla) + opening_time = NumericProperty(0.5) + + +class CustomSwipeToDeleteItem(MDCardSwipe): + """Custom swipe delete class for App UI""" + text = StringProperty() + cla = Window.size[0] / 2 + swipe_distance = NumericProperty(cla) + opening_time = NumericProperty(0.5) + + +def empty_screen_label(label_str=None, no_search_res_found=None): + """Returns default text on screen when no address is there.""" + kivy_state = kivy_state_variables() + content = MDLabel( + font_style='Caption', + theme_text_color='Primary', + text=no_search_res_found if kivy_state.searching_text else label_str, + halign='center', + size_hint_y=None, + valign='top') + return content + + +def retrieve_secondary_text(mail): + """Retriving mail details""" + secondary_txt_len = 10 + third_txt_len = 25 + dot_str = '...........' + dot_str2 = '...!' + third_text = mail[3].replace('\n', ' ') + + if len(third_text) > third_txt_len: + if len(mail[2]) > secondary_txt_len: # pylint: disable=no-else-return + return mail[2][:secondary_txt_len] + dot_str + else: + return mail[2] + '\n' + " " + (third_text[:third_txt_len] + dot_str2) + else: + return third_text + + +def set_mail_details(mail): + """Setting mail details""" + mail_details_data = { + 'text': mail[1].strip(), + 'secondary_text': retrieve_secondary_text(mail), + 'ackdata': mail[5], + 'senttime': mail[6] + } + return mail_details_data + + +def mdlist_message_content(queryreturn, data): + """Set Mails details in MD_list""" + for mail in queryreturn: + mdlist_data = set_mail_details(mail) + data.append(mdlist_data) + + +def msg_content_length(body, subject, max_length=50): + """This function concatinate body and subject if len(subject) > 50""" + continue_str = '........' + if len(subject) >= max_length: + subject = subject[:max_length] + continue_str + else: + subject = ((subject + ',' + body)[0:50] + continue_str).replace('\t', '').replace(' ', '') + return subject + + +def composer_common_dialog(alert_msg): + """Common alert popup for message composer""" + is_android_width = .8 + other_platform_width = .55 + dialog_height = .25 + width = is_android_width if platform == 'android' else other_platform_width + + dialog_box = MDDialog( + text=alert_msg, + size_hint=(width, dialog_height), + buttons=[ + MDFlatButton( + text="Ok", on_release=lambda x: callback_for_menu_items("Ok") + ), + ], + ) + dialog_box.open() + + def callback_for_menu_items(text_item, *arg): + """Callback of alert box""" + dialog_box.dismiss() + toast(text_item) diff --git a/src/bitmessagekivy/baseclass/common_mail_detail.py b/src/bitmessagekivy/baseclass/common_mail_detail.py new file mode 100644 index 00000000..78ae88d5 --- /dev/null +++ b/src/bitmessagekivy/baseclass/common_mail_detail.py @@ -0,0 +1,22 @@ +# pylint: disable=no-name-in-module, attribute-defined-outside-init, import-error +""" + All Common widgets of kivy are managed here. +""" + +from pybitmessage.bitmessagekivy.baseclass.maildetail import MailDetail +from pybitmessage.bitmessagekivy.baseclass.common import kivy_state_variables + + +def mail_detail_screen(screen_name, msg_id, instance, folder, *args): # pylint: disable=unused-argument + """Common function for all screens to open Mail detail.""" + kivy_state = kivy_state_variables() + if instance.open_progress == 0.0: + kivy_state.detail_page_type = folder + kivy_state.mail_id = msg_id + if screen_name.manager: + src_mng_obj = screen_name.manager + else: + src_mng_obj = screen_name.parent.parent + src_mng_obj.screens[11].clear_widgets() + src_mng_obj.screens[11].add_widget(MailDetail()) + src_mng_obj.current = "mailDetail" diff --git a/src/bitmessagekivy/baseclass/draft.py b/src/bitmessagekivy/baseclass/draft.py new file mode 100644 index 00000000..1b3ddf21 --- /dev/null +++ b/src/bitmessagekivy/baseclass/draft.py @@ -0,0 +1,58 @@ +# pylint: disable=unused-argument, import-error, too-many-arguments +# pylint: disable=unnecessary-comprehension, no-member, no-name-in-module + +""" +draft.py +============== + +Draft screen + +""" +from kivy.clock import Clock +from kivy.properties import ( + ListProperty, + StringProperty +) +from kivy.uix.screenmanager import Screen +from kivy.app import App +from pybitmessage.bitmessagekivy.baseclass.common import ( + show_limited_cnt, empty_screen_label, + kivy_state_variables +) +import logging +logger = logging.getLogger('default') + + +class Draft(Screen): + """Draft screen class for kivy Ui""" + + data = ListProperty() + account = StringProperty() + queryreturn = ListProperty() + has_refreshed = True + label_str = "yet no message for this account!!!!!!!!!!!!!" + + def __init__(self, *args, **kwargs): + """Method used for storing draft messages""" + super(Draft, self).__init__(*args, **kwargs) + self.kivy_state = kivy_state_variables() + if self.kivy_state.selected_address == '': + if App.get_running_app().identity_list: + self.kivy_state.selected_address = App.get_running_app().identity_list[0] + Clock.schedule_once(self.init_ui, 0) + + def init_ui(self, dt=0): + """Clock Schedule for method draft accounts""" + self.load_draft() + logger.debug(dt) + + def load_draft(self, where="", what=""): + """Load draft list for Draft messages""" + self.set_draft_count('0') + self.ids.ml.add_widget(empty_screen_label(self.label_str)) + + @staticmethod + def set_draft_count(Count): + """Set the count of draft mails""" + draftCnt_obj = App.get_running_app().root.ids.content_drawer.ids.draft_cnt + draftCnt_obj.ids.badge_txt.text = show_limited_cnt(int(Count)) diff --git a/src/bitmessagekivy/baseclass/inbox.py b/src/bitmessagekivy/baseclass/inbox.py new file mode 100644 index 00000000..2e145871 --- /dev/null +++ b/src/bitmessagekivy/baseclass/inbox.py @@ -0,0 +1,64 @@ +# pylint: disable=unused-import, too-many-public-methods, unused-variable, too-many-ancestors +# pylint: disable=no-name-in-module, too-few-public-methods, import-error, unused-argument, too-many-arguments +# pylint: disable=attribute-defined-outside-init, global-variable-not-assigned, too-many-instance-attributes + +""" +Kivy UI for inbox screen +""" +from kivy.clock import Clock +from kivy.properties import ( + ListProperty, + StringProperty +) +from kivy.app import App +from kivy.uix.screenmanager import Screen + +from pybitmessage.bitmessagekivy.baseclass.common import kivy_state_variables, load_image_path + + +class Inbox(Screen): + """Inbox Screen class for kivy Ui""" + + queryreturn = ListProperty() + has_refreshed = True + account = StringProperty() + no_search_res_found = "No search result found" + label_str = "Yet no message for this account!" + + def __init__(self, *args, **kwargs): + """Initialize kivy variables""" + super(Inbox, self).__init__(*args, **kwargs) + self.kivy_running_app = App.get_running_app() + self.kivy_state = kivy_state_variables() + self.image_dir = load_image_path() + Clock.schedule_once(self.init_ui, 0) + + def set_defaultAddress(self): + """Set default address""" + if self.kivy_state.selected_address == "": + if self.kivy_running_app.identity_list: + self.kivy_state.selected_address = self.kivy_running_app.identity_list[0] + + def init_ui(self, dt=0): + """loadMessagelist() call at specific interval""" + self.loadMessagelist() + + def loadMessagelist(self, where="", what=""): + """Load inbox list for inbox messages""" + self.set_defaultAddress() + self.account = self.kivy_state.selected_address + + def refresh_callback(self, *args): + """Load inbox messages while wating-loader spins & called in inbox.kv""" + + def refresh_on_scroll_down(interval): + """Reset fields and load data on scrolling upside down""" + self.kivy_state.searching_text = "" + self.children[2].children[1].ids.search_field.text = "" + self.ids.ml.clear_widgets() + self.loadMessagelist(self.kivy_state.selected_address) + self.has_refreshed = True + self.ids.refresh_layout.refresh_done() + self.tick = 0 + + Clock.schedule_once(refresh_on_scroll_down, 1) diff --git a/src/bitmessagekivy/baseclass/login.py b/src/bitmessagekivy/baseclass/login.py new file mode 100644 index 00000000..c5dd9ef4 --- /dev/null +++ b/src/bitmessagekivy/baseclass/login.py @@ -0,0 +1,97 @@ +# pylint: disable=no-member, too-many-arguments, too-few-public-methods +# pylint: disable=no-name-in-module, unused-argument, arguments-differ + +""" +Login screen appears when the App is first time starts and when new Address is generated. +""" + + +from kivy.clock import Clock +from kivy.properties import StringProperty, BooleanProperty +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.screenmanager import Screen +from kivy.app import App + +from kivymd.uix.behaviors.elevation import RectangularElevationBehavior + +from pybitmessage.backend.address_generator import AddressGenerator +from pybitmessage.bitmessagekivy.baseclass.common import toast +from pybitmessage.bmconfigparser import config + + +class Login(Screen): + """Login Screeen class for kivy Ui""" + log_text1 = ( + 'You may generate addresses by using either random numbers' + ' or by using a passphrase If you use a passphrase, the address' + ' is called a deterministic; address The Random Number option is' + ' selected by default but deterministic addresses have several pros' + ' and cons:') + log_text2 = ('If talk about pros You can recreate your addresses on any computer' + ' from memory, You need-not worry about backing up your keys.dat file' + ' as long as you can remember your passphrase and aside talk about cons' + ' You must remember (or write down) your You must remember the address' + ' version number and the stream number along with your passphrase If you' + ' choose a weak passphrase and someone on the Internet can brute-force it,' + ' they can read your messages and send messages as you') + + +class Random(Screen): + """Random Screen class for Ui""" + + is_active = BooleanProperty(False) + checked = StringProperty("") + + def generateaddress(self): + """Method for Address Generator""" + entered_label = str(self.ids.add_random_bx.children[0].ids.lab.text).strip() + if not entered_label: + self.ids.add_random_bx.children[0].ids.lab.focus = True + is_address = AddressGenerator.random_address_generation( + entered_label, streamNumberForAddress=1, eighteenByteRipe=False, + nonceTrialsPerByte=1000, payloadLengthExtraBytes=1000 + ) + if is_address: + toast('Creating New Address ...') + self.parent.parent.ids.toolbar.opacity = 1 + self.parent.parent.ids.toolbar.disabled = False + App.get_running_app().loadMyAddressScreen(True) + self.manager.current = 'myaddress' + Clock.schedule_once(self.address_created_callback, 6) + + def address_created_callback(self, dt=0): + """New address created""" + App.get_running_app().loadMyAddressScreen(False) + App.get_running_app().root.ids.id_myaddress.ids.ml.clear_widgets() + App.get_running_app().root.ids.id_myaddress.is_add_created = True + App.get_running_app().root.ids.id_myaddress.init_ui() + self.reset_address_spinner() + toast('New address created') + + def reset_address_spinner(self): + """reseting spinner address and UI""" + addresses = [addr for addr in config.addresses() + if config.getboolean(str(addr), 'enabled')] + self.manager.parent.ids.content_drawer.ids.identity_dropdown.values = [] + self.manager.parent.ids.content_drawer.ids.identity_dropdown.values = addresses + self.manager.parent.ids.id_create.children[1].ids.composer_dropdown.values = [] + self.manager.parent.ids.id_create.children[1].ids.composer_dropdown.values = addresses + + @staticmethod + def add_validation(instance): + """Retrieve created labels and validate""" + entered_label = str(instance.text.strip()) + AddressGenerator.address_validation(instance, entered_label) + + def reset_address_label(self): + """Resetting address labels""" + if not self.ids.add_random_bx.children: + self.ids.add_random_bx.add_widget(RandomBoxlayout()) + + +class InfoLayout(BoxLayout, RectangularElevationBehavior): + """InfoLayout class for kivy Ui""" + + +class RandomBoxlayout(BoxLayout): + """RandomBoxlayout class for BoxLayout behaviour""" diff --git a/src/bitmessagekivy/baseclass/maildetail.py b/src/bitmessagekivy/baseclass/maildetail.py new file mode 100644 index 00000000..6ddf322d --- /dev/null +++ b/src/bitmessagekivy/baseclass/maildetail.py @@ -0,0 +1,242 @@ +# pylint: disable=unused-argument, consider-using-f-string, import-error, attribute-defined-outside-init +# pylint: disable=unnecessary-comprehension, no-member, no-name-in-module, too-few-public-methods + +""" +Maildetail screen for inbox, sent, draft and trash. +""" + +import os +from datetime import datetime + +from kivy.core.clipboard import Clipboard +from kivy.clock import Clock +from kivy.properties import ( + StringProperty, + NumericProperty +) +from kivy.uix.screenmanager import Screen +from kivy.factory import Factory +from kivy.app import App + +from kivymd.uix.button import MDFlatButton, MDIconButton +from kivymd.uix.dialog import MDDialog +from kivymd.uix.list import ( + OneLineListItem, + IRightBodyTouch +) + +from pybitmessage.bitmessagekivy.baseclass.common import ( + toast, avatar_image_first_letter, show_time_history, kivy_state_variables +) +from pybitmessage.bitmessagekivy.baseclass.popup import SenderDetailPopup +from pybitmessage.bitmessagekivy.get_platform import platform +from pybitmessage.helper_sql import sqlQuery + + +class OneLineListTitle(OneLineListItem): + """OneLineListTitle class for kivy Ui""" + __events__ = ('on_long_press', ) + long_press_time = NumericProperty(1) + + def on_state(self, instance, value): + """On state""" + if value == 'down': + lpt = self.long_press_time + self._clockev = Clock.schedule_once(self._do_long_press, lpt) + else: + self._clockev.cancel() + + def _do_long_press(self, dt): + """Do long press""" + self.dispatch('on_long_press') + + def on_long_press(self, *largs): + """On long press""" + self.copymessageTitle(self.text) + + def copymessageTitle(self, title_text): + """this method is for displaying dialog box""" + self.title_text = title_text + width = .8 if platform == 'android' else .55 + self.dialog_box = MDDialog( + text=title_text, + size_hint=(width, .25), + buttons=[ + MDFlatButton( + text="Copy", on_release=self.callback_for_copy_title + ), + MDFlatButton( + text="Cancel", on_release=self.callback_for_copy_title, + ), + ],) + self.dialog_box.open() + + def callback_for_copy_title(self, instance): + """Callback of alert box""" + if instance.text == 'Copy': + Clipboard.copy(self.title_text) + self.dialog_box.dismiss() + toast(instance.text) + + +class IconRightSampleWidget(IRightBodyTouch, MDIconButton): + """IconRightSampleWidget class for kivy Ui""" + + +class MailDetail(Screen): # pylint: disable=too-many-instance-attributes + """MailDetail Screen class for kivy Ui""" + + to_addr = StringProperty() + from_addr = StringProperty() + subject = StringProperty() + message = StringProperty() + status = StringProperty() + page_type = StringProperty() + time_tag = StringProperty() + avatarImg = StringProperty() + no_subject = '(no subject)' + + def __init__(self, *args, **kwargs): + """Mail Details method""" + super(MailDetail, self).__init__(*args, **kwargs) + self.kivy_state = kivy_state_variables() + Clock.schedule_once(self.init_ui, 0) + + def init_ui(self, dt=0): + """Clock Schdule for method MailDetail mails""" + self.page_type = self.kivy_state.detail_page_type if self.kivy_state.detail_page_type else '' + try: + if self.kivy_state.detail_page_type in ('sent', 'draft'): + App.get_running_app().set_mail_detail_header() + elif self.kivy_state.detail_page_type == 'inbox': + data = sqlQuery( + "select toaddress, fromaddress, subject, message, received from inbox" + " where msgid = ?", self.kivy_state.mail_id) + self.assign_mail_details(data) + App.get_running_app().set_mail_detail_header() + except Exception as e: # pylint: disable=unused-variable + print('Something wents wrong!!') + + def assign_mail_details(self, data): + """Assigning mail details""" + subject = data[0][2].decode() if isinstance(data[0][2], bytes) else data[0][2] + body = data[0][3].decode() if isinstance(data[0][2], bytes) else data[0][3] + self.to_addr = data[0][0] if len(data[0][0]) > 4 else ' ' + self.from_addr = data[0][1] + + self.subject = subject.capitalize( + ) if subject.capitalize() else self.no_subject + self.message = body + if len(data[0]) == 7: + self.status = data[0][4] + self.time_tag = show_time_history(data[0][4]) if self.kivy_state.detail_page_type == 'inbox' \ + else show_time_history(data[0][6]) + self.avatarImg = os.path.join(self.kivy_state.imageDir, 'draft-icon.png') \ + if self.kivy_state.detail_page_type == 'draft' \ + else (os.path.join(self.kivy_state.imageDir, 'text_images', '{0}.png'.format(avatar_image_first_letter( + self.subject.strip())))) + self.timeinseconds = data[0][4] if self.kivy_state.detail_page_type == 'inbox' else data[0][6] + + def delete_mail(self): + """Method for mail delete""" + msg_count_objs = App.get_running_app().root.ids.content_drawer.ids + self.kivy_state.searching_text = '' + self.children[0].children[0].active = True + if self.kivy_state.detail_page_type == 'sent': + App.get_running_app().root.ids.id_sent.ids.sent_search.ids.search_field.text = '' + msg_count_objs.send_cnt.ids.badge_txt.text = str(int(self.kivy_state.sent_count) - 1) + self.kivy_state.sent_count = str(int(self.kivy_state.sent_count) - 1) + self.parent.screens[2].ids.ml.clear_widgets() + self.parent.screens[2].loadSent(self.kivy_state.selected_address) + elif self.kivy_state.detail_page_type == 'inbox': + App.get_running_app().root.ids.id_inbox.ids.inbox_search.ids.search_field.text = '' + msg_count_objs.inbox_cnt.ids.badge_txt.text = str( + int(self.kivy_state.inbox_count) - 1) + self.kivy_state.inbox_count = str(int(self.kivy_state.inbox_count) - 1) + self.parent.screens[0].ids.ml.clear_widgets() + self.parent.screens[0].loadMessagelist(self.kivy_state.selected_address) + + elif self.kivy_state.detail_page_type == 'draft': + msg_count_objs.draft_cnt.ids.badge_txt.text = str( + int(self.kivy_state.draft_count) - 1) + self.kivy_state.draft_count = str(int(self.kivy_state.draft_count) - 1) + self.parent.screens[13].clear_widgets() + self.parent.screens[13].add_widget(Factory.Draft()) + + if self.kivy_state.detail_page_type != 'draft': + msg_count_objs.trash_cnt.ids.badge_txt.text = str( + int(self.kivy_state.trash_count) + 1) + msg_count_objs.allmail_cnt.ids.badge_txt.text = str( + int(self.kivy_state.all_count) - 1) + self.kivy_state.trash_count = str(int(self.kivy_state.trash_count) + 1) + self.kivy_state.all_count = str(int(self.kivy_state.all_count) - 1) if \ + int(self.kivy_state.all_count) else '0' + self.parent.screens[3].clear_widgets() + self.parent.screens[3].add_widget(Factory.Trash()) + self.parent.screens[14].clear_widgets() + self.parent.screens[14].add_widget(Factory.Allmails()) + Clock.schedule_once(self.callback_for_delete, 4) + + def callback_for_delete(self, dt=0): + """Delete method from allmails""" + if self.kivy_state.detail_page_type: + self.children[0].children[0].active = False + App.get_running_app().set_common_header() + self.parent.current = 'allmails' \ + if self.kivy_state.is_allmail else self.kivy_state.detail_page_type + self.kivy_state.detail_page_type = '' + toast('Deleted') + + def get_message_details_to_reply(self, data): + """Getting message details and fill into fields when reply""" + sender_address = ' wrote:--------------\n' + message_time = '\n\n --------------On ' + composer_obj = self.parent.screens[1].children[1].ids + composer_obj.ti.text = data[0][0] + composer_obj.composer_dropdown.text = data[0][0] + composer_obj.txt_input.text = data[0][1] + split_subject = data[0][2].split('Re:', 1) + composer_obj.subject.text = 'Re: ' + (split_subject[1] if len(split_subject) > 1 else split_subject[0]) + time_obj = datetime.fromtimestamp(int(data[0][4])) + time_tag = time_obj.strftime("%d %b %Y, %I:%M %p") + sender_name = data[0][1] + composer_obj.body.text = ( + message_time + time_tag + ', ' + sender_name + sender_address + data[0][3]) + composer_obj.body.focus = True + composer_obj.body.cursor = (0, 0) + + def inbox_reply(self): + """Reply inbox messages""" + self.kivy_state.in_composer = True + App.get_running_app().root.ids.id_create.children[1].ids.rv.data = '' + App.get_running_app().root.ids.sc3.children[1].ids.rv.data = '' + self.parent.current = 'create' + App.get_running_app().set_navbar_for_composer() + + def get_message_details_for_draft_reply(self, data): + """Getting and setting message details fill into fields when draft reply""" + composer_ids = ( + self.parent.parent.ids.id_create.children[1].ids) + composer_ids.ti.text = data[0][1] + composer_ids.btn.text = data[0][1] + composer_ids.txt_input.text = data[0][0] + composer_ids.subject.text = data[0][2] if data[0][2] != self.no_subject else '' + composer_ids.body.text = data[0][3] + + def write_msg(self, navApp): + """Write on draft mail""" + self.kivy_state.send_draft_mail = self.kivy_state.mail_id + self.parent.current = 'create' + navApp.set_navbar_for_composer() + + def detailedPopup(self): + """Detailed popup""" + obj = SenderDetailPopup() + obj.open() + arg = (self.to_addr, self.from_addr, self.timeinseconds) + obj.assignDetail(*arg) + + @staticmethod + def callback_for_menu_items(text_item, *arg): + """Callback of alert box""" + toast(text_item) diff --git a/src/bitmessagekivy/baseclass/msg_composer.py b/src/bitmessagekivy/baseclass/msg_composer.py new file mode 100644 index 00000000..a36996e0 --- /dev/null +++ b/src/bitmessagekivy/baseclass/msg_composer.py @@ -0,0 +1,188 @@ +# pylint: disable=unused-argument, consider-using-f-string, too-many-ancestors +# pylint: disable=no-member, no-name-in-module, too-few-public-methods, no-name-in-module +""" + Message composer screen UI +""" + +import logging + +from kivy.app import App +from kivy.properties import ( + BooleanProperty, + ListProperty, + NumericProperty, + ObjectProperty, +) +from kivy.uix.behaviors import FocusBehavior +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.label import Label +from kivy.uix.recycleview import RecycleView +from kivy.uix.recycleboxlayout import RecycleBoxLayout +from kivy.uix.recycleview.layout import LayoutSelectionBehavior +from kivy.uix.recycleview.views import RecycleDataViewBehavior +from kivy.uix.screenmanager import Screen + +from kivymd.uix.textfield import MDTextField + +from pybitmessage import state +from pybitmessage.bitmessagekivy.get_platform import platform +from pybitmessage.bitmessagekivy.baseclass.common import ( + toast, kivy_state_variables, composer_common_dialog +) + +logger = logging.getLogger('default') + + +class Create(Screen): + """Creates Screen class for kivy Ui""" + + def __init__(self, **kwargs): + """Getting Labels and address from addressbook""" + super(Create, self).__init__(**kwargs) + self.kivy_running_app = App.get_running_app() + self.kivy_state = kivy_state_variables() + self.dropdown_widget = DropDownWidget() + self.dropdown_widget.ids.txt_input.starting_no = 2 + self.add_widget(self.dropdown_widget) + self.children[0].ids.id_scroll.bind(scroll_y=self.check_scroll_y) + + def check_scroll_y(self, instance, somethingelse): # pylint: disable=unused-argument + """show data on scroll down""" + if self.children[1].ids.composer_dropdown.is_open: + self.children[1].ids.composer_dropdown.is_open = False + + +class RV(RecycleView): + """Recycling View class for kivy Ui""" + + def __init__(self, **kwargs): + """Recycling Method""" + super(RV, self).__init__(**kwargs) + + +class SelectableRecycleBoxLayout( + FocusBehavior, LayoutSelectionBehavior, RecycleBoxLayout +): + """Adds selection and focus behaviour to the view""" + # pylint: disable = duplicate-bases + + +class DropDownWidget(BoxLayout): + """DropDownWidget class for kivy Ui""" + + # pylint: disable=too-many-statements + + txt_input = ObjectProperty() + rv = ObjectProperty() + + def __init__(self, **kwargs): + super(DropDownWidget, self).__init__(**kwargs) + self.kivy_running_app = App.get_running_app() + self.kivy_state = kivy_state_variables() + + @staticmethod + def callback_for_msgsend(dt=0): # pylint: disable=unused-argument + """Callback method for messagesend""" + state.kivyapp.root.ids.id_create.children[0].active = False + state.in_sent_method = True + state.kivyapp.back_press() + toast("sent") + + def reset_composer(self): + """Method will reset composer""" + self.ids.ti.text = "" + self.ids.composer_dropdown.text = "Select" + self.ids.txt_input.text = "" + self.ids.subject.text = "" + self.ids.body.text = "" + toast("Reset message") + + def auto_fill_fromaddr(self): + """Fill the text automatically From Address""" + self.ids.ti.text = self.ids.composer_dropdown.text + self.ids.ti.focus = True + + def is_camara_attached(self): + """Checks the camera availability in device""" + self.parent.parent.parent.ids.id_scanscreen.check_camera() + is_available = self.parent.parent.parent.ids.id_scanscreen.camera_available + return is_available + + @staticmethod + def camera_alert(): + """Show camera availability alert message""" + feature_unavailable = 'Currently this feature is not available!' + cam_not_available = 'Camera is not available!' + alert_text = feature_unavailable if platform == 'android' else cam_not_available + composer_common_dialog(alert_text) + + +class MyTextInput(MDTextField): + """MyTextInput class for kivy Ui""" + + txt_input = ObjectProperty() + flt_list = ObjectProperty() + word_list = ListProperty() + starting_no = NumericProperty(3) + suggestion_text = '' + + def __init__(self, **kwargs): + """Getting Text Input.""" + super(MyTextInput, self).__init__(**kwargs) + self.__lineBreak__ = 0 + + def on_text(self, instance, value): # pylint: disable=unused-argument + """Find all the occurrence of the word""" + self.parent.parent.parent.parent.parent.ids.rv.data = [] + max_recipient_len = 10 + box_height = 250 + box_max_height = 400 + + matches = [self.word_list[i] for i in range( + len(self.word_list)) if self.word_list[ + i][:self.starting_no] == value[:self.starting_no]] + display_data = [] + for i in matches: + display_data.append({'text': i}) + self.parent.parent.parent.parent.parent.ids.rv.data = display_data + if len(matches) <= max_recipient_len: + self.parent.height = (box_height + (len(matches) * 20)) + else: + self.parent.height = box_max_height + + def keyboard_on_key_down(self, window, keycode, text, modifiers): + """Keyboard on key Down""" + if self.suggestion_text and keycode[1] == 'tab' and modifiers is None: + self.insert_text(self.suggestion_text + ' ') + return True + return super(MyTextInput, self).keyboard_on_key_down( + window, keycode, text, modifiers) + + +class SelectableLabel(RecycleDataViewBehavior, Label): + """Add selection support to the Label""" + + index = None + selected = BooleanProperty(False) + selectable = BooleanProperty(True) + + def refresh_view_attrs(self, rv, index, data): + """Catch and handle the view changes""" + self.index = index + return super(SelectableLabel, self).refresh_view_attrs(rv, index, data) + + def on_touch_down(self, touch): # pylint: disable=inconsistent-return-statements + """Add selection on touch down""" + if super(SelectableLabel, self).on_touch_down(touch): + return True + if self.collide_point(*touch.pos) and self.selectable: + return self.parent.select_with_touch(self.index, touch) + + def apply_selection(self, rv, index, is_selected): + """Respond to the selection of items in the view""" + self.selected = is_selected + if is_selected: + logger.debug("selection changed to %s", rv.data[index]) + rv.parent.txt_input.text = rv.parent.txt_input.text.replace( + rv.parent.txt_input.text, rv.data[index]["text"] + ) diff --git a/src/bitmessagekivy/baseclass/myaddress.py b/src/bitmessagekivy/baseclass/myaddress.py new file mode 100644 index 00000000..0a46bae9 --- /dev/null +++ b/src/bitmessagekivy/baseclass/myaddress.py @@ -0,0 +1,230 @@ +# pylint: disable=unused-argument, import-error, no-member, attribute-defined-outside-init +# pylint: disable=no-name-in-module, too-few-public-methods, too-many-instance-attributes + +""" +myaddress.py +============== +All generated addresses are managed in MyAddress +""" + +import os +from functools import partial + +from kivy.clock import Clock +from kivy.properties import ( + ListProperty, + StringProperty +) +from kivy.uix.screenmanager import Screen, ScreenManagerException +from kivy.app import App + +from kivymd.uix.list import ( + IRightBodyTouch, + TwoLineAvatarIconListItem, +) +from kivymd.uix.selectioncontrol import MDSwitch + +from pybitmessage.bmconfigparser import config + +from pybitmessage.bitmessagekivy.get_platform import platform +from pybitmessage.bitmessagekivy.baseclass.common import ( + avatar_image_first_letter, AvatarSampleWidget, ThemeClsColor, + toast, empty_screen_label, load_image_path +) + +from pybitmessage.bitmessagekivy.baseclass.popup import MyaddDetailPopup +from pybitmessage.bitmessagekivy.baseclass.myaddress_widgets import HelperMyAddress + + +class ToggleBtn(IRightBodyTouch, MDSwitch): + """ToggleBtn class for kivy UI""" + + +class CustomTwoLineAvatarIconListItem(TwoLineAvatarIconListItem): + """CustomTwoLineAvatarIconListItem class for kivy Ui""" + + +class MyAddress(Screen, HelperMyAddress): + """MyAddress screen class for kivy Ui""" + + address_label = StringProperty() + text_address = StringProperty() + addresses_list = ListProperty() + has_refreshed = True + is_add_created = False + label_str = "Yet no address is created by user!!!!!!!!!!!!!" + no_search_res_found = "No search result found" + min_scroll_y_limit = -0.0 + scroll_y_step = 0.06 + number_of_addresses = 20 + addresses_at_a_time = 15 + canvas_color_black = [0, 0, 0, 0] + canvas_color_gray = [0.5, 0.5, 0.5, 0.5] + is_android_width = .9 + other_platform_width = .6 + disabled_addr_width = .8 + other_platform_disabled_addr_width = .55 + max_scroll_limit = 1.0 + + def __init__(self, *args, **kwargs): + """Clock schdule for method Myaddress accounts""" + super(MyAddress, self).__init__(*args, **kwargs) + self.image_dir = load_image_path() + self.kivy_running_app = App.get_running_app() + self.kivy_state = self.kivy_running_app.kivy_state_obj + + Clock.schedule_once(self.init_ui, 0) + + def init_ui(self, dt=0): + """Clock schdule for method Myaddress accounts""" + self.addresses_list = config.addresses() + if self.kivy_state.searching_text: + self.ids.refresh_layout.scroll_y = self.max_scroll_limit + filtered_list = [ + x for x in config.addresses() + if self.filter_address(x) + ] + self.addresses_list = filtered_list + self.addresses_list = [obj for obj in reversed(self.addresses_list)] + self.ids.tag_label.text = '' + if self.addresses_list: + self.ids.tag_label.text = 'My Addresses' + self.has_refreshed = True + self.set_mdList(0, self.addresses_at_a_time) + self.ids.refresh_layout.bind(scroll_y=self.check_scroll_y) + else: + self.ids.ml.add_widget(empty_screen_label(self.label_str, self.no_search_res_found)) + if not self.kivy_state.searching_text and not self.is_add_created: + try: + self.manager.current = 'login' + except ScreenManagerException: + pass + + def get_address_list(self, first_index, last_index, data): + """Getting address and append to the list""" + for address in self.addresses_list[first_index:last_index]: + data.append({ + 'text': config.get(address, 'label'), + 'secondary_text': address} + ) + return data + + def set_address_to_widget(self, item): + """Setting address to the widget""" + is_enable = config.getboolean(item['secondary_text'], 'enabled') + meny = CustomTwoLineAvatarIconListItem( + text=item['text'], secondary_text=item['secondary_text'], + theme_text_color='Custom' if is_enable else 'Primary', + text_color=ThemeClsColor,) + meny.canvas.children[3].rgba = \ + self.canvas_color_black if is_enable else self.canvas_color + meny.add_widget(AvatarSampleWidget( + source=os.path.join( + self.image_dir, "text_images", "{}.png".format(avatar_image_first_letter( + item["text"].strip()))) + )) + meny.bind(on_press=partial( + self.myadd_detail, item['secondary_text'], item['text'])) + self.set_address_status(item, meny, is_enable) + + def set_address_status(self, item, meny, is_enable): + """Setting the identity status enable/disable on UI""" + if self.kivy_state.selected_address == item['secondary_text'] and is_enable: + meny.add_widget(self.is_active_badge()) + else: + meny.add_widget(ToggleBtn(active=True if is_enable else False)) + self.ids.ml.add_widget(meny) + + def set_mdList(self, first_index, last_index): + """Creating the mdlist""" + data = [] + self.get_address_list(first_index, last_index, data) + for item in data: + self.set_address_to_widget(item) + + def check_scroll_y(self, instance, somethingelse): + """Load data on Scroll down""" + if self.ids.refresh_layout.scroll_y <= self.min_scroll_y_limit and self.has_refreshed: + self.ids.refresh_layout.scroll_y = self.scroll_y_step + my_addresses = len(self.ids.ml.children) + if my_addresses != len(self.addresses_list): + self.update_addressBook_on_scroll(my_addresses) + self.has_refreshed = ( + True if my_addresses != len(self.addresses_list) else False + ) + + def update_addressBook_on_scroll(self, my_addresses): + """Loads more data on scroll down""" + self.set_mdList(my_addresses, my_addresses + self.number_of_addresses) + + def myadd_detail(self, fromaddress, label, *args): + """Load myaddresses details""" + if config.getboolean(fromaddress, 'enabled'): + obj = MyaddDetailPopup() + self.address_label = obj.address_label = label + self.text_address = obj.address = fromaddress + width = self.is_android_width if platform == 'android' else self.other_platform_width + self.myadddetail_popup = self.myaddress_detail_popup(obj, width) + self.myadddetail_popup.auto_dismiss = False + self.myadddetail_popup.open() + else: + width = self.disabled_addr_width if platform == 'android' else self.other_platform_disabled_addr_width + self.dialog_box = self.inactive_address_popup(width, self.callback_for_menu_items) + self.dialog_box.open() + + def callback_for_menu_items(self, text_item, *arg): + """Callback of inactive address alert box""" + self.dialog_box.dismiss() + toast(text_item) + + def refresh_callback(self, *args): + """Method updates the state of application, + While the spinner remains on the screen""" + def refresh_callback(interval): + """Method used for loading the myaddress screen data""" + self.kivy_state.searching_text = '' + self.ids.search_bar.ids.search_field.text = '' + self.has_refreshed = True + self.ids.ml.clear_widgets() + self.init_ui() + self.ids.refresh_layout.refresh_done() + Clock.schedule_once(self.address_permision_callback, 0) + Clock.schedule_once(refresh_callback, 1) + + @staticmethod + def filter_address(address): + """It will return True if search is matched""" + searched_text = App.get_running_app().kivy_state_obj.searching_text.lower() + return bool(config.search_addresses(address, searched_text)) + + def disable_address_ui(self, address, instance): + """This method is used to disable addresses from UI""" + config.disable_address(address) + instance.parent.parent.theme_text_color = 'Primary' + instance.parent.parent.canvas.children[3].rgba = MyAddress.canvas_color_gray + toast('Address disabled') + Clock.schedule_once(self.address_permision_callback, 0) + + def enable_address_ui(self, address, instance): + """This method is used to enable addresses from UI""" + config.enable_address(address) + instance.parent.parent.theme_text_color = 'Custom' + instance.parent.parent.canvas.children[3].rgba = MyAddress.canvas_color_black + toast('Address Enabled') + Clock.schedule_once(self.address_permision_callback, 0) + + def address_permision_callback(self, dt=0): + """callback for enable or disable addresses""" + addresses = [addr for addr in config.addresses() + if config.getboolean(str(addr), 'enabled')] + self.parent.parent.ids.content_drawer.ids.identity_dropdown.values = addresses + self.parent.parent.ids.id_create.children[1].ids.composer_dropdown.values = addresses + self.kivy_running_app.identity_list = addresses + + def toggleAction(self, instance): + """This method is used for enable or disable address""" + addr = instance.parent.parent.secondary_text + if instance.active: + self.enable_address_ui(addr, instance) + else: + self.disable_address_ui(addr, instance) diff --git a/src/bitmessagekivy/baseclass/myaddress_widgets.py b/src/bitmessagekivy/baseclass/myaddress_widgets.py new file mode 100644 index 00000000..23e2342f --- /dev/null +++ b/src/bitmessagekivy/baseclass/myaddress_widgets.py @@ -0,0 +1,64 @@ +# pylint: disable=too-many-arguments, no-name-in-module, import-error +# pylint: disable=too-few-public-methods, no-member, too-many-ancestors + +""" +MyAddress widgets are here. +""" + +from kivymd.uix.button import MDFlatButton +from kivymd.uix.dialog import MDDialog +from kivymd.uix.label import MDLabel +from kivymd.uix.list import IRightBodyTouch + +from pybitmessage.bitmessagekivy.get_platform import platform +from pybitmessage.bitmessagekivy.baseclass.common import ThemeClsColor + + +class BadgeText(IRightBodyTouch, MDLabel): + """BadgeText class for kivy UI""" + + +class HelperMyAddress(object): + """Widget used in MyAddress are here""" + dialog_height = .25 + + @staticmethod + def is_active_badge(): + """This function show the 'active' label of active Address.""" + active_status = 'Active' + is_android_width = 90 + width = 50 + height = 60 + badge_obj = BadgeText( + size_hint=(None, None), + size=[is_android_width if platform == 'android' else width, height], + text=active_status, halign='center', + font_style='Body1', theme_text_color='Custom', + text_color=ThemeClsColor, font_size='13sp' + ) + return badge_obj + + @staticmethod + def myaddress_detail_popup(obj, width): + """This method show the details of address as popup opens.""" + show_myaddress_dialogue = MDDialog( + type="custom", + size_hint=(width, HelperMyAddress.dialog_height), + content_cls=obj, + ) + return show_myaddress_dialogue + + @staticmethod + def inactive_address_popup(width, callback_for_menu_items): + """This method shows the warning popup if the address is inactive""" + dialog_text = 'Address is not currently active. Please click on Toggle button to active it.' + dialog_box = MDDialog( + text=dialog_text, + size_hint=(width, HelperMyAddress.dialog_height), + buttons=[ + MDFlatButton( + text="Ok", on_release=lambda x: callback_for_menu_items("Ok") + ), + ], + ) + return dialog_box diff --git a/src/bitmessagekivy/baseclass/network.py b/src/bitmessagekivy/baseclass/network.py new file mode 100644 index 00000000..dcb3f082 --- /dev/null +++ b/src/bitmessagekivy/baseclass/network.py @@ -0,0 +1,54 @@ +# pylint: disable=unused-argument, consider-using-f-string +# pylint: disable=no-name-in-module, too-few-public-methods + +""" + Network status +""" + +import os + +from kivy.clock import Clock +from kivy.properties import StringProperty +from kivy.uix.screenmanager import Screen + +from pybitmessage import state + +if os.environ.get('INSTALL_TESTS', False) and not state.backend_py3_compatible: + from pybitmessage.mockbm import kivy_main + stats = kivy_main.network.stats + objectracker = kivy_main.network.objectracker +else: + from pybitmessage.network import stats, objectracker + + +class NetworkStat(Screen): + """NetworkStat class for kivy Ui""" + + text_variable_1 = StringProperty( + '{0}::{1}'.format('Total Connections', '0')) + text_variable_2 = StringProperty( + 'Processed {0} per-to-per messages'.format('0')) + text_variable_3 = StringProperty( + 'Processed {0} brodcast messages'.format('0')) + text_variable_4 = StringProperty( + 'Processed {0} public keys'.format('0')) + text_variable_5 = StringProperty( + 'Processed {0} object to be synced'.format('0')) + + def __init__(self, *args, **kwargs): + """Init method for network stat""" + super(NetworkStat, self).__init__(*args, **kwargs) + Clock.schedule_interval(self.init_ui, 1) + + def init_ui(self, dt=0): + """Clock Schdule for method networkstat screen""" + self.text_variable_1 = '{0} :: {1}'.format( + 'Total Connections', str(len(stats.connectedHostsList()))) + self.text_variable_2 = 'Processed {0} per-to-per messages'.format( + str(state.numberOfMessagesProcessed)) + self.text_variable_3 = 'Processed {0} brodcast messages'.format( + str(state.numberOfBroadcastsProcessed)) + self.text_variable_4 = 'Processed {0} public keys'.format( + str(state.numberOfPubkeysProcessed)) + self.text_variable_5 = '{0} object to be synced'.format( + len(objectracker.missingObjects)) diff --git a/src/bitmessagekivy/baseclass/payment.py b/src/bitmessagekivy/baseclass/payment.py new file mode 100644 index 00000000..6749e1bb --- /dev/null +++ b/src/bitmessagekivy/baseclass/payment.py @@ -0,0 +1,66 @@ +# pylint: disable=import-error, no-name-in-module, too-few-public-methods, too-many-ancestors + +''' + Payment/subscription frontend +''' + +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.screenmanager import Screen +from kivy.app import App + +from kivymd.uix.behaviors.elevation import RectangularElevationBehavior +from kivymd.uix.label import MDLabel +from kivymd.uix.list import ( + IRightBodyTouch, + OneLineAvatarIconListItem +) + +from pybitmessage.bitmessagekivy.baseclass.common import toast, kivy_state_variables + + +class Payment(Screen): + """Payment Screen class for kivy Ui""" + + def __init__(self, *args, **kwargs): + """Instantiate kivy state variable""" + super(Payment, self).__init__(*args, **kwargs) + self.kivy_state = kivy_state_variables() + + # TODO: get_free_credits() is not used anywhere, will be used later for Payment/subscription. + def get_free_credits(self, instance): # pylint: disable=unused-argument + """Get the available credits""" + # pylint: disable=no-self-use + self.kivy_state.available_credit = 0 + existing_credits = 0 + if existing_credits > 0: + toast( + 'We already have added free credit' + ' for the subscription to your account!') + else: + toast('Credit added to your account!') + # TODO: There is no sc18 screen id is available, + # need to create sc18 for Credits screen inside main.kv + App.get_running_app().root.ids.sc18.ids.cred.text = '{0}'.format( + self.kivy_state.available_credit) + + +class Category(BoxLayout, RectangularElevationBehavior): + """Category class for kivy Ui""" + elevation_normal = .01 + + +class ProductLayout(BoxLayout, RectangularElevationBehavior): + """ProductLayout class for kivy Ui""" + elevation_normal = .01 + + +class PaymentMethodLayout(BoxLayout): + """PaymentMethodLayout class for kivy Ui""" + + +class ListItemWithLabel(OneLineAvatarIconListItem): + """ListItemWithLabel class for kivy Ui""" + + +class RightLabel(IRightBodyTouch, MDLabel): + """RightLabel class for kivy Ui""" diff --git a/src/bitmessagekivy/baseclass/popup.py b/src/bitmessagekivy/baseclass/popup.py new file mode 100644 index 00000000..d2a4c859 --- /dev/null +++ b/src/bitmessagekivy/baseclass/popup.py @@ -0,0 +1,231 @@ +# pylint: disable=import-error, attribute-defined-outside-init +# pylint: disable=no-member, no-name-in-module, unused-argument, too-few-public-methods + +""" +All the popup are managed here. + +""" +import logging +from datetime import datetime + +from kivy.clock import Clock +from kivy.metrics import dp +from kivy.properties import StringProperty +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.popup import Popup +from kivy.app import App + +from pybitmessage.bitmessagekivy import kivy_helper_search +from pybitmessage.bitmessagekivy.get_platform import platform + +from pybitmessage.bitmessagekivy.baseclass.common import toast + +from pybitmessage.addresses import decodeAddress + +logger = logging.getLogger('default') + + +class AddressChangingLoader(Popup): + """Run a Screen Loader when changing the Identity for kivy UI""" + + def __init__(self, **kwargs): + super(AddressChangingLoader, self).__init__(**kwargs) + Clock.schedule_once(self.dismiss_popup, 0.5) + + def dismiss_popup(self, dt): + """Dismiss popups""" + self.dismiss() + + +class AddAddressPopup(BoxLayout): + """Popup for adding new address to addressbook""" + + validation_dict = { + "missingbm": "The address should start with ''BM-''", + "checksumfailed": "The address is not typed or copied correctly", + "versiontoohigh": "The version number of this address is higher than this" + " software can support. Please upgrade Bitmessage.", + "invalidcharacters": "The address contains invalid characters.", + "ripetooshort": "Some data encoded in the address is too short.", + "ripetoolong": "Some data encoded in the address is too long.", + "varintmalformed": "Some data encoded in the address is malformed." + } + valid = False + + def __init__(self, **kwargs): + super(AddAddressPopup, self).__init__(**kwargs) + + def checkAddress_valid(self, instance): + """Checking address is valid or not""" + my_addresses = ( + App.get_running_app().root.ids.content_drawer.ids.identity_dropdown.values) + add_book = [addr[1] for addr in kivy_helper_search.search_sql( + folder="addressbook")] + entered_text = str(instance.text).strip() + if entered_text in add_book: + text = 'Address is already in the addressbook.' + elif entered_text in my_addresses: + text = 'You can not save your own address.' + elif entered_text: + text = self.addressChanged(entered_text) + + if entered_text in my_addresses or entered_text in add_book: + self.ids.address.error = True + self.ids.address.helper_text = text + elif entered_text and self.valid: + self.ids.address.error = False + elif entered_text: + self.ids.address.error = True + self.ids.address.helper_text = text + else: + self.ids.address.error = True + self.ids.address.helper_text = 'This field is required' + + def checkLabel_valid(self, instance): + """Checking address label is unique or not""" + entered_label = instance.text.strip() + addr_labels = [labels[0] for labels in kivy_helper_search.search_sql( + folder="addressbook")] + if entered_label in addr_labels: + self.ids.label.error = True + self.ids.label.helper_text = 'Label name already exists.' + elif entered_label: + self.ids.label.error = False + else: + self.ids.label.error = True + self.ids.label.helper_text = 'This field is required' + + def _onSuccess(self, addressVersion, streamNumber, ripe): + pass + + def addressChanged(self, addr): + """Address validation callback, performs validation and gives feedback""" + status, addressVersion, streamNumber, ripe = decodeAddress( + str(addr)) + self.valid = status == 'success' + + if self.valid: + text = "Address is valid." + self._onSuccess(addressVersion, streamNumber, ripe) + return text + return self.validation_dict.get(status) + + +class SavedAddressDetailPopup(BoxLayout): + """Pop-up for Saved Address details for kivy UI""" + + address_label = StringProperty() + address = StringProperty() + + def __init__(self, **kwargs): + """Set screen of address detail page""" + super(SavedAddressDetailPopup, self).__init__(**kwargs) + + def checkLabel_valid(self, instance): + """Checking address label is unique of not""" + entered_label = str(instance.text.strip()) + address_list = kivy_helper_search.search_sql(folder="addressbook") + addr_labels = [labels[0] for labels in address_list] + add_dict = dict(address_list) + if self.address and entered_label in addr_labels \ + and self.address != add_dict[entered_label]: + self.ids.add_label.error = True + self.ids.add_label.helper_text = 'label name already exists.' + elif entered_label: + self.ids.add_label.error = False + else: + self.ids.add_label.error = True + self.ids.add_label.helper_text = 'This field is required' + + +class MyaddDetailPopup(BoxLayout): + """MyaddDetailPopup class for kivy Ui""" + + address_label = StringProperty() + address = StringProperty() + + def __init__(self, **kwargs): + """My Address Details screen setting""" + super(MyaddDetailPopup, self).__init__(**kwargs) + + def send_message_from(self): + """Method used to fill from address of composer autofield""" + App.get_running_app().set_navbar_for_composer() + window_obj = App.get_running_app().root.ids + window_obj.id_create.children[1].ids.ti.text = self.address + window_obj.id_create.children[1].ids.composer_dropdown.text = self.address + window_obj.id_create.children[1].ids.txt_input.text = '' + window_obj.id_create.children[1].ids.subject.text = '' + window_obj.id_create.children[1].ids.body.text = '' + window_obj.scr_mngr.current = 'create' + self.parent.parent.parent.dismiss() + + def close_pop(self): + """Pop is Cancelled""" + self.parent.parent.parent.dismiss() + toast('Cancelled') + + +class AppClosingPopup(Popup): + """AppClosingPopup class for kivy Ui""" + + def __init__(self, **kwargs): + super(AppClosingPopup, self).__init__(**kwargs) + + def closingAction(self, text): + """Action on closing window""" + exit_message = "*******************EXITING FROM APPLICATION*******************" + if text == 'Yes': + logger.debug(exit_message) + import shutdown + shutdown.doCleanShutdown() + else: + self.dismiss() + toast(text) + + +class SenderDetailPopup(Popup): + """SenderDetailPopup class for kivy Ui""" + + to_addr = StringProperty() + from_addr = StringProperty() + time_tag = StringProperty() + + def __init__(self, **kwargs): + """this metthod initialized the send message detial popup""" + super(SenderDetailPopup, self).__init__(**kwargs) + + def assignDetail(self, to_addr, from_addr, timeinseconds): + """Detailes assigned""" + self.to_addr = to_addr + self.from_addr = from_addr + time_obj = datetime.fromtimestamp(int(timeinseconds)) + self.time_tag = time_obj.strftime("%d %b %Y, %I:%M %p") + device_type = 2 if platform == 'android' else 1.5 + pop_height = 1.2 * device_type * (self.ids.sd_label.height + self.ids.dismiss_btn.height) + if len(to_addr) > 3: + self.height = pop_height + self.ids.to_addId.size_hint_y = None + self.ids.to_addId.height = 50 + self.ids.to_addtitle.add_widget(ToAddressTitle()) + frmaddbox = ToAddrBoxlayout() + frmaddbox.set_toAddress(to_addr) + self.ids.to_addId.add_widget(frmaddbox) + else: + self.ids.space_1.height = dp(0) + self.ids.space_2.height = dp(0) + self.ids.myadd_popup_box.spacing = dp(8 if platform == 'android' else 3) + self.height = pop_height / 1.2 + + +class ToAddrBoxlayout(BoxLayout): + """ToAddrBoxlayout class for kivy Ui""" + to_addr = StringProperty() + + def set_toAddress(self, to_addr): + """This method is use to set to address""" + self.to_addr = to_addr + + +class ToAddressTitle(BoxLayout): + """ToAddressTitle class for BoxLayout behaviour""" diff --git a/src/bitmessagekivy/baseclass/qrcode.py b/src/bitmessagekivy/baseclass/qrcode.py new file mode 100644 index 00000000..4c6d99a0 --- /dev/null +++ b/src/bitmessagekivy/baseclass/qrcode.py @@ -0,0 +1,34 @@ +# pylint: disable=import-error, no-name-in-module, too-few-public-methods + +""" +Generate QRcode of saved addresses in addressbook. +""" + +import logging + +from kivy.app import App +from kivy.uix.screenmanager import Screen +from kivy.properties import StringProperty +from kivy_garden.qrcode import QRCodeWidget + +logger = logging.getLogger('default') + + +class ShowQRCode(Screen): + """ShowQRCode Screen class for kivy Ui""" + address = StringProperty() + + def __init__(self, *args, **kwargs): + """Instantiate kivy state variable""" + super(ShowQRCode, self).__init__(*args, **kwargs) + self.kivy_running_app = App.get_running_app() + + def qrdisplay(self, instance, address): + """Method used for showing QR Code""" + self.ids.qr.clear_widgets() + self.kivy_running_app.set_toolbar_for_QrCode() + self.address = address # used for label + self.ids.qr.add_widget(QRCodeWidget(data=self.address)) + self.ids.qr.children[0].show_border = False + instance.parent.parent.parent.dismiss() + logger.debug('Show QR code') diff --git a/src/bitmessagekivy/baseclass/scan_screen.py b/src/bitmessagekivy/baseclass/scan_screen.py new file mode 100644 index 00000000..3321a4fa --- /dev/null +++ b/src/bitmessagekivy/baseclass/scan_screen.py @@ -0,0 +1,105 @@ +# pylint: disable=no-member, too-many-arguments, too-few-public-methods +# pylint: disable=no-name-in-module, unused-argument, arguments-differ + +""" +QR code Scan Screen used in message composer to get recipient address + +""" + +import os +import logging +import cv2 + +from kivy.clock import Clock +from kivy.lang import Builder +from kivy.properties import ( + BooleanProperty, + ObjectProperty, + StringProperty +) +from kivy.uix.screenmanager import Screen + +from pybitmessage.bitmessagekivy.get_platform import platform + +logger = logging.getLogger('default') + + +class ScanScreen(Screen): + """ScanScreen is for scaning Qr code""" + # pylint: disable=W0212 + camera_available = BooleanProperty(False) + previous_open_screen = StringProperty() + pop_up_instance = ObjectProperty() + + def __init__(self, *args, **kwargs): + """Getting AddressBook Details""" + super(ScanScreen, self).__init__(*args, **kwargs) + self.check_camera() + + def check_camera(self): + """This method is used for checking camera avaibility""" + if platform != "android": + cap = cv2.VideoCapture(0) + is_cam_open = cap.isOpened() + while is_cam_open: + logger.debug('Camera is available!') + self.camera_available = True + break + else: + logger.debug("Camera is not available!") + self.camera_available = False + else: + self.camera_available = True + + def get_screen(self, screen_name, instance=None): + """This method is used for getting previous screen name""" + self.previous_open_screen = screen_name + if screen_name != 'composer': + self.pop_up_instance = instance + + def on_pre_enter(self): + """ + on_pre_enter works little better on android + It affects screen transition on linux + """ + if not self.children: + tmp = Builder.load_file( + os.path.join( + os.path.dirname(os.path.dirname(__file__)), "kv", "{}.kv").format("scanner") + ) + self.add_widget(tmp) + if platform == "android": + Clock.schedule_once(self.start_camera, 0) + + def on_enter(self): + """ + on_enter works better on linux + It creates a black screen on android until camera gets loaded + """ + if platform != "android": + Clock.schedule_once(self.start_camera, 0) + + def on_leave(self): + """This method will call on leave""" + Clock.schedule_once(self.stop_camera, 0) + + def start_camera(self, *args): + """Its used for starting camera for scanning qrcode""" + # pylint: disable=attribute-defined-outside-init + self.xcam = self.children[0].ids.zbarcam.ids.xcamera + if platform == "android": + self.xcam.play = True + else: + Clock.schedule_once(self.open_cam, 0) + + def stop_camera(self, *args): + """Its used for stop the camera""" + self.xcam.play = False + if platform != "android": + self.xcam._camera._device.release() + + def open_cam(self, *args): + """It will open up the camera""" + if not self.xcam._camera._device.isOpened(): + self.xcam._camera._device.open(self.xcam._camera._index) + self.xcam.play = True diff --git a/src/bitmessagekivy/baseclass/sent.py b/src/bitmessagekivy/baseclass/sent.py new file mode 100644 index 00000000..59db0ab9 --- /dev/null +++ b/src/bitmessagekivy/baseclass/sent.py @@ -0,0 +1,47 @@ +# pylint: disable=import-error, attribute-defined-outside-init, too-many-arguments +# pylint: disable=no-member, no-name-in-module, unused-argument, too-few-public-methods + +""" + Sent screen; All sent message managed here. +""" + +from kivy.properties import StringProperty, ListProperty +from kivy.uix.screenmanager import Screen +from kivy.app import App + +from pybitmessage.bitmessagekivy.baseclass.common import kivy_state_variables + + +class Sent(Screen): + """Sent Screen class for kivy UI""" + + queryreturn = ListProperty() + account = StringProperty() + has_refreshed = True + no_search_res_found = "No search result found" + label_str = "Yet no message for this account!" + + def __init__(self, *args, **kwargs): + """Association with the screen""" + super(Sent, self).__init__(*args, **kwargs) + self.kivy_state = kivy_state_variables() + + if self.kivy_state.selected_address == '': + if App.get_running_app().identity_list: + self.kivy_state.selected_address = App.get_running_app().identity_list[0] + + def init_ui(self, dt=0): + """Clock Schdule for method sent accounts""" + self.loadSent() + print(dt) + + def set_defaultAddress(self): + """Set default address""" + if self.kivy_state.selected_address == "": + if self.kivy_running_app.identity_list: + self.kivy_state.selected_address = self.kivy_running_app.identity_list[0] + + def loadSent(self, where="", what=""): + """Load Sent list for Sent messages""" + self.set_defaultAddress() + self.account = self.kivy_state.selected_address diff --git a/src/bitmessagekivy/baseclass/settings.py b/src/bitmessagekivy/baseclass/settings.py new file mode 100644 index 00000000..1ceb35ee --- /dev/null +++ b/src/bitmessagekivy/baseclass/settings.py @@ -0,0 +1,10 @@ +# pylint: disable=unused-argument, no-name-in-module, too-few-public-methods +""" +Settings screen UI +""" + +from kivy.uix.screenmanager import Screen + + +class Setting(Screen): + """Setting Screen for kivy Ui""" diff --git a/src/bitmessagekivy/baseclass/trash.py b/src/bitmessagekivy/baseclass/trash.py new file mode 100644 index 00000000..eb62fdaa --- /dev/null +++ b/src/bitmessagekivy/baseclass/trash.py @@ -0,0 +1,33 @@ +# pylint: disable=unused-argument, consider-using-f-string, import-error, attribute-defined-outside-init +# pylint: disable=unnecessary-comprehension, no-member, no-name-in-module, too-few-public-methods + +""" + Trash screen +""" + +from kivy.properties import ( + ListProperty, + StringProperty +) +from kivy.uix.screenmanager import Screen +from kivy.app import App + +from pybitmessage.bitmessagekivy.baseclass.common import kivy_state_variables + + +class Trash(Screen): + """Trash Screen class for kivy Ui""" + + trash_messages = ListProperty() + has_refreshed = True + delete_index = None + table_name = StringProperty() + no_msg_found_str = "Yet no trashed message for this account!" + + def __init__(self, *args, **kwargs): + """Trash method, delete sent message and add in Trash""" + super(Trash, self).__init__(*args, **kwargs) + self.kivy_state = kivy_state_variables() + if self.kivy_state.selected_address == '': + if App.get_running_app().identity_list: + self.kivy_state.selected_address = App.get_running_app().identity_list[0] diff --git a/src/bitmessagekivy/get_platform.py b/src/bitmessagekivy/get_platform.py new file mode 100644 index 00000000..654b31f4 --- /dev/null +++ b/src/bitmessagekivy/get_platform.py @@ -0,0 +1,31 @@ +# pylint: disable=no-else-return, too-many-return-statements + +"""To check the platform""" + +from sys import platform as _sys_platform +from os import environ + + +def _get_platform(): + kivy_build = environ.get("KIVY_BUILD", "") + if kivy_build in {"android", "ios"}: + return kivy_build + elif "P4A_BOOTSTRAP" in environ: + return "android" + elif "ANDROID_ARGUMENT" in environ: + return "android" + elif _sys_platform in ("win32", "cygwin"): + return "win" + elif _sys_platform == "darwin": + return "macosx" + elif _sys_platform.startswith("linux"): + return "linux" + elif _sys_platform.startswith("freebsd"): + return "linux" + return "unknown" + + +platform = _get_platform() + +if platform not in ("android", "unknown"): + environ["KIVY_CAMERA"] = "opencv" diff --git a/src/bitmessagekivy/identiconGeneration.py b/src/bitmessagekivy/identiconGeneration.py new file mode 100644 index 00000000..2e2f2e93 --- /dev/null +++ b/src/bitmessagekivy/identiconGeneration.py @@ -0,0 +1,80 @@ +""" +Core classes for loading images and converting them to a Texture. +The raw image data can be keep in memory for further access +""" +import hashlib +from io import BytesIO + +from PIL import Image +from kivy.core.image import Image as CoreImage +from kivy.uix.image import Image as kiImage + + +# constants +RESOLUTION = 300, 300 +V_RESOLUTION = 7, 7 +BACKGROUND_COLOR = 255, 255, 255, 255 +MODE = "RGB" + + +def generate(Generate_string=None): + """Generating string""" + hash_string = generate_hash(Generate_string) + color = random_color(hash_string) + image = Image.new(MODE, V_RESOLUTION, BACKGROUND_COLOR) + image = generate_image(image, color, hash_string) + image = image.resize(RESOLUTION, 0) + data = BytesIO() + image.save(data, format='png') + data.seek(0) + # yes you actually need this + im = CoreImage(BytesIO(data.read()), ext='png') + beeld = kiImage() + # only use this line in first code instance + beeld.texture = im.texture + return beeld + + +def generate_hash(string): + """Generating hash""" + try: + # make input case insensitive + string = str.lower(string) + hash_object = hashlib.md5( # nosec B324, B303 + str.encode(string)) + print(hash_object.hexdigest()) + # returned object is a hex string + return hash_object.hexdigest() + except IndexError: + print("Error: Please enter a string as an argument.") + + +def random_color(hash_string): + """Getting random color""" + # remove first three digits from hex string + split = 6 + rgb = hash_string[:split] + split = 2 + r = rgb[:split] + g = rgb[split:2 * split] + b = rgb[2 * split:3 * split] + color = (int(r, 16), int(g, 16), int(b, 16), 0xFF) + return color + + +def generate_image(image, color, hash_string): + """Generating images""" + hash_string = hash_string[6:] + lower_x = 1 + lower_y = 1 + upper_x = int(V_RESOLUTION[0] / 2) + 1 + upper_y = V_RESOLUTION[1] - 1 + limit_x = V_RESOLUTION[0] - 1 + index = 0 + for x in range(lower_x, upper_x): + for y in range(lower_y, upper_y): + if int(hash_string[index], 16) % 2 == 0: + image.putpixel((x, y), color) + image.putpixel((limit_x - x, y), color) + index = index + 1 + return image diff --git a/src/bitmessagekivy/kivy_helper_search.py b/src/bitmessagekivy/kivy_helper_search.py new file mode 100644 index 00000000..c48ca3ad --- /dev/null +++ b/src/bitmessagekivy/kivy_helper_search.py @@ -0,0 +1,71 @@ +""" +Sql queries for bitmessagekivy +""" +from pybitmessage.helper_sql import sqlQuery + + +def search_sql( + xAddress="toaddress", account=None, folder="inbox", where=None, + what=None, unreadOnly=False, start_indx=0, end_indx=20): + # pylint: disable=too-many-arguments, too-many-branches + """Method helping for searching mails""" + if what is not None and what != "": + what = "%" + what + "%" + else: + what = None + if folder in ("sent", "draft"): + sqlStatementBase = ( + '''SELECT toaddress, fromaddress, subject, message, status,''' + ''' ackdata, senttime FROM sent ''' + ) + elif folder == "addressbook": + sqlStatementBase = '''SELECT label, address From addressbook ''' + else: + sqlStatementBase = ( + '''SELECT folder, msgid, toaddress, message, fromaddress,''' + ''' subject, received, read FROM inbox ''' + ) + sqlStatementParts = [] + sqlArguments = [] + if account is not None: + if xAddress == 'both': + sqlStatementParts.append("(fromaddress = ? OR toaddress = ?)") + sqlArguments.append(account) + sqlArguments.append(account) + else: + sqlStatementParts.append(xAddress + " = ? ") + sqlArguments.append(account) + if folder != "addressbook": + if folder is not None: + if folder == "new": + folder = "inbox" + unreadOnly = True + sqlStatementParts.append("folder = ? ") + sqlArguments.append(folder) + else: + sqlStatementParts.append("folder != ?") + sqlArguments.append("trash") + if what is not None: + for colmns in where: + if len(where) > 1: + if where[0] == colmns: + filter_col = "(%s LIKE ?" % (colmns) + else: + filter_col += " or %s LIKE ? )" % (colmns) + else: + filter_col = "%s LIKE ?" % (colmns) + sqlArguments.append(what) + sqlStatementParts.append(filter_col) + if unreadOnly: + sqlStatementParts.append("read = 0") + if sqlStatementParts: + sqlStatementBase += "WHERE " + " AND ".join(sqlStatementParts) + if folder in ("sent", "draft"): + sqlStatementBase += \ + "ORDER BY senttime DESC limit {0}, {1}".format( + start_indx, end_indx) + elif folder == "inbox": + sqlStatementBase += \ + "ORDER BY received DESC limit {0}, {1}".format( + start_indx, end_indx) + return sqlQuery(sqlStatementBase, sqlArguments) diff --git a/src/bitmessagekivy/kivy_state.py b/src/bitmessagekivy/kivy_state.py new file mode 100644 index 00000000..42051ff9 --- /dev/null +++ b/src/bitmessagekivy/kivy_state.py @@ -0,0 +1,42 @@ +# pylint: disable=too-many-instance-attributes, too-few-public-methods + +""" +Kivy State variables are assigned here, they are separated from state.py +================================= +""" + +import os +import threading + + +class KivyStateVariables(object): + """This Class hold all the kivy state variables""" + + def __init__(self): + self.selected_address = '' + self.navinstance = None + self.mail_id = 0 + self.my_address_obj = None + self.detail_page_type = None + self.ackdata = None + self.status = None + self.screen_density = None + self.msg_counter_objs = None + self.check_sent_acc = None + self.sent_count = 0 + self.inbox_count = 0 + self.trash_count = 0 + self.draft_count = 0 + self.all_count = 0 + self.searching_text = '' + self.search_screen = '' + self.send_draft_mail = None + self.is_allmail = False + self.in_composer = False + self.available_credit = 0 + self.in_sent_method = False + self.in_search_mode = False + self.image_dir = os.path.abspath(os.path.join('images', 'kivy')) + self.kivyui_ready = threading.Event() + self.file_manager = None + self.manager_open = False diff --git a/src/bitmessagekivy/kv/addressbook.kv b/src/bitmessagekivy/kv/addressbook.kv new file mode 100644 index 00000000..73b4c1ef --- /dev/null +++ b/src/bitmessagekivy/kv/addressbook.kv @@ -0,0 +1,26 @@ +: + name: 'addressbook' + BoxLayout: + orientation: 'vertical' + spacing: dp(5) + SearchBar: + id: address_search + GridLayout: + id: identi_tag + padding: [20, 0, 0, 5] + cols: 1 + size_hint_y: None + height: self.minimum_height + MDLabel: + id: tag_label + text: '' + font_style: 'Subtitle2' + BoxLayout: + orientation:'vertical' + ScrollView: + id: scroll_y + do_scroll_x: False + MDList: + id: ml + Loader: + ComposerButton: \ No newline at end of file diff --git a/src/bitmessagekivy/kv/allmails.kv b/src/bitmessagekivy/kv/allmails.kv new file mode 100644 index 00000000..f1b9387e --- /dev/null +++ b/src/bitmessagekivy/kv/allmails.kv @@ -0,0 +1,25 @@ +: + name: 'allmails' + BoxLayout: + orientation: 'vertical' + spacing: dp(5) + GridLayout: + id: identi_tag + padding: [20, 20, 0, 5] + spacing: dp(5) + cols: 1 + size_hint_y: None + height: self.minimum_height + MDLabel: + id: tag_label + text: '' + font_style: 'Subtitle2' + BoxLayout: + orientation:'vertical' + ScrollView: + id: scroll_y + do_scroll_x: False + MDList: + id: ml + Loader: + ComposerButton: \ No newline at end of file diff --git a/src/bitmessagekivy/kv/chat.kv b/src/bitmessagekivy/kv/chat.kv new file mode 100644 index 00000000..e21ed503 --- /dev/null +++ b/src/bitmessagekivy/kv/chat.kv @@ -0,0 +1,82 @@ +#:import C kivy.utils.get_color_from_hex +#:import MDTextField kivymd.uix.textfield.MDTextField +: + name: 'chat' + BoxLayout: + orientation: 'vertical' + canvas.before: + Color: + rgba: 1,1,1,1 + Rectangle: + pos: self.pos + size: self.size + ScrollView: + Label: + id: chat_logs + text: '' + color: C('#101010') + text_size: (self.width, None) + halign: 'left' + valign: 'top' + padding: (0, 0) # fixed in Kivy 1.8.1 + size_hint: (1, None) + height: self.texture_size[1] + markup: True + font_size: sp(20) + MDBoxLayout: + size_hint_y: None + spacing:5 + orientation: 'horizontal' + pos_hint: {'center_y': 1, 'center_x': 1} + halign: 'right' + pos_hint: {'left': 0} + pos_hint: {'x':.8} + height: dp(50) + self.minimum_height + MDFillRoundFlatButton: + text: app.tr._("First message") + opposite_colors: True + pos_hint: {'center_x':0.8,'center_y':0.7} + + + + BoxLayout: + height: 50 + orientation: 'horizontal' + padding: 0 + size_hint: (1, None) + + MDTextField: + id:'id_message_body' + hint_text: 'Empty field' + icon_left: "message" + hint_text: "please enter your text" + mode: "fill" + fill_color: 1/255, 144/255, 254/255, 0.1 + multiline: True + font_color_normal: 0, 0, 0, .4 + icon_right: 'grease-pencil' + icon_right_color: app.theme_cls.primary_light + pos_hint: {'center_x':0.2,'center_y':0.7} + + MDIconButton: + id: file_manager + icon: "attachment" + opposite_colors: True + on_release: app.file_manager_open() + theme_text_color: "Custom" + text_color: app.theme_cls.primary_color + + MDIconButton: + icon: 'camera' + opposite_colors: True + theme_text_color: "Custom" + text_color: app.theme_cls.primary_color + MDIconButton: + id: send_message + icon: "send" + # x: root.parent.x + dp(10) + # pos_hint: {"top": 1, 'left': 1} + color: [1,0,0,1] + on_release: app.rest_default_avatar_img() + theme_text_color: "Custom" + text_color: app.theme_cls.primary_color diff --git a/src/bitmessagekivy/kv/chat_list.kv b/src/bitmessagekivy/kv/chat_list.kv new file mode 100644 index 00000000..e59c32d7 --- /dev/null +++ b/src/bitmessagekivy/kv/chat_list.kv @@ -0,0 +1,58 @@ +: + name: 'chlist' + canvas.before: + Color: + rgba: 1,1,1,1 + Rectangle: + pos: self.pos + size: self.size + MDTabs: + id: chat_panel + tab_display_mode:'text' + + Tab: + text: app.tr._("Chats") + BoxLayout: + id: chat_box + orientation: 'vertical' + ScrollView: + id: scroll_y + do_scroll_x: False + MDList: + id: ml + MDLabel: + font_style: 'Caption' + theme_text_color: 'Primary' + text: app.tr._('No Chat') + halign: 'center' + size_hint_y: None + bold: True + valign: 'top' + # OneLineAvatarListItem: + # text: "Single-line item with avatar" + # divider: None + # _no_ripple_effect: True + # ImageLeftWidget: + # source: './images/text_images/A.png' + # OneLineAvatarListItem: + # text: "Single-line item with avatar" + # divider: None + # _no_ripple_effect: True + # ImageLeftWidget: + # source: './images/text_images/B.png' + # OneLineAvatarListItem: + # text: "Single-line item with avatar" + # divider: None + # _no_ripple_effect: True + # ImageLeftWidget: + # source: './images/text_images/A.png' + Tab: + text: app.tr._("Contacts") + BoxLayout: + id: contact_box + orientation: 'vertical' + ScrollView: + id: scroll_y + do_scroll_x: False + MDList: + id: ml diff --git a/src/bitmessagekivy/kv/chat_room.kv b/src/bitmessagekivy/kv/chat_room.kv new file mode 100644 index 00000000..40843c47 --- /dev/null +++ b/src/bitmessagekivy/kv/chat_room.kv @@ -0,0 +1,45 @@ +#:import C kivy.utils.get_color_from_hex + +: + name: 'chroom' + BoxLayout: + orientation: 'vertical' + canvas.before: + Color: + rgba: 1,1,1,1 + Rectangle: + pos: self.pos + size: self.size + ScrollView: + Label: + id: chat_logs + text: '' + color: C('#101010') + text_size: (self.width, None) + halign: 'left' + valign: 'top' + padding: (0, 0) # fixed in Kivy 1.8.1 + size_hint: (1, None) + height: self.texture_size[1] + markup: True + font_size: sp(20) + BoxLayout: + height: 50 + orientation: 'horizontal' + padding: 0 + size_hint: (1, None) + + TextInput: + id: message + size_hint: (1, 1) + multiline: False + font_size: sp(20) + on_text_validate: root.send_msg() + + MDRaisedButton: + text: app.tr._("Send") + elevation_normal: 2 + opposite_colors: True + size_hint: (0.3, 1) + pos_hint: {"center_x": .5} + on_press: root.send_msg() diff --git a/src/bitmessagekivy/kv/common_widgets.kv b/src/bitmessagekivy/kv/common_widgets.kv new file mode 100644 index 00000000..275bd12c --- /dev/null +++ b/src/bitmessagekivy/kv/common_widgets.kv @@ -0,0 +1,62 @@ +: + source: app.image_path +('/down-arrow.png' if self.parent.is_open == True else '/right-arrow.png') + size: 15, 15 + x: self.parent.x + self.parent.width - self.width - 5 + y: self.parent.y + self.parent.height/2 - self.height + 5 + +: + # id: search_bar + size_hint_y: None + height: self.minimum_height + + MDIconButton: + icon: 'magnify' + + MDTextField: + id: search_field + hint_text: 'Search' + on_text: app.searchQuery(self) + canvas.before: + Color: + rgba: (0,0,0,1) + +: + id: spinner + size_hint: None, None + size: dp(46), dp(46) + pos_hint: {'center_x': 0.5, 'center_y': 0.5} + active: False + +: + size_hint_y: None + height: dp(56) + spacing: '10dp' + pos_hint: {'center_x':0.45, 'center_y': .1} + + Widget: + + MDFloatingActionButton: + icon: 'plus' + opposite_colors: True + elevation_normal: 8 + md_bg_color: [0.941, 0, 0,1] + on_press: app.set_screen('create') + on_press: app.clear_composer() + + + +: + size_hint: None, None + size: dp(36), dp(48) + pos_hint: {'center_x': .95, 'center_y': .4} + on_active: app.root.ids.id_myaddress.toggleAction(self) + +: + canvas: + Color: + id: set_clr + # rgba: 0.5, 0.5, 0.5, 0.5 + rgba: 0,0,0,0 + Rectangle: #woohoo!!! + size: self.size + pos: self.pos \ No newline at end of file diff --git a/src/bitmessagekivy/kv/credits.kv b/src/bitmessagekivy/kv/credits.kv new file mode 100644 index 00000000..1680d6f0 --- /dev/null +++ b/src/bitmessagekivy/kv/credits.kv @@ -0,0 +1,28 @@ +: + name: 'credits' + ScrollView: + do_scroll_x: False + BoxLayout: + size_hint_y: None + orientation: 'vertical' + OneLineListTitle: + id: cred + text: app.tr._("Available Credits") + divider: None + theme_text_color: 'Primary' + _no_ripple_effect: True + long_press_time: 1 + + OneLineListTitle: + id: cred + text: app.tr._(root.available_credits) + divider: None + font_style: 'H5' + theme_text_color: 'Primary' + _no_ripple_effect: True + long_press_time: 1 + AnchorLayout: + MDRaisedButton: + height: dp(38) + text: app.tr._("+Add more credits") + on_press: app.set_screen('payment') \ No newline at end of file diff --git a/src/bitmessagekivy/kv/draft.kv b/src/bitmessagekivy/kv/draft.kv new file mode 100644 index 00000000..56682d2b --- /dev/null +++ b/src/bitmessagekivy/kv/draft.kv @@ -0,0 +1,23 @@ +: + name: 'draft' + BoxLayout: + orientation: 'vertical' + spacing: dp(5) + GridLayout: + id: identi_tag + padding: [20, 20, 0, 5] + cols: 1 + size_hint_y: None + height: self.minimum_height + MDLabel: + id: tag_label + text: '' + font_style: 'Subtitle2' + BoxLayout: + orientation:'vertical' + ScrollView: + id: scroll_y + do_scroll_x: False + MDList: + id: ml + ComposerButton: \ No newline at end of file diff --git a/src/bitmessagekivy/kv/inbox.kv b/src/bitmessagekivy/kv/inbox.kv new file mode 100644 index 00000000..b9cc8566 --- /dev/null +++ b/src/bitmessagekivy/kv/inbox.kv @@ -0,0 +1,39 @@ +: + name: 'inbox' + #transition: NoTransition() + BoxLayout: + orientation: 'vertical' + spacing: dp(5) + SearchBar: + id:inbox_search + GridLayout: + id: identi_tag + padding: [20, 0, 0, 5] + cols: 1 + size_hint_y: None + height: self.minimum_height + MDLabel: + id: tag_label + text: '' + font_style: 'Subtitle2' + #FloatLayout: + # MDScrollViewRefreshLayout: + # id: refresh_layout + # refresh_callback: root.refresh_callback + # root_layout: root.set_root_layout() + # MDList: + # id: ml + BoxLayout: + orientation:'vertical' + ScrollView: + id: scroll_y + do_scroll_x: False + MDList: + id: ml + Loader: + ComposerButton: + +: + size_hint:(None, None) + font_style: 'Caption' + halign: 'center' diff --git a/src/bitmessagekivy/kv/login.kv b/src/bitmessagekivy/kv/login.kv new file mode 100644 index 00000000..d3e2f7f9 --- /dev/null +++ b/src/bitmessagekivy/kv/login.kv @@ -0,0 +1,264 @@ +#:import SlideTransition kivy.uix.screenmanager.SlideTransition +: + name:"login" + BoxLayout: + orientation: "vertical" + + #buttons-area-outer + BoxLayout: + size_hint_y: .53 + canvas: + Color: + rgba: 1,1,1,1 + Rectangle: + pos: self.pos + size: self.size + + ScreenManager: + id: check_screenmgr + Screen: + name: "check_screen" + BoxLayout: + orientation: "vertical" + padding: 0, dp(5), 0, dp(5) + spacing: dp(5) + + #label area + AnchorLayout: + size_hint_y: None + height: dp(50) + MDLabel: + text: app.tr._("Select method to make an address:") + bold: True + halign: "center" + theme_text_color: "Custom" + text_color: .4,.4,.4,1 + + #upper-checkbor-area + AnchorLayout: + size_hint_y: None + height: dp(40) + BoxLayout: + size_hint_x: None + width: self.minimum_width + + #check-container + AnchorLayout: + size_hint_x: None + width: dp(40) + Check: + active: True + + #text-container + AnchorLayout: + size_hint_x: None + width: dp(200) + MDLabel: + text: app.tr._("Pseudorandom Generator") + + AnchorLayout: + size_hint_y: None + height: dp(40) + BoxLayout: + size_hint_x: None + width: self.minimum_width + + #check-container + AnchorLayout: + size_hint_x: None + width: dp(40) + Check: + + #text-container + AnchorLayout: + size_hint_x: None + width: dp(200) + MDLabel: + text: app.tr._("Passphrase (deterministic) Generator") + AnchorLayout: + MDFillRoundFlatIconButton: + icon: "chevron-double-right" + text: app.tr._("Proceed Next") + on_release: + app.set_screen('random') + on_press: + app.root.ids.id_newidentity.reset_address_label() + + #info-area-outer + BoxLayout: + size_hint_y: .47 + padding: dp(7) + InfoLayout: + orientation:"vertical" + padding: 0, dp(5), 0, dp(5) + canvas: + Color: + rgba:1,1,1,1 + Rectangle: + pos: self.pos + size: self.size + Color: + rgba: app.theme_cls.primary_color + Line: + rounded_rectangle: (self.pos[0]+4, self.pos[1]+4, self.width-8,self.height-8, 10, 10, 10, 10, 50) + width: dp(1) + ScreenManager: + id: info_screenmgr + + Screen: + name: "info1" + ScrollView: + bar_width:0 + do_scroll_x: False + + BoxLayout: + orientation: "vertical" + size_hint_y: None + height: self.minimum_height + + #note area + ContentHead: + section_name: "NOTE:" + ContentBody: + section_text: ("You may generate addresses by using either random numbers or by using a pass-phrase.If you use a pass-phrase, the address is called a deterministic address. The Random Number option is selected by default but deterministic addresses may have several pros and cons.") + + + #pros area + ContentHead: + section_name: "PROS:" + ContentBody: + section_text: ("You can re-create your addresses on any computer from memory you need-not-to worry about backing up your keys.dat file as long as you can remember your pass-phrase.") + + #cons area + ContentHead: + section_name: "CONS:" + ContentBody: + section_text: ("You must remember (or write down) your address version number and the stream number along with your pass-phrase.If you choose a weak pass-phrase and someone on the internet can brute-force it, they can read your messages and send messages as you.") + +: + name:"random" + ScrollView: + id:add_random_bx + +: + orientation: "vertical" + #buttons-area-outer + BoxLayout: + orientation: "vertical" + # padding: 0, dp(5), 0, dp(5) + # spacing: dp(5) + size_hint_y: .53 + canvas: + Color: + rgba: 1,1,1,1 + Rectangle: + pos: self.pos + size: self.size + + #label area + AnchorLayout: + size_hint_y: None + height: dp(50) + MDLabel: + text: app.tr._("Enter a label to generate address for:") + bold: True + halign: "center" + theme_text_color: "Custom" + text_color: .4,.4,.4,1 + + AnchorLayout: + size_hint_y: None + height: dp(40) + MDTextField: + id:lab + hint_text: "Label" + required: True + size_hint_x: None + width: dp(190) + helper_text_mode: "on_error" + # helper_text: "Please enter your label name" + on_text: app.root.ids.id_newidentity.add_validation(self) + canvas.before: + Color: + rgba: (0,0,0,1) + + AnchorLayout: + MDFillRoundFlatIconButton: + icon: "chevron-double-right" + text: app.tr._("Proceed Next") + on_release: app.root.ids.id_newidentity.generateaddress() + + Widget: + + #info-area-outer + BoxLayout: + size_hint_y: .47 + padding: dp(7) + InfoLayout: + orientation:"vertical" + padding: 0, dp(5), 0, dp(5) + canvas: + Color: + rgba:1,1,1,1 + Rectangle: + pos: self.pos + size: self.size + Color: + rgba: app.theme_cls.primary_color + Line: + rounded_rectangle: (self.pos[0]+4, self.pos[1]+4, self.width-8,self.height-8, 10, 10, 10, 10, 50) + width: dp(1) + ScreenManager: + id: info_screenmgr + + Screen: + name: "info2" + ScrollView: + bar_width:0 + do_scroll_x: False + + BoxLayout: + orientation: "vertical" + size_hint_y: None + height: self.minimum_height + + #note area + ContentHead: + section_name: "NOTE:" + ContentBody: + section_text: ("Here you may generate as many addresses as you like..Indeed creating and abandoning addresses is not encouraged.") + +: + group: 'group' + size_hint: None, None + size: dp(48), dp(48) + +: + section_name: "" + orientation: "vertical" + size_hint_y: None + height: dp(50) + padding: dp(20), 0, 0, 0 + Widget: + size_hint_y: None + height: dp(25) + MDLabel: + theme_text_color: "Custom" + text_color: .1,.1,.1,.9 + text: app.tr._(root.section_name) + bold: True + font_style: "Button" + +: + section_text: "" + size_hint_y: None + height: self.minimum_height + padding: dp(50), 0, dp(10), 0 + + MDLabel: + size_hint_y: None + height: self.texture_size[1]+dp(10) + theme_text_color: "Custom" + text_color: 0.3,0.3,0.3,1 + font_style: "Body1" + text: app.tr._(root.section_text) diff --git a/src/bitmessagekivy/kv/maildetail.kv b/src/bitmessagekivy/kv/maildetail.kv new file mode 100644 index 00000000..e98b8661 --- /dev/null +++ b/src/bitmessagekivy/kv/maildetail.kv @@ -0,0 +1,87 @@ +: + name: 'mailDetail' + ScrollView: + do_scroll_x: False + BoxLayout: + size_hint_y: None + orientation: 'vertical' + # height: dp(bod.height) + self.minimum_height + height: self.minimum_height + padding: dp(10) + # MDLabel: + # size_hint_y: None + # id: subj + # text: root.subject + # theme_text_color: 'Primary' + # halign: 'left' + # font_style: 'H5' + # height: dp(40) + # on_touch_down: root.allclick(self) + OneLineListTitle: + id: subj + text: app.tr._(root.subject) + divider: None + font_style: 'H5' + theme_text_color: 'Primary' + _no_ripple_effect: True + long_press_time: 1 + TwoLineAvatarIconListItem: + id: subaft + text: app.tr._(root.from_addr) + secondary_text: app.tr._('to ' + root.to_addr) + divider: None + on_press: root.detailedPopup() + BadgeText: + size_hint:(None, None) + size:[120, 140] if app.app_platform == 'android' else [64, 80] + text: app.tr._(root.time_tag) + halign:'center' + font_style:'Caption' + pos_hint: {'center_y': .8} + _txt_right_pad: dp(70) + font_size: '11sp' + MDChip: + size_hint: (.16 if app.app_platform == 'android' else .08 , None) + text: app.tr._(root.page_type) + icon: '' + text_color: (1,1,1,1) + pos_hint: {'center_x': .91 if app.app_platform == 'android' else .95, 'center_y': .3} + radius: [8] + height: self.parent.height/4 + AvatarSampleWidget: + source: root.avatarImg + MDLabel: + text: root.status + disabled: True + font_style: 'Body2' + halign:'left' + padding_x: 20 + # MDLabel: + # id: bod + # font_style: 'Subtitle2' + # theme_text_color: 'Primary' + # text: root.message + # halign: 'left' + # height: self.texture_size[1] + MyMDTextField: + id: bod + size_hint_y: None + font_style: 'Subtitle2' + text: root.message + multiline: True + readonly: True + line_color_normal: [0,0,0,0] + _current_line_color: [0,0,0,0] + line_color_focus: [0,0,0,0] + markup: True + font_size: '15sp' + canvas.before: + Color: + rgba: (0,0,0,1) + Loader: + + +: + canvas.before: + Color: + rgba: (0,0,0,1) diff --git a/src/bitmessagekivy/kv/msg_composer.kv b/src/bitmessagekivy/kv/msg_composer.kv new file mode 100644 index 00000000..13db4f4e --- /dev/null +++ b/src/bitmessagekivy/kv/msg_composer.kv @@ -0,0 +1,161 @@ +: + name: 'create' + Loader: + + +: + ScrollView: + id: id_scroll + BoxLayout: + orientation: 'vertical' + size_hint_y: None + height: self.minimum_height + 3 * self.parent.height/5 + padding: dp(20) + spacing: 15 + BoxLayout: + orientation: 'vertical' + MDTextField: + id: ti + size_hint_y: None + hint_text: 'Type or Select sender address' + icon_right: 'account' + icon_right_color: app.theme_cls.primary_light + font_size: '15sp' + multiline: False + required: True + height: 100 + current_hint_text_color: 0,0,0,0.5 + helper_text_mode: "on_error" + canvas.before: + Color: + rgba: (0,0,0,1) + + + BoxLayout: + size_hint_y: None + height: dp(40) + IdentitySpinner: + id: composer_dropdown + background_color: app.theme_cls.primary_dark + values: app.identity_list + on_text: root.auto_fill_fromaddr() if self.text != 'Select' else '' + option_cls: Factory.get("ComposerSpinnerOption") + background_normal: '' + background_color: app.theme_cls.primary_color + color: color_font + font_size: '13.5sp' + ArrowImg: + + + RelativeLayout: + orientation: 'horizontal' + BoxLayout: + orientation: 'vertical' + txt_input: txt_input + rv: rv + size : (890, 60) + MyTextInput: + id: txt_input + size_hint_y: None + font_size: '15sp' + color: color_font + current_hint_text_color: 0,0,0,0.5 + height: 100 + hint_text: app.tr._('Type or Scan QR code for recipients address') + canvas.before: + Color: + rgba: (0,0,0,1) + + RV: + id: rv + MDIconButton: + icon: 'qrcode-scan' + pos_hint: {'center_x': 0.95, 'y': 0.6} + on_release: + if root.is_camara_attached(): app.set_screen('scanscreen') + else: root.camera_alert() + on_press: + app.root.ids.id_scanscreen.get_screen('composer') + + MyMDTextField: + id: subject + hint_text: 'Subject' + height: 100 + font_size: '15sp' + icon_right: 'notebook-outline' + icon_right_color: app.theme_cls.primary_light + current_hint_text_color: 0,0,0,0.5 + font_color_normal: 0, 0, 0, 1 + size_hint_y: None + required: True + multiline: False + helper_text_mode: "on_focus" + canvas.before: + Color: + rgba: (0,0,0,1) + + ScrollView: + id: scrlv + MDTextField: + id: body + hint_text: 'Body' + mode: "fill" + fill_color: 1/255, 144/255, 254/255, 0.1 + multiline: True + font_color_normal: 0, 0, 0, .4 + icon_right: 'grease-pencil' + icon_right_color: app.theme_cls.primary_light + size_hint: 1, 1 + height: app.window_size[1]/4 + canvas.before: + Color: + rgba: 125/255, 125/255, 125/255, 1 + BoxLayout: + spacing:50 + +: + readonly: False + multiline: False + + +: + # Draw a background to indicate selection + color: 0,0,0,1 + canvas.before: + Color: + rgba: app.theme_cls.primary_dark if self.selected else (1, 1, 1, 0) + Rectangle: + pos: self.pos + size: self.size + +: + canvas: + Color: + rgba: 0,0,0,.2 + + Line: + rectangle: self.x +1 , self.y, self.width - 2, self.height -2 + bar_width: 10 + scroll_type:['bars'] + viewclass: 'SelectableLabel' + SelectableRecycleBoxLayout: + default_size: None, dp(20) + default_size_hint: 1, None + size_hint_y: None + height: self.minimum_height + orientation: 'vertical' + multiselect: False + + +: + canvas.before: + Color: + rgba: (0,0,0,1) + + + +: + font_size: '13.5sp' + background_normal: 'atlas://data/images/defaulttheme/textinput_active' + background_color: app.theme_cls.primary_color + color: color_font diff --git a/src/bitmessagekivy/kv/myaddress.kv b/src/bitmessagekivy/kv/myaddress.kv new file mode 100644 index 00000000..71dde54a --- /dev/null +++ b/src/bitmessagekivy/kv/myaddress.kv @@ -0,0 +1,33 @@ +: + name: 'myaddress' + BoxLayout: + id: main_box + orientation: 'vertical' + spacing: dp(5) + SearchBar: + id: search_bar + GridLayout: + id: identi_tag + padding: [20, 0, 0, 5] + cols: 1 + size_hint_y: None + height: self.minimum_height + MDLabel: + id: tag_label + text: app.tr._('My Addresses') + font_style: 'Subtitle2' + FloatLayout: + MDScrollViewRefreshLayout: + id: refresh_layout + refresh_callback: root.refresh_callback + root_layout: root + MDList: + id: ml + Loader: + + +: + size_hint: None, None + size: dp(36), dp(48) + pos_hint: {'center_x': .95, 'center_y': .4} + on_active: app.root.ids.id_myaddress.toggleAction(self) diff --git a/src/bitmessagekivy/kv/network.kv b/src/bitmessagekivy/kv/network.kv new file mode 100644 index 00000000..17211e98 --- /dev/null +++ b/src/bitmessagekivy/kv/network.kv @@ -0,0 +1,131 @@ +: + name: 'networkstat' + MDTabs: + id: tab_panel + tab_display_mode:'text' + + Tab: + title: app.tr._("Total connections") + ScrollView: + do_scroll_x: False + MDList: + id: ml + size_hint_y: None + height: dp(200) + OneLineListItem: + text: app.tr._("Total Connections") + _no_ripple_effect: True + BoxLayout: + orientation: 'vertical' + size_hint_y: None + height: dp(58) + MDRaisedButton: + _no_ripple_effect: True + # size_hint: .6, 0 + # height: dp(40) + text: app.tr._(root.text_variable_1) + elevation_normal: 2 + opposite_colors: True + pos_hint: {'center_x': .5} + # MDLabel: + # font_style: 'H6' + # text: app.tr._(root.text_variable_1) + # font_size: '13sp' + # color: (1,1,1,1) + # halign: 'center' + Tab: + title: app.tr._('Processes') + ScrollView: + do_scroll_x: False + MDList: + id: ml + size_hint_y: None + height: dp(500) + OneLineListItem: + text: app.tr._("person-to-person") + _no_ripple_effect: True + + BoxLayout: + orientation: 'vertical' + size_hint_y: None + height: dp(58) + MDRaisedButton: + _no_ripple_effect: True + # size_hint: .6, 0 + # height: dp(40) + text: app.tr._(root.text_variable_2) + elevation_normal: 2 + opposite_colors: True + pos_hint: {'center_x': .5} + # MDLabel: + # font_style: 'H6' + # text: app.tr._(root.text_variable_2) + # font_size: '13sp' + # color: (1,1,1,1) + # halign: 'center' + OneLineListItem: + text: app.tr._("Brodcast") + _no_ripple_effect: True + + BoxLayout: + orientation: 'vertical' + size_hint_y: None + height: dp(58) + MDRaisedButton: + _no_ripple_effect: True + # size_hint: .6, 0 + # height: dp(40) + text: app.tr._(root.text_variable_3) + elevation_normal: 2 + opposite_colors: True + pos_hint: {'center_x': .5} + # MDLabel: + # font_style: 'H6' + # text: app.tr._(root.text_variable_3) + # font_size: '13sp' + # color: (1,1,1,1) + # halign: 'center' + OneLineListItem: + text: app.tr._("publickeys") + _no_ripple_effect: True + + BoxLayout: + orientation: 'vertical' + size_hint_y: None + height: dp(58) + MDRaisedButton: + _no_ripple_effect: True + # size_hint: .6, 0 + # height: dp(40) + text: app.tr._(root.text_variable_4) + elevation_normal: 2 + opposite_colors: True + pos_hint: {'center_x': .5} + # MDLabel: + # font_style: 'H6' + # text: app.tr._(root.text_variable_4) + # font_size: '13sp' + # color: (1,1,1,1) + # halign: 'center' + OneLineListItem: + text: app.tr._("objects") + _no_ripple_effect: True + + BoxLayout: + orientation: 'vertical' + size_hint_y: None + height: dp(58) + MDRaisedButton: + _no_ripple_effect: True + # size_hint: .6, 0 + #height: dp(40) + text: app.tr._(root.text_variable_5) + elevation_normal: 2 + opposite_colors: True + pos_hint: {'center_x': .5} + # MDLabel: + # font_style: 'H6' + # text: app.tr._(root.text_variable_5) + # font_size: '13sp' + # color: (1,1,1,1) + # halign: 'center' diff --git a/src/bitmessagekivy/kv/payment.kv b/src/bitmessagekivy/kv/payment.kv new file mode 100644 index 00000000..6d475f56 --- /dev/null +++ b/src/bitmessagekivy/kv/payment.kv @@ -0,0 +1,325 @@ +: + name: "payment" + id: id_payment_screen + payment_plan_id: "" + MDTabs: + id: tab_panel + tab_display_mode:'text' + Tab: + title: app.tr._("Payment") + id: id_payment plan + padding: "12dp" + spacing: "12dp" + BoxLayout: + ScrollView: + bar_width:0 + do_scroll_x: False + BoxLayout: + spacing: dp(5) + padding: dp(5) + size_hint_y: None + height: self.minimum_height + orientation: "vertical" + ProductCategoryLayout: + MDCard: + orientation: "vertical" + padding: "8dp" + spacing: "12dp" + size_hint: None, None + size: "560dp", "40dp" + pos_hint: {"center_x": .5, "center_y": .5} + MDLabel: + text: f"You have {app.encrypted_messages_per_month} messages left" + bold: True + halign:'center' + size_hint_y: None + pos_hint: {"center_x": .5, "center_y": .5} + + MDCard: + orientation: "vertical" + padding: "8dp" + spacing: "12dp" + size_hint: None, None + size: "560dp", "300dp" + md_bg_color: [1, 0.6, 0,0.5] + pos_hint: {"center_x": .5, "center_y": .5} + MDLabel: + text: "Free" + bold: True + halign:'center' + size_hint_y: None + pos_hint: {"center_x": .5, "center_y": .5} + MDRectangleFlatIconButton: + text: "[Currently this plan is active.]" + icon: 'shield-check' + line_color: 0, 0, 0, 0 + text_color: 'ffffff' + pos_hint: {"center_x": .5, "center_y": .5} + font_size: '18sp' + MDSeparator: + height: "1dp" + MDLabel: + text: "You can get zero encrypted message per month" + halign:'center' + bold: True + MDCard: + orientation: "vertical" + padding: "8dp" + spacing: "12dp" + size_hint: None, None + size: "560dp", "300dp" + md_bg_color: [0, 0.6, 0.8,0.8] + pos_hint: {"center_x": .5, "center_y": .5} + payment_plan_id: "sub_standard" + MDLabel: + text: "Standard" + bold: True + halign:'center' + size_hint_y: None + MDSeparator: + height: "1dp" + MDLabel: + text: "You can get 100 encrypted message per month" + halign:'center' + MDRaisedButton: + text: "Get it now" + theme_text_color: 'Primary' + md_bg_color: [1, 1, 1,1] + pos_hint: {'center_x': .5} + on_release:app.open_payment_layout(root.payment_plan_id) + MDCard: + orientation: "vertical" + padding: "8dp" + spacing: "12dp" + size_hint: None, None + size: "560dp", "300dp" + md_bg_color: [1, 0.6, 0.8,0.5] + pos_hint: {"center_x": .5, "center_y": .5} + payment_plan_id: "sub_premium" + MDLabel: + text: "Premium" + bold: True + halign:'center' + size_hint_y: None + MDSeparator: + height: "1dp" + MDLabel: + text: "You can get 1000 encrypted message per month" + halign:'center' + MDRaisedButton: + text: "Get it now" + theme_text_color: 'Primary' + md_bg_color: [1, 1, 1,1] + pos_hint: {'center_x': .5} + on_release:app.open_payment_layout(root.payment_plan_id) + Tab: + title: app.tr._("Extra-Messages") + id: id_payment_tab + BoxLayout: + ScrollView: + bar_width:0 + do_scroll_x: False + BoxLayout: + spacing: dp(8) + padding: dp(5) + size_hint_y: None + height: self.minimum_height + orientation: "vertical" + ProductCategoryLayout: + category_text: "Extra-Messages" + ProductLayout: + heading_text: "100 Encrypted messages " + price_text: "$0.99" + source: app.image_path + "/payment/buynew1.png" + description_text: "Buy extra one hundred encrypted messages!" + product_id: "SKUGASBILLING" + ProductLayout: + heading_text: "1000 Encrypted messages " + price_text: "$1.49" + source: app.image_path + "/payment/buynew1.png" + description_text: "Buy extra one thousand encrypted messages!" + product_id: "SKUUPGRADECAR" +: + size_hint_y: None + height: self.minimum_height + category_text:"" + + orientation: "vertical" + spacing: 2 + + #category area + Category: + text_: root.category_text + +: + canvas: + Color: + rgba: 1,1,1,1 + Rectangle: + pos: self.pos + size: self.size + text_: "" + size_hint_y: None + height: dp(30) + Widget: + size_hint_x: None + width: dp(20) + MDLabel: + text: root.text_ + font_size: sp(15) +: + heading_text: "" + price_text: "" + source: "" + description_text: "" + + product_id: "" + + canvas: + Color: + rgba: 1,1,1,1 + Rectangle: + pos: self.pos + size: self.size + + size_hint_y: None + height: dp(200) + orientation: "vertical" + + #heading area + BoxLayout: + size_hint_y: 0.3 + #text heading + BoxLayout: + Widget: + size_hint_x: None + width: dp(20) + MDLabel: + text: root.heading_text + bold: True + + #price text + BoxLayout: + size_hint_x:.3 + MDLabel: + text: root.price_text + bold: True + halign: "right" + theme_text_color: "Custom" + text_color: 0,0,1,1 + Widget: + size_hint_x: None + width: dp(20) + + #details area + BoxLayout: + size_hint_y: 0.3 + Widget: + size_hint_x: None + width: dp(20) + + #image area + AnchorLayout: + size_hint_x: None + width: self.height + BoxLayout: + canvas: + Color: + rgba: 1,1,1,1 + Ellipse: + size: self.size + pos: self.pos + source: root.source + Widget: + size_hint_x: None + width: dp(10) + + #description text + BoxLayout: + #size_hint_x: 1 + MDLabel: + text: root.description_text + font_size: sp(15) + #Button Area + BoxLayout: + size_hint_y: 0.4 + Widget: + + AnchorLayout: + anchor_x: "right" + MDRaisedButton: + elevation_normal: 5 + text: "BUY" + on_release: + #print(app) + app.open_payment_layout(root.product_id) + + Widget: + size_hint_x: None + width: dp(20) + +: + on_release: app.initiate_purchase(self.method_name) + recent: False + source: "" + method_name: "" + right_label_text: "Recent" if self.recent else "" + + ImageLeftWidget: + source: root.source + + RightLabel: + text: root.right_label_text + theme_text_color: "Custom" + text_color: 0,0,0,0.5 + font_size: sp(12) + +: + orientation: "vertical" + size_hint_y: None + height: "200dp" + + BoxLayout: + size_hint_y: None + height: dp(40) + + Widget: + size_hint_x: None + width: dp(20) + MDLabel: + text: "Select Payment Method" + font_size: sp(14) + bold: True + theme_text_color: "Custom" + text_color: 0,0,0,.5 + + + ScrollView: + + GridLayout: + cols: 1 + size_hint_y:None + height:self.minimum_height + + ListItemWithLabel: + source: app.image_path + "/payment/gplay.png" + text: "Google Play" + method_name: "gplay" + recent: True + + ListItemWithLabel: + source: app.image_path + "/payment/btc.png" + text: "BTC (Currently this feature is not available)" + method_name: "btc" + theme_text_color: 'Secondary' + md_bg_color: [0, 0, 0,1] + ListItemWithLabel: + source: app.image_path + "/payment/paypal.png" + text: "Paypal (Currently this feature is not available)" + method_name: "som" + theme_text_color: 'Secondary' + md_bg_color: [0, 0, 0,1] + ListItemWithLabel: + source: app.image_path + "/payment/buy.png" + text: "One more method" + method_name: "omm" diff --git a/src/bitmessagekivy/kv/popup.kv b/src/bitmessagekivy/kv/popup.kv new file mode 100644 index 00000000..2db74525 --- /dev/null +++ b/src/bitmessagekivy/kv/popup.kv @@ -0,0 +1,333 @@ +: + separator_color: 1, 1, 1, 1 + background: "White.png" + Button: + id: btn + disabled: True + background_disabled_normal: "White.png" + Image: + source: app.image_path + '/loader.gif' + anim_delay: 0 + #mipmap: True + size: root.size + + +: + id: popup_box + orientation: 'vertical' + # spacing:dp(20) + # spacing: "12dp" + size_hint_y: None + # height: "120dp" + height: label.height+address.height + BoxLayout: + orientation: 'vertical' + MDTextField: + id: label + multiline: False + hint_text: app.tr._("Label") + required: True + icon_right: 'label' + helper_text_mode: "on_error" + # TODO: on_text: root.checkLabel_valid(self) is not used now but it will be used later + canvas.before: + Color: + rgba: (0,0,0,1) + MDTextField: + id: address + hint_text: app.tr._("Address") + required: True + icon_right: 'book-plus' + helper_text_mode: "on_error" + multiline: False + # TODO: on_text: root.checkAddress_valid(self) is not used now but it will be used later + canvas.before: + Color: + rgba: (0,0,0,1) + +: + id: addbook_popup_box + size_hint_y: None + height: 2.5*(add_label.height) + orientation: 'vertical' + spacing:dp(5) + MDLabel + font_style: 'Subtitle2' + theme_text_color: 'Primary' + text: app.tr._("Label") + font_size: '17sp' + halign: 'left' + MDTextField: + id: add_label + font_style: 'Body1' + font_size: '15sp' + halign: 'left' + text: app.tr._(root.address_label) + theme_text_color: 'Primary' + required: True + helper_text_mode: "on_error" + on_text: root.checkLabel_valid(self) + canvas.before: + Color: + rgba: (0,0,0,1) + MDLabel: + font_style: 'Subtitle2' + theme_text_color: 'Primary' + text: app.tr._("Address") + font_size: '17sp' + halign: 'left' + Widget: + size_hint_y: None + height: dp(1) + BoxLayout: + orientation: 'horizontal' + MDLabel: + id: address + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._(root.address) + font_size: '15sp' + halign: 'left' + IconRightSampleWidget: + pos_hint: {'center_x': 0, 'center_y': 1} + icon: 'content-copy' + on_press: app.copy_composer_text(root.address) + + +: + id: myadd_popup + size_hint_y: None + height: "130dp" + spacing:dp(25) + + #height: dp(1.5*(myaddr_label.height)) + orientation: 'vertical' + MDLabel: + id: myaddr_label + font_style: 'Subtitle2' + theme_text_color: 'Primary' + text: app.tr._("Label") + font_size: '17sp' + halign: 'left' + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: root.address_label + font_size: '15sp' + halign: 'left' + MDLabel: + font_style: 'Subtitle2' + theme_text_color: 'Primary' + text: app.tr._("Address") + font_size: '17sp' + halign: 'left' + BoxLayout: + orientation: 'horizontal' + MDLabel: + id: label_address + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._(root.address) + font_size: '15sp' + halign: 'left' + IconRightSampleWidget: + pos_hint: {'center_x': 0, 'center_y': 1} + icon: 'content-copy' + on_press: app.copy_composer_text(root.address) + BoxLayout: + id: my_add_btn + spacing:5 + orientation: 'horizontal' + size_hint_y: None + height: self.minimum_height + MDRaisedButton: + size_hint: 2, None + height: dp(40) + on_press: root.send_message_from() + MDLabel: + font_style: 'H6' + text: app.tr._('Send message from') + font_size: '13sp' + color: (1,1,1,1) + halign: 'center' + MDRaisedButton: + size_hint: 1.5, None + height: dp(40) + on_press: app.set_screen('showqrcode') + on_press: app.root.ids.id_showqrcode.qrdisplay(root, root.address) + MDLabel: + font_style: 'H6' + text: app.tr._('Show QR code') + font_size: '13sp' + color: (1,1,1,1) + halign: 'center' + MDRaisedButton: + size_hint: 1.5, None + height: dp(40) + on_press: root.close_pop() + MDLabel: + font_style: 'H6' + text: app.tr._('Cancel') + font_size: '13sp' + color: (1,1,1,1) + halign: 'center' + +: + id: closing_popup + size_hint : (None,None) + height: 1.4*(popup_label.height+ my_add_btn.children[0].height) + width :app.window_size[0] - (app.window_size[0]/10 if app.app_platform == 'android' else app.window_size[0]/4) + background: app.image_path + '/popup.jpeg' + auto_dismiss: False + separator_height: 0 + BoxLayout: + id: myadd_popup_box + size_hint_y: None + spacing:dp(70) + orientation: 'vertical' + BoxLayout: + size_hint_y: None + orientation: 'vertical' + spacing:dp(25) + MDLabel: + id: popup_label + font_style: 'Subtitle2' + theme_text_color: 'Primary' + text: app.tr._("Bitmessage isn't connected to the network.\n If you quit now, it may cause delivery delays.\n Wait until connected and the synchronisation finishes?") + font_size: '17sp' + halign: 'center' + BoxLayout: + id: my_add_btn + spacing:5 + orientation: 'horizontal' + MDRaisedButton: + size_hint: 1.5, None + height: dp(40) + on_press: root.closingAction(self.children[0].text) + on_press: app.stop() + MDLabel: + font_style: 'H6' + text: app.tr._('Yes') + font_size: '13sp' + color: (1,1,1,1) + halign: 'center' + MDRaisedButton: + size_hint: 1.5, None + height: dp(40) + on_press: root.closingAction(self.children[0].text) + MDLabel: + font_style: 'H6' + text: app.tr._('No') + font_size: '13sp' + color: (1,1,1,1) + halign: 'center' + MDRaisedButton: + size_hint: 1.5, None + height: dp(40) + #on_press: root.dismiss() + on_press: root.closingAction(self.children[0].text) + MDLabel: + font_style: 'H6' + text: app.tr._('Cancel') + font_size: '13sp' + color: (1,1,1,1) + halign: 'center' + +: + id: myadd_popup + size_hint : (None,None) + # height: 2*(sd_label.height+ sd_btn.children[0].height) + width :app.window_size[0] - (app.window_size[0]/10 if app.app_platform == 'android' else app.window_size[0]/4) + background: app.image_path + '/popup.jpeg' + auto_dismiss: False + separator_height: 0 + BoxLayout: + id: myadd_popup_box + size_hint_y: None + orientation: 'vertical' + spacing:dp(8 if app.app_platform == 'android' else 3) + BoxLayout: + orientation: 'vertical' + MDLabel: + id: from_add_label + font_style: 'Subtitle2' + theme_text_color: 'Primary' + text: app.tr._("From :") + font_size: '15sp' + halign: 'left' + Widget: + size_hint_y: None + height: dp(1 if app.app_platform == 'android' else 0) + BoxLayout: + size_hint_y: None + height: 50 + orientation: 'horizontal' + MDLabel: + id: sd_label + font_style: 'Body2' + theme_text_color: 'Primary' + text: app.tr._("[b]" + root.from_addr + "[/b]") + font_size: '15sp' + halign: 'left' + markup: True + IconRightSampleWidget: + icon: 'content-copy' + on_press: app.copy_composer_text(root.from_addr) + Widget: + id: space_1 + size_hint_y: None + height: dp(2 if app.app_platform == 'android' else 0) + BoxLayout: + id: to_addtitle + Widget: + id:space_2 + size_hint_y: None + height: dp(1 if app.app_platform == 'android' else 0) + BoxLayout: + id: to_addId + BoxLayout: + size_hint_y: None + orientation: 'vertical' + height: 50 + MDLabel: + font_style: 'Body2' + theme_text_color: 'Primary' + text: app.tr._("Date : " + root.time_tag) + font_size: '15sp' + halign: 'left' + BoxLayout: + id: sd_btn + orientation: 'vertical' + MDRaisedButton: + id: dismiss_btn + on_press: root.dismiss() + size_hint: .2, 0 + pos_hint: {'x': 0.8, 'y': 0} + MDLabel: + font_style: 'H6' + text: app.tr._('Cancel') + font_size: '13sp' + color: (1,1,1,1) + halign: 'center' + +: + orientation: 'horizontal' + MDLabel: + font_style: 'Body2' + theme_text_color: 'Primary' + text: app.tr._(root.to_addr) + font_size: '15sp' + halign: 'left' + IconRightSampleWidget: + icon: 'content-copy' + on_press: app.copy_composer_text(root.to_addr) + +: + orientation: 'vertical' + MDLabel: + id: to_add_label + font_style: 'Subtitle2' + theme_text_color: 'Primary' + text: "To :" + font_size: '15sp' + halign: 'left' \ No newline at end of file diff --git a/src/bitmessagekivy/kv/qrcode.kv b/src/bitmessagekivy/kv/qrcode.kv new file mode 100644 index 00000000..cadaa996 --- /dev/null +++ b/src/bitmessagekivy/kv/qrcode.kv @@ -0,0 +1,33 @@ +: + name: 'showqrcode' + BoxLayout: + orientation: 'vertical' + size_hint: (None, None) + pos_hint:{'center_x': .5, 'top': 0.9} + size: (app.window_size[0]/1.8, app.window_size[0]/1.8) + id: qr + BoxLayout: + orientation: 'vertical' + MyMDTextField: + size_hint_y: None + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._(root.address) + multiline: True + readonly: True + line_color_normal: [0,0,0,0] + _current_line_color: [0,0,0,0] + line_color_focus: [0,0,0,0] + halign: 'center' + font_size: dp(15) + bold: True + canvas.before: + Color: + rgba: (0,0,0,1) + # MDLabel: + # size_hint_y: None + # font_style: 'Body1' + # theme_text_color: 'Primary' + # text: "[b]BM-2cV7Y8imvAevK6z6YmhYRcj2t7rghBtDSZ[/b]" + # markup: True + # pos_hint: {'x': .28, 'y': 0.6} \ No newline at end of file diff --git a/src/bitmessagekivy/kv/scan_screen.kv b/src/bitmessagekivy/kv/scan_screen.kv new file mode 100644 index 00000000..dbcff5a1 --- /dev/null +++ b/src/bitmessagekivy/kv/scan_screen.kv @@ -0,0 +1,2 @@ +: + name:'scanscreen' \ No newline at end of file diff --git a/src/bitmessagekivy/kv/scanner.kv b/src/bitmessagekivy/kv/scanner.kv new file mode 100644 index 00000000..1c56f6c2 --- /dev/null +++ b/src/bitmessagekivy/kv/scanner.kv @@ -0,0 +1,37 @@ +#:import ZBarSymbol pyzbar.pyzbar.ZBarSymbol + +BoxLayout: + orientation: 'vertical' + ZBarCam: + id: zbarcam + # optional, by default checks all types + code_types: ZBarSymbol.QRCODE, ZBarSymbol.EAN13 + scan_callback: app._after_scan + scanner_line_y_initial: self.size[1]/2 +self.qrwidth/2 + scanner_line_y_final: self.size[1]/2-self.qrwidth/2 + + canvas: + Color: + rgba: 0,0,0,.25 + + #left rect + Rectangle: + pos: self.pos[0], self.pos[1] + size: self.size[0]/2-self.qrwidth/2, self.size[1] + + #right rect + Rectangle: + pos: self.size[0]/2+self.qrwidth/2, 0 + size: self.size[0]/2-self.qrwidth/2, self.size[1] + + #top rect + Rectangle: + pos: self.size[0]/2-self.qrwidth/2, self.size[1]/2+self.qrwidth/2 + size: self.qrwidth, self.size[1]/2-self.qrwidth/2 + + #bottom rect + Rectangle: + pos: self.size[0]/2-self.qrwidth/2, 0 + size: self.qrwidth, self.size[1]/2-self.qrwidth/2 + + \ No newline at end of file diff --git a/src/bitmessagekivy/kv/sent.kv b/src/bitmessagekivy/kv/sent.kv new file mode 100644 index 00000000..11477ed6 --- /dev/null +++ b/src/bitmessagekivy/kv/sent.kv @@ -0,0 +1,26 @@ +: + name: 'sent' + BoxLayout: + orientation: 'vertical' + spacing: dp(5) + SearchBar: + id: sent_search + GridLayout: + id: identi_tag + padding: [20, 0, 0, 5] + cols: 1 + size_hint_y: None + height: self.minimum_height + MDLabel: + id: tag_label + text: '' + font_style: 'Subtitle2' + BoxLayout: + orientation:'vertical' + ScrollView: + id: scroll_y + do_scroll_x: False + MDList: + id: ml + Loader: + ComposerButton: \ No newline at end of file diff --git a/src/bitmessagekivy/kv/settings.kv b/src/bitmessagekivy/kv/settings.kv new file mode 100644 index 00000000..f5796060 --- /dev/null +++ b/src/bitmessagekivy/kv/settings.kv @@ -0,0 +1,948 @@ +: + name: 'set' + MDTabs: + id: tab_panel + tab_display_mode:'text' + + Tab: + title: app.tr._("User Interface") + ScrollView: + do_scroll_x: False + BoxLayout: + size_hint_y: None + orientation: 'vertical' + height: dp(250) + self.minimum_height + padding: 10 + BoxLayout: + size_hint_y: None + orientation: 'horizontal' + height: self.minimum_height + MDCheckbox: + id: chkbox + size_hint: None, None + size: dp(48), dp(50) + # active: True + halign: 'center' + disabled: True + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("Start-on-login not yet supported on your OS") + halign: 'left' + pos_hint: {'center_x': 0, 'center_y': 0.6} + disabled: True + BoxLayout: + size_hint_y: None + orientation: 'vertical' + padding: [20, 0, 0, 0] + spacing: dp(10) + height: dp(100) + self.minimum_height + # pos_hint: {'center_x': 0, 'center_y': 0.6} + BoxLayout: + id: box_height + orientation: 'vertical' + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("Tray") + halign: 'left' + bold: True + BoxLayout: + orientation: 'horizontal' + MDCheckbox: + id: chkbox + size_hint: None, None + size: dp(48), dp(50) + # active: True + halign: 'center' + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("Start Bitmessage in the tray(don't show main window)") + halign: 'left' + pos_hint: {'x': 0, 'y': .5} + BoxLayout: + orientation: 'horizontal' + MDCheckbox: + id: chkbox + size_hint: None, None + size: dp(48), dp(50) + # active: True + halign: 'center' + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("Minimize to tray") + halign: 'left' + pos_hint: {'x': 0, 'y': .5} + BoxLayout: + orientation: 'horizontal' + MDCheckbox: + id: chkbox + size_hint: None, None + size: dp(48), dp(50) + # active: True + halign: 'center' + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("Close to tray") + halign: 'left' + pos_hint: {'x': 0, 'y': .5} + BoxLayout: + size_hint_y: None + orientation: 'vertical' + BoxLayout: + orientation: 'horizontal' + MDCheckbox: + id: chkbox + size_hint: None, None + size: dp(48), dp(50) + # active: True + halign: 'center' + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("Hide connection notifications") + halign: 'left' + BoxLayout: + orientation: 'horizontal' + MDCheckbox: + id: chkbox + size_hint: None, None + size: dp(48), dp(50) + active: True + halign: 'center' + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("Show notification when message received") + halign: 'left' + + BoxLayout: + orientation: 'vertical' + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._('In portable Mode, messages and config files are stored in the same directory as the program rather then the normal application-data folder. This makes it convenient to run Bitmessage from a USB thumb drive.') + # text: 'huiiiii' + halign: 'left' + BoxLayout: + size_hint_y: None + orientation: 'vertical' + height: dp(100) + self.minimum_height + BoxLayout: + orientation: 'horizontal' + MDCheckbox: + id: chkbox + size_hint: None, None + size: dp(48), dp(50) + halign: 'center' + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("Willingly include unencrypted destination address when sending to a mobile device") + halign: 'left' + pos_hint: {'x': 0, 'y': 0.2} + BoxLayout: + orientation: 'horizontal' + MDCheckbox: + id: chkbox + size_hint: None, None + size: dp(48), dp(50) + active: True + halign: 'center' + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("Use identicons") + halign: 'left' + pos_hint: {'x': 0, 'y': 0.2} + BoxLayout: + orientation: 'horizontal' + MDCheckbox: + id: chkbox + size_hint: None, None + size: dp(48), dp(50) + halign: 'center' + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("Reply below Quote") + halign: 'left' + pos_hint: {'x': 0, 'y': 0.2} + Widget: + size_hint_y: None + height: 10 + BoxLayout: + size_hint_y: None + orientation: 'vertical' + # padding: [0, 10, 0, 0] + spacing: 10 + padding: [20, 0, 0, 0] + height: dp(20) + self.minimum_height + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("Interface Language") + # halign: 'right' + bold: True + MDDropDownItem: + id: dropdown_item + text: "System Setting" + # pos_hint: {"center_x": .5, "center_y": .6} + # current_item: "Item 0" + # on_release: root.menu.open() + BoxLayout: + spacing:5 + orientation: 'horizontal' + # pos_hint: {'x':.76} + BoxLayout: + orientation: 'horizontal' + spacing: 10 + MDRaisedButton: + text: app.tr._('Apply') + # on_press: root.change_language() + Tab: + title: 'Network Settings' + ScrollView: + do_scroll_x: False + BoxLayout: + size_hint_y: None + orientation: 'vertical' + height: dp(500) + self.minimum_height + padding: 10 + BoxLayout: + id: box_height + orientation: 'vertical' + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("Listening port") + halign: 'left' + bold: True + BoxLayout: + orientation: 'horizontal' + padding: [10, 0, 0, 0] + BoxLayout: + orientation: 'horizontal' + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("Listen for connections on port:") + halign: 'left' + BoxLayout: + orientation: 'horizontal' + MDTextFieldRect: + size_hint: None, None + size: dp(100), dp(30) + text: app.tr._('8444') + pos_hint: {'center_y': .5, 'center_x': .5} + input_filter: "int" + BoxLayout: + orientation: 'horizontal' + padding_left: 10 + MDCheckbox: + id: chkbox + size_hint: None, None + size: dp(48), dp(50) + # active: True + halign: 'center' + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("UPnP") + halign: 'left' + pos_hint: {'x': 0, 'y': 0} + BoxLayout: + orientation: 'vertical' + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("Proxy server / Tor") + halign: 'left' + bold: True + + GridLayout: + cols: 2 + padding: [10, 0, 0, 0] + MDLabel: + size_hint_x: None + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("Type:") + halign: 'left' + MDDropDownItem: + id: dropdown_item2 + dropdown_bg: [1, 1, 1, 1] + text: 'none' + pos_hint: {'x': 0.9, 'y': 0} + items: [f"{i}" for i in ['System Setting','U.S. English']] + BoxLayout: + size_hint_y: None + orientation: 'vertical' + padding: [30, 0, 0, 0] + spacing: 10 + height: dp(100) + self.minimum_height + BoxLayout: + orientation: 'horizontal' + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("Server hostname:") + halign: 'left' + MDTextFieldRect: + size_hint: None, None + size: dp(app.window_size[0]/4), dp(30) + hint_text: app.tr._('localhost') + pos_hint: {'center_y': .5, 'center_x': .5} + BoxLayout: + orientation: 'horizontal' + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("Port:") + halign: 'left' + # TextInput: + # size_hint: None, None + # hint_text: '9050' + # size: dp(app.window_size[0]/4), dp(30) + # input_filter: "int" + # readonly: False + # multiline: False + # font_size: '15sp' + MDTextFieldRect: + size_hint: None, None + size: dp(app.window_size[0]/4), dp(30) + hint_text: app.tr._('9050') + pos_hint: {'center_y': .5, 'center_x': .5} + input_filter: "int" + BoxLayout: + orientation: 'horizontal' + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("Username:") + halign: 'left' + MDTextFieldRect: + size_hint: None, None + size: dp(app.window_size[0]/4), dp(30) + pos_hint: {'center_y': .5, 'center_x': .5} + BoxLayout: + orientation: 'horizontal' + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("Pass:") + halign: 'left' + MDTextFieldRect: + size_hint: None, None + size: dp(app.window_size[0]/4), dp(30) + pos_hint: {'center_y': .5, 'center_x': .5} + BoxLayout: + orientation: 'horizontal' + padding: [30, 0, 0, 0] + MDCheckbox: + id: chkbox + size_hint: None, None + size: dp(48), dp(50) + # active: True + halign: 'center' + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("Authentication") + halign: 'left' + pos_hint: {'x': 0, 'y': 0} + BoxLayout: + orientation: 'horizontal' + padding: [30, 0, 0, 0] + MDCheckbox: + id: chkbox + size_hint: None, None + size: dp(48), dp(50) + # active: True + halign: 'center' + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("Listen for incoming connections when using proxy") + halign: 'left' + pos_hint: {'x': 0, 'y': 0} + BoxLayout: + orientation: 'horizontal' + padding: [30, 0, 0, 0] + MDCheckbox: + id: chkbox + size_hint: None, None + size: dp(48), dp(50) + # active: True + halign: 'center' + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("Only connect to onion services(*.onion)") + halign: 'left' + pos_hint: {'x': 0, 'y': 0} + BoxLayout: + orientation: 'vertical' + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("Bandwidth limit") + halign: 'left' + bold: True + BoxLayout: + size_hint_y: None + orientation: 'horizontal' + padding: [30, 0, 0, 0] + height: dp(30) + self.minimum_height + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("Maximum download rate (kB/s):[0:unlimited]") + halign: 'left' + MDTextFieldRect: + size_hint: None, None + size: app.window_size[0]/2, dp(30) + hint_text: app.tr._('0') + pos_hint: {'center_y': .5, 'center_x': .5} + input_filter: "int" + BoxLayout: + size_hint_y: None + orientation: 'horizontal' + padding: [30, 0, 0, 0] + height: dp(30) + self.minimum_height + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("Maximum upload rate (kB/s):[0:unlimited]") + halign: 'left' + MDTextFieldRect: + size_hint: None, None + size: app.window_size[0]/2, dp(30) + hint_text: '0' + pos_hint: {'center_y': .5, 'center_x': .5} + input_filter: "int" + BoxLayout: + size_hint_y: None + orientation: 'horizontal' + padding: [30, 0, 0, 0] + height: dp(30) + self.minimum_height + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("Maximum outbound connections:[0:none]") + halign: 'left' + MDTextFieldRect: + size_hint: None, None + size: app.window_size[0]/2, dp(30) + hint_text: '8' + pos_hint: {'center_y': .5, 'center_x': .5} + input_filter: "int" + BoxLayout: + spacing:5 + orientation: 'horizontal' + # pos_hint: {'x':.76} + + MDRaisedButton: + text: app.tr._('Apply') + Tab: + title: 'Demanded Difficulty' + ScrollView: + do_scroll_x: False + + BoxLayout: + size_hint_y: None + orientation: 'vertical' + height: dp(300) + self.minimum_height + padding: 10 + BoxLayout: + id: box_height + orientation: 'vertical' + # MDLabel: + # font_style: 'Body1' + # theme_text_color: 'Primary' + # text: app.tr._("Listening port") + # halign: 'left' + # bold: True + + # BoxLayout: + # size_hint_y: None + # orientation: 'vertical' + # height: dp(210 if app.app_platform == 'android' else 100)+ self.minimum_height + # padding: 20 + # # spacing: 10 + # BoxLayout: + # # size_hint_y: None + # id: box1_height + # # orientation: 'vertical' + # # height: dp(100) + self.minimum_height + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + # text: app.tr._(root.exp_text) + text: "\n\n\nWhen someone sends you a message, their computer must first complete some work. The difficulty of this work, by default, is 1. You may raise this default for new addresses you create by changing the values here. Any new addresses you create will require senders to meet the higher difficulty. There is one exception: if you add a friend or acquaintance to your address book, Bitmessage will automatically notify them when you next send a message that they need only complete the minimum amount of work: difficulty 1.\n\n" + halign: 'left' + + BoxLayout: + orientation: 'horizontal' + padding: 5 + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("Total difficulty:") + halign: 'left' + MDTextFieldRect: + size_hint: None, None + size: dp(app.window_size[0]/4), dp(30) + hint_text: app.tr._('00000.0') + pos_hint: {'center_y': .5, 'center_x': .5} + input_filter: "int" + + BoxLayout: + # size_hint_y: None + id: box1_height + orientation: 'vertical' + padding: 5 + # height: dp(100) + self.minimum_height + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + # text: app.tr._(root.exp_text) + text: "The 'Total difficulty' affects the absolute amount of work the sender must complete. Doubling this value doubles the amount of work." + halign: 'left' + + BoxLayout: + orientation: 'horizontal' + spacing: 0 + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("Small message difficulty:") + halign: 'left' + MDTextFieldRect: + size_hint: None, None + size: dp(app.window_size[0]/4), dp(30) + hint_text: app.tr._('00000.0') + pos_hint: {'center_y': .5, 'center_x': .5} + input_filter: "int" + + + BoxLayout: + size_hint_y: None + padding: 0 + id: box1_height + orientation: 'vertical' + # height: dp(100) + self.minimum_height + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + # text: app.tr._(root.exp_text) + text: "The 'Small message difficulty' mostly only affects the difficulty of sending small messages. Doubling this value makes it almost twice as difficult to send a small message but doesn't really affect large messages." + halign: 'left' + + + # BoxLayout: + # id: box2_height + # size_hint_y: None + # orientation: 'vertical' + # height: dp(30) + self.minimum_height + # MDLabel: + # font_style: 'Body1' + # theme_text_color: 'Primary' + # text: app.tr._("Leave these input fields blank for the default behavior.") + # halign: 'left' + # BoxLayout: + # size_hint_y: None + # orientation: 'vertical' + # padding: [10, 0, 0, 0] + # height: dp(50) + self.minimum_height + # BoxLayout: + # orientation: 'horizontal' + # MDLabel: + # font_style: 'Body1' + # theme_text_color: 'Primary' + # text: app.tr._("Give up after") + # halign: 'left' + # MDTextFieldRect: + # size_hint: None, None + # size: dp(70), dp(30) + # text: app.tr._('0') + # # pos_hint: {'center_y': .5, 'center_x': .5} + # input_filter: "int" + # MDLabel: + # font_style: 'Body1' + # theme_text_color: 'Primary' + # text: app.tr._("days and") + # halign: 'left' + # MDTextFieldRect: + # size_hint: None, None + # size: dp(70), dp(30) + # text: '0' + # # pos_hint: {'center_y': .5, 'center_x': .5} + # input_filter: "int" + # MDLabel: + # font_style: 'Body1' + # theme_text_color: 'Primary' + # text: "months" + # halign: 'left' + BoxLayout: + size_hint_y: None + spacing:10 + orientation: 'horizontal' + # pos_hint: {'left': 0} + # pos_hint: {'x':.75} + height: dp(10) + self.minimum_height + MDRaisedButton: + text: app.tr._('Cancel') + MDRaisedButton: + text: app.tr._('Apply') + + Tab: + title: 'Max acceptable Difficulty' + ScrollView: + do_scroll_x: False + BoxLayout: + size_hint_y: None + orientation: 'vertical' + height: dp(210 if app.app_platform == 'android' else 100)+ self.minimum_height + padding: 20 + + # spacing: 10 + BoxLayout: + # size_hint_y: None + id: box1_height + orientation: 'vertical' + spacing: 10 + + # pos_hint: {'x': 0, 'y': 0.2} + # height: dp(100) + self.minimum_height + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + # text: app.tr._(root.exp_text) + text: "\n\n\nHere you may set the maximum amount of work you are willing to do to send a message to another person. Setting these values to 0 means that any value is acceptable." + halign: 'left' + # BoxLayout: + # id: box2_height + # size_hint_y: None + # orientation: 'vertical' + # height: dp(40) + self.minimum_height + # BoxLayout: + # size_hint_y: None + # orientation: 'vertical' + # padding: [10, 0, 0, 0] + # height: dp(50) + self.minimum_height + + GridLayout: + cols: 2 + padding: [10, 0, 0, 0] + + BoxLayout: + size_hint_y: None + orientation: 'vertical' + padding: [10, 0, 0, 0] + spacing: 10 + height: dp(50) + self.minimum_height + BoxLayout: + orientation: 'horizontal' + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("Maximum acceptable total difficulty:") + halign: 'left' + MDTextFieldRect: + size_hint: None, None + size: dp(app.window_size[0]/4), dp(30) + hint_text: app.tr._('00000.0') + pos_hint: {'center_y': .5, 'center_x': .5} + input_filter: "int" + + BoxLayout: + orientation: 'horizontal' + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("Hardware GPU acceleration (OpenCL):") + halign: 'left' + MDDropDownItem: + id: dropdown_item + text: "None" + pos_hint: {"center_x": 0, "center_y": 0} + # current_item: "Item 0" + # on_release: root.menu.open() + + # BoxLayout: + # size_hint_y: None + # spacing:5 + # orientation: 'horizontal' + # pos_hint: {'center_y': .4, 'center_x': 1.15} + # halign: 'right' + + BoxLayout: + size_hint_y: None + spacing:5 + orientation: 'horizontal' + pos_hint: {'center_y': 1, 'center_x': 1.15} + halign: 'right' + # pos_hint: {'left': 0} + # pos_hint: {'x':.75} + height: dp(50) + self.minimum_height + MDRaisedButton: + text: app.tr._('Cancel') + MDRaisedButton: + text: app.tr._('OK') + Tab: + title: 'Resends Expire' + ScrollView: + do_scroll_x: False + BoxLayout: + size_hint_y: None + orientation: 'vertical' + height: dp(210 if app.app_platform == 'android' else 100)+ self.minimum_height + padding: 20 + # spacing: 10 + BoxLayout: + # size_hint_y: None + id: box1_height + orientation: 'vertical' + # height: dp(100) + self.minimum_height + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + # text: app.tr._(root.exp_text) + text: "By default, if you send a message to someone and he is offline for more than two days, Bitmessage will send the message again after an additional two days. This will be continued with exponential backoff forever; messages will be resent after 5, 10, 20 days ect. until the receiver acknowledges them. Here you may change that behavior by having Bitmessage give up after a certain number of days or months." + halign: 'left' + BoxLayout: + id: box2_height + size_hint_y: None + orientation: 'vertical' + height: dp(30) + self.minimum_height + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("Leave these input fields blank for the default behavior.") + halign: 'left' + BoxLayout: + size_hint_y: None + orientation: 'vertical' + padding: [10, 0, 0, 0] + height: dp(50) + self.minimum_height + BoxLayout: + orientation: 'horizontal' + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("Give up after") + halign: 'left' + MDTextFieldRect: + size_hint: None, None + size: dp(70), dp(30) + text: app.tr._('0') + pos_hint: {'center_y': .5, 'center_x': .5} + input_filter: "int" + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("days and") + halign: 'left' + MDTextFieldRect: + size_hint: None, None + size: dp(70), dp(30) + text: '0' + pos_hint: {'center_y': .5, 'center_x': .5} + input_filter: "int" + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: "months" + halign: 'left' + BoxLayout: + size_hint_y: None + spacing:5 + orientation: 'horizontal' + # pos_hint: {'left': 0} + # pos_hint: {'x':.75} + height: dp(50) + self.minimum_height + # MDRaisedButton: + # text: app.tr._('Cancel') + MDRaisedButton: + text: app.tr._('Apply') + + Tab: + title: 'Namecoin Integration' + ScrollView: + do_scroll_x: False + BoxLayout: + size_hint_y: None + orientation: 'vertical' + height: dp(210 if app.app_platform == 'android' else 100)+ self.minimum_height + padding: 20 + + # spacing: 10 + BoxLayout: + # size_hint_y: None + id: box1_height + orientation: 'vertical' + spacing: 10 + + # pos_hint: {'x': 0, 'y': 0.2} + # height: dp(100) + self.minimum_height + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + # text: app.tr._(root.exp_text) + text: "\n\n\n\n\n\nBitmessage can utilize a different Bitcoin-based program called Namecoin to make addresses human-friendly. For example, instead of having to tell your friend your long Bitmessage address, you can simply tell him to send a message to test.\n\n(Getting your own Bitmessage address into Namecoin is still rather difficult).\n\nBitmessage can use either namecoind directly or a running nmcontrol instance\n\n" + halign: 'left' + + BoxLayout: + id: box2_height + size_hint_y: None + orientation: 'vertical' + height: dp(40) + self.minimum_height + BoxLayout: + size_hint_y: None + orientation: 'vertical' + padding: [10, 0, 0, 0] + height: dp(50) + self.minimum_height + + BoxLayout: + orientation: 'horizontal' + padding: [10, 0, 0, 0] + + BoxLayout: + orientation: 'horizontal' + + # padding_left: 10 + # MDCheckbox: + # id: chkbox + # size_hint: None, None + # size: dp(48), dp(50) + # # active: True + # halign: 'center' + # MDLabel: + # font_style: 'Body1' + # theme_text_color: 'Primary' + # text: app.tr._("UPnP") + # halign: 'left' + # pos_hint: {'x': 0, 'y': 0} + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("Connect to:") + halign: 'left' + + # MDCheckbox: + # id: chkbox + # size_hint: None, None + # size: dp(48), dp(50) + # # active: True + # halign: 'center' + Check: + active: True + pos_hint: {'x': 0, 'y': -0.2} + + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("Namecoind") + halign: 'left' + pos_hint: {'x': 0, 'y': 0} + + Check: + active: False + pos_hint: {'x': 0, 'y': -0.2} + + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("NMControl") + halign: 'left' + pos_hint: {'x': 0, 'y': 0} + + GridLayout: + cols: 2 + padding: [10, 0, 0, 0] + + BoxLayout: + size_hint_y: None + orientation: 'vertical' + padding: [30, 0, 0, 0] + spacing: 10 + height: dp(100) + self.minimum_height + BoxLayout: + orientation: 'horizontal' + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("hostname:") + halign: 'left' + MDTextFieldRect: + size_hint: None, None + size: dp(app.window_size[0]/4), dp(30) + hint_text: app.tr._('localhost') + pos_hint: {'center_y': .5, 'center_x': .5} + BoxLayout: + orientation: 'horizontal' + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("Port:") + halign: 'left' + # TextInput: + # size_hint: None, None + # hint_text: '9050' + # size: dp(app.window_size[0]/4), dp(30) + # input_filter: "int" + # readonly: False + # multiline: False + # font_size: '15sp' + MDTextFieldRect: + size_hint: None, None + size: dp(app.window_size[0]/4), dp(30) + hint_text: app.tr._('9050') + pos_hint: {'center_y': .5, 'center_x': .5} + input_filter: "int" + BoxLayout: + orientation: 'horizontal' + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("Username:") + halign: 'left' + MDTextFieldRect: + size_hint: None, None + size: dp(app.window_size[0]/4), dp(30) + pos_hint: {'center_y': .5, 'center_x': .5} + BoxLayout: + orientation: 'horizontal' + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: app.tr._("Password:") + halign: 'left' + + MDTextFieldRect: + size_hint: None, None + size: dp(app.window_size[0]/4), dp(30) + pos_hint: {'center_y': .5, 'center_x': .5} + password: True + + + BoxLayout: + size_hint_y: None + spacing:5 + orientation: 'horizontal' + pos_hint: {'center_y': .4, 'center_x': 1.15} + halign: 'right' + # pos_hint: {'left': 0} + # pos_hint: {'x':.75} + height: dp(50) + self.minimum_height + MDRaisedButton: + text: app.tr._('Cancel') + MDRaisedButton: + text: app.tr._('Apply') + MDRaisedButton: + text: app.tr._('OK') + Loader: \ No newline at end of file diff --git a/src/bitmessagekivy/kv/trash.kv b/src/bitmessagekivy/kv/trash.kv new file mode 100644 index 00000000..97bcf7d7 --- /dev/null +++ b/src/bitmessagekivy/kv/trash.kv @@ -0,0 +1,25 @@ +: + name: 'trash' + BoxLayout: + orientation: 'vertical' + spacing: dp(5) + GridLayout: + id: identi_tag + padding: [20, 20, 0, 5] + spacing: dp(5) + cols: 1 + size_hint_y: None + height: self.minimum_height + MDLabel: + id: tag_label + text: '' + font_style: 'Subtitle2' + BoxLayout: + orientation:'vertical' + ScrollView: + id: scroll_y + do_scroll_x: False + MDList: + id: ml + Loader: + ComposerButton: diff --git a/src/bitmessagekivy/load_kivy_screens_data.py b/src/bitmessagekivy/load_kivy_screens_data.py new file mode 100644 index 00000000..b4132927 --- /dev/null +++ b/src/bitmessagekivy/load_kivy_screens_data.py @@ -0,0 +1,25 @@ +""" + Load kivy screens data from json +""" +import os +import json +import importlib + + +data_screen_dict = {} + + +def load_screen_json(data_file="screens_data.json"): + """Load screens data from json""" + + with open(os.path.join(os.path.dirname(__file__), data_file)) as read_file: + all_data = json.load(read_file) + data_screens = list(all_data.keys()) + + for key in all_data: + if all_data[key]['Import']: + import_data = all_data.get(key)['Import'] + import_to = import_data.split("import")[1].strip() + import_from = import_data.split("import")[0].split('from')[1].strip() + data_screen_dict[import_to] = importlib.import_module(import_from, import_to) + return data_screens, all_data, data_screen_dict, 'success' diff --git a/src/bitmessagekivy/main.kv b/src/bitmessagekivy/main.kv new file mode 100644 index 00000000..42e66762 --- /dev/null +++ b/src/bitmessagekivy/main.kv @@ -0,0 +1,388 @@ +#:import get_color_from_hex kivy.utils.get_color_from_hex +#:import Factory kivy.factory.Factory +#:import Spinner kivy.uix.spinner.Spinner + +#:import colors kivymd.color_definitions.colors +#:import images_path kivymd.images_path + +#:import IconLeftWidget kivymd.uix.list.IconLeftWidget +#:import MDCard kivymd.uix.card.MDCard +#:import MDCheckbox kivymd.uix.selectioncontrol.MDCheckbox +#:import MDFloatingActionButton kivymd.uix.button.MDFloatingActionButton +#:import MDList kivymd.uix.list.MDList +#:import MDScrollViewRefreshLayout kivymd.uix.refreshlayout.MDScrollViewRefreshLayout +#:import MDSpinner kivymd.uix.spinner.MDSpinner +#:import MDTextField kivymd.uix.textfield.MDTextField +#:import MDTabs kivymd.uix.tab.MDTabs +#:import MDTabsBase kivymd.uix.tab.MDTabsBase +#:import OneLineListItem kivymd.uix.list.OneLineListItem + + + +#:set color_button (0.784, 0.443, 0.216, 1) # brown +#:set color_button_pressed (0.659, 0.522, 0.431, 1) # darker brown +#:set color_font (0.957, 0.890, 0.843, 1) # off white + +: + font_size: '12.5sp' + background_normal: 'atlas://data/images/defaulttheme/textinput_active' + background_color: app.theme_cls.primary_color + color: color_font + + + on_press: root.currentlyActive() + active_color: root.theme_cls.primary_color if root.active else root.theme_cls.text_color + + IconLeftWidget: + icon: root.icon + theme_text_color: "Custom" + text_color: root.active_color + + BadgeText: + id: badge_txt + text: f"{root.badge_text}" + theme_text_color: "Custom" + halign: 'right' + +: + canvas: + Color: + rgba: self.theme_cls.divider_color + Line: + points: root.x, root.y + dp(8), root.x + self.width, root.y + dp(8) + + + + BoxLayout: + orientation: 'vertical' + + FloatLayout: + size_hint_y: None + height: "200dp" + + MDIconButton: + id: reset_image + icon: "refresh" + x: root.parent.x + dp(10) + pos_hint: {"top": 1, 'left': 1} + color: [1,0,0,1] + on_release: app.rest_default_avatar_img() + theme_text_color: "Custom" + text_color: app.theme_cls.primary_color + opacity: 1 if app.have_any_address() else 0 + disabled: False if app.have_any_address() else True + + MDIconButton: + id: file_manager + icon: "file-image" + x: root.parent.x + dp(10) + pos_hint: {"top": 1, 'right': 1} + color: [1,0,0,1] + on_release: app.file_manager_open() + theme_text_color: "Custom" + text_color: app.theme_cls.primary_color + opacity: 1 if app.have_any_address() else 0 + disabled: False if app.have_any_address() else True + + BoxLayout: + id: top_box + size_hint_y: None + height: "200dp" + x: root.parent.x + pos_hint: {"top": 1} + Image: + source: app.get_default_logo(self) + + ScrollView: + id: scroll_y + pos_hint: {"top": 1} + + GridLayout: + id: box_item + cols: 1 + size_hint_y: None + height: self.minimum_height + NavigationDrawerDivider: + NavigationDrawerSubheader: + text: app.tr._('Accounts') + height:"35dp" + NavigationItem: + text: 'dropdown_nav_item' + height: dp(48) + IdentitySpinner: + id: identity_dropdown + pos_hint:{"x":0,"y":0} + name: "identity_dropdown" + option_cls: Factory.get("MySpinnerOption") + font_size: '12.5sp' + text: app.getDefaultAccData(self) + color: color_font + background_normal: '' + background_color: app.theme_cls.primary_color + on_text: app.getCurrentAccountData(self.text) + ArrowImg: + NavigationItem: + id: inbox_cnt + text: app.tr._('Inbox') + icon: 'email-open' + divider: None + on_release: app.set_screen('inbox') + on_release: root.parent.set_state() + on_press: app.load_screen(self) + NavigationItem: + id: send_cnt + text: app.tr._('Sent') + icon: 'send' + divider: None + on_release: app.set_screen('sent') + on_release: root.parent.set_state() + NavigationItem: + id: draft_cnt + text: app.tr._('Draft') + icon: 'message-draw' + divider: None + on_release: app.root.ids.scr_mngr.current = 'draft' + on_release: root.parent.set_state() + NavigationItem: + id: trash_cnt + text: app.tr._('Trash') + icon: 'delete' + divider: None + on_release: app.root.ids.scr_mngr.current = 'trash' + on_press: root.parent.set_state() + on_press: app.load_screen(self) + NavigationItem: + id: allmail_cnt + text: app.tr._('All Mails') + icon: 'mailbox' + divider: None + on_release: app.root.ids.scr_mngr.current = 'allmails' + on_release: root.parent.set_state() + on_press: app.load_screen(self) + NavigationDrawerDivider: + NavigationDrawerSubheader: + text: app.tr._('Chat') + NavigationItem: + id: draft_cnt + text: app.tr._('Chat') + icon: 'chat' + divider: None + on_release: app.root.ids.scr_mngr.current = 'chat' + on_release: root.parent.set_state() + NavigationDrawerDivider: + NavigationDrawerSubheader: + text: app.tr._("All labels") + NavigationItem: + text: app.tr._('Address Book') + icon: 'book-multiple' + divider: None + on_release: app.root.ids.scr_mngr.current = 'addressbook' + on_release: root.parent.set_state() + NavigationItem: + text: app.tr._('Settings') + icon: 'application-settings' + divider: None + on_release: app.root.ids.scr_mngr.current = 'set' + on_release: root.parent.set_state() + NavigationItem: + text: app.tr._('Payment plan') + icon: 'shopping' + divider: None + on_release: app.root.ids.scr_mngr.current = 'payment' + on_release: root.parent.set_state() + NavigationItem: + text: app.tr._('New address') + icon: 'account-plus' + divider: None + on_release: app.root.ids.scr_mngr.current = 'login' + on_release: root.parent.set_state() + on_press: app.reset_login_screen() + NavigationItem: + text: app.tr._('Network status') + icon: 'server-network' + divider: None + on_release: app.root.ids.scr_mngr.current = 'networkstat' + on_release: root.parent.set_state() + NavigationItem: + text: app.tr._('My addresses') + icon: 'account-multiple' + divider: None + on_release: app.root.ids.scr_mngr.current = 'myaddress' + on_release: root.parent.set_state() + +MDNavigationLayout: + id: nav_layout + + MDTopAppBar: + id: toolbar + title: app.format_address_and_label() + opacity: 1 if app.have_any_address() else 0 + disabled: False if app.have_any_address() else True + pos_hint: {"top": 1} + md_bg_color: app.theme_cls.primary_color + elevation: 10 + left_action_items: [['menu', lambda x: nav_drawer.set_state("toggle")]] + right_action_items: [['account-plus', lambda x: app.addingtoaddressbook()]] + + ScreenManager: + id: scr_mngr + size_hint_y: None + height: root.height - toolbar.height + Inbox: + id:id_inbox + Login: + id:sc6 + Random: + id:id_newidentity + MyAddress: + id:id_myaddress + ScanScreen: + id:id_scanscreen + Payment: + id:id_payment + Create: + id:id_create + NetworkStat: + id:id_networkstat + Setting: + id:id_settings + Sent: + id:id_sent + Trash: + id:id_trash + Allmails: + id:id_allmail + Draft: + id:id_draft + AddressBook: + id:id_addressbook + ShowQRCode: + id:id_showqrcode + Chat: + id: id_chat + + MDNavigationDrawer: + id: nav_drawer + + ContentNavigationDrawer: + id: content_drawer + +: + source: app.image_path +('/down-arrow.png' if self.parent.is_open == True else '/right-arrow.png') + size: 15, 15 + x: self.parent.x + self.parent.width - self.width - 5 + y: self.parent.y + self.parent.height/2 - self.height + 5 + + +: + size_hint_y: None + height: self.minimum_height + + MDIconButton: + icon: 'magnify' + + MDTextField: + id: search_field + hint_text: 'Search' + canvas.before: + Color: + rgba: (0,0,0,1) + + +: + id: spinner + size_hint: None, None + size: dp(46), dp(46) + pos_hint: {'center_x': 0.5, 'center_y': 0.5} + active: False + +: + size_hint_y: None + height: dp(56) + spacing: '10dp' + pos_hint: {'center_x':0.45, 'center_y': .1} + + Widget: + + MDFloatingActionButton: + icon: 'plus' + opposite_colors: True + elevation_normal: 8 + md_bg_color: [0.941, 0, 0,1] + on_press: app.root.ids.scr_mngr.current = 'create' + on_press: app.clear_composer() + + +: + size_hint_y: None + height: content.height + + MDCardSwipeLayerBox: + padding: "8dp" + + MDIconButton: + id: delete_msg + icon: "trash-can" + pos_hint: {"center_y": .5} + md_bg_color: (1, 0, 0, 1) + disabled: True + + MDCardSwipeFrontBox: + + TwoLineAvatarIconListItem: + id: content + text: root.text + _no_ripple_effect: True + + AvatarSampleWidget: + id: avater_img + source: None + + TimeTagRightSampleWidget: + id: time_tag + text: '' + font_size: "11sp" + font_style: "Caption" + size: [120, 140] if app.app_platform == "android" else [64, 80] + + +: + size_hint_y: None + height: content.height + + MDCardSwipeLayerBox: + padding: "8dp" + + MDIconButton: + id: delete_msg + icon: "trash-can" + pos_hint: {"center_y": .5} + md_bg_color: (1, 0, 0, 1) + disabled: True + + MDCardSwipeFrontBox: + + TwoLineAvatarIconListItem: + id: content + text: root.text + _no_ripple_effect: True + + AvatarSampleWidget: + id: avater_img + source: None + + TimeTagRightSampleWidget: + id: time_tag + text: 'time' + font_size: "11sp" + font_style: "Caption" + size: [120, 140] if app.app_platform == "android" else [64, 80] + MDChip: + id: chip_tag + size_hint: (0.16 if app.app_platform == "android" else 0.08, None) + text: 'test' + icon: "" + pos_hint: {"center_x": 0.91 if app.app_platform == "android" else 0.94, "center_y": 0.3} + height: '18dp' + text_color: (1,1,1,1) + radius: [8] diff --git a/src/bitmessagekivy/mpybit.py b/src/bitmessagekivy/mpybit.py index b44b1070..8c3ab4ab 100644 --- a/src/bitmessagekivy/mpybit.py +++ b/src/bitmessagekivy/mpybit.py @@ -1,27 +1,488 @@ +# pylint: disable=too-many-public-methods, unused-variable, too-many-ancestors +# pylint: disable=no-name-in-module, too-few-public-methods, unused-argument +# pylint: disable=attribute-defined-outside-init, too-many-instance-attributes + """ - Dummy implementation for kivy Desktop and android(mobile) interface +Bitmessage android(mobile) interface """ -# pylint: disable=too-few-public-methods -from kivy.app import App -from kivy.uix.label import Label +import os +import logging +import sys +from functools import partial +from PIL import Image as PilImage + +from kivy.clock import Clock +from kivy.lang import Builder +from kivy.core.window import Window +from kivy.uix.boxlayout import BoxLayout + +from kivymd.app import MDApp +from kivymd.uix.label import MDLabel +from kivymd.uix.dialog import MDDialog +from kivymd.uix.list import ( + IRightBodyTouch +) +from kivymd.uix.button import MDRaisedButton +from kivymd.uix.bottomsheet import MDCustomBottomSheet +from kivymd.uix.filemanager import MDFileManager + +from pybitmessage.bitmessagekivy.kivy_state import KivyStateVariables +from pybitmessage.bitmessagekivy.base_navigation import ( + BaseLanguage, BaseNavigationItem, BaseNavigationDrawerDivider, + BaseNavigationDrawerSubheader, BaseContentNavigationDrawer, + BaseIdentitySpinner +) +from pybitmessage.bmconfigparser import config # noqa: F401 +from pybitmessage.bitmessagekivy import identiconGeneration +from pybitmessage.bitmessagekivy.get_platform import platform +from pybitmessage.bitmessagekivy.baseclass.common import toast, load_image_path, get_identity_list +from pybitmessage.bitmessagekivy.load_kivy_screens_data import load_screen_json + +from pybitmessage.bitmessagekivy.baseclass.popup import ( + AddAddressPopup, AppClosingPopup, AddressChangingLoader +) +from pybitmessage.bitmessagekivy.baseclass.login import * # noqa: F401, F403 +from pybitmessage.bitmessagekivy.uikivysignaler import UIkivySignaler + +from pybitmessage.mockbm.helper_startup import loadConfig, total_encrypted_messages_per_month + +logger = logging.getLogger('default') -class NavigateApp(App): +class Lang(BaseLanguage): + """UI Language""" + + +class NavigationItem(BaseNavigationItem): + """NavigationItem class for kivy Ui""" + + +class NavigationDrawerDivider(BaseNavigationDrawerDivider): + """ + A small full-width divider that can be placed + in the :class:`MDNavigationDrawer` + """ + + +class NavigationDrawerSubheader(BaseNavigationDrawerSubheader): + """ + A subheader for separating content in :class:`MDNavigationDrawer` + + Works well alongside :class:`NavigationDrawerDivider` + """ + + +class ContentNavigationDrawer(BaseContentNavigationDrawer): + """ContentNavigationDrawer class for kivy Uir""" + + +class BadgeText(IRightBodyTouch, MDLabel): + """BadgeText class for kivy Ui""" + + +class IdentitySpinner(BaseIdentitySpinner): + """Identity Dropdown in Side Navigation bar""" + + +class NavigateApp(MDApp): """Navigation Layout of class""" + kivy_state = KivyStateVariables() + title = "PyBitmessage" + identity_list = get_identity_list() + image_path = load_image_path() + app_platform = platform + encrypted_messages_per_month = total_encrypted_messages_per_month() + tr = Lang("en") # for changing in franch replace en with fr + + def __init__(self): + super(NavigateApp, self).__init__() + # workaround for relative imports + sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..')) + self.data_screens, self.all_data, self.data_screen_dict, response = load_screen_json() + self.kivy_state_obj = KivyStateVariables() + self.image_dir = load_image_path() + self.kivy_state_obj.screen_density = Window.size + self.window_size = self.kivy_state_obj.screen_density + def build(self): """Method builds the widget""" - # pylint: disable=no-self-use - return Label(text="Hello World !") + for kv in self.data_screens: + Builder.load_file( + os.path.join( + os.path.dirname(__file__), + 'kv', + '{0}.kv'.format(self.all_data[kv]["kv_string"]), + ) + ) + Window.bind(on_request_close=self.on_request_close) + return Builder.load_file(os.path.join(os.path.dirname(__file__), 'main.kv')) - def clickNavDrawer(self): - """method for clicking navigation drawer""" - pass + def set_screen(self, screen_name): + """Set the screen name when navigate to other screens""" + self.root.ids.scr_mngr.current = screen_name + + def run(self): + """Running the widgets""" + loadConfig() + kivysignalthread = UIkivySignaler() + kivysignalthread.daemon = True + kivysignalthread.start() + self.kivy_state_obj.kivyui_ready.set() + super(NavigateApp, self).run() def addingtoaddressbook(self): - """method for clicking address book popup""" - pass + """Dialog for saving address""" + width = .85 if platform == 'android' else .8 + self.add_popup = MDDialog( + title='Add contact', + type="custom", + size_hint=(width, .23), + content_cls=AddAddressPopup(), + buttons=[ + MDRaisedButton( + text="Save", + on_release=self.savecontact, + ), + MDRaisedButton( + text="Cancel", + on_release=self.close_pop, + ), + MDRaisedButton( + text="Scan QR code", + on_release=self.scan_qr_code, + ), + ], + ) + self.add_popup.auto_dismiss = False + self.add_popup.open() + + def scan_qr_code(self, instance): + """this method is used for showing QR code scanner""" + if self.is_camara_attached(): + self.add_popup.dismiss() + self.root.ids.id_scanscreen.get_screen(self.root.ids.scr_mngr.current, self.add_popup) + self.root.ids.scr_mngr.current = 'scanscreen' + else: + alert_text = ( + 'Currently this feature is not avaialbe!' if platform == 'android' else 'Camera is not available!') + self.add_popup.dismiss() + toast(alert_text) + + def is_camara_attached(self): + """This method is for checking the camera is available or not""" + self.root.ids.id_scanscreen.check_camera() + is_available = self.root.ids.id_scanscreen.camera_available + return is_available + + def savecontact(self, instance): + """Method is used for saving contacts""" + popup_obj = self.add_popup.content_cls + label = popup_obj.ids.label.text.strip() + address = popup_obj.ids.address.text.strip() + popup_obj.ids.label.focus = not label + # default focus on address field + popup_obj.ids.address.focus = label or not address + + def close_pop(self, instance): + """Close the popup""" + self.add_popup.dismiss() + toast('Canceled') + + def loadMyAddressScreen(self, action): + """loadMyAddressScreen method spin the loader""" + if len(self.root.ids.id_myaddress.children) <= 2: + self.root.ids.id_myaddress.children[0].active = action + else: + self.root.ids.id_myaddress.children[1].active = action + + def load_screen(self, instance): + """This method is used for loading screen on every click""" + if instance.text == 'Inbox': + self.root.ids.scr_mngr.current = 'inbox' + self.root.ids.id_inbox.children[1].active = True + elif instance.text == 'Trash': + self.root.ids.scr_mngr.current = 'trash' + try: + self.root.ids.id_trash.children[1].active = True + except Exception as e: + self.root.ids.id_trash.children[0].children[1].active = True + Clock.schedule_once(partial(self.load_screen_callback, instance), 1) + + def load_screen_callback(self, instance, dt=0): + """This method is rotating loader for few seconds""" + if instance.text == 'Inbox': + self.root.ids.id_inbox.ids.ml.clear_widgets() + self.root.ids.id_inbox.loadMessagelist(self.kivy_state_obj.selected_address) + self.root.ids.id_inbox.children[1].active = False + elif instance.text == 'Trash': + self.root.ids.id_trash.clear_widgets() + self.root.ids.id_trash.add_widget(self.data_screen_dict['Trash'].Trash()) + try: + self.root.ids.id_trash.children[1].active = False + except Exception as e: + self.root.ids.id_trash.children[0].children[1].active = False + + @staticmethod + def get_enabled_addresses(): + """Getting list of all the enabled addresses""" + addresses = [addr for addr in config.addresses() + if config.getboolean(str(addr), 'enabled')] + return addresses + + @staticmethod + def format_address(address): + """Formatting address""" + return " ({0})".format(address) + + @staticmethod + def format_label(label): + """Formatting label""" + if label: + f_name = label.split() + truncate_string = '...' + f_name_max_length = 15 + formatted_label = f_name[0][:14].capitalize() + truncate_string if len( + f_name[0]) > f_name_max_length else f_name[0].capitalize() + return formatted_label + return '' + + @staticmethod + def format_address_and_label(address=None): + """Getting formatted address information""" + if not address: + try: + address = NavigateApp.get_enabled_addresses()[0] + except IndexError: + return '' + return "{0}{1}".format( + NavigateApp.format_label(config.get(address, "label")), + NavigateApp.format_address(address) + ) + + def getDefaultAccData(self, instance): + """Getting Default Account Data""" + if self.identity_list: + self.kivy_state_obj.selected_address = first_addr = self.identity_list[0] + return first_addr + return 'Select Address' + + def getCurrentAccountData(self, text): + """Get Current Address Account Data""" + if text != '': + if os.path.exists(os.path.join( + self.image_dir, 'default_identicon', '{}.png'.format(text)) + ): + self.load_selected_Image(text) + else: + self.set_identicon(text) + self.root.ids.content_drawer.ids.reset_image.opacity = 0 + self.root.ids.content_drawer.ids.reset_image.disabled = True + address_label = self.format_address_and_label(text) + self.root_window.children[1].ids.toolbar.title = address_label + self.kivy_state_obj.selected_address = text + AddressChangingLoader().open() + for nav_obj in self.root.ids.content_drawer.children[ + 0].children[0].children[0].children: + nav_obj.active = True if nav_obj.text == 'Inbox' else False + self.fileManagerSetting() + Clock.schedule_once(self.setCurrentAccountData, 0.5) + + def setCurrentAccountData(self, dt=0): + """This method set the current accout data on all the screens""" + self.root.ids.id_inbox.ids.ml.clear_widgets() + self.root.ids.id_inbox.loadMessagelist(self.kivy_state_obj.selected_address) + + self.root.ids.id_sent.ids.ml.clear_widgets() + self.root.ids.id_sent.children[2].children[2].ids.search_field.text = '' + self.root.ids.id_sent.loadSent(self.kivy_state_obj.selected_address) + + def fileManagerSetting(self): + """This method is for file manager setting""" + if not self.root.ids.content_drawer.ids.file_manager.opacity and \ + self.root.ids.content_drawer.ids.file_manager.disabled: + self.root.ids.content_drawer.ids.file_manager.opacity = 1 + self.root.ids.content_drawer.ids.file_manager.disabled = False + + def on_request_close(self, *args): # pylint: disable=no-self-use + """This method is for app closing request""" + AppClosingPopup().open() + return True + + def clear_composer(self): + """If slow down, the new composer edit screen""" + self.set_navbar_for_composer() + composer_obj = self.root.ids.id_create.children[1].ids + composer_obj.ti.text = '' + composer_obj.composer_dropdown.text = 'Select' + composer_obj.txt_input.text = '' + composer_obj.subject.text = '' + composer_obj.body.text = '' + self.kivy_state_obj.in_composer = True + self.kivy_state_obj = False + + def set_navbar_for_composer(self): + """Clearing toolbar data when composer open""" + self.root.ids.toolbar.left_action_items = [ + ['arrow-left', lambda x: self.back_press()]] + self.root.ids.toolbar.right_action_items = [ + ['refresh', + lambda x: self.root.ids.id_create.children[1].reset_composer()], + ['send', + lambda x: self.root.ids.id_create.children[1].send(self)]] + + def set_identicon(self, text): + """Show identicon in address spinner""" + img = identiconGeneration.generate(text) + self.root.ids.content_drawer.ids.top_box.children[0].texture = (img.texture) + + # pylint: disable=import-outside-toplevel + def file_manager_open(self): + """This method open the file manager of local system""" + if not self.kivy_state_obj.file_manager: + self.file_manager = MDFileManager( + exit_manager=self.exit_manager, + select_path=self.select_path, + ext=['.png', '.jpg'] + ) + self.file_manager.previous = False + self.file_manager.current_path = '/' + if platform == 'android': + # pylint: disable=import-error + from android.permissions import request_permissions, Permission, check_permission + if check_permission(Permission.WRITE_EXTERNAL_STORAGE) and \ + check_permission(Permission.READ_EXTERNAL_STORAGE): + self.file_manager.show(os.getenv('EXTERNAL_STORAGE')) + self.kivy_state_obj.manager_open = True + else: + request_permissions([Permission.WRITE_EXTERNAL_STORAGE, Permission.READ_EXTERNAL_STORAGE]) + else: + self.file_manager.show(os.environ["HOME"]) + self.kivy_state_obj.manager_open = True + + def select_path(self, path): + """This method is used to set the select image""" + try: + newImg = PilImage.open(path).resize((300, 300)) + if platform == 'android': + android_path = os.path.join( + os.path.join(os.environ['ANDROID_PRIVATE'], 'app', 'images', 'kivy') + ) + if not os.path.exists(os.path.join(android_path, 'default_identicon')): + os.makedirs(os.path.join(android_path, 'default_identicon')) + newImg.save(os.path.join(android_path, 'default_identicon', '{}.png'.format( + self.kivy_state_obj.selected_address)) + ) + else: + if not os.path.exists(os.path.join(self.image_dir, 'default_identicon')): + os.makedirs(os.path.join(self.image_dir, 'default_identicon')) + newImg.save(os.path.join(self.image_dir, 'default_identicon', '{0}.png'.format( + self.kivy_state_obj.selected_address)) + ) + self.load_selected_Image(self.kivy_state_obj.selected_address) + toast('Image changed') + except Exception: + toast('Exit') + self.exit_manager() + + def exit_manager(self, *args): + """Called when the user reaches the root of the directory tree.""" + self.kivy_state_obj.manager_open = False + self.file_manager.close() + + def load_selected_Image(self, curerentAddr): + """This method load the selected image on screen""" + top_box_obj = self.root.ids.content_drawer.ids.top_box.children[0] + top_box_obj.source = os.path.join(self.image_dir, 'default_identicon', '{0}.png'.format(curerentAddr)) + self.root.ids.content_drawer.ids.reset_image.opacity = 1 + self.root.ids.content_drawer.ids.reset_image.disabled = False + top_box_obj.reload() + + def rest_default_avatar_img(self): + """set default avatar generated image""" + self.set_identicon(self.kivy_state_obj.selected_address) + img_path = os.path.join( + self.image_dir, 'default_identicon', '{}.png'.format(self.kivy_state_obj.selected_address) + ) + if os.path.exists(img_path): + os.remove(img_path) + self.root.ids.content_drawer.ids.reset_image.opacity = 0 + self.root.ids.content_drawer.ids.reset_image.disabled = True + toast('Avatar reset') + + def get_default_logo(self, instance): + """Getting default logo image""" + if self.identity_list: + first_addr = self.identity_list[0] + if config.getboolean(str(first_addr), 'enabled'): + if os.path.exists( + os.path.join( + self.image_dir, 'default_identicon', '{}.png'.format(first_addr) + ) + ): + return os.path.join( + self.image_dir, 'default_identicon', '{}.png'.format(first_addr) + ) + else: + img = identiconGeneration.generate(first_addr) + instance.texture = img.texture + return + return os.path.join(self.image_dir, 'drawer_logo1.png') + + @staticmethod + def have_any_address(): + """Checking existance of any address""" + if config.addresses(): + return True + return False + + def reset_login_screen(self): + """This method is used for clearing the widgets of random screen""" + if self.root.ids.id_newidentity.ids.add_random_bx.children: + self.root.ids.id_newidentity.ids.add_random_bx.clear_widgets() + + def reset(self, *args): + """Set transition direction""" + self.root.ids.scr_mngr.transition.direction = 'left' + self.root.ids.scr_mngr.transition.unbind(on_complete=self.reset) + + def back_press(self): + """Method for, reverting composer to previous page""" + if self.root.ids.scr_mngr.current == 'showqrcode': + self.set_common_header() + self.root.ids.scr_mngr.current = 'myaddress' + self.root.ids.scr_mngr.transition.bind(on_complete=self.reset) + self.kivy_state.in_composer = False + + def set_toolbar_for_QrCode(self): + """This method is use for setting Qr code toolbar.""" + self.root.ids.toolbar.left_action_items = [ + ['arrow-left', lambda x: self.back_press()]] + self.root.ids.toolbar.right_action_items = [] + + def set_common_header(self): + """Common header for all the Screens""" + self.root.ids.toolbar.right_action_items = [ + ['account-plus', lambda x: self.addingtoaddressbook()]] + self.root.ids.toolbar.left_action_items = [ + ['menu', lambda x: self.root.ids.nav_drawer.set_state("toggle")]] + return + + def open_payment_layout(self, sku): + """It basically open up a payment layout for kivy UI""" + pml = PaymentMethodLayout() + self.product_id = sku + self.custom_sheet = MDCustomBottomSheet(screen=pml) + self.custom_sheet.open() + + def initiate_purchase(self, method_name): + """initiate_purchase module""" + logger.debug("Purchasing %s through %s", self.product_id, method_name) + + +class PaymentMethodLayout(BoxLayout): + """PaymentMethodLayout class for kivy Ui""" if __name__ == '__main__': diff --git a/src/bitmessagekivy/screens_data.json b/src/bitmessagekivy/screens_data.json new file mode 100644 index 00000000..86ddea19 --- /dev/null +++ b/src/bitmessagekivy/screens_data.json @@ -0,0 +1,83 @@ +{ + "Inbox": { + "kv_string": "inbox", + "name_screen": "inbox", + "Import": "from pybitmessage.bitmessagekivy.baseclass.inbox import Inbox" + }, + "Login": { + "kv_string": "login", + "Import": "from pybitmessage.bitmessagekivy.baseclass.login import *" + }, + "My addresses": { + "kv_string": "myaddress", + "name_screen": "myaddress", + "Import": "from pybitmessage.bitmessagekivy.baseclass.myaddress import MyAddress" + }, + "Scanner": { + "kv_string": "scan_screen", + "Import": "from pybitmessage.bitmessagekivy.baseclass.scan_screen import ScanScreen" + }, + "Payment": { + "kv_string": "payment", + "name_screen": "payment", + "Import": "from pybitmessage.bitmessagekivy.baseclass.payment import Payment" + }, + "Create": { + "kv_string": "msg_composer", + "name_screen": "create", + "Import": "from pybitmessage.bitmessagekivy.baseclass.msg_composer import Create" + }, + "Network status": { + "kv_string": "network", + "name_screen": "networkstat", + "Import": "from pybitmessage.bitmessagekivy.baseclass.network import NetworkStat" + }, + "Settings": { + "kv_string": "settings", + "name_screen": "set", + "Import": "from pybitmessage.bitmessagekivy.baseclass.settings import Setting" + }, + "Sent": { + "kv_string": "sent", + "name_screen": "sent", + "Import": "from pybitmessage.bitmessagekivy.baseclass.sent import Sent" + }, + "Trash": { + "kv_string": "trash", + "name_screen": "trash", + "Import": "from pybitmessage.bitmessagekivy.baseclass.trash import Trash" + }, + "Address Book": { + "kv_string": "addressbook", + "name_screen": "addressbook", + "Import": "from pybitmessage.bitmessagekivy.baseclass.addressbook import AddressBook" + }, + "Popups": { + "kv_string": "popup", + "Import": "from pybitmessage.bitmessagekivy.baseclass.popup import *" + }, + "All Mails": { + "kv_string": "allmails", + "name_screen": "allmails", + "Import": "from pybitmessage.bitmessagekivy.baseclass.allmail import Allmails" + }, + "MailDetail": { + "kv_string": "maildetail", + "name_screen": "mailDetail", + "Import": "from pybitmessage.bitmessagekivy.baseclass.maildetail import MailDetail" + }, + "Draft": { + "kv_string": "draft", + "name_screen": "draft", + "Import": "from pybitmessage.bitmessagekivy.baseclass.draft import Draft" + }, + "Qrcode": { + "kv_string": "qrcode", + "Import": "from pybitmessage.bitmessagekivy.baseclass.qrcode import ShowQRCode" + }, + "Chat": { + "kv_string": "chat", + "Import": "from pybitmessage.bitmessagekivy.baseclass.chat import Chat" + } + +} diff --git a/src/bitmessagekivy/tests/telenium_process.py b/src/bitmessagekivy/tests/telenium_process.py index f9a397e8..0a81044d 100644 --- a/src/bitmessagekivy/tests/telenium_process.py +++ b/src/bitmessagekivy/tests/telenium_process.py @@ -32,7 +32,7 @@ def cleanup(files=_files): class TeleniumTestProcess(TeleniumTestCase): """Setting Screen Functionality Testing""" - cmd_entrypoint = [os.path.join(os.path.abspath(os.getcwd()), 'src', 'mock', 'kivy_main.py')] + cmd_entrypoint = [os.path.join(os.path.abspath(os.getcwd()), 'src', 'mockbm', 'kivy_main.py')] @classmethod def setUpClass(cls): @@ -54,15 +54,7 @@ class TeleniumTestProcess(TeleniumTestCase): def tearDownClass(cls): """Ensures that pybitmessage stopped and removes files""" # pylint: disable=no-member - try: - cls.cli.app_quit() - except: - pass - - try: - cls.process.kill() - except: - pass + super(TeleniumTestProcess, cls).tearDownClass() cleanup() def assert_wait_no_except(self, selector, timeout=-1, value='inbox'): @@ -73,19 +65,20 @@ class TeleniumTestProcess(TeleniumTestCase): try: if self.cli.getattr(selector, 'current') == value: self.assertTrue(selector, value) - break + return except TeleniumHttpException: sleep(0.1) continue finally: - # Finally Sleep is used to make the menu button funcationlly available for the click process. - # (because Transition is little bit slow) + # Finally Sleep is used to make the menu button functionally available for the click process. + # (because screen transition is little bit slow) sleep(0.2) + raise AssertionError("Timeout") def drag(self, xpath1, xpath2): """this method is for dragging""" self.cli.drag(xpath1, xpath2, 1) - self.cli.sleep(0.3) + self.cli.sleep(1) def assertCheckScrollDown(self, selector, timeout=-1): """this method is for checking scroll""" @@ -99,7 +92,7 @@ class TeleniumTestProcess(TeleniumTestCase): return False if timeout > 0 and time() - start > timeout: raise Exception("Timeout") - sleep(0.1) + sleep(0.5) def assertCheckScrollUp(self, selector, timeout=-1): """this method is for checking scroll UP""" @@ -113,15 +106,16 @@ class TeleniumTestProcess(TeleniumTestCase): return False if timeout > 0 and time() - start > timeout: raise Exception("Timeout") - sleep(0.1) + sleep(0.5) def open_side_navbar(self): """Common method for opening Side navbar (Side Drawer)""" # Checking the drawer is in 'closed' state + self.cli.execute('app.ContentNavigationDrawer.MDNavigationDrawer.opening_time=0') self.assertExists('//MDNavigationDrawer[@status~=\"closed\"]', timeout=5) # This is for checking the menu button is appeared - self.assertExists('//MDActionTopAppBarButton[@icon~=\"menu\"]', timeout=5) + self.assertExists('//ActionTopAppBarButton[@icon~=\"menu\"]', timeout=5) # this is for opening Nav drawer - self.cli.wait_click('//MDActionTopAppBarButton[@icon=\"menu\"]', timeout=5) + self.cli.wait_click('//ActionTopAppBarButton[@icon=\"menu\"]', timeout=5) # checking state of Nav drawer self.assertExists("//MDNavigationDrawer[@state~=\"open\"]", timeout=5) diff --git a/src/bitmessagekivy/tests/test_addressbook.py b/src/bitmessagekivy/tests/test_addressbook.py index af03ef3e..e61fef2a 100644 --- a/src/bitmessagekivy/tests/test_addressbook.py +++ b/src/bitmessagekivy/tests/test_addressbook.py @@ -16,7 +16,7 @@ class AddressBook(TeleniumTestProcess): test_subject = 'Test Subject' test_body = 'Hey,This is draft Message Body from Address Book' - @skip_screen_checks + # @skip_screen_checks @ordered def test_save_address(self): """Saving a new Address On Address Book Screen/Window""" @@ -28,83 +28,73 @@ class AddressBook(TeleniumTestProcess): self.drag("//NavigationItem[@text=\"Sent\"]", "//NavigationItem[@text=\"Inbox\"]") # assert for checking scroll function self.assertCheckScrollDown('//ContentNavigationDrawer//ScrollView[0]', timeout=5) - # this is for opening setting screen + # this is for opening addressbook screen self.cli.wait_click('//NavigationItem[@text=\"Address Book\"]', timeout=5) - # Checking current screen - self.assertExists("//ScreenManager[@current=\"addressbook\"]", timeout=5) # This is for checking the Side nav Bar is closed self.assertExists('//MDNavigationDrawer[@status~=\"closed\"]', timeout=5) + # Checking current screen + self.assertExists("//ScreenManager[@current=\"addressbook\"]", timeout=5) # Check for rendered button - self.assertExists('//MDActionTopAppBarButton[@icon=\"account-plus\"]', timeout=5) + self.assertExists('//ActionTopAppBarButton[@icon=\"account-plus\"]', timeout=5) # Click on "Account-Plus' Icon to open popup to save a new Address - self.cli.wait_click('//MDActionTopAppBarButton[@icon=\"account-plus\"]', timeout=5) + self.cli.wait_click('//ActionTopAppBarButton[@icon=\"account-plus\"]', timeout=5) # Checking the Label Field shows Validation for empty string - self.assertExists('//GrashofPopup/BoxLayout[0]/MDTextField[@hint_text=\"Label\"][@text=\"\"]', timeout=5) + self.assertExists('//AddAddressPopup/BoxLayout[0]/MDTextField[@hint_text=\"Label\"][@text=\"\"]', timeout=5) # Checking the Address Field shows Validation for empty string - self.assertExists('//GrashofPopup/BoxLayout[0]/MDTextField[@hint_text=\"Address\"][@text=\"\"]', timeout=5) - # Click On save Button to check Field validation - self.cli.wait_click('//MDRaisedButton[@text=\"Save\"]', timeout=5) + self.assertExists('//AddAddressPopup/BoxLayout[0]/MDTextField[@hint_text=\"Address\"][@text=\"\"]', timeout=5) # Add an address Label to label Field - self.cli.setattr('//GrashofPopup/BoxLayout[0]/MDTextField[@hint_text=\"Label\"]', 'text', self.test_label) + self.cli.setattr('//AddAddressPopup/BoxLayout[0]/MDTextField[@hint_text=\"Label\"]', 'text', self.test_label) # Checking the Label Field should not be empty self.assertExists( - '//GrashofPopup/BoxLayout[0]/MDTextField[0][@text=\"{}\"]'.format(self.test_label), timeout=2) - # Add incorrect Address to Address Field to check validation - self.cli.setattr( - '//GrashofPopup/BoxLayout[0]/MDTextField[@hint_text=\"Address\"]', - 'text', test_address['invalid_address']) - # Checking the Address Field should not be empty - self.assertExists( - '//GrashofPopup/BoxLayout[0]/MDTextField[1][@text=\"{}\"]'.format(test_address['invalid_address']), - timeout=2) + '//AddAddressPopup/BoxLayout[0]/MDTextField[0][@text=\"{}\"]'.format(self.test_label), timeout=5) # Add Correct Address self.cli.setattr( - '//GrashofPopup/BoxLayout[0]/MDTextField[@hint_text=\"Address\"]', 'text', + '//AddAddressPopup/BoxLayout[0]/MDTextField[@hint_text=\"Address\"]', 'text', test_address['autoresponder_address']) # Checking the Address Field contains correct address self.assertEqual( - self.cli.getattr('//GrashofPopup/BoxLayout[0]/MDTextField[1][@text]', 'text'), + self.cli.getattr('//AddAddressPopup/BoxLayout[0]/MDTextField[1][@text]', 'text'), test_address['autoresponder_address']) # Validating the Label field self.assertExists( - '//GrashofPopup/BoxLayout[0]/MDTextField[0][@text=\"{}\"]'.format(self.test_label), timeout=2) + '//AddAddressPopup/BoxLayout[0]/MDTextField[0][@text=\"{}\"]'.format(self.test_label), timeout=5) # Validating the Valid Address is entered self.assertExists( - '//GrashofPopup/BoxLayout[0]/MDTextField[1][@text=\"{}\"]'.format( - test_address['autoresponder_address']), timeout=3) - # Click on Save Button to save the address in address book - self.cli.wait_click('//MDRaisedButton[@text=\"Save\"]', timeout=2) - # Check Current Screen (Address Book) + '//AddAddressPopup/BoxLayout[0]/MDTextField[1][@text=\"{}\"]'.format( + test_address['autoresponder_address']), timeout=5) + # Checking cancel button + self.assertExists('//MDRaisedButton[@text=\"Cancel\"]', timeout=5) + # Click on 'Cancel' Button to dismiss the popup + self.cli.wait_click('//MDRaisedButton[@text=\"Cancel\"]', timeout=5) + # Checking current screen self.assertExists("//ScreenManager[@current=\"addressbook\"]", timeout=5) - # Checking new address should be added - self.assertExists('//SwipeToDeleteItem', timeout=5) @skip_screen_checks @ordered def test_dismiss_addressbook_popup(self): """This method is to perform Dismiss add Address popup""" # Checking the "Address saving" Popup is not opened - self.assertNotExists('//GrashofPopup', timeout=5) + self.assertNotExists('//AddAddressPopup', timeout=5) # Checking the "Add account" Button is rendered - self.assertExists('//MDActionTopAppBarButton[@icon=\"account-plus\"]', timeout=6) + self.assertExists('//ActionTopAppBarButton[@icon=\"account-plus\"]', timeout=6) # Click on Account-Plus Icon to open popup for add Address - self.cli.wait_click('//MDActionTopAppBarButton[@icon=\"account-plus\"]', timeout=5) + self.cli.wait_click('//ActionTopAppBarButton[@icon=\"account-plus\"]', timeout=5) # Add Label to label Field - self.cli.setattr('//GrashofPopup/BoxLayout[0]/MDTextField[0]', 'text', 'test_label2') + self.cli.setattr('//AddAddressPopup/BoxLayout[0]/MDTextField[0]', 'text', 'test_label2') # Checking the Label Field should not be empty self.assertExists( - '//GrashofPopup/BoxLayout[0]/MDTextField[0][@text=\"{}\"]'.format('test_label2'), timeout=2) + '//AddAddressPopup/BoxLayout[0]/MDTextField[0][@text=\"{}\"]'.format('test_label2'), timeout=5) # Add Address to Address Field self.cli.setattr( - '//GrashofPopup/BoxLayout[0]/MDTextField[1]', 'text', test_address['recipient']) + '//AddAddressPopup/BoxLayout[0]/MDTextField[1]', 'text', test_address['recipient']) # Checking the Address Field should not be empty self.assertExists( - '//GrashofPopup/BoxLayout[0]/MDTextField[1][@text=\"{}\"]'.format(test_address['recipient']), - timeout=2) + '//AddAddressPopup/BoxLayout[0]/MDTextField[1][@text=\"{}\"]'.format(test_address['recipient']), + timeout=5) # Checking for "Cancel" button is rendered self.assertExists('//MDRaisedButton[@text=\"Cancel\"]', timeout=5) # Click on 'Cancel' Button to dismiss the popup - self.cli.wait_click('//MDRaisedButton[@text=\"Cancel\"]', timeout=2) + self.cli.wait_click('//MDRaisedButton[@text=\"Cancel\"]', timeout=5) # Check Current Screen (Address Book) self.assertExists("//ScreenManager[@current=\"addressbook\"]", timeout=5) @@ -113,13 +103,13 @@ class AddressBook(TeleniumTestProcess): def test_send_message_to_saved_address(self): """This method is to send msg to the saved address from addressbook""" # Checking the Message detail Dialog box is not opened - self.assertNotExists('//MDDialog', timeout=3) + self.assertNotExists('//MDDialog', timeout=5) # Checking the saved address is rendered self.assertExists('//AddressBook/BoxLayout[0]//SwipeToDeleteItem[0]', timeout=5) # Click on a Address to open address Details popup self.cli.wait_click('//AddressBook/BoxLayout[0]//SwipeToDeleteItem[0]', timeout=5) # Checking the Message detail Dialog is opened - self.assertExists('//MDDialog', timeout=3) + self.assertExists('//MDDialog', timeout=5) # Checking the buttons are rendered self.assertExists('//MDRaisedButton', timeout=5) # Click on the Send to message Button @@ -142,9 +132,9 @@ class AddressBook(TeleniumTestProcess): # Method to open side navbar (written in telenium_process.py) self.open_side_navbar() # this is for opening setting screen - self.cli.wait_click('//NavigationItem[@text=\"Address Book\"]', timeout=2) + self.cli.wait_click('//NavigationItem[@text=\"Address Book\"]', timeout=5) # checking state of Nav drawer(closed) - self.assertExists("//MDNavigationDrawer[@state~=\"close\"]", timeout=2) + self.assertExists("//MDNavigationDrawer[@state~=\"close\"]", timeout=5) # Checking current screen self.assertExists("//ScreenManager[@current=\"addressbook\"]", timeout=8) # Checking the Address is rendered diff --git a/src/bitmessagekivy/tests/test_allmail_message.py b/src/bitmessagekivy/tests/test_allmail_message.py index 35359b92..b08e0f29 100644 --- a/src/bitmessagekivy/tests/test_allmail_message.py +++ b/src/bitmessagekivy/tests/test_allmail_message.py @@ -6,7 +6,7 @@ from .common import ordered class AllMailMessage(TeleniumTestProcess): """AllMail Screen Functionality Testing""" - @skip_screen_checks + # @skip_screen_checks @ordered def test_show_allmail_list(self): """Show All Messages on Mail Screen/Window""" @@ -16,6 +16,7 @@ class AllMailMessage(TeleniumTestProcess): self.open_side_navbar() # this is for opening All Mail screen self.cli.wait_click('//NavigationItem[@text=\"All Mails\"]', timeout=5) + self.assertExists('//MDNavigationDrawer[@status~=\"closed\"]', timeout=5) # Assert for checking Current Screen(All mail) self.assertExists("//ScreenManager[@current=\"allmails\"]", timeout=5) @@ -25,7 +26,7 @@ class AllMailMessage(TeleniumTestProcess): """Delete Message From Message body of Mail Screen/Window""" # click on a Message to get message details screen self.cli.wait_click( - '//MDList[0]/CustomSwipeToDeleteItem[0]', timeout=3) + '//MDList[0]/CustomSwipeToDeleteItem[0]', timeout=5) # Assert for checking Current Screen(Mail Detail) self.assertExists("//ScreenManager[@current=\"mailDetail\"]", timeout=5) # CLicking on Trash-Can icon to delete Message diff --git a/src/bitmessagekivy/tests/test_chat_screen.py b/src/bitmessagekivy/tests/test_chat_screen.py new file mode 100644 index 00000000..98464bf3 --- /dev/null +++ b/src/bitmessagekivy/tests/test_chat_screen.py @@ -0,0 +1,26 @@ +from .telenium_process import TeleniumTestProcess +from .common import ordered + + +class ChatScreen(TeleniumTestProcess): + """Chat Screen Functionality Testing""" + + @ordered + def test_open_chat_screen(self): + """Opening Chat screen""" + # Checking current Screen(Inbox screen) + self.assert_wait_no_except('//ScreenManager[@current]', timeout=15, value='inbox') + # Method to open side navbar + self.open_side_navbar() + # this is for scrolling Nav drawer + self.drag("//NavigationItem[@text=\"Sent\"]", "//NavigationItem[@text=\"Inbox\"]") + # assert for checking scroll function + self.assertCheckScrollDown('//ContentNavigationDrawer//ScrollView[0]', timeout=10) + # Checking Chat screen label on side nav bar + self.assertExists('//NavigationItem[@text=\"Chat\"]', timeout=5) + # this is for opening Chat screen + self.cli.wait_click('//NavigationItem[@text=\"Chat\"]', timeout=5) + # Checking navigation bar state + self.assertExists('//MDNavigationDrawer[@status~=\"closed\"]', timeout=5) + # Checking current screen + self.assertExists("//Chat[@name~=\"chat\"]", timeout=5) diff --git a/src/bitmessagekivy/tests/test_create_random_address.py b/src/bitmessagekivy/tests/test_create_random_address.py index 3f5e7b67..691a9d46 100644 --- a/src/bitmessagekivy/tests/test_create_random_address.py +++ b/src/bitmessagekivy/tests/test_create_random_address.py @@ -1,20 +1,19 @@ -from time import time +""" + Test for creating new identity +""" + from random import choice from string import ascii_lowercase -from telenium.client import TeleniumHttpException from .telenium_process import TeleniumTestProcess -from .common import skip_screen_checks from .common import ordered class CreateRandomAddress(TeleniumTestProcess): """This is for testing randrom address creation""" - @staticmethod def populate_test_data(): pass - @skip_screen_checks @ordered # This method tests the landing screen when the app runs first time and # the landing screen should be "login" where we can create new address @@ -22,45 +21,61 @@ class CreateRandomAddress(TeleniumTestProcess): """Click on Proceed Button to Proceed to Next Screen.""" # Checking current Screen(Login screen) self.assert_wait_no_except('//ScreenManager[@current]', timeout=15, value='login') - # Click on "Proceed Next" Button to "generate random label for address" screen - # Some widgets cannot renders suddenly and become not functional so we used loop with a timeout. - start = time() - deadline = start + 2 - while time() < deadline: - try: - # Clicking on Proceed Next Button to redirect to "random" screen - self.cli.wait_click('//Screen[0]//MDFillRoundFlatIconButton[@text=\"Proceed Next\"]', timeout=5) - except TeleniumHttpException: - # Checking Current Screen(Random Screen) after Clicking on "Proceed Next" Button - self.assertExists("//ScreenManager[@current=\"random\"]", timeout=5) + # Dragging from sent to PROS: to NOTE: + self.drag( + '''//Login//Screen//ContentHead[1][@section_name=\"PROS:\"]''', + '''//Login//Screen//ContentHead[0][@section_name=\"NOTE:\"]''' + ) + # Assert the checkbox is rendered + self.assertExists( + '//Login//Screen[@name=\"check_screen\"]//AnchorLayout[1]/Check[@active=false]', timeout=5 + ) + # Clicking on the checkbox + self.cli.wait_click( + '//Login//Screen[@name=\"check_screen\"]//AnchorLayout[1]/Check', timeout=5 + ) + # Checking Status of checkbox after click + self.assertExists( + '//Login//Screen[@name=\"check_screen\"]//AnchorLayout[1]/Check[@active=true]', timeout=5 + ) + # Checking the Proceed Next button is rendered or not + self.assertExists( + '''//Login//Screen[@name=\"check_screen\"]''' + '''//MDFillRoundFlatIconButton[@text=\"Proceed Next\"]''', timeout=5 + ) + # Clicking on Proceed Next Button to redirect to "random" screen + self.cli.wait_click( + '''//Login//Screen[@name=\"check_screen\"]''' + '''//MDFillRoundFlatIconButton[@text=\"Proceed Next\"]''', timeout=5 + ) self.assertExists("//ScreenManager[@current=\"random\"]", timeout=5) - @skip_screen_checks @ordered def test_generate_random_address_label(self): """Creating New Adress For New User.""" # Checking the Button is rendered self.assertExists( - '//RandomBoxlayout/BoxLayout[0]/AnchorLayout[1]/MDTextField[@hint_text=\"Label\"]', timeout=2) + '//Random//RandomBoxlayout//MDTextField[@hint_text=\"Label\"]', timeout=5) # Click on Label Text Field to give address Label self.cli.wait_click( - '//RandomBoxlayout/BoxLayout[0]/AnchorLayout[1]/MDTextField[@hint_text=\"Label\"]', timeout=2) + '//Random//RandomBoxlayout//MDTextField[@hint_text=\"Label\"]', timeout=5) # Enter a Label Randomly random_label = "" for _ in range(10): random_label += choice(ascii_lowercase) - self.cli.setattr('//RandomBoxlayout//AnchorLayout[1]/MDTextField[0]', "text", random_label) + self.cli.setattr('//Random//MDTextField[0]', "text", random_label) self.cli.sleep(0.1) # Checking the Button is rendered - self.assertExists('//RandomBoxlayout//MDFillRoundFlatIconButton[@text=\"Proceed Next\"]', timeout=3) + self.assertExists( + '//Random//RandomBoxlayout//MDFillRoundFlatIconButton[@text=\"Proceed Next\"]', timeout=5) # Click on Proceed Next button to generate random Address - self.cli.wait_click('//RandomBoxlayout//MDFillRoundFlatIconButton[@text=\"Proceed Next\"]', timeout=3) + self.cli.wait_click( + '//Random//RandomBoxlayout//MDFillRoundFlatIconButton[@text=\"Proceed Next\"]', timeout=5) # Checking "My Address" Screen after creating a address self.assertExists("//ScreenManager[@current=\"myaddress\"]", timeout=5) # Checking the new address is created - self.assertExists('//MDList[0]/CustomTwoLineAvatarIconListItem', timeout=10) + self.assertExists('//MyAddress//MDList[0]/CustomTwoLineAvatarIconListItem', timeout=10) - @skip_screen_checks @ordered def test_set_default_address(self): """Select First Address From Drawer-Box""" @@ -69,13 +84,46 @@ class CreateRandomAddress(TeleniumTestProcess): # This is for opening side navbar self.open_side_navbar() # Click to open Address Dropdown - self.cli.wait_click('//NavigationItem[0]/CustomSpinner[0]', timeout=5) - # Checking the dropdown option is exist - self.assertExists('//MySpinnerOption[0]', timeout=5) - is_open = self.cli.getattr('//NavigationItem[0]/CustomSpinner[@is_open]', 'is_open') + self.assertExists('//NavigationItem[0][@text=\"dropdown_nav_item\"]', timeout=5) + self.assertExists( + '//NavigationItem[0][@text=\"dropdown_nav_item\"]' + '/IdentitySpinner[@name=\"identity_dropdown\"]', timeout=5 + ) + self.assertExists( + '//NavigationItem[0][@text=\"dropdown_nav_item\"]' + '/IdentitySpinner[@name=\"identity_dropdown\"][@is_open=false]', timeout=5 + ) + self.cli.wait( + '//NavigationItem[0][@text=\"dropdown_nav_item\"]' + '/IdentitySpinner[@name=\"identity_dropdown\"][@state=\"normal\"]', timeout=5 + ) + # Click to open Address Dropdown + self.cli.wait_click( + '//NavigationItem[0][@text=\"dropdown_nav_item\"]' + '/IdentitySpinner[@name=\"identity_dropdown\"]', timeout=5 + ) + self.cli.wait( + '//NavigationItem[0][@text=\"dropdown_nav_item\"]' + '/IdentitySpinner[@name=\"identity_dropdown\"][@state=\"normal\"]', timeout=5 + ) # Check the state of dropdown. - self.assertEqual(is_open, True) + self.assertExists( + '//NavigationItem[0][@text=\"dropdown_nav_item\"]' + '/IdentitySpinner[@name=\"identity_dropdown\"][@is_open=true]', timeout=5 + ) + # List of addresses + addresses_in_dropdown = self.cli.getattr( + '//NavigationItem[0][@text=\"dropdown_nav_item\"]/IdentitySpinner[@values]', 'values' + ) + # Checking the dropdown options are exists + self.assertGreaterEqual(len(self.cli.getattr( + '//MySpinnerOption[@text]', 'text')), len(addresses_in_dropdown) + ) # Selection of an address to set as a default address. self.cli.wait_click('//MySpinnerOption[0]', timeout=5) - # Checking current screen - self.assertExists("//ScreenManager[@current=\"inbox\"]", timeout=5) + + self.assertExists( + '//NavigationItem[0][@text=\"dropdown_nav_item\"]' + '/IdentitySpinner[@name=\"identity_dropdown\"][@is_open=false]', timeout=5 + ) + self.cli.sleep(0.5) diff --git a/src/bitmessagekivy/tests/test_draft_message.py b/src/bitmessagekivy/tests/test_draft_message.py index 23844640..e876f418 100644 --- a/src/bitmessagekivy/tests/test_draft_message.py +++ b/src/bitmessagekivy/tests/test_draft_message.py @@ -12,6 +12,20 @@ class DraftMessage(TeleniumTestProcess): test_subject = 'Test Subject text' test_body = 'Hey, This is draft Message Body' + @ordered + def test_draft_screen(self): + """Test draft screen is open""" + # Checking current Screen(Inbox screen) + self.assert_wait_no_except('//ScreenManager[@current]', timeout=15, value='inbox') + # Method to open side navbar + self.open_side_navbar() + # this is for opening Draft screen + self.cli.wait_click('//NavigationItem[@text=\"Draft\"]', timeout=5) + # Checking the drawer is in 'closed' state + self.assertExists('//MDNavigationDrawer[@status~=\"closed\"]', timeout=5) + # Checking Draft Screen + self.assertExists("//ScreenManager[@current=\"draft\"]", timeout=5) + @skip_screen_checks @ordered def test_save_message_to_draft(self): @@ -56,9 +70,9 @@ class DraftMessage(TeleniumTestProcess): # Checking Receiver Address filled or not self.assertExists( '//DropDownWidget/ScrollView[0]//MyTextInput[@text=\"{}\"]'.format(test_address['auto_responder']), - timeout=2) + timeout=5) # Checking the sender's Field is empty - self.assertExists('//DropDownWidget/ScrollView[0]//BoxLayout[1]/MDTextField[@text=\"\"]', timeout=3) + self.assertExists('//DropDownWidget/ScrollView[0]//BoxLayout[1]/MDTextField[@text=\"\"]', timeout=5) # Assert to check Sender's address dropdown open or not self.assertEqual(self.cli.getattr('//Create//CustomSpinner[@is_open]', 'is_open'), False) # Open Sender's Address DropDown @@ -74,7 +88,7 @@ class DraftMessage(TeleniumTestProcess): # Checking sender address is selected sender_address = self.cli.getattr('//DropDownWidget/ScrollView[0]//BoxLayout[1]/MDTextField[@text]', 'text') self.assertExists( - '//DropDownWidget/ScrollView[0]//BoxLayout[1]/MDTextField[@text=\"{}\"]'.format(sender_address), timeout=3) + '//DropDownWidget/ScrollView[0]//BoxLayout[1]/MDTextField[@text=\"{}\"]'.format(sender_address), timeout=5) # CLICK BACK-BUTTON self.cli.wait_click('//MDToolbar/BoxLayout[0]/MDActionTopAppBarButton[@icon=\"arrow-left\"]', timeout=5) # Checking current screen(Login) after "BACK" Press @@ -90,11 +104,11 @@ class DraftMessage(TeleniumTestProcess): # Checking messages in draft box self.assertEqual(len(self.cli.select('//SwipeToDeleteItem[0]//TwoLineAvatarIconListItem')), 1) # Wait to render the widget - self.cli.wait('//SwipeToDeleteItem[0]//TwoLineAvatarIconListItem[0]', timeout=3) + self.cli.wait('//SwipeToDeleteItem[0]//TwoLineAvatarIconListItem[0]', timeout=5) # Click on a Message to view its details (Message Detail screen) self.cli.wait_click('//SwipeToDeleteItem[0]//TwoLineAvatarIconListItem[0]', timeout=5) # Checking current screen Mail Detail - self.assertExists("//ScreenManager[@current=\"mailDetail\"]", timeout=3) + self.assertExists("//ScreenManager[@current=\"mailDetail\"]", timeout=5) # CLICK on EDIT(Pencil) BUTTON self.cli.wait_click('//MDToolbar/BoxLayout[2]/MDActionTopAppBarButton[@icon=\"pencil\"]', timeout=5) @@ -103,11 +117,11 @@ class DraftMessage(TeleniumTestProcess): # Checking the recipient is in the receiver field self.assertExists( '//DropDownWidget/ScrollView[0]//MyTextInput[@text=\"{}\"]'.format(test_address['auto_responder']), - timeout=2) + timeout=5) # Checking the sender address is in the sender field sender_address = self.cli.getattr('//DropDownWidget/ScrollView[0]//BoxLayout[1]/MDTextField[@text]', 'text') self.assertExists( - '//DropDownWidget/ScrollView[0]//BoxLayout[1]/MDTextField[@text=\"{}\"]'.format(sender_address), timeout=3) + '//DropDownWidget/ScrollView[0]//BoxLayout[1]/MDTextField[@text=\"{}\"]'.format(sender_address), timeout=5) # Checking the subject text is in the subject field self.assertExists( '//DropDownWidget/ScrollView[0]//MyMDTextField[@text=\"{}\"]'.format(self.test_subject), timeout=5) diff --git a/src/bitmessagekivy/tests/test_filemanager.py b/src/bitmessagekivy/tests/test_filemanager.py new file mode 100644 index 00000000..6d25553c --- /dev/null +++ b/src/bitmessagekivy/tests/test_filemanager.py @@ -0,0 +1,68 @@ +from .telenium_process import TeleniumTestProcess +from .common import ordered + + +class FileManagerOpening(TeleniumTestProcess): + """File-manager Opening Functionality Testing""" + @ordered + def test_open_file_manager(self): + """Opening and Closing File-manager""" + # Checking current Screen(Inbox screen) + self.assert_wait_no_except('//ScreenManager[@current]', timeout=15, value='inbox') + # Method to open side navbar + self.open_side_navbar() + # Click to open Address Dropdown + self.assertExists('//NavigationItem[0][@text=\"dropdown_nav_item\"]', timeout=5) + self.assertExists( + '//NavigationItem[0][@text=\"dropdown_nav_item\"]' + '/IdentitySpinner[@name=\"identity_dropdown\"]', timeout=1 + ) + # Check the state of dropdown + self.assertExists( + '//NavigationItem[0][@text=\"dropdown_nav_item\"]' + '/IdentitySpinner[@name=\"identity_dropdown\"][@is_open=false]', timeout=1 + ) + self.cli.wait( + '//NavigationItem[0][@text=\"dropdown_nav_item\"]' + '/IdentitySpinner[@name=\"identity_dropdown\"][@state=\"normal\"]', timeout=5 + ) + self.cli.wait_click( + '//NavigationItem[0][@text=\"dropdown_nav_item\"]' + '/IdentitySpinner[@name=\"identity_dropdown\"]', timeout=1 + ) + # Check the state of dropdown. + self.assertExists( + '//NavigationItem[0][@text=\"dropdown_nav_item\"]' + '/IdentitySpinner[@name=\"identity_dropdown\"][@is_open=true]', timeout=1 + ) + # List of addresses + addresses_in_dropdown = self.cli.getattr( + '//NavigationItem[0][@text=\"dropdown_nav_item\"]/IdentitySpinner[@values]', 'values' + ) + # Checking the dropdown options are exists + self.assertGreaterEqual(len(self.cli.getattr( + '//MySpinnerOption[@text]', 'text')), len(addresses_in_dropdown) + ) + # Selection of an address to set as a default address. + self.cli.wait_click('//MySpinnerOption[0]', timeout=5) + # this is for scrolling Nav drawer + self.drag("//NavigationItem[@text=\"Sent\"]", "//NavigationItem[@text=\"Inbox\"]") + # assert for checking scroll function + self.assertCheckScrollDown('//ContentNavigationDrawer//ScrollView[0]', timeout=5) + # checking state of Nav drawer + self.assertExists("//MDNavigationDrawer[@state~=\"open\"]", timeout=5) + # Checking File-manager icon + self.assertExists( + '//ContentNavigationDrawer//MDIconButton[1][@icon=\"file-image\"]', + timeout=5 + ) + # Clicking on file manager icon + self.cli.wait_click( + '//ContentNavigationDrawer//MDIconButton[1][@icon=\"file-image\"]', + timeout=5) + # Checking the state of file manager is it open or not + self.assertTrue(self.cli.execute('app.file_manager_open')) + # Closing the filemanager + self.cli.execute('app.exit_manager()') + # Checking the state of file manager is it closed or not + self.assertTrue(self.cli.execute('app.exit_manager()')) diff --git a/src/bitmessagekivy/tests/test_load_screen_data_file.py b/src/bitmessagekivy/tests/test_load_screen_data_file.py new file mode 100644 index 00000000..619daf25 --- /dev/null +++ b/src/bitmessagekivy/tests/test_load_screen_data_file.py @@ -0,0 +1,21 @@ + +import unittest +from pybitmessage.bitmessagekivy.load_kivy_screens_data import load_screen_json +from .common import ordered + + +class TestLoadScreenData(unittest.TestCase): + """Screen Data Json test""" + + @ordered + def test_load_json(self): + """Test to load a valid json""" + loaded_screen_names = load_screen_json() + self.assertEqual(loaded_screen_names[3], 'success') + + @ordered + def test_load_invalid_file(self): + """Test to load an invalid json""" + file_name = 'invalid_screens_data.json' + with self.assertRaises(OSError): + load_screen_json(file_name) diff --git a/src/bitmessagekivy/tests/test_myaddress_screen.py b/src/bitmessagekivy/tests/test_myaddress_screen.py new file mode 100644 index 00000000..78740b79 --- /dev/null +++ b/src/bitmessagekivy/tests/test_myaddress_screen.py @@ -0,0 +1,187 @@ +from .telenium_process import TeleniumTestProcess +from .common import ordered + + +data = [ + 'BM-2cWmjntZ47WKEUtocrdvs19y5CivpKoi1h', + 'BM-2cVpswZo8rWLXDVtZEUNcDQvnvHJ6TLRYr' +] + + +class MyAddressScreen(TeleniumTestProcess): + """MyAddress Screen Functionality Testing""" + @ordered + def test_myaddress_screen(self): + """Open MyAddress Screen""" + # Checking current Screen(Inbox screen) + self.assert_wait_no_except('//ScreenManager[@current]', timeout=15, value='inbox') + # Method to open side navbar + self.open_side_navbar() + # this is for scrolling Nav drawer + self.drag("//NavigationItem[@text=\"Sent\"]", "//NavigationItem[@text=\"Inbox\"]") + # assert for checking scroll function + self.assertCheckScrollDown('//ContentNavigationDrawer//ScrollView[0]', timeout=10) + # Checking My address label on side nav bar + self.assertExists('//NavigationItem[@text=\"My addresses\"]', timeout=5) + # this is for opening setting screen + self.cli.wait_click('//NavigationItem[@text=\"My addresses\"]', timeout=5) + self.assertExists('//MDNavigationDrawer[@status~=\"closed\"]', timeout=5) + # Checking current screen + self.assertExists("//MyAddress[@name~=\"myaddress\"]", timeout=5) + + @ordered + def test_disable_address(self): + """Disable Addresses""" + # Dragging for loading addreses + self.drag( + '//MyAddress//MDList[0]/CustomTwoLineAvatarIconListItem[@text=\"test2\"]', + '//MyAddress//MDList[0]/CustomTwoLineAvatarIconListItem[@text=\"test1\"]' + ) + self.assertCheckScrollDown('//ContentNavigationDrawer//ScrollView[0]', timeout=10) + # Checking list of Addresses + self.assertExists("//MyAddress//CustomTwoLineAvatarIconListItem", timeout=5) + # Checking the Toggle button is rendered on addresses + self.assertExists("//MyAddress//CustomTwoLineAvatarIconListItem//ToggleBtn", timeout=5) + # Clicking on the Toggle button of first address to make it disable + self.assertExists( + "//MyAddress//CustomTwoLineAvatarIconListItem[@text=\"test2\"]//ToggleBtn[@active=true]", + timeout=5 + ) + # Clicking on Toggle button of first address + self.cli.wait_click( + "//MyAddress//CustomTwoLineAvatarIconListItem[@text=\"test2\"]//ToggleBtn/Thumb", + timeout=5 + ) + # Checking the address is disabled + self.assertExists( + "//MyAddress//CustomTwoLineAvatarIconListItem[@text=\"test2\"]//ToggleBtn[@active=false]", + timeout=5 + ) + # CLICKING ON DISABLE ACCOUNT TO OPEN POPUP + self.cli.wait_click("//MyAddress//CustomTwoLineAvatarIconListItem[@text=\"test2\"]", timeout=5) + # Checking the popup is Opened + self.assertExists( + '//MDDialog[@text=\"Address is not currently active. Please click on Toggle button to active it.\"]', + timeout=5 + ) + # Clicking on 'Ok' Button To Dismiss the popup + self.cli.wait_click('//MDFlatButton[@text=\"Ok\"]', timeout=5) + # Checking the address is disabled + self.assertExists( + "//MyAddress//CustomTwoLineAvatarIconListItem[@text=\"test2\"]//ToggleBtn[@active=false]", + timeout=5 + ) + + @ordered + def test_show_Qrcode(self): + """Show the Qr code of selected address""" + # Checking the current screen is MyAddress + self.assertExists("//MyAddress[@name~=\"myaddress\"]", timeout=5) + # Checking first label + self.assertExists( + '//MyAddress//MDList[0]/CustomTwoLineAvatarIconListItem[1][@text=\"test1\"]', + timeout=5 + ) + # Checking second label + self.assertExists( + '//MyAddress//MDList[0]/CustomTwoLineAvatarIconListItem[0][@text=\"test2\"]', + timeout=5 + ) + self.assertExists( + "//MyAddress//CustomTwoLineAvatarIconListItem[@text=\"test2\"]//ToggleBtn[@active=false]", + timeout=5 + ) + self.assertExists( + "//MyAddress//CustomTwoLineAvatarIconListItem[@text=\"test1\"]//ToggleBtn[@active=true]", + timeout=5 + ) + # Click on Address to open popup + self.cli.wait_click('//MDList[0]/CustomTwoLineAvatarIconListItem[@text=\"test1\"]', timeout=5) + # Check the Popup is opened + self.assertExists('//MyaddDetailPopup//MDLabel[@text=\"Show QR code\"]', timeout=5) + # Cick on 'Show QR code' button to view QR Code + self.cli.wait_click('//MyaddDetailPopup//MDLabel[@text=\"Show QR code\"]', timeout=5) + # Check Current screen is QR Code screen + self.assertExists("//ShowQRCode[@name~=\"showqrcode\"]", timeout=5) + # Check BACK button + self.assertExists('//ActionTopAppBarButton[@icon~=\"arrow-left\"]', timeout=5) + # Click on BACK button + self.cli.wait_click('//ActionTopAppBarButton[@icon~=\"arrow-left\"]', timeout=5) + # Checking current screen(My Address) after BACK press + self.assertExists("//MyAddress[@name~=\"myaddress\"]", timeout=5) + # Checking first label + self.assertExists( + '//MyAddress//MDList[0]/CustomTwoLineAvatarIconListItem[1][@text=\"test1\"]', + timeout=5 + ) + # Checking second label + self.assertExists( + '//MyAddress//MDList[0]/CustomTwoLineAvatarIconListItem[0][@text=\"test2\"]', + timeout=5 + ) + self.cli.sleep(0.3) + + @ordered + def test_enable_address(self): + """Test to enable the disabled address""" + # Checking list of Addresses + self.assertExists("//MyAddress//CustomTwoLineAvatarIconListItem", timeout=5) + # Check the thumb button on address + self.assertExists( + "//MyAddress//CustomTwoLineAvatarIconListItem[@text=\"test2\"]//ToggleBtn/Thumb", + timeout=5 + ) + # Checking the address is disabled + self.assertExists( + "//MyAddress//CustomTwoLineAvatarIconListItem[@text=\"test2\"]//ToggleBtn[@active=false]", + timeout=5 + ) + self.cli.sleep(0.3) + # Clicking on toggle button to enable the address + self.cli.wait_click( + "//MyAddress//CustomTwoLineAvatarIconListItem[@text=\"test2\"]//ToggleBtn/Thumb", + timeout=10 + ) + # Checking the address is enabled + self.assertExists( + "//MyAddress//CustomTwoLineAvatarIconListItem[@text=\"test2\"]//ToggleBtn[@active=true]", + timeout=10 + ) + + @ordered + def test_send_message_from(self): + """Send Message From Send Message From Button""" + # this is for scrolling Myaddress screen + self.drag( + '//MyAddress//MDList[0]/CustomTwoLineAvatarIconListItem[@text=\"test2\"]', + '//MyAddress//MDList[0]/CustomTwoLineAvatarIconListItem[@text=\"test1\"]' + ) + # Checking the addresses + self.assertExists( + '//MyAddress//MDList[0]/CustomTwoLineAvatarIconListItem[@text=\"test1\"]', + timeout=5 + ) + # Click on Address to open popup + self.cli.wait_click('//MDList[0]/CustomTwoLineAvatarIconListItem[@text=\"test1\"]', timeout=5) + # Checking Popup Opened + self.assertExists('//MyaddDetailPopup//MDLabel[@text=\"Send message from\"]', timeout=5) + # Click on Send Message Button to redirect Create Screen + self.cli.wait_click('//MyaddDetailPopup//MDRaisedButton[0]/MDLabel[0]', timeout=5) + # Checking Current screen(Create) + self.assertExists("//Create[@name~=\"create\"]", timeout=5) + # Entering Receiver Address + self.cli.setattr( + '//DropDownWidget/ScrollView[0]//MyTextInput[0]', "text", data[1]) + # Checking Receiver Address filled or not + self.assertNotEqual('//DropDownWidget//MyTextInput[0]', '') + # ADD SUBJECT + self.cli.setattr('//DropDownWidget/ScrollView[0]//MyMDTextField[0]', 'text', 'Hey this is Demo Subject') + # Checking Subject Field is Entered + self.assertNotEqual('//DropDownWidget/ScrollView[0]//MyMDTextField[0]', '') + # ADD MESSAGE BODY + self.cli.setattr( + '//DropDownWidget/ScrollView[0]//ScrollView[0]/MDTextField[0]', + 'text', 'Hey,i am sending message directly from MyAddress book' + ) + # Checking Message body is Entered + self.assertNotEqual('//DropDownWidget/ScrollView[0]//ScrollView[0]/MDTextField[@text]', '') diff --git a/src/bitmessagekivy/tests/test_network_screen.py b/src/bitmessagekivy/tests/test_network_screen.py index 6e073be0..ca398dc1 100644 --- a/src/bitmessagekivy/tests/test_network_screen.py +++ b/src/bitmessagekivy/tests/test_network_screen.py @@ -1,35 +1,50 @@ # pylint: disable=too-few-public-methods +""" + Kivy Networkstat UI test +""" from .telenium_process import TeleniumTestProcess -from .common import skip_screen_checks class NetworkStatusScreen(TeleniumTestProcess): """NetworkStatus Screen Functionality Testing""" - @skip_screen_checks def test_network_status(self): """Show NetworkStatus""" # This is for checking Current screen self.assert_wait_no_except('//ScreenManager[@current]', timeout=15, value='inbox') # Method to open side navbar + # due to rapid transition effect, it doesn't click on menu-bar self.open_side_navbar() # this is for scrolling Nav drawer self.drag("//NavigationItem[@text=\"Sent\"]", "//NavigationItem[@text=\"Inbox\"]") # assert for checking scroll function self.assertCheckScrollDown('//ContentNavigationDrawer//ScrollView[0]', timeout=5) # Clicking on Network Status tab - self.cli.wait_click('//NavigationItem[@text=\"Network status\"]', timeout=5) - # checking current screen - self.assertExists("//ScreenManager[@current=\"networkstat\"]", timeout=5) - # Checking the state of "Total Connection" tab + self.cli.wait_click('//NavigationItem[@text=\"Network status\"]', timeout=2) + # Checking the drawer is in 'closed' state + self.assertExists('//MDNavigationDrawer[@status~=\"closed\"]', timeout=5) + # Checking for current screen (Network Status) + self.assertExists("//NetworkStat[@name~=\"networkstat\"]", timeout=2) + # Checking state of Total Connections tab self.assertExists( - '//NetworkStat/MDTabs[0]//MDTabsLabel[@text=\"Total connections\"][@state=\"down\"]', timeout=3) - # Checking the state of "Processes" tab - self.assertExists('//NetworkStat/MDTabs[0]//MDTabsLabel[@text=\"Processes\"][@state=\"normal\"]', timeout=3) - # Checking the "Tab" is rendered - self.assertExists('//NetworkStat/MDTabs[0]//MDTabsLabel[@text=\"Processes\"]', timeout=4) + '//NetworkStat/MDTabs[0]//MDTabsLabel[@text=\"Total connections\"][@state=\"down\"]', timeout=5 + ) + # Getting the value of total connections + total_connection_text = self.cli.getattr('//NetworkStat//MDRaisedButton[@text]', 'text') + # Splitting the string from total connection numbers + number_of_connections = int(total_connection_text.split(' ')[-1]) + # Checking Total connections + self.assertGreaterEqual(number_of_connections, 1) + # Checking the state of Process tab + self.assertExists( + '//NetworkStat/MDTabs[0]//MDTabsLabel[@text=\"Processes\"][@state=\"normal\"]', timeout=5 + ) # Clicking on Processes Tab - self.cli.wait_click('//NetworkStat/MDTabs[0]//MDTabsLabel[@text=\"Processes\"]', timeout=4) - # Checking the state of "Processes" tab - self.assertExists('//NetworkStat/MDTabs[0]//MDTabsLabel[@text=\"Processes\"][@state=\"normal\"]', timeout=3) + self.cli.wait_click( + '//NetworkStat/MDTabs[0]//MDTabsLabel[@text=\"Processes\"]', timeout=1 + ) + # Checking the state of Process tab + self.assertExists( + '//NetworkStat/MDTabs[0]//MDTabsLabel[@text=\"Processes\"][@state=\"down\"]', timeout=5 + ) diff --git a/src/bitmessagekivy/tests/test_payment_subscription.py b/src/bitmessagekivy/tests/test_payment_subscription.py index 22cfe844..42309001 100644 --- a/src/bitmessagekivy/tests/test_payment_subscription.py +++ b/src/bitmessagekivy/tests/test_payment_subscription.py @@ -1,13 +1,13 @@ from .telenium_process import TeleniumTestProcess -from .common import skip_screen_checks +from .common import ordered class PaymentScreen(TeleniumTestProcess): - """SubscriptionPayment Screen Functionality Testing""" + """Payment Plan Screen Functionality Testing""" - @skip_screen_checks - def test_select_subscription(self): - """Select Subscription From List of Subscriptions""" + @ordered + def test_select_payment_plan(self): + """Select Payment plan From List of payments""" # This is for checking Current screen self.assert_wait_no_except('//ScreenManager[@current]', timeout=15, value='inbox') # Method to open the side navbar @@ -15,31 +15,56 @@ class PaymentScreen(TeleniumTestProcess): # Dragging from sent to inbox to get Payment tab self.drag("//NavigationItem[@text=\"Sent\"]", "//NavigationItem[@text=\"Inbox\"]") # assert for checking scroll function - self.assertCheckScrollDown('//ContentNavigationDrawer//ScrollView[0]', timeout=3) + self.assertCheckScrollDown('//ContentNavigationDrawer//ScrollView[0]', timeout=10) # this is for opening Payment screen - self.cli.wait_click('//NavigationItem[@text=\"Purchase\"]', timeout=5) + self.cli.wait_click('//NavigationItem[@text=\"Payment plan\"]', timeout=5) + # Checking the navbar is in closed state + self.assertExists('//MDNavigationDrawer[@status~=\"closed\"]', timeout=5) # Assert for checking Current Screen self.assertExists("//ScreenManager[@current=\"payment\"]", timeout=5) - # Scrolling Down Product list + # Checking state of Current tab Payment plan + self.assertExists( + '//Payment/MDTabs[0]//MDTabsLabel[@text=\"Payment\"][@state=\"down\"]', timeout=5 + ) + # Scrolling Down Payment plan Cards self.drag( - '//ProductCategoryLayout[0]/ProductLayout[0]', - '//ProductCategoryLayout[0]/ProductLayout[1]') - # assert for checking scroll function - self.assertCheckScrollDown('//Payment//ScrollView[0]', timeout=3) - # Scrolling Up Product list - self.drag( - '//ProductCategoryLayout[0]/ProductLayout[1]', - '//ProductCategoryLayout[0]/ProductLayout[0]') - # assert for checking scroll function - self.assertCheckScrollDown('//Payment//ScrollView[0]', timeout=3) - # Click on BUY Button - self.cli.wait_click('//MDRaisedButton[@text=\"BUY\"]', timeout=2) - # self.assertEqual('//PaymentMethodLayout[@disabled]', 'True') #Returns None when condition True + '//Payment//MDTabs[0]//MDCard[2]//MDLabel[@text=\"Standard\"]', + '//Payment//MDTabs[0]//MDCard[1]//MDLabel[@text=\"You can get zero encrypted message per month\"]' + ) + # Checking the subscription offer cards + self.assertExists( + '//Payment/MDTabs[0]//MDCard[3]//MDLabel[@text=\"Premium\"]', + timeout=10 + ) + # Checking the get it now button + self.assertExists( + '//Payment/MDTabs[0]//MDCard[3]//MDRaisedButton[@text=\"Get it now\"]', + timeout=10 + ) + # Clicking on the get it now button + self.cli.wait_click( + '//Payment/MDTabs[0]//MDCard[3]//MDRaisedButton[@text=\"Get it now\"]', + timeout=10 + ) + # Checking the Payment method popup + self.assertExists('//PaymentMethodLayout//ScrollView[0]//ListItemWithLabel[0]', timeout=10) # CLick on the Payment Method - self.cli.click_on('//ScrollView[0]/ListItemWithLabel[0]') + self.cli.wait_click( + '//PaymentMethodLayout//ScrollView[0]//ListItemWithLabel[0]', + timeout=10 + ) # Check pop up is opened - self.assertTrue('//PaymentMethodLayout[@disabled]', 'False') + self.assertExists('//PaymentMethodLayout[@disabled=false]', timeout=10) # Click out side to dismiss the popup - self.cli.wait_click('//MDRaisedButton[3]', timeout=2) - # Checking Current screen(Payment screen) - self.assertExists("//ScreenManager[@current=\"payment\"]", timeout=3) + self.cli.wait_click('//MDRaisedButton[1]', timeout=10) + # Checking state of next tab Payment + self.assertExists( + '//Payment/MDTabs[0]//MDTabsLabel[@text=\"Extra-Messages\"][@state=\"normal\"]', timeout=5 + ) + # Clicking on Payment tab + self.cli.wait_click('//Payment/MDTabs[0]//MDTabsLabel[@text=\"Extra-Messages\"]', timeout=5) + # Checking state of payment tab after click + self.assertExists( + '//Payment/MDTabs[0]//MDTabsLabel[@text=\"Extra-Messages\"][@state=\"down\"]', timeout=5 + ) + self.cli.sleep(1) diff --git a/src/bitmessagekivy/tests/test_sent_message.py b/src/bitmessagekivy/tests/test_sent_message.py index 3e9f450e..a7fd576b 100644 --- a/src/bitmessagekivy/tests/test_sent_message.py +++ b/src/bitmessagekivy/tests/test_sent_message.py @@ -20,17 +20,17 @@ class SendMessage(TeleniumTestProcess): # Checking current Screen(Inbox screen) self.assert_wait_no_except('//ScreenManager[@current]', timeout=10, value='inbox') # Click on Composer Icon(Plus icon) - self.cli.wait_click('//ComposerButton[0]/MDFloatingActionButton[@icon=\"plus\"]', timeout=2) + self.cli.wait_click('//ComposerButton[0]/MDFloatingActionButton[@icon=\"plus\"]', timeout=5) # Checking Message Composer Screen(Create) - self.assertExists("//ScreenManager[@current=\"create\"]", timeout=4) + self.assertExists("//ScreenManager[@current=\"create\"]", timeout=5) # Checking State of Sender's Address Input Field (should be Empty) - self.assertExists('//DropDownWidget/ScrollView[0]//MDTextField[@text=\"\"]', timeout=3) + self.assertExists('//DropDownWidget/ScrollView[0]//MDTextField[@text=\"\"]', timeout=5) # Checking State of Receiver's Address Input Field (should be Empty) - self.assertExists('//DropDownWidget/ScrollView[0]//MyTextInput[@text=\"\"]', timeout=2) + self.assertExists('//DropDownWidget/ScrollView[0]//MyTextInput[@text=\"\"]', timeout=5) # Checking State of Subject Input Field (shoudl be Empty) - self.assertExists('//DropDownWidget/ScrollView[0]//MyMDTextField[@text=\"\"]', timeout=2) + self.assertExists('//DropDownWidget/ScrollView[0]//MyMDTextField[@text=\"\"]', timeout=5) # Click on Send Icon to check validation working - self.cli.wait_click('//MDActionTopAppBarButton[@icon=\"send\"]', timeout=2) + self.cli.wait_click('//MDActionTopAppBarButton[@icon=\"send\"]', timeout=5) # Checking validation Pop up is Opened self.assertExists('//MDDialog[@open]', timeout=5) # Checking the 'Ok' Button is rendered @@ -49,10 +49,10 @@ class SendMessage(TeleniumTestProcess): Validate the half filled form and press back button to save message in draft box. """ # Checking current screen (Msg composer screen) - self.assertExists("//ScreenManager[@current=\"create\"]", timeout=2) + self.assertExists("//ScreenManager[@current=\"create\"]", timeout=5) # ADD SENDER'S ADDRESS # Checking State of Sender's Address Input Field (Empty) - self.assertExists('//DropDownWidget/ScrollView[0]//MDTextField[@text=\"\"]', timeout=2) + self.assertExists('//DropDownWidget/ScrollView[0]//MDTextField[@text=\"\"]', timeout=5) # Assert to check Sender's address dropdown closed is_open = self.cli.getattr('//Create//CustomSpinner[@is_open]', 'is_open') self.assertEqual(is_open, False) @@ -61,7 +61,7 @@ class SendMessage(TeleniumTestProcess): # Checking the Address Dropdown is in open State is_open = self.cli.getattr('//Create//CustomSpinner[@is_open]', 'is_open') # Select Sender's Address from Dropdown - self.cli.wait_click('//ComposerSpinnerOption[0]', timeout=3) + self.cli.wait_click('//ComposerSpinnerOption[0]', timeout=5) # Assert to check Sender's address dropdown closed is_open = self.cli.getattr('//Create//CustomSpinner[@is_open]', 'is_open') self.assertEqual(is_open, False) @@ -69,7 +69,7 @@ class SendMessage(TeleniumTestProcess): sender_address = self.cli.getattr( '//DropDownWidget/ScrollView[0]/BoxLayout[0]/ScrollView[0]/MDTextField[@text]', 'text') self.assertExists( - '//DropDownWidget/ScrollView[0]//MDTextField[@text=\"{}\"]'.format(sender_address), timeout=2) + '//DropDownWidget/ScrollView[0]//MDTextField[@text=\"{}\"]'.format(sender_address), timeout=5) # Assert check for empty Subject Field self.assertExists('//DropDownWidget/ScrollView[0]//MyMDTextField[@text=\"\"]', timeout=5) # ADD SUBJECT @@ -78,14 +78,14 @@ class SendMessage(TeleniumTestProcess): self.assertExists( '//DropDownWidget/ScrollView[0]//MyMDTextField[@text=\"{}\"]'.format(self.test_subject), timeout=5) # Checking BODY Field(EMPTY) - self.assertExists('//DropDownWidget/ScrollView[0]//ScrollView[0]/MDTextField[@text=\"\"]', timeout=2) + self.assertExists('//DropDownWidget/ScrollView[0]//ScrollView[0]/MDTextField[@text=\"\"]', timeout=5) # ADD BODY self.cli.setattr( '//DropDownWidget/ScrollView[0]/BoxLayout[0]/ScrollView[0]/MDTextField[0]', 'text', self.test_body) # Checking BODY is Entered self.assertExists( '//DropDownWidget/ScrollView[0]//ScrollView[0]/MDTextField[@text=\"{}\"]'.format(self.test_body), - timeout=2) + timeout=5) # click on send icon self.cli.wait_click('//MDActionTopAppBarButton[@icon=\"send\"]', timeout=5) # Checking validation Pop up is Opened @@ -103,7 +103,7 @@ class SendMessage(TeleniumTestProcess): """ # ADD RECEIVER ADDRESS # Checking Receiver Address Field - self.assertExists('//DropDownWidget/ScrollView[0]//MyTextInput[@text=\"\"]', timeout=2) + self.assertExists('//DropDownWidget/ScrollView[0]//MyTextInput[@text=\"\"]', timeout=5) # Entering Receiver Address self.cli.setattr( '//DropDownWidget/ScrollView[0]//MyTextInput[0]', "text", test_address['autoresponder_address']) diff --git a/src/bitmessagekivy/tests/test_setting_screen.py b/src/bitmessagekivy/tests/test_setting_screen.py index 949a6f51..4f3a0a59 100644 --- a/src/bitmessagekivy/tests/test_setting_screen.py +++ b/src/bitmessagekivy/tests/test_setting_screen.py @@ -1,13 +1,11 @@ # pylint: disable=too-few-public-methods from .telenium_process import TeleniumTestProcess -from .common import skip_screen_checks class SettingScreen(TeleniumTestProcess): """Setting Screen Functionality Testing""" - @skip_screen_checks def test_setting_screen(self): """Show Setting Screen""" # This is for checking Current screen @@ -17,11 +15,12 @@ class SettingScreen(TeleniumTestProcess): # this is for scrolling Nav drawer self.drag("//NavigationItem[@text=\"Sent\"]", "//NavigationItem[@text=\"Inbox\"]") # assert for checking scroll function - self.assertCheckScrollDown('//ContentNavigationDrawer//ScrollView[0]', timeout=5) + self.assertCheckScrollDown('//ContentNavigationDrawer//ScrollView[0]', timeout=10) # this is for opening setting screen - self.cli.wait_click('//NavigationItem[@text=\"Settings\"]', timeout=3) + self.cli.wait_click('//NavigationItem[@text=\"Settings\"]', timeout=5) + self.assertExists('//MDNavigationDrawer[@status~=\"closed\"]', timeout=5) # Checking current screen - self.assertExists("//ScreenManager[@current=\"set\"]", timeout=3) + self.assertExists("//ScreenManager[@current=\"set\"]", timeout=5) # Scrolling down currrent screen self.cli.wait_drag( '//MDTabs[0]//MDLabel[@text=\"Close to tray\"]', @@ -37,7 +36,3 @@ class SettingScreen(TeleniumTestProcess): '//MDTabs[0]//MDLabel[@text=\"Username:\"]', '//MDTabs[0]//MDLabel[@text=\"Port:\"]', 1, timeout=5) # Checking state of 'Resends Expire' sub tab should be 'normal'(inactive) self.assertExists('//MDTabs[0]//MDTabsLabel[@text=\"Resends Expire\"][@state=\"normal\"]', timeout=5) - # Scrolling down currrent screen - self.cli.wait_click('//MDTabs[0]//MDTabsLabel[@text=\"Resends Expire\"]', timeout=5) - # Checking state of 'Resends Expire' sub tab should be 'down'(active) - self.assertExists('//MDTabs[0]//MDTabsLabel[@text=\"Resends Expire\"][@state=\"down\"]', timeout=5) diff --git a/src/bitmessagekivy/tests/test_trash_message.py b/src/bitmessagekivy/tests/test_trash_message.py index 57e4f604..d7cdb467 100644 --- a/src/bitmessagekivy/tests/test_trash_message.py +++ b/src/bitmessagekivy/tests/test_trash_message.py @@ -1,12 +1,10 @@ from .telenium_process import TeleniumTestProcess -from .common import skip_screen_checks from .common import ordered class TrashMessage(TeleniumTestProcess): """Trash Screen Functionality Testing""" - @skip_screen_checks @ordered def test_delete_trash_message(self): """Delete Trash message permanently from trash message listing""" @@ -15,33 +13,9 @@ class TrashMessage(TeleniumTestProcess): # Method to open side navbar self.open_side_navbar() # this is for opening Trash screen - self.cli.wait_click('//NavigationItem[@text=\"Trash\"]', timeout=2) + self.cli.wait_click('//NavigationItem[@text=\"Trash\"]', timeout=5) + # Checking the drawer is in 'closed' state + self.assertExists('//MDNavigationDrawer[@status~=\"closed\"]', timeout=5) # Checking Trash Screen self.assertExists("//ScreenManager[@current=\"trash\"]", timeout=5) - # This is for swiping message to activate delete icon. - self.cli.wait_drag( - '//Trash[0]//TwoLineAvatarIconListItem[0]/BoxLayout[1]', - '//Trash[0]//TwoLineAvatarIconListItem[0]/BoxLayout[2]', 2, timeout=5) - # Checking the "trash-can" is rendered - self.assertExists( - "//MDList[0]/CutsomSwipeToDeleteItem[0]//MDIconButton[@icon~=\"trash-can\"]", timeout=2) - # Delete icon is enabled - self.cli.setattr('//MDList[0]/CutsomSwipeToDeleteItem[0]//MDIconButton', 'disabled', False) - # Checking the Dialog popup is closed - self.assertNotExists('//MDDialog[@open]', timeout=5) - # Checking the delete icon is rendered and functional - self.assertExists('//MDList[0]/CutsomSwipeToDeleteItem[0]//MDIconButton[@icon=\"trash-can\"]', timeout=5) - # Click on the delete icon to delete the current message - self.cli.wait_click('//MDList[0]/CutsomSwipeToDeleteItem[0]//MDIconButton[@icon=\"trash-can\"]', timeout=5) - # Checking Confirm Popup is Opened - self.assertExists('//MDDialog[@open]', timeout=5) - # Checking the popup's 'Yes' button is rendered. - self.assertExists("//MDDialog//MDFlatButton[@text=\"Yes\"]", timeout=5) - # Clicking on 'Yes' Button on Popup to confirm delete. - self.cli.wait_click('//MDFlatButton[@text=\"Yes\"]', timeout=5) - # Checking the Dialog is closed on click "Yes" button - self.assertNotExists('//MDDialog[@open]', timeout=5) - # Checking the message is rendered on Trash screen - self.assertExists('//MDList[0]/CutsomSwipeToDeleteItem[0]', timeout=5) - # Checking Current screen is Trash Screen - self.assertExists("//ScreenManager[@current=\"trash\"]", timeout=5) + self.cli.sleep(0.5) diff --git a/src/bitmessagekivy/uikivysignaler.py b/src/bitmessagekivy/uikivysignaler.py new file mode 100644 index 00000000..6f73247e --- /dev/null +++ b/src/bitmessagekivy/uikivysignaler.py @@ -0,0 +1,38 @@ +""" + UI Singnaler for kivy interface +""" + +import logging + +from threading import Thread +from kivy.app import App + +from pybitmessage import queues +from pybitmessage import state + +from pybitmessage.bitmessagekivy.baseclass.common import kivy_state_variables + +logger = logging.getLogger('default') + + +class UIkivySignaler(Thread): + """Kivy ui signaler""" + + def __init__(self, *args, **kwargs): + super(UIkivySignaler, self).__init__(*args, **kwargs) + self.kivy_state = kivy_state_variables() + + def run(self): + self.kivy_state.kivyui_ready.wait() + while state.shutdown == 0: + try: + command, data = queues.UISignalQueue.get() + if command == 'writeNewAddressToTable': + address = data[1] + App.get_running_app().identity_list.append(address) + elif command == 'updateSentItemStatusByAckdata': + App.get_running_app().status_dispatching(data) + elif command == 'writeNewpaymentAddressToTable': + pass + except Exception as e: + logger.debug(e) diff --git a/src/bitmessagemain.py b/src/bitmessagemain.py index 84313ab9..9acd1278 100755 --- a/src/bitmessagemain.py +++ b/src/bitmessagemain.py @@ -3,7 +3,7 @@ The PyBitmessage startup script """ # Copyright (c) 2012-2016 Jonathan Warren -# Copyright (c) 2012-2020 The Bitmessage developers +# Copyright (c) 2012-2022 The Bitmessage developers # Distributed under the MIT/X11 software license. See the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. @@ -30,22 +30,17 @@ import time import traceback import defaults -import shared +# Network subsystem +import network import shutdown import state from testmode_init import populate_api_test_data -from bmconfigparser import BMConfigParser +from bmconfigparser import config from debug import logger # this should go before any threads from helper_startup import ( adjustHalfOpenConnectionsLimit, fixSocket, start_proxyconfig) from inventory import Inventory -# Network objects and threads -from network import ( - BMConnectionPool, Dandelion, AddrThread, AnnounceThread, BMNetworkThread, - InvThread, ReceiveQueueThread, DownloadThread, UploadThread -) -from network.knownnodes import readKnownNodes from singleinstance import singleinstance # Synchronous threads from threads import ( @@ -92,7 +87,6 @@ class Main(object): fixSocket() adjustHalfOpenConnectionsLimit() - config = BMConfigParser() daemon = config.safeGetBoolean('bitmessagesettings', 'daemon') try: @@ -162,12 +156,12 @@ class Main(object): set_thread_name("PyBitmessage") - state.dandelion = config.safeGetInt('network', 'dandelion') + state.dandelion_enabled = config.safeGetInt('network', 'dandelion') # dandelion requires outbound connections, without them, # stem objects will get stuck forever - if state.dandelion and not config.safeGetBoolean( + if state.dandelion_enabled and not config.safeGetBoolean( 'bitmessagesettings', 'sendoutgoingconnections'): - state.dandelion = 0 + state.dandelion_enabled = 0 if state.testmode or config.safeGetBoolean( 'bitmessagesettings', 'extralowdifficulty'): @@ -176,11 +170,15 @@ class Main(object): defaults.networkDefaultPayloadLengthExtraBytes = int( defaults.networkDefaultPayloadLengthExtraBytes / 100) - readKnownNodes() - - # Not needed if objproc is disabled - if state.enableObjProc: + # Start the SQL thread + sqlLookup = sqlThread() + # DON'T close the main program even if there are threads left. + # The closeEvent should command this thread to exit gracefully. + sqlLookup.daemon = False + sqlLookup.start() + state.Inventory = Inventory() # init + if state.enableObjProc: # Not needed if objproc is disabled # Start the address generation thread addressGeneratorThread = addressGenerator() # close the main program even if there are threads left @@ -193,19 +191,13 @@ class Main(object): singleWorkerThread.daemon = True singleWorkerThread.start() - # Start the SQL thread - sqlLookup = sqlThread() - # DON'T close the main program even if there are threads left. - # The closeEvent should command this thread to exit gracefully. - sqlLookup.daemon = False - sqlLookup.start() - - Inventory() # init - # init, needs to be early because other thread may access it early - Dandelion() - - # Enable object processor and SMTP only if objproc enabled - if state.enableObjProc: + # Start the object processing thread + objectProcessorThread = objectProcessor() + # DON'T close the main program even if the thread remains. + # This thread checks the shutdown variable after processing + # each object. + objectProcessorThread.daemon = False + objectProcessorThread.start() # SMTP delivery thread if daemon and config.safeGet( @@ -221,25 +213,6 @@ class Main(object): smtpServerThread = smtpServer() smtpServerThread.start() - # Start the thread that calculates POWs - objectProcessorThread = objectProcessor() - # DON'T close the main program even the thread remains. - # This thread checks the shutdown variable after processing - # each object. - objectProcessorThread.daemon = False - objectProcessorThread.start() - - # Start the cleanerThread - singleCleanerThread = singleCleaner() - # close the main program even if there are threads left - singleCleanerThread.daemon = True - singleCleanerThread.start() - - # Not needed if objproc disabled - if state.enableObjProc: - shared.reloadMyAddressHashes() - shared.reloadBroadcastSendersForWhichImWatching() - # API is also objproc dependent if config.safeGetBoolean('bitmessagesettings', 'apienabled'): import api # pylint: disable=relative-import @@ -248,41 +221,23 @@ class Main(object): singleAPIThread.daemon = True singleAPIThread.start() + # Start the cleanerThread + singleCleanerThread = singleCleaner() + # close the main program even if there are threads left + singleCleanerThread.daemon = True + singleCleanerThread.start() + # start network components if networking is enabled if state.enableNetwork: start_proxyconfig() - BMConnectionPool().connectToStream(1) - asyncoreThread = BMNetworkThread() - asyncoreThread.daemon = True - asyncoreThread.start() - for i in range(config.getint('threads', 'receive')): - receiveQueueThread = ReceiveQueueThread(i) - receiveQueueThread.daemon = True - receiveQueueThread.start() - if config.safeGetBoolean('bitmessagesettings', 'udp'): - state.announceThread = AnnounceThread() - state.announceThread.daemon = True - state.announceThread.start() - state.invThread = InvThread() - state.invThread.daemon = True - state.invThread.start() - state.addrThread = AddrThread() - state.addrThread.daemon = True - state.addrThread.start() - state.downloadThread = DownloadThread() - state.downloadThread.daemon = True - state.downloadThread.start() - state.uploadThread = UploadThread() - state.uploadThread.daemon = True - state.uploadThread.start() + network.start(config, state) if config.safeGetBoolean('bitmessagesettings', 'upnp'): import upnp upnpThread = upnp.uPnPThread() upnpThread.start() else: - # Populate with hardcoded value (same as connectToStream above) - state.streamsInWhichIAmParticipating.append(1) + network.connectionpool.pool.connectToStream(1) if not daemon and state.enableGUI: if state.curses: @@ -314,8 +269,11 @@ class Main(object): # pylint: disable=relative-import from tests import core as test_core except ImportError: - self.stop() - return + try: + from pybitmessage.tests import core as test_core + except ImportError: + self.stop() + return test_core_result = test_core.run() self.stop() @@ -408,11 +366,11 @@ All parameters are optional. @staticmethod def getApiAddress(): """This function returns API address and port""" - if not BMConfigParser().safeGetBoolean( + if not config.safeGetBoolean( 'bitmessagesettings', 'apienabled'): return None - address = BMConfigParser().get('bitmessagesettings', 'apiinterface') - port = BMConfigParser().getint('bitmessagesettings', 'apiport') + address = config.get('bitmessagesettings', 'apiinterface') + port = config.getint('bitmessagesettings', 'apiport') return {'address': address, 'port': port} diff --git a/src/bitmessageqt/__init__.py b/src/bitmessageqt/__init__.py index 5c6bebdc..43b958a2 100644 --- a/src/bitmessageqt/__init__.py +++ b/src/bitmessageqt/__init__.py @@ -18,41 +18,50 @@ from sqlite3 import register_adapter from PyQt5 import QtCore, QtGui, QtWidgets, QtNetwork -# This is needed for tray icon -import bitmessage_icons_rc # noqa:F401 pylint: disable=unused-import import dialogs import helper_addressbook import helper_search -import helper_sent import l10n -import namecoin -import paths -import queues import settingsmixin import shared -import shutdown -import sound import state -import support +from debug import logger +from tr import _translate from account import ( - getSortedAccounts, getSortedSubscriptions, accountClass, BMAccount, - GatewayAccount, MailchuckAccount, AccountColor) + accountClass, getSortedSubscriptions, + BMAccount, GatewayAccount, MailchuckAccount, AccountColor) from addresses import decodeAddress, addBMIfNotPresent from bitmessageui import Ui_MainWindow -from bmconfigparser import BMConfigParser +from bmconfigparser import config +import namecoin +from messageview import MessageView +from migrationwizard import Ui_MigrationWizard from foldertree import ( AccountMixin, Ui_FolderWidget, Ui_AddressWidget, Ui_SubscriptionWidget, MessageList_AddressWidget, MessageList_SubjectWidget, Ui_AddressBookWidgetItemLabel, Ui_AddressBookWidgetItemAddress, MessageList_TimeWidget) +import settingsmixin +import support from helper_sql import ( sqlQuery, sqlExecute, sqlExecuteChunked, sqlStoredProcedure) -from messageview import MessageView +from helper_sql import sqlQuery, sqlExecute, sqlExecuteChunked, sqlStoredProcedure +import helper_addressbook +import helper_search +import l10n +from utils import str_broadcast_subscribers, avatarize +import dialogs from network.stats import pendingDownload, pendingUpload -from proofofwork import getPowType -from statusbar import BMStatusBar -from tr import _translate from uisignaler import UISignaler +import paths +from proofofwork import getPowType +import queues +import shutdown +from statusbar import BMStatusBar +import sound +# This is needed for tray icon +import bitmessage_icons_rc # noqa:F401 pylint: disable=unused-import +import helper_sent from utils import str_broadcast_subscribers, avatarize try: @@ -62,7 +71,6 @@ except ImportError: logger = logging.getLogger('default') - # TODO: rewrite def powQueueSize(): """Returns the size of queues.workerQueue including current unfinished work""" @@ -502,12 +510,12 @@ class MyForm(settingsmixin.SMainWindow): db = {} enabled = {} - for toAddress in getSortedAccounts(): - isEnabled = BMConfigParser().getboolean( + for toAddress in config.addresses(True): + isEnabled = config.getboolean( toAddress, 'enabled') - isChan = BMConfigParser().safeGetBoolean( + isChan = config.safeGetBoolean( toAddress, 'chan') - # isMaillinglist = BMConfigParser().safeGetBoolean( + # isMaillinglist = config.safeGetBoolean( # toAddress, 'mailinglist') if treeWidget == self.ui.treeWidgetYourIdentities: @@ -616,7 +624,7 @@ class MyForm(settingsmixin.SMainWindow): # Ask the user if we may delete their old version 1 addresses if they # have any. - for addressInKeysFile in getSortedAccounts(): + for addressInKeysFile in config.addresses(): status, addressVersionNumber, streamNumber, hash = decodeAddress( addressInKeysFile) if addressVersionNumber == 1: @@ -628,8 +636,8 @@ class MyForm(settingsmixin.SMainWindow): reply = QtWidgets.QMessageBox.question( self, 'Message', displayMsg, QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No) if reply == QtWidgets.QMessageBox.Yes: - BMConfigParser().remove_section(addressInKeysFile) - BMConfigParser().save() + config.remove_section(addressInKeysFile) + config.save() self.change_translation() @@ -789,7 +797,7 @@ class MyForm(settingsmixin.SMainWindow): self.rerenderComboBoxSendFromBroadcast() # Put the TTL slider in the correct spot - TTL = BMConfigParser().getint('bitmessagesettings', 'ttl') + TTL = config.getint('bitmessagesettings', 'ttl') if TTL < 3600: # an hour TTL = 3600 elif TTL > 28 * 24 * 60 * 60: # 28 days @@ -808,7 +816,7 @@ class MyForm(settingsmixin.SMainWindow): self.ui.updateNetworkSwitchMenuLabel() - self._firstrun = BMConfigParser().safeGetBoolean( + self._firstrun = config.safeGetBoolean( 'bitmessagesettings', 'dontconnect') self._contact_selected = None @@ -827,7 +835,7 @@ class MyForm(settingsmixin.SMainWindow): Configure Bitmessage to start on startup (or remove the configuration) based on the setting in the keys.dat file """ - startonlogon = BMConfigParser().safeGetBoolean( + startonlogon = config.safeGetBoolean( 'bitmessagesettings', 'startonlogon') if sys.platform.startswith('win'): # Auto-startup for Windows RUN_PATH = "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" @@ -849,8 +857,8 @@ class MyForm(settingsmixin.SMainWindow): def updateTTL(self, sliderPosition): TTL = int(sliderPosition ** 3.199 + 3600) self.updateHumanFriendlyTTLDescription(TTL) - BMConfigParser().set('bitmessagesettings', 'ttl', str(TTL)) - BMConfigParser().save() + config.set('bitmessagesettings', 'ttl', str(TTL)) + config.save() def updateHumanFriendlyTTLDescription(self, TTL): numberOfHours = int(round(TTL / (60 * 60))) @@ -905,7 +913,7 @@ class MyForm(settingsmixin.SMainWindow): self.appIndicatorShowOrHideWindow() def appIndicatorSwitchQuietMode(self): - BMConfigParser().set( + config.set( 'bitmessagesettings', 'showtraynotifications', str(not self.actionQuiet.isChecked()) ) @@ -1267,7 +1275,7 @@ class MyForm(settingsmixin.SMainWindow): # show bitmessage self.actionShow = QtWidgets.QAction(_translate( "MainWindow", "Show Bitmessage"), m, checkable=True) - self.actionShow.setChecked(not BMConfigParser().getboolean( + self.actionShow.setChecked(not config.getboolean( 'bitmessagesettings', 'startintray')) self.actionShow.triggered.connect(self.appIndicatorShowOrHideWindow) if not sys.platform[0:3] == 'win': @@ -1276,7 +1284,7 @@ class MyForm(settingsmixin.SMainWindow): # quiet mode self.actionQuiet = QtWidgets.QAction(_translate( "MainWindow", "Quiet Mode"), m, checkable=True) - self.actionQuiet.setChecked(not BMConfigParser().getboolean( + self.actionQuiet.setChecked(not config.getboolean( 'bitmessagesettings', 'showtraynotifications')) self.actionQuiet.triggered.connect(self.appIndicatorSwitchQuietMode) m.addAction(self.actionQuiet) @@ -1597,11 +1605,20 @@ class MyForm(settingsmixin.SMainWindow): self.rerenderTabTreeSubscriptions() self.rerenderTabTreeChans() if self.getCurrentFolder(self.ui.treeWidgetYourIdentities) == "trash": - self.loadMessagelist(self.ui.tableWidgetInbox, self.getCurrentAccount(self.ui.treeWidgetYourIdentities), "trash") + self.loadMessagelist( + self.ui.tableWidgetInbox, + self.getCurrentAccount(self.ui.treeWidgetYourIdentities), + "trash") elif self.getCurrentFolder(self.ui.treeWidgetSubscriptions) == "trash": - self.loadMessagelist(self.ui.tableWidgetInboxSubscriptions, self.getCurrentAccount(self.ui.treeWidgetSubscriptions), "trash") + self.loadMessagelist( + self.ui.tableWidgetInboxSubscriptions, + self.getCurrentAccount(self.ui.treeWidgetSubscriptions), + "trash") elif self.getCurrentFolder(self.ui.treeWidgetChans) == "trash": - self.loadMessagelist(self.ui.tableWidgetInboxChans, self.getCurrentAccount(self.ui.treeWidgetChans), "trash") + self.loadMessagelist( + self.ui.tableWidgetInboxChans, + self.getCurrentAccount(self.ui.treeWidgetChans), + "trash") # menu button 'regenerate deterministic addresses' def click_actionRegenerateDeterministicAddresses(self): @@ -1660,14 +1677,21 @@ class MyForm(settingsmixin.SMainWindow): if dialog.exec_(): if dialog.radioButtonConnectNow.isChecked(): self.ui.updateNetworkSwitchMenuLabel(False) - BMConfigParser().remove_option( + config.remove_option( 'bitmessagesettings', 'dontconnect') - BMConfigParser().save() + config.save() elif dialog.radioButtonConfigureNetwork.isChecked(): self.click_actionSettings() else: self._firstrun = False + def showMigrationWizard(self, level): + self.migrationWizardInstance = Ui_MigrationWizard(["a"]) + if self.migrationWizardInstance.exec_(): + pass + else: + pass + def changeEvent(self, event): if event.type() == QtCore.QEvent.LanguageChange: self.ui.retranslateUi(self) @@ -1680,7 +1704,7 @@ class MyForm(settingsmixin.SMainWindow): self.ui.blackwhitelist.init_blacklist_popup_menu(False) if event.type() == QtCore.QEvent.WindowStateChange: if self.windowState() & QtCore.Qt.WindowMinimized: - if BMConfigParser().getboolean('bitmessagesettings', 'minimizetotray') and not 'darwin' in sys.platform: + if config.getboolean('bitmessagesettings', 'minimizetotray') and not 'darwin' in sys.platform: QtCore.QTimer.singleShot(0, self.appIndicatorHide) elif event.oldState() & QtCore.Qt.WindowMinimized: # The window state has just been changed to @@ -1696,68 +1720,65 @@ class MyForm(settingsmixin.SMainWindow): connected = False def setStatusIcon(self, color): - # print 'setting status icon color' - _notifications_enabled = not BMConfigParser().getboolean( + _notifications_enabled = not config.getboolean( 'bitmessagesettings', 'hidetrayconnectionnotifications') + if color not in ('red', 'yellow', 'green'): + return + + self.pushButtonStatusIcon.setIcon( + QtGui.QIcon(":/newPrefix/images/%sicon.png" % color)) + state.statusIconColor = color if color == 'red': - self.pushButtonStatusIcon.setIcon( - QtGui.QIcon(":/newPrefix/images/redicon.png")) - state.statusIconColor = 'red' # if the connection is lost then show a notification if self.connected and _notifications_enabled: self.notifierShow( 'Bitmessage', _translate("MainWindow", "Connection lost"), sound.SOUND_DISCONNECTED) - if not BMConfigParser().safeGetBoolean('bitmessagesettings', 'upnp') and \ - BMConfigParser().get('bitmessagesettings', 'socksproxytype') == "none": + proxy = config.safeGet( + 'bitmessagesettings', 'socksproxytype', 'none') + if proxy == 'none' and not config.safeGetBoolean( + 'bitmessagesettings', 'upnp'): self.updateStatusBar( _translate( "MainWindow", "Problems connecting? Try enabling UPnP in the Network" " Settings" )) + elif proxy == 'SOCKS5' and config.safeGetBoolean( + 'bitmessagesettings', 'onionservicesonly'): + self.updateStatusBar(( + _translate( + "MainWindow", + "With recent tor you may never connect having" + " 'onionservicesonly' set in your config."), 1 + )) self.connected = False if self.actionStatus is not None: self.actionStatus.setText(_translate( "MainWindow", "Not Connected")) self.setTrayIconFile("can-icon-24px-red.png") - if color == 'yellow': - if self.statusbar.currentMessage() == 'Warning: You are currently not connected. Bitmessage will do the work necessary to send the message but it won\'t send until you connect.': - self.statusbar.clearMessage() - self.pushButtonStatusIcon.setIcon( - QtGui.QIcon(":/newPrefix/images/yellowicon.png")) - state.statusIconColor = 'yellow' - # if a new connection has been established then show a notification - if not self.connected and _notifications_enabled: - self.notifierShow( - 'Bitmessage', - _translate("MainWindow", "Connected"), - sound.SOUND_CONNECTED) - self.connected = True + return - if self.actionStatus is not None: - self.actionStatus.setText(_translate( - "MainWindow", "Connected")) - self.setTrayIconFile("can-icon-24px-yellow.png") - if color == 'green': - if self.statusbar.currentMessage() == 'Warning: You are currently not connected. Bitmessage will do the work necessary to send the message but it won\'t send until you connect.': - self.statusbar.clearMessage() - self.pushButtonStatusIcon.setIcon( - QtGui.QIcon(":/newPrefix/images/greenicon.png")) - state.statusIconColor = 'green' - if not self.connected and _notifications_enabled: - self.notifierShow( - 'Bitmessage', - _translate("MainWindow", "Connected"), - sound.SOUND_CONNECTION_GREEN) - self.connected = True + if self.statusbar.currentMessage() == ( + "Warning: You are currently not connected. Bitmessage will do" + " the work necessary to send the message but it won't send" + " until you connect." + ): + self.statusbar.clearMessage() + # if a new connection has been established then show a notification + if not self.connected and _notifications_enabled: + self.notifierShow( + 'Bitmessage', + _translate("MainWindow", "Connected"), + sound.SOUND_CONNECTED) + self.connected = True - if self.actionStatus is not None: - self.actionStatus.setText(_translate( - "MainWindow", "Connected")) - self.setTrayIconFile("can-icon-24px-green.png") + if self.actionStatus is not None: + self.actionStatus.setText(_translate( + "MainWindow", "Connected")) + self.setTrayIconFile("can-icon-24px-%s.png" % color) def initTrayIcon(self, iconFileName, app): self.currentTrayIconFileName = iconFileName @@ -1794,6 +1815,7 @@ class MyForm(settingsmixin.SMainWindow): painter = QtGui.QPainter() painter.begin(pixmap) painter.setPen(QtGui.QPen(QtGui.QColor(255, 0, 0))) + painter.setBrush(QtCore.Qt.SolidPattern) painter.setFont(font) painter.drawText(24-rect.right()-marginX, -rect.top()+marginY, txt) painter.end() @@ -1918,20 +1940,16 @@ class MyForm(settingsmixin.SMainWindow): os._exit(0) def rerenderMessagelistFromLabels(self): - for messagelist in ( - self.ui.tableWidgetInbox, - self.ui.tableWidgetInboxChans, - self.ui.tableWidgetInboxSubscriptions - ): + for messagelist in (self.ui.tableWidgetInbox, + self.ui.tableWidgetInboxChans, + self.ui.tableWidgetInboxSubscriptions): for i in range(messagelist.rowCount()): messagelist.item(i, 1).setLabel() def rerenderMessagelistToLabels(self): - for messagelist in ( - self.ui.tableWidgetInbox, - self.ui.tableWidgetInboxChans, - self.ui.tableWidgetInboxSubscriptions - ): + for messagelist in (self.ui.tableWidgetInbox, + self.ui.tableWidgetInboxChans, + self.ui.tableWidgetInboxSubscriptions): for i in range(messagelist.rowCount()): messagelist.item(i, 0).setLabel() @@ -1962,12 +1980,11 @@ class MyForm(settingsmixin.SMainWindow): newRows[address] = [ label.decode('utf-8'), AccountMixin.SUBSCRIPTION] # chans - addresses = getSortedAccounts() - for address in addresses: + for address in config.addresses(True): account = accountClass(address) if ( account.type == AccountMixin.CHAN - and BMConfigParser().safeGetBoolean(address, 'enabled') + and config.safeGetBoolean(address, 'enabled') ): newRows[address] = [account.getLabel(), AccountMixin.CHAN] # normal accounts @@ -2106,9 +2123,9 @@ class MyForm(settingsmixin.SMainWindow): ) + "@mailchuck.com" acct = MailchuckAccount(fromAddress) acct.register(email) - BMConfigParser().set(fromAddress, 'label', email) - BMConfigParser().set(fromAddress, 'gateway', 'mailchuck') - BMConfigParser().save() + config.set(fromAddress, 'label', email) + config.set(fromAddress, 'gateway', 'mailchuck') + config.save() self.updateStatusBar(_translate( "MainWindow", "Error: Your account wasn't registered at" @@ -2322,20 +2339,22 @@ class MyForm(settingsmixin.SMainWindow): self.ui.tabWidgetSend.setCurrentIndex( self.ui.tabWidgetSend.indexOf( self.ui.sendBroadcast - if BMConfigParser().safeGetBoolean(str(address), 'mailinglist') + if config.safeGetBoolean(str(address), 'mailinglist') else self.ui.sendDirect )) def rerenderComboBoxSendFrom(self): self.ui.comboBoxSendFrom.clear() - for addressInKeysFile in getSortedAccounts(): - isEnabled = BMConfigParser().getboolean( - addressInKeysFile, 'enabled') # I realize that this is poor programming practice but I don't care. It's easier for others to read. - isMaillinglist = BMConfigParser().safeGetBoolean( + for addressInKeysFile in config.addresses(True): + # I realize that this is poor programming practice but I don't care. + # It's easier for others to read. + isEnabled = config.getboolean( + addressInKeysFile, 'enabled') + isMaillinglist = config.safeGetBoolean( addressInKeysFile, 'mailinglist') if isEnabled and not isMaillinglist: label = ( - BMConfigParser().get(addressInKeysFile, 'label').decode( + config.get(addressInKeysFile, 'label').decode( 'utf-8', 'ignore').strip() or addressInKeysFile) self.ui.comboBoxSendFrom.addItem( avatarize(addressInKeysFile), label, addressInKeysFile) @@ -2354,13 +2373,13 @@ class MyForm(settingsmixin.SMainWindow): def rerenderComboBoxSendFromBroadcast(self): self.ui.comboBoxSendFromBroadcast.clear() - for addressInKeysFile in getSortedAccounts(): - isEnabled = BMConfigParser().getboolean( - addressInKeysFile, 'enabled') # I realize that this is poor programming practice but I don't care. It's easier for others to read. - isChan = BMConfigParser().safeGetBoolean(addressInKeysFile, 'chan') + for addressInKeysFile in config.addresses(True): + isEnabled = config.getboolean( + addressInKeysFile, 'enabled') + isChan = config.safeGetBoolean(addressInKeysFile, 'chan') if isEnabled and not isChan: label = ( - BMConfigParser().get(addressInKeysFile, 'label').decode( + config.get(addressInKeysFile, 'label').decode( 'utf-8', 'ignore').strip() or addressInKeysFile) self.ui.comboBoxSendFromBroadcast.addItem( avatarize(addressInKeysFile), label, addressInKeysFile) @@ -2463,7 +2482,7 @@ class MyForm(settingsmixin.SMainWindow): else: acct = ret self.propagateUnreadCount(widget=treeWidget if ret else None) - if BMConfigParser().safeGetBoolean( + if config.safeGetBoolean( 'bitmessagesettings', 'showtraynotifications'): self.notifierShow( _translate("MainWindow", "New Message"), @@ -2474,7 +2493,7 @@ class MyForm(settingsmixin.SMainWindow): (self.getCurrentFolder(treeWidget) != "inbox" and self.getCurrentFolder(treeWidget) is not None) or self.getCurrentAccount(treeWidget) != acct.address): - # Ubuntu should notify of new message irespective of + # Ubuntu should notify of new message irrespective of # whether it's in current message list or not self.indicatorUpdate(True, to_label=acct.toLabel) @@ -2482,7 +2501,7 @@ class MyForm(settingsmixin.SMainWindow): if acct.feedback != GatewayAccount.ALL_OK: if acct.feedback == GatewayAccount.REGISTRATION_DENIED: dialogs.EmailGatewayDialog( - self, BMConfigParser(), acct).exec_() + self, config, acct).exec_() # possible other branches? except AttributeError: pass @@ -2564,7 +2583,7 @@ class MyForm(settingsmixin.SMainWindow): )) def click_pushButtonStatusIcon(self): - dialogs.IconGlossaryDialog(self, config=BMConfigParser()).exec_() + dialogs.IconGlossaryDialog(self, config=config).exec_() def click_actionHelp(self): dialogs.HelpDialog(self).exec_() @@ -2591,10 +2610,10 @@ class MyForm(settingsmixin.SMainWindow): def on_action_SpecialAddressBehaviorDialog(self): """Show SpecialAddressBehaviorDialog""" - dialogs.SpecialAddressBehaviorDialog(self, BMConfigParser()) + dialogs.SpecialAddressBehaviorDialog(self, config) def on_action_EmailGatewayDialog(self): - dialog = dialogs.EmailGatewayDialog(self, config=BMConfigParser()) + dialog = dialogs.EmailGatewayDialog(self, config=config) # For Modal dialogs dialog.exec_() acct = dialog.data @@ -2655,7 +2674,7 @@ class MyForm(settingsmixin.SMainWindow): dialogs.NewAddressDialog(self) def network_switch(self): - dontconnect_option = not BMConfigParser().safeGetBoolean( + dontconnect_option = not config.safeGetBoolean( 'bitmessagesettings', 'dontconnect') reply = QtWidgets.QMessageBox.question( self, _translate("MainWindow", "Disconnecting") @@ -2670,9 +2689,9 @@ class MyForm(settingsmixin.SMainWindow): QtWidgets.QMessageBox.Cancel) if reply != QtWidgets.QMessageBox.Yes: return - BMConfigParser().set( + config.set( 'bitmessagesettings', 'dontconnect', str(dontconnect_option)) - BMConfigParser().save() + config.save() self.ui.updateNetworkSwitchMenuLabel(dontconnect_option) self.ui.pushButtonFetchNamecoinID.setHidden( @@ -2733,8 +2752,7 @@ class MyForm(settingsmixin.SMainWindow): elif reply == QtWidgets.QMessageBox.Cancel: return - if state.statusIconColor == 'red' \ - and not BMConfigParser().safeGetBoolean( + if state.statusIconColor == 'red' and not config.safeGetBoolean( 'bitmessagesettings', 'dontconnect'): reply = QtWidgets.QMessageBox.question( self, _translate("MainWindow", "Not connected"), @@ -2869,7 +2887,7 @@ class MyForm(settingsmixin.SMainWindow): def closeEvent(self, event): """window close event""" event.ignore() - trayonclose = BMConfigParser().safeGetBoolean( + trayonclose = config.safeGetBoolean( 'bitmessagesettings', 'trayonclose') if trayonclose: self.appIndicatorHide() @@ -2938,8 +2956,7 @@ class MyForm(settingsmixin.SMainWindow): # Format predefined text on message reply. def quoted_text(self, message): - if not BMConfigParser().safeGetBoolean( - 'bitmessagesettings', 'replybelow'): + if not config.safeGetBoolean('bitmessagesettings', 'replybelow'): return ( '\n\n------------------------------------------------------\n' + message @@ -3030,7 +3047,7 @@ class MyForm(settingsmixin.SMainWindow): self.ui.tabWidgetSend.indexOf(self.ui.sendDirect) ) # toAddressAtCurrentInboxRow = fromAddressAtCurrentInboxRow - elif not BMConfigParser().has_section(toAddressAtCurrentInboxRow): + elif not config.has_section(toAddressAtCurrentInboxRow): QtWidgets.QMessageBox.information( self, _translate("MainWindow", "Address is gone"), @@ -3040,7 +3057,7 @@ class MyForm(settingsmixin.SMainWindow): " removed it?" ).format(toAddressAtCurrentInboxRow), QtWidgets.QMessageBox.Ok) - elif not BMConfigParser().getboolean( + elif not config.getboolean( toAddressAtCurrentInboxRow, 'enabled'): QtWidgets.QMessageBox.information( self, @@ -3132,7 +3149,8 @@ class MyForm(settingsmixin.SMainWindow): 'SELECT * FROM blacklist WHERE address=?', addressAtCurrentInboxRow) if queryreturn == []: - label = "\"" + tableWidget.item(currentInboxRow, 2).subject + "\" in " + BMConfigParser().get(recipientAddress, "label") + label = "\"" + tableWidget.item(currentInboxRow, 2).subject + "\" in " + config.get( + recipientAddress, "label") sqlExecute('''INSERT INTO blacklist VALUES (?,?, ?)''', label, addressAtCurrentInboxRow, True) @@ -3654,12 +3672,12 @@ class MyForm(settingsmixin.SMainWindow): " delete the channel?" ), QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No ) == QtWidgets.QMessageBox.Yes: - BMConfigParser().remove_section(str(account.address)) + config.remove_section(str(account.address)) else: return else: return - BMConfigParser().save() + config.save() shared.reloadMyAddressHashes() self.rerenderAddressBook() self.rerenderComboBoxSendFrom() @@ -3675,8 +3693,8 @@ class MyForm(settingsmixin.SMainWindow): account.setEnabled(True) def enableIdentity(self, address): - BMConfigParser().set(address, 'enabled', 'true') - BMConfigParser().save() + config.set(address, 'enabled', 'true') + config.save() shared.reloadMyAddressHashes() self.rerenderAddressBook() @@ -3687,8 +3705,8 @@ class MyForm(settingsmixin.SMainWindow): account.setEnabled(False) def disableIdentity(self, address): - BMConfigParser().set(str(address), 'enabled', 'false') - BMConfigParser().save() + config.set(str(address), 'enabled', 'false') + config.save() shared.reloadMyAddressHashes() self.rerenderAddressBook() @@ -4048,7 +4066,9 @@ class MyForm(settingsmixin.SMainWindow): if column != 0: return # only account names of normal addresses (no chans/mailinglists) - if (not isinstance(item, Ui_AddressWidget)) or (not self.getCurrentTreeWidget()) or self.getCurrentTreeWidget().currentItem() is None: + if (not isinstance(item, Ui_AddressWidget)) or \ + (not self.getCurrentTreeWidget()) or \ + self.getCurrentTreeWidget().currentItem() is None: return # not visible if (not self.getCurrentItem()) or (not isinstance(self.getCurrentItem(), Ui_AddressWidget)): @@ -4178,7 +4198,7 @@ class MyForm(settingsmixin.SMainWindow): # Check to see whether we can connect to namecoin. # Hide the 'Fetch Namecoin ID' button if we can't. - if BMConfigParser().safeGetBoolean( + if config.safeGetBoolean( 'bitmessagesettings', 'dontconnect' ) or self.namecoin.test()[0] == 'failed': logger.warning( @@ -4272,8 +4292,16 @@ def run(): if myapp._firstrun: myapp.showConnectDialog() # ask the user if we may connect +# try: +# if config.get('bitmessagesettings', 'mailchuck') < 1: +# myapp.showMigrationWizard(config.get('bitmessagesettings', 'mailchuck')) +# except: +# myapp.showMigrationWizard(0) + # only show after wizards and connect dialogs have completed - if not BMConfigParser().getboolean('bitmessagesettings', 'startintray'): + if not config.getboolean('bitmessagesettings', 'startintray'): myapp.show() + QtCore.QTimer.singleShot( + 30000, lambda: myapp.setStatusIcon(state.statusIconColor)) app.exec_() diff --git a/src/bitmessageqt/about.ui b/src/bitmessageqt/about.ui index 7073bbd1..49bd4eca 100644 --- a/src/bitmessageqt/about.ui +++ b/src/bitmessageqt/about.ui @@ -46,7 +46,7 @@ - <html><head/><body><p>Copyright © 2012-2016 Jonathan Warren<br/>Copyright © 2012-2020 The Bitmessage Developers</p></body></html> + <html><head/><body><p>Copyright © 2012-2016 Jonathan Warren<br/>Copyright © 2012-2022 The Bitmessage Developers</p></body></html> Qt::AlignLeft diff --git a/src/bitmessageqt/account.py b/src/bitmessageqt/account.py index 852eb836..18586610 100644 --- a/src/bitmessageqt/account.py +++ b/src/bitmessageqt/account.py @@ -10,7 +10,7 @@ import time import queues from addresses import decodeAddress -from bmconfigparser import BMConfigParser +from bmconfigparser import config from helper_ackPayload import genAckPayload from helper_sql import sqlQuery, sqlExecute from foldertree import AccountMixin @@ -18,15 +18,6 @@ from utils import str_broadcast_subscribers from tr import _translate -def getSortedAccounts(): - """Get a sorted list of address config sections""" - configSections = BMConfigParser().addresses() - configSections.sort( - key=lambda item: - BMConfigParser().get(item, 'label').decode('utf-8').lower()) - return configSections - - def getSortedSubscriptions(count=False): """ Actually return a grouped dictionary rather than a sorted list @@ -62,7 +53,7 @@ def getSortedSubscriptions(count=False): def accountClass(address): """Return a BMAccount for the address""" - if not BMConfigParser().has_section(address): + if not config.has_section(address): # .. todo:: This BROADCAST section makes no sense if address == str_broadcast_subscribers: subscription = BroadcastAccount(address) @@ -75,7 +66,7 @@ def accountClass(address): return NoAccount(address) return subscription try: - gateway = BMConfigParser().get(address, "gateway") + gateway = config.get(address, "gateway") for _, cls in inspect.getmembers( sys.modules[__name__], inspect.isclass): if issubclass(cls, GatewayAccount) and cls.gatewayName == gateway: @@ -97,9 +88,9 @@ class AccountColor(AccountMixin): if address_type is None: if address is None: self.type = AccountMixin.ALL - elif BMConfigParser().safeGetBoolean(self.address, 'mailinglist'): + elif config.safeGetBoolean(self.address, 'mailinglist'): self.type = AccountMixin.MAILINGLIST - elif BMConfigParser().safeGetBoolean(self.address, 'chan'): + elif config.safeGetBoolean(self.address, 'chan'): self.type = AccountMixin.CHAN elif sqlQuery( 'SELECT label FROM subscriptions WHERE address=?', @@ -141,10 +132,10 @@ class BMAccount(NoAccount): def __init__(self, address=None): super(BMAccount, self).__init__(address) - if BMConfigParser().has_section(address): - if BMConfigParser().safeGetBoolean(self.address, 'chan'): + if config.has_section(address): + if config.safeGetBoolean(self.address, 'chan'): self.type = AccountMixin.CHAN - elif BMConfigParser().safeGetBoolean(self.address, 'mailinglist'): + elif config.safeGetBoolean(self.address, 'mailinglist'): self.type = AccountMixin.MAILINGLIST elif self.address == str_broadcast_subscribers: self.type = AccountMixin.BROADCAST @@ -156,7 +147,7 @@ class BMAccount(NoAccount): def getLabel(self, address=None): """Get a label for this bitmessage account""" address = super(BMAccount, self).getLabel(address) - label = BMConfigParser().safeGet(address, 'label', address) + label = config.safeGet(address, 'label', address) queryreturn = sqlQuery( 'SELECT label FROM addressbook WHERE address=?', address) if queryreturn: @@ -189,7 +180,7 @@ class GatewayAccount(BMAccount): def send(self): """The send method for gateway accounts""" streamNumber, ripe = decodeAddress(self.toAddress)[2:] - stealthLevel = BMConfigParser().safeGetInt( + stealthLevel = config.safeGetInt( 'bitmessagesettings', 'ackstealthlevel') ackdata = genAckPayload(streamNumber, stealthLevel) sqlExecute( @@ -209,7 +200,7 @@ class GatewayAccount(BMAccount): 'sent', # folder 2, # encodingtype # not necessary to have a TTL higher than 2 days - min(BMConfigParser().getint('bitmessagesettings', 'ttl'), 86400 * 2) + min(config.getint('bitmessagesettings', 'ttl'), 86400 * 2) ) queues.workerQueue.put(('sendmessage', self.toAddress)) diff --git a/src/bitmessageqt/address_dialogs.py b/src/bitmessageqt/address_dialogs.py index 92677f09..92b18ae7 100644 --- a/src/bitmessageqt/address_dialogs.py +++ b/src/bitmessageqt/address_dialogs.py @@ -10,12 +10,12 @@ from PyQt5 import QtGui, QtWidgets import queues import widgets +import state from account import ( - GatewayAccount, MailchuckAccount, AccountMixin, accountClass, - getSortedAccounts + GatewayAccount, MailchuckAccount, AccountMixin, accountClass ) -from addresses import decodeAddress, encodeVarint, addBMIfNotPresent -from inventory import Inventory +from addresses import addBMIfNotPresent, decodeAddress, encodeVarint +from bmconfigparser import config as global_config from tr import _translate @@ -128,7 +128,7 @@ class NewAddressDialog(QtWidgets.QDialog): # Let's fill out the 'existing address' combo box with addresses # from the 'Your Identities' tab. - for address in getSortedAccounts(): + for address in global_config.addresses(True): self.radioButtonExisting.click() self.comboBoxExisting.addItem(address) self.groupBoxDeterministic.setHidden(True) @@ -198,13 +198,13 @@ class NewSubscriptionDialog(AddressDataDialog): " broadcasts." )) else: - Inventory().flush() + state.Inventory.flush() doubleHashOfAddressData = hashlib.sha512(hashlib.sha512( encodeVarint(addressVersion) + encodeVarint(streamNumber) + ripe ).digest()).digest() tag = doubleHashOfAddressData[32:] - self.recent = Inventory().by_type_and_tag(3, tag) + self.recent = state.Inventory.by_type_and_tag(3, tag) count = len(self.recent) if count == 0: self.checkBoxDisplayMessagesAlreadyInInventory.setText( @@ -238,7 +238,7 @@ class SpecialAddressBehaviorDialog(QtWidgets.QDialog): QDialog for special address behaviour (e.g. mailing list functionality) """ - def __init__(self, parent=None, config=None): + def __init__(self, parent=None, config=global_config): super(SpecialAddressBehaviorDialog, self).__init__(parent) widgets.load('specialaddressbehavior.ui', self) self.address = parent.getCurrentAccount() @@ -302,7 +302,7 @@ class SpecialAddressBehaviorDialog(QtWidgets.QDialog): class EmailGatewayDialog(QtWidgets.QDialog): """QDialog for email gateway control""" - def __init__(self, parent, config=None, account=None): + def __init__(self, parent, config=global_config, account=None): super(EmailGatewayDialog, self).__init__(parent) widgets.load('emailgateway.ui', self) self.parent = parent diff --git a/src/bitmessageqt/addressvalidator.py b/src/bitmessageqt/addressvalidator.py index 0b969173..ac1e70bc 100644 --- a/src/bitmessageqt/addressvalidator.py +++ b/src/bitmessageqt/addressvalidator.py @@ -8,9 +8,9 @@ from Queue import Empty from PyQt5 import QtGui -from account import getSortedAccounts from addresses import decodeAddress, addBMIfNotPresent -from queues import addressGeneratorQueue, apiAddressGeneratorReturnQueue +from bmconfigparser import config +from queues import apiAddressGeneratorReturnQueue, addressGeneratorQueue from tr import _translate from utils import str_chan @@ -126,7 +126,7 @@ class AddressPassPhraseValidatorMixin(object): if self.addressMandatory or address: # check if address already exists: - if address in getSortedAccounts(): + if address in config.addresses(True): self.setError(_translate( "AddressValidator", "Address already present as one of your identities." diff --git a/src/bitmessageqt/bitmessageui.py b/src/bitmessageqt/bitmessageui.py index f7a59459..bd888309 100644 --- a/src/bitmessageqt/bitmessageui.py +++ b/src/bitmessageqt/bitmessageui.py @@ -3,7 +3,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets from tr import _translate -from bmconfigparser import BMConfigParser +from bmconfigparser import config from foldertree import AddressBookCompleter from messageview import MessageView from messagecompose import MessageCompose @@ -540,7 +540,7 @@ class Ui_MainWindow(object): self.blackwhitelist = Blacklist() self.tabWidget.addTab(self.blackwhitelist, QtGui.QIcon(":/newPrefix/images/blacklist.png"), "") # Initialize the Blacklist or Whitelist - if BMConfigParser().get('bitmessagesettings', 'blackwhitelist') == 'white': + if config.get('bitmessagesettings', 'blackwhitelist') == 'white': self.blackwhitelist.radioButtonWhitelist.click() self.blackwhitelist.rerenderBlackWhiteList() @@ -642,7 +642,7 @@ class Ui_MainWindow(object): def updateNetworkSwitchMenuLabel(self, dontconnect=None): if dontconnect is None: - dontconnect = BMConfigParser().safeGetBoolean( + dontconnect = config.safeGetBoolean( 'bitmessagesettings', 'dontconnect') self.actionNetworkSwitch.setText( _translate("MainWindow", "Go online", None) @@ -680,16 +680,19 @@ class Ui_MainWindow(object): self.label_3.setText(_translate("MainWindow", "Subject:", None)) self.label_2.setText(_translate("MainWindow", "From:", None)) self.label.setText(_translate("MainWindow", "To:", None)) - #self.textEditMessage.setHtml("") - self.tabWidgetSend.setTabText(self.tabWidgetSend.indexOf(self.sendDirect), _translate("MainWindow", "Send ordinary Message", None)) + self.tabWidgetSend.setTabText( + self.tabWidgetSend.indexOf(self.sendDirect), _translate("MainWindow", "Send ordinary Message", None) + ) self.label_8.setText(_translate("MainWindow", "From:", None)) self.label_7.setText(_translate("MainWindow", "Subject:", None)) - #self.textEditMessageBroadcast.setHtml("") - self.tabWidgetSend.setTabText(self.tabWidgetSend.indexOf(self.sendBroadcast), _translate("MainWindow", "Send Message to your Subscribers", None)) + self.tabWidgetSend.setTabText( + self.tabWidgetSend.indexOf(self.sendBroadcast), + _translate("MainWindow", "Send Message to your Subscribers", None) + ) self.pushButtonTTL.setText(_translate("MainWindow", "TTL:", None)) hours = 48 try: - hours = int(BMConfigParser().getint('bitmessagesettings', 'ttl')/60/60) + hours = int(config.getint('bitmessagesettings', 'ttl') / 60 / 60) except: pass self.labelHumanFriendlyTTLDescription.setText(_translate("MainWindow", "%n hour(s)", None, hours)) @@ -712,7 +715,10 @@ class Ui_MainWindow(object): item.setText(_translate("MainWindow", "Subject", None)) item = self.tableWidgetInboxSubscriptions.horizontalHeaderItem(3) item.setText(_translate("MainWindow", "Received", None)) - self.tabWidget.setTabText(self.tabWidget.indexOf(self.subscriptions), _translate("MainWindow", "Subscriptions", None)) + self.tabWidget.setTabText( + self.tabWidget.indexOf(self.subscriptions), + _translate("MainWindow", "Subscriptions", None) + ) self.treeWidgetChans.headerItem().setText(0, _translate("MainWindow", "Chans", None)) self.pushButtonAddChan.setText(_translate("MainWindow", "Add Chan", None)) self.inboxSearchLineEditChans.setPlaceholderText(_translate("MainWindow", "Search", None)) @@ -732,9 +738,15 @@ class Ui_MainWindow(object): item.setText(_translate("MainWindow", "Received", None)) self.tabWidget.setTabText(self.tabWidget.indexOf(self.chans), _translate("MainWindow", "Chans", None)) self.blackwhitelist.retranslateUi() - self.tabWidget.setTabText(self.tabWidget.indexOf(self.blackwhitelist), _translate("blacklist", "Blacklist", None)) + self.tabWidget.setTabText( + self.tabWidget.indexOf(self.blackwhitelist), + _translate("blacklist", "Blacklist", None) + ) self.networkstatus.retranslateUi() - self.tabWidget.setTabText(self.tabWidget.indexOf(self.networkstatus), _translate("networkstatus", "Network Status", None)) + self.tabWidget.setTabText( + self.tabWidget.indexOf(self.networkstatus), + _translate("networkstatus", "Network Status", None) + ) self.menuFile.setTitle(_translate("MainWindow", "File", None)) self.menuSettings.setTitle(_translate("MainWindow", "Settings", None)) self.menuHelp.setTitle(_translate("MainWindow", "Help", None)) @@ -747,7 +759,9 @@ class Ui_MainWindow(object): self.actionSupport.setText(_translate("MainWindow", "Contact support", None)) self.actionAbout.setText(_translate("MainWindow", "About", None)) self.actionSettings.setText(_translate("MainWindow", "Settings", None)) - self.actionRegenerateDeterministicAddresses.setText(_translate("MainWindow", "Regenerate deterministic addresses", None)) + self.actionRegenerateDeterministicAddresses.setText( + _translate("MainWindow", "Regenerate deterministic addresses", None) + ) self.actionDeleteAllTrashedMessages.setText(_translate("MainWindow", "Delete all trashed messages", None)) self.actionJoinChan.setText(_translate("MainWindow", "Join / Create chan", None)) self.updateNetworkSwitchMenuLabel() diff --git a/src/bitmessageqt/blacklist.py b/src/bitmessageqt/blacklist.py index 0a1a2a3a..52a366ac 100644 --- a/src/bitmessageqt/blacklist.py +++ b/src/bitmessageqt/blacklist.py @@ -2,7 +2,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets import widgets from addresses import addBMIfNotPresent -from bmconfigparser import BMConfigParser +from bmconfigparser import config from dialogs import AddAddressDialog from helper_sql import sqlExecute, sqlQuery from queues import UISignalQueue @@ -39,17 +39,17 @@ class Blacklist(QtWidgets.QWidget, RetranslateMixin): self.rerenderBlackWhiteList) def click_radioButtonBlacklist(self): - if BMConfigParser().get('bitmessagesettings', 'blackwhitelist') == 'white': - BMConfigParser().set('bitmessagesettings', 'blackwhitelist', 'black') - BMConfigParser().save() + if config.get('bitmessagesettings', 'blackwhitelist') == 'white': + config.set('bitmessagesettings', 'blackwhitelist', 'black') + config.save() # self.tableWidgetBlacklist.clearContents() self.tableWidgetBlacklist.setRowCount(0) self.rerenderBlackWhiteList() def click_radioButtonWhitelist(self): - if BMConfigParser().get('bitmessagesettings', 'blackwhitelist') == 'black': - BMConfigParser().set('bitmessagesettings', 'blackwhitelist', 'white') - BMConfigParser().save() + if config.get('bitmessagesettings', 'blackwhitelist') == 'black': + config.set('bitmessagesettings', 'blackwhitelist', 'white') + config.save() # self.tableWidgetBlacklist.clearContents() self.tableWidgetBlacklist.setRowCount(0) self.rerenderBlackWhiteList() @@ -65,7 +65,7 @@ class Blacklist(QtWidgets.QWidget, RetranslateMixin): # address book. The user cannot add it again or else it will # cause problems when updating and deleting the entry. t = (address,) - if BMConfigParser().get('bitmessagesettings', 'blackwhitelist') == 'black': + if config.get('bitmessagesettings', 'blackwhitelist') == 'black': sql = '''select * from blacklist where address=?''' else: sql = '''select * from whitelist where address=?''' @@ -83,7 +83,7 @@ class Blacklist(QtWidgets.QWidget, RetranslateMixin): self.tableWidgetBlacklist.setItem(0, 1, newItem) self.tableWidgetBlacklist.setSortingEnabled(True) t = (self.NewBlacklistDialogInstance.lineEditLabel.text(), address, True) - if BMConfigParser().get('bitmessagesettings', 'blackwhitelist') == 'black': + if config.get('bitmessagesettings', 'blackwhitelist') == 'black': sql = '''INSERT INTO blacklist VALUES (?,?,?)''' else: sql = '''INSERT INTO whitelist VALUES (?,?,?)''' @@ -157,12 +157,12 @@ class Blacklist(QtWidgets.QWidget, RetranslateMixin): def rerenderBlackWhiteList(self): tabs = self.parent().parent() - if BMConfigParser().get('bitmessagesettings', 'blackwhitelist') == 'black': + if config.get('bitmessagesettings', 'blackwhitelist') == 'black': tabs.setTabText(tabs.indexOf(self), _translate('blacklist', 'Blacklist')) else: tabs.setTabText(tabs.indexOf(self), _translate('blacklist', 'Whitelist')) self.tableWidgetBlacklist.setRowCount(0) - listType = BMConfigParser().get('bitmessagesettings', 'blackwhitelist') + listType = config.get('bitmessagesettings', 'blackwhitelist') if listType == 'black': queryreturn = sqlQuery('''SELECT label, address, enabled FROM blacklist''') else: @@ -194,7 +194,7 @@ class Blacklist(QtWidgets.QWidget, RetranslateMixin): currentRow, 0).text() addressAtCurrentRow = self.tableWidgetBlacklist.item( currentRow, 1).text() - if BMConfigParser().get('bitmessagesettings', 'blackwhitelist') == 'black': + if config.get('bitmessagesettings', 'blackwhitelist') == 'black': sqlExecute( '''DELETE FROM blacklist WHERE label=? AND address=?''', labelAtCurrentRow, addressAtCurrentRow) @@ -223,8 +223,7 @@ class Blacklist(QtWidgets.QWidget, RetranslateMixin): QtWidgets.QApplication.palette().text().color()) self.tableWidgetBlacklist.item(currentRow, 1).setForeground( QtWidgets.QApplication.palette().text().color()) - if BMConfigParser().get( - 'bitmessagesettings', 'blackwhitelist') == 'black': + if config.get('bitmessagesettings', 'blackwhitelist') == 'black': sqlExecute( '''UPDATE blacklist SET enabled=1 WHERE address=?''', str(addressAtCurrentRow)) @@ -241,8 +240,7 @@ class Blacklist(QtWidgets.QWidget, RetranslateMixin): QtGui.QColor(128, 128, 128)) self.tableWidgetBlacklist.item(currentRow, 1).setForeground( QtGui.QColor(128, 128, 128)) - if BMConfigParser().get( - 'bitmessagesettings', 'blackwhitelist') == 'black': + if config.get('bitmessagesettings', 'blackwhitelist') == 'black': sqlExecute( '''UPDATE blacklist SET enabled=0 WHERE address=?''', str(addressAtCurrentRow)) else: diff --git a/src/bitmessageqt/dialogs.py b/src/bitmessageqt/dialogs.py index bcceb86a..fc828fef 100644 --- a/src/bitmessageqt/dialogs.py +++ b/src/bitmessageqt/dialogs.py @@ -45,7 +45,7 @@ class AboutDialog(QtWidgets.QDialog): try: self.label_2.setText( self.label_2.text().replace( - '2020', str(last_commit.get('time').year) + '2022', str(last_commit.get('time').year) )) except AttributeError: pass diff --git a/src/bitmessageqt/foldertree.py b/src/bitmessageqt/foldertree.py index 0b64dab4..235aaf5c 100644 --- a/src/bitmessageqt/foldertree.py +++ b/src/bitmessageqt/foldertree.py @@ -8,7 +8,7 @@ from cgi import escape from PyQt5 import QtCore, QtGui, QtWidgets -from bmconfigparser import BMConfigParser +from bmconfigparser import config from helper_sql import sqlExecute, sqlQuery from settingsmixin import SettingsMixin from tr import _translate @@ -107,9 +107,9 @@ class AccountMixin(object): if self.address is None: self.type = self.ALL self.setFlags(self.flags() & ~QtCore.Qt.ItemIsEditable) - elif BMConfigParser().safeGetBoolean(self.address, 'chan'): + elif config.safeGetBoolean(self.address, 'chan'): self.type = self.CHAN - elif BMConfigParser().safeGetBoolean(self.address, 'mailinglist'): + elif config.safeGetBoolean(self.address, 'mailinglist'): self.type = self.MAILINGLIST elif sqlQuery( 'SELECT label FROM subscriptions WHERE address=?', @@ -126,7 +126,7 @@ class AccountMixin(object): AccountMixin.NORMAL, AccountMixin.CHAN, AccountMixin.MAILINGLIST): try: - retval = BMConfigParser().get(self.address, 'label') + retval = config.get(self.address, 'label') except Exception: queryreturn = sqlQuery( 'SELECT label FROM addressbook WHERE address=?', @@ -238,8 +238,7 @@ class Ui_AddressWidget(BMTreeWidgetItem, SettingsMixin): return _translate("MainWindow", "All accounts") try: - return BMConfigParser().get( - self.address, 'label').decode('utf-8', 'ignore') + return config.get(self.address, 'label').decode('utf-8', 'ignore') except: return self.address.decode('utf-8') @@ -267,9 +266,8 @@ class Ui_AddressWidget(BMTreeWidgetItem, SettingsMixin): """ if role == QtCore.Qt.EditRole \ and self.type != AccountMixin.SUBSCRIPTION: - BMConfigParser().set( - str(self.address), 'label', value.encode('utf-8')) - BMConfigParser().save() + config.set(str(self.address), 'label', value.encode('utf-8')) + config.save() return super(Ui_AddressWidget, self).setData(column, role, value) def setAddress(self, address): @@ -379,7 +377,7 @@ class BMAddressWidget(BMTableWidgetItem, AccountMixin): if role == QtCore.Qt.ToolTipRole: return self.label + " (" + self.address + ")" elif role == QtCore.Qt.DecorationRole: - if BMConfigParser().safeGetBoolean( + if config.safeGetBoolean( 'bitmessagesettings', 'useidenticons'): return avatarize(self.address or self.label) elif role == QtCore.Qt.ForegroundRole: @@ -404,7 +402,7 @@ class MessageList_AddressWidget(BMAddressWidget): AccountMixin.NORMAL, AccountMixin.CHAN, AccountMixin.MAILINGLIST): try: - newLabel = BMConfigParser().get(self.address, 'label') + newLabel = config.get(self.address, 'label') except: queryreturn = sqlQuery( 'SELECT label FROM addressbook WHERE address=?', @@ -513,9 +511,9 @@ class Ui_AddressBookWidgetItem(BMAddressWidget): AccountMixin.NORMAL, AccountMixin.MAILINGLIST, AccountMixin.CHAN): try: - BMConfigParser().get(self.address, 'label') - BMConfigParser().set(self.address, 'label', self.label) - BMConfigParser().save() + config.get(self.address, 'label') + config.set(self.address, 'label', self.label) + config.save() except: sqlExecute( 'UPDATE addressbook SET label=? WHERE address=?', diff --git a/src/bitmessageqt/languagebox.py b/src/bitmessageqt/languagebox.py index d6b6fa4a..1fd69334 100644 --- a/src/bitmessageqt/languagebox.py +++ b/src/bitmessageqt/languagebox.py @@ -6,7 +6,7 @@ import os from PyQt5 import QtCore, QtWidgets import paths -from bmconfigparser import BMConfigParser +from bmconfigparser import config from tr import _translate @@ -49,7 +49,7 @@ class LanguageBox(QtWidgets.QComboBox): self.addItem( locale.nativeLanguageName() or localeShort, localeShort) - configuredLocale = BMConfigParser().safeGet( + configuredLocale = config.safeGet( 'bitmessagesettings', 'userlocale', 'system') for i in range(self.count()): if self.itemData(i) == configuredLocale: diff --git a/src/bitmessageqt/migrationwizard.py b/src/bitmessageqt/migrationwizard.py index 6e80f1dc..2bc32849 100644 --- a/src/bitmessageqt/migrationwizard.py +++ b/src/bitmessageqt/migrationwizard.py @@ -1,16 +1,16 @@ #!/usr/bin/env python2.7 -from PyQt4 import QtCore, QtGui +from PyQt5 import QtCore, QtWidgets -class MigrationWizardIntroPage(QtGui.QWizardPage): +class MigrationWizardIntroPage(QtWidgets.QWizardPage): def __init__(self): - super(QtGui.QWizardPage, self).__init__() + super(QtWidgets.QWizardPage, self).__init__() self.setTitle("Migrating configuration") - label = QtGui.QLabel("This wizard will help you to migrate your configuration. " + label = QtWidgets.QLabel("This wizard will help you to migrate your configuration. " "You can still keep using PyBitMessage once you migrate, the changes are backwards compatible.") label.setWordWrap(True) - layout = QtGui.QVBoxLayout() + layout = QtWidgets.QVBoxLayout() layout.addWidget(label) self.setLayout(layout) @@ -18,15 +18,15 @@ class MigrationWizardIntroPage(QtGui.QWizardPage): return 1 -class MigrationWizardAddressesPage(QtGui.QWizardPage): +class MigrationWizardAddressesPage(QtWidgets.QWizardPage): def __init__(self, addresses): - super(QtGui.QWizardPage, self).__init__() + super(QtWidgets.QWizardPage, self).__init__() self.setTitle("Addresses") - label = QtGui.QLabel("Please select addresses that you are already using with mailchuck. ") + label = QtWidgets.QLabel("Please select addresses that you are already using with mailchuck. ") label.setWordWrap(True) - layout = QtGui.QVBoxLayout() + layout = QtWidgets.QVBoxLayout() layout.addWidget(label) self.setLayout(layout) @@ -34,15 +34,15 @@ class MigrationWizardAddressesPage(QtGui.QWizardPage): return 10 -class MigrationWizardGPUPage(QtGui.QWizardPage): +class MigrationWizardGPUPage(QtWidgets.QWizardPage): def __init__(self): - super(QtGui.QWizardPage, self).__init__() + super(QtWidgets.QWizardPage, self).__init__() self.setTitle("GPU") - label = QtGui.QLabel("Are you using a GPU? ") + label = QtWidgets.QLabel("Are you using a GPU? ") label.setWordWrap(True) - layout = QtGui.QVBoxLayout() + layout = QtWidgets.QVBoxLayout() layout.addWidget(label) self.setLayout(layout) @@ -50,22 +50,22 @@ class MigrationWizardGPUPage(QtGui.QWizardPage): return 10 -class MigrationWizardConclusionPage(QtGui.QWizardPage): +class MigrationWizardConclusionPage(QtWidgets.QWizardPage): def __init__(self): - super(QtGui.QWizardPage, self).__init__() + super(QtWidgets.QWizardPage, self).__init__() self.setTitle("All done!") - label = QtGui.QLabel("You successfully migrated.") + label = QtWidgets.QLabel("You successfully migrated.") label.setWordWrap(True) - layout = QtGui.QVBoxLayout() + layout = QtWidgets.QVBoxLayout() layout.addWidget(label) self.setLayout(layout) -class Ui_MigrationWizard(QtGui.QWizard): +class Ui_MigrationWizard(QtWidgets.QWizard): def __init__(self, addresses): - super(QtGui.QWizard, self).__init__() + super(QtWidgets.QWizard, self).__init__() self.pages = {} @@ -81,4 +81,4 @@ class Ui_MigrationWizard(QtGui.QWizard): self.setWindowTitle("Migration from PyBitMessage wizard") self.adjustSize() - self.show() \ No newline at end of file + self.show() diff --git a/src/bitmessageqt/networkstatus.py b/src/bitmessageqt/networkstatus.py index ef1265af..318d44ab 100644 --- a/src/bitmessageqt/networkstatus.py +++ b/src/bitmessageqt/networkstatus.py @@ -10,8 +10,7 @@ import l10n import network.stats import state import widgets -from inventory import Inventory -from network import BMConnectionPool, knownnodes +from network import connectionpool, knownnodes from retranslateui import RetranslateMixin from tr import _translate from uisignaler import UISignaler @@ -49,7 +48,7 @@ class NetworkStatus(QtWidgets.QWidget, RetranslateMixin): def startUpdate(self): """Start a timer to update counters every 2 seconds""" - Inventory().numberOfInventoryLookupsPerformed = 0 + state.Inventory.numberOfInventoryLookupsPerformed = 0 self.runEveryTwoSeconds() self.timer.start(2000) # milliseconds @@ -123,16 +122,16 @@ class NetworkStatus(QtWidgets.QWidget, RetranslateMixin): # pylint: disable=too-many-branches,undefined-variable if outbound: try: - c = BMConnectionPool().outboundConnections[destination] + c = connectionpool.pool.outboundConnections[destination] except KeyError: if add: return else: try: - c = BMConnectionPool().inboundConnections[destination] + c = connectionpool.pool.inboundConnections[destination] except KeyError: try: - c = BMConnectionPool().inboundConnections[destination.host] + c = connectionpool.pool.inboundConnections[destination.host] except KeyError: if add: return @@ -172,7 +171,7 @@ class NetworkStatus(QtWidgets.QWidget, RetranslateMixin): self.tableWidgetConnectionCount.item(0, 1).setData( QtCore.Qt.UserRole, outbound) else: - if not BMConnectionPool().inboundConnections: + if not connectionpool.pool.inboundConnections: self.window().setStatusIcon('yellow') for i in range(self.tableWidgetConnectionCount.rowCount()): if self.tableWidgetConnectionCount.item(i, 0).data( @@ -203,8 +202,8 @@ class NetworkStatus(QtWidgets.QWidget, RetranslateMixin): """Updates counters, runs every 2 seconds if the timer is running""" self.labelLookupsPerSecond.setText(_translate( "networkstatus", "Inventory lookups per second: {0}" - ).format(Inventory().numberOfInventoryLookupsPerformed / 2)) - Inventory().numberOfInventoryLookupsPerformed = 0 + ).format(state.Inventory.numberOfInventoryLookupsPerformed / 2)) + state.Inventory.numberOfInventoryLookupsPerformed = 0 self.updateNumberOfBytes() self.updateNumberOfObjectsToBeSynced() diff --git a/src/bitmessageqt/newaddressdialog.ui b/src/bitmessageqt/newaddressdialog.ui index 0f8c33d7..8a7cc6ae 100644 --- a/src/bitmessageqt/newaddressdialog.ui +++ b/src/bitmessageqt/newaddressdialog.ui @@ -309,9 +309,10 @@ The 'Random Number' option is selected by default but deterministic addresses ha - radioButtonRandomAddress - radioButtonDeterministicAddress newaddresslabel + buttonBox + radioButtonDeterministicAddress + radioButtonRandomAddress radioButtonMostAvailable radioButtonExisting comboBoxExisting @@ -319,7 +320,6 @@ The 'Random Number' option is selected by default but deterministic addresses ha lineEditPassphraseAgain spinBoxNumberOfAddressesToMake checkBoxEighteenByteRipe - buttonBox diff --git a/src/bitmessageqt/settings.py b/src/bitmessageqt/settings.py index a6353131..34c9c5d0 100644 --- a/src/bitmessageqt/settings.py +++ b/src/bitmessageqt/settings.py @@ -8,6 +8,7 @@ import sys import tempfile from PyQt5 import QtCore, QtGui, QtWidgets +import six import debug import defaults @@ -17,10 +18,11 @@ import paths import queues import state import widgets -from bmconfigparser import BMConfigParser +from bmconfigparser import config as config_obj from helper_sql import sqlExecute, sqlStoredProcedure from helper_startup import start_proxyconfig -from network import knownnodes, AnnounceThread +from network import connectionpool, knownnodes +from network.announcethread import AnnounceThread from network.asyncore_pollchoose import set_rates from tr import _translate @@ -46,7 +48,7 @@ class SettingsDialog(QtWidgets.QDialog): self.parent = parent self.firstrun = firstrun - self.config = BMConfigParser() + self.config = config_obj self.net_restart_needed = False self.timer = QtCore.QTimer() @@ -161,6 +163,26 @@ class SettingsDialog(QtWidgets.QDialog): else self.comboBoxProxyType.findText(self._proxy_type)) self.comboBoxProxyTypeChanged(self.comboBoxProxyType.currentIndex()) + if self._proxy_type: + for node, info in six.iteritems( + knownnodes.knownNodes.get( + min(connectionpool.pool.streams), []) + ): + if ( + node.host.endswith('.onion') and len(node.host) > 22 + and not info.get('self') + ): + break + else: + if self.checkBoxOnionOnly.isChecked(): + self.checkBoxOnionOnly.setText( + self.checkBoxOnionOnly.text() + ", " + _translate( + "MainWindow", "may cause connection problems!")) + self.checkBoxOnionOnly.setStyleSheet( + "QCheckBox { color : red; }") + else: + self.checkBoxOnionOnly.setEnabled(False) + self.lineEditSocksHostname.setText( config.get('bitmessagesettings', 'sockshostname')) self.lineEditSocksPort.setText(str( diff --git a/src/bitmessageqt/support.py b/src/bitmessageqt/support.py index 08125446..5d455e59 100644 --- a/src/bitmessageqt/support.py +++ b/src/bitmessageqt/support.py @@ -13,15 +13,15 @@ import paths import proofofwork import queues import state -from bmconfigparser import BMConfigParser +from bmconfigparser import config from foldertree import AccountMixin from helper_sql import sqlExecute, sqlQuery from l10n import getTranslationLanguage from openclpow import openclEnabled from pyelliptic.openssl import OpenSSL from settings import getSOCKSProxyType -from tr import _translate from version import softwareVersion +from tr import _translate # this is BM support address going to Peter Surda @@ -81,9 +81,9 @@ def checkAddressBook(myapp): def checkHasNormalAddress(): """Returns first enabled normal address or False if not found.""" - for address in account.getSortedAccounts(): + for address in config.addresses(True): acct = account.accountClass(address) - if acct.type == AccountMixin.NORMAL and BMConfigParser().safeGetBoolean( + if acct.type == AccountMixin.NORMAL and config.safeGetBoolean( address, 'enabled'): return address return False @@ -153,11 +153,11 @@ def createSupportMessage(myapp): portablemode = str(state.appdata == paths.lookupExeFolder()) cpow = "True" if proofofwork.bmpow else "False" openclpow = str( - BMConfigParser().safeGet('bitmessagesettings', 'opencl') + config.safeGet('bitmessagesettings', 'opencl') ) if openclEnabled() else "None" locale = getTranslationLanguage() - socks = getSOCKSProxyType(BMConfigParser()) or 'N/A' - upnp = BMConfigParser().safeGet('bitmessagesettings', 'upnp', 'N/A') + socks = getSOCKSProxyType(config) or 'N/A' + upnp = config.safeGet('bitmessagesettings', 'upnp', 'N/A') connectedhosts = len(network.stats.connectedHostsList()) myapp.ui.textEditMessage.setText(SUPPORT_MESSAGE.format( diff --git a/src/bitmessageqt/tests/settings.py b/src/bitmessageqt/tests/settings.py index c0708b5c..0dcf8cf3 100644 --- a/src/bitmessageqt/tests/settings.py +++ b/src/bitmessageqt/tests/settings.py @@ -2,7 +2,7 @@ import threading import time from main import TestBase -from bmconfigparser import BMConfigParser +from bmconfigparser import config from bitmessageqt import settings @@ -14,14 +14,14 @@ class TestSettings(TestBase): def test_udp(self): """Test the effect of checkBoxUDP""" - udp_setting = BMConfigParser().safeGetBoolean( + udp_setting = config.safeGetBoolean( 'bitmessagesettings', 'udp') self.assertEqual(udp_setting, self.dialog.checkBoxUDP.isChecked()) self.dialog.checkBoxUDP.setChecked(not udp_setting) self.dialog.accept() self.assertEqual( not udp_setting, - BMConfigParser().safeGetBoolean('bitmessagesettings', 'udp')) + config.safeGetBoolean('bitmessagesettings', 'udp')) time.sleep(5) for thread in threading.enumerate(): if thread.name == 'Announcer': # find Announcer thread diff --git a/src/bitmessageqt/uisignaler.py b/src/bitmessageqt/uisignaler.py index 7efda169..6b72344e 100644 --- a/src/bitmessageqt/uisignaler.py +++ b/src/bitmessageqt/uisignaler.py @@ -1,4 +1,3 @@ - import sys from PyQt5 import QtCore diff --git a/src/bitmessageqt/utils.py b/src/bitmessageqt/utils.py index a4f035c7..cb5aedcb 100644 --- a/src/bitmessageqt/utils.py +++ b/src/bitmessageqt/utils.py @@ -5,7 +5,7 @@ from PyQt5 import QtGui import state from addresses import addBMIfNotPresent -from bmconfigparser import BMConfigParser +from bmconfigparser import config str_broadcast_subscribers = '[Broadcast subscribers]' str_chan = '[chan]' @@ -14,14 +14,14 @@ str_chan = '[chan]' def identiconize(address): size = 48 - if not BMConfigParser().getboolean('bitmessagesettings', 'useidenticons'): + if not config.getboolean('bitmessagesettings', 'useidenticons'): return QtGui.QIcon() # If you include another identicon library, please generate an # example identicon with the following md5 hash: # 3fd4bf901b9d4ea1394f0fb358725b28 - identicon_lib = BMConfigParser().safeGet( + identicon_lib = config.safeGet( 'bitmessagesettings', 'identiconlib', 'qidenticon_two_x') # As an 'identiconsuffix' you could put "@bitmessge.ch" or "@bm.addr" @@ -30,7 +30,7 @@ def identiconize(address): # It can be used as a pseudo-password to salt the generation of # the identicons to decrease the risk of attacks where someone creates # an address to mimic someone else's identicon. - data = addBMIfNotPresent(address) + BMConfigParser().get( + data = addBMIfNotPresent(address) + config.get( 'bitmessagesettings', 'identiconsuffix') if identicon_lib[:len('qidenticon')] == 'qidenticon': # originally by: diff --git a/src/bmconfigparser.py b/src/bmconfigparser.py index 4798dda4..c3a4b201 100644 --- a/src/bmconfigparser.py +++ b/src/bmconfigparser.py @@ -4,7 +4,7 @@ BMConfigParser class definition and default configuration settings import os import shutil -import sys # FIXME: bad style! write more generally +from threading import Event from datetime import datetime from six import string_types @@ -12,47 +12,13 @@ from six.moves import configparser try: import state - from singleton import Singleton except ImportError: from pybitmessage import state - from pybitmessage.singleton import Singleton SafeConfigParser = configparser.SafeConfigParser +config_ready = Event() -BMConfigDefaults = { - "bitmessagesettings": { - "maxaddrperstreamsend": 500, - "maxbootstrapconnections": 20, - "maxdownloadrate": 0, - "maxoutboundconnections": 8, - "maxtotalconnections": 200, - "maxuploadrate": 0, - "apiinterface": "127.0.0.1", - "apiport": 8442, - "udp": "True" - }, - "threads": { - "receive": 3, - }, - "network": { - "bind": "", - "dandelion": 90, - }, - "inventory": { - "storage": "sqlite", - "acceptmismatch": "False", - }, - "knownnodes": { - "maxnodes": 20000, - }, - "zlib": { - "maxsize": 1048576 - } -} - - -@Singleton class BMConfigParser(SafeConfigParser): """ Singleton class inherited from :class:`ConfigParser.SafeConfigParser` @@ -69,47 +35,14 @@ class BMConfigParser(SafeConfigParser): raise ValueError("Invalid value %s" % value) return SafeConfigParser.set(self, section, option, value) - def get(self, section, option, raw=False, vars=None): - if sys.version_info[0] == 3: - # pylint: disable=arguments-differ - try: - if section == "bitmessagesettings" and option == "timeformat": - return SafeConfigParser.get( - self, section, option, raw=True, vars=vars) - try: - return self._temp[section][option] - except KeyError: - pass - return SafeConfigParser.get( - self, section, option, raw=True, vars=vars) - except configparser.InterpolationError: - return SafeConfigParser.get( - self, section, option, raw=True, vars=vars) - except (configparser.NoSectionError, configparser.NoOptionError) as e: - try: - return BMConfigDefaults[section][option] - except (KeyError, ValueError, AttributeError): - raise e - else: - # pylint: disable=arguments-differ - try: - if section == "bitmessagesettings" and option == "timeformat": - return SafeConfigParser.get( - self, section, option, raw, vars) - try: - return self._temp[section][option] - except KeyError: - pass - return SafeConfigParser.get( - self, section, option, True, vars) - except configparser.InterpolationError: - return SafeConfigParser.get( - self, section, option, True, vars) - except (configparser.NoSectionError, configparser.NoOptionError) as e: - try: - return BMConfigDefaults[section][option] - except (KeyError, ValueError, AttributeError): - raise e + def get(self, section, option, **kwargs): + """Try returning temporary value before using parent get()""" + try: + return self._temp[section][option] + except KeyError: + pass + return SafeConfigParser.get( + self, section, option, **kwargs) def setTemp(self, section, option, value=None): """Temporary set option to value, not saving.""" @@ -118,41 +51,36 @@ class BMConfigParser(SafeConfigParser): except KeyError: self._temp[section] = {option: value} - def safeGetBoolean(self, section, field): + def safeGetBoolean(self, section, option): """Return value as boolean, False on exceptions""" try: - # Used in the python2.7 - # return self.getboolean(section, field) - # Used in the python3.5.2 - # print(config, section, field) - return self.getboolean(section, field) + return self.getboolean(section, option) except (configparser.NoSectionError, configparser.NoOptionError, ValueError, AttributeError): return False - def safeGetInt(self, section, field, default=0): + def safeGetInt(self, section, option, default=0): """Return value as integer, default on exceptions, 0 if default missing""" try: - # Used in the python2.7 - # return self.getint(section, field) - # Used in the python3.7.0 - return int(self.get(section, field)) + return int(self.get(section, option)) except (configparser.NoSectionError, configparser.NoOptionError, ValueError, AttributeError): return default - def safeGetFloat(self, section, field, default=0.0): + def safeGetFloat(self, section, option, default=0.0): """Return value as float, default on exceptions, 0.0 if default missing""" try: - return self.getfloat(section, field) + return self.getfloat(section, option) except (configparser.NoSectionError, configparser.NoOptionError, ValueError, AttributeError): return default def safeGet(self, section, option, default=None): - """Return value as is, default on exceptions, None if default missing""" + """ + Return value as is, default on exceptions, None if default missing + """ try: return self.get(section, option) except (configparser.NoSectionError, configparser.NoOptionError, @@ -166,69 +94,29 @@ class BMConfigParser(SafeConfigParser): return SafeConfigParser.items(self, section, True, variables) def _reset(self): - """Reset current config. There doesn't appear to be a built in - method for this""" + """ + Reset current config. + There doesn't appear to be a built in method for this. + """ + self._temp = {} sections = self.sections() for x in sections: self.remove_section(x) - if sys.version_info[0] == 3: - @staticmethod - def addresses(hidden=False): - """Return a list of local bitmessage addresses (from section labels)""" - return [x for x in BMConfigParser().sections() if x.startswith('BM-') and ( - hidden or not BMConfigParser().safeGetBoolean(x, 'hidden'))] - - def read(self, filenames): - self._reset() + def read(self, filenames=None): + self._reset() + SafeConfigParser.read( + self, os.path.join(os.path.dirname(__file__), 'default.ini')) + if filenames: SafeConfigParser.read(self, filenames) - for section in self.sections(): - for option in self.options(section): - try: - if not self.validate( - section, option, - self[section][option] - ): - try: - newVal = BMConfigDefaults[section][option] - except configparser.NoSectionError: - continue - except KeyError: - continue - SafeConfigParser.set( - self, section, option, newVal) - except configparser.InterpolationError: - continue - def readfp(self, fp, filename=None): - # pylint: disable=no-member - SafeConfigParser.read_file(self, fp) - else: - @staticmethod - def addresses(): - """Return a list of local bitmessage addresses (from section labels)""" - return [ - x for x in BMConfigParser().sections() if x.startswith('BM-')] - - def read(self, filenames): - """Read config and populate defaults""" - self._reset() - SafeConfigParser.read(self, filenames) - for section in self.sections(): - for option in self.options(section): - try: - if not self.validate( - section, option, - SafeConfigParser.get(self, section, option) - ): - try: - newVal = BMConfigDefaults[section][option] - except KeyError: - continue - SafeConfigParser.set( - self, section, option, newVal) - except configparser.InterpolationError: - continue + def addresses(self, sort=False): + """Return a list of local bitmessage addresses (from section labels)""" + sections = [x for x in self.sections() if x.startswith('BM-')] + if sort: + sections.sort(key=lambda item: self.get(item, 'label') \ + .decode('utf-8').lower()) + return sections def save(self): """Save the runtime config onto the filesystem""" @@ -241,7 +129,7 @@ class BMConfigParser(SafeConfigParser): shutil.copyfile(fileName, fileNameBak) # The backup succeeded. fileNameExisted = True - except (IOError, Exception): + except(IOError, Exception): # The backup failed. This can happen if the file # didn't exist before. fileNameExisted = False @@ -269,3 +157,24 @@ class BMConfigParser(SafeConfigParser): if value < 0 or value > 8: return False return True + + def search_addresses(self, address, searched_text): + """Return the searched label of MyAddress""" + return [x for x in [self.get(address, 'label').lower(), + address.lower()] if searched_text in x] + + def disable_address(self, address): + """"Disabling the specific Address""" + self.set(str(address), 'enabled', 'false') + self.save() + + def enable_address(self, address): + """"Enabling the specific Address""" + self.set(address, 'enabled', 'true') + self.save() + + +if not getattr(BMConfigParser, 'read_file', False): + BMConfigParser.read_file = BMConfigParser.readfp + +config = BMConfigParser() # TODO: remove this crutch diff --git a/src/build_osx.py b/src/build_osx.py index 83d2f280..d83e9b9b 100644 --- a/src/build_osx.py +++ b/src/build_osx.py @@ -9,7 +9,8 @@ version = os.getenv("PYBITMESSAGEVERSION", "custom") mainscript = ["bitmessagemain.py"] DATA_FILES = [ - ('', ['sslkeys', 'images']), + ('', ['sslkeys', 'images', 'default.ini']), + ('sql', glob('sql/*.sql')), ('bitmsghash', ['bitmsghash/bitmsghash.cl', 'bitmsghash/bitmsghash.so']), ('translations', glob('translations/*.qm')), ('ui', glob('bitmessageqt/*.ui')), diff --git a/src/class_addressGenerator.py b/src/class_addressGenerator.py index 1fa0ce0e..4927b333 100644 --- a/src/class_addressGenerator.py +++ b/src/class_addressGenerator.py @@ -1,23 +1,21 @@ """ addressGenerator thread class definition """ -import hashlib + import time from binascii import hexlify +from six.moves import configparser, queue + import defaults import highlevelcrypto import queues import shared import state -import tr from addresses import decodeAddress, encodeAddress, encodeVarint -from bmconfigparser import BMConfigParser -from fallback import RIPEMD160Hash +from bmconfigparser import config from network import StoppableThread -from pyelliptic import arithmetic -from pyelliptic.openssl import OpenSSL -from six.moves import configparser, queue +from tr import _translate class AddressGeneratorException(Exception): @@ -31,6 +29,7 @@ class addressGenerator(StoppableThread): name = "addressGenerator" def stopThread(self): + """Tell the thread to stop putting a special command to it's queue""" try: queues.addressGeneratorQueue.put(("stopThread", "data")) except queue.Full: @@ -43,8 +42,7 @@ class addressGenerator(StoppableThread): Process the requests for addresses generation from `.queues.addressGeneratorQueue` """ - # pylint: disable=too-many-locals, too-many-branches - # pylint: disable=protected-access, too-many-statements + # pylint: disable=too-many-locals,too-many-branches,too-many-statements # pylint: disable=too-many-nested-blocks while state.shutdown == 0: @@ -72,7 +70,7 @@ class addressGenerator(StoppableThread): eighteenByteRipe = queueValue numberOfNullBytesDemandedOnFrontOfRipeHash = \ - BMConfigParser().safeGetInt( + config.safeGetInt( 'bitmessagesettings', 'numberofnullbytesonaddress', 2 if eighteenByteRipe else 1 @@ -84,7 +82,7 @@ class addressGenerator(StoppableThread): payloadLengthExtraBytes = queueValue numberOfNullBytesDemandedOnFrontOfRipeHash = \ - BMConfigParser().safeGetInt( + config.safeGetInt( 'bitmessagesettings', 'numberofnullbytesonaddress', 2 if eighteenByteRipe else 1 @@ -103,14 +101,14 @@ class addressGenerator(StoppableThread): ' one version %s address which it cannot do.\n', addressVersionNumber) if nonceTrialsPerByte == 0: - nonceTrialsPerByte = BMConfigParser().getint( + nonceTrialsPerByte = config.getint( 'bitmessagesettings', 'defaultnoncetrialsperbyte') if nonceTrialsPerByte < \ defaults.networkDefaultProofOfWorkNonceTrialsPerByte: nonceTrialsPerByte = \ defaults.networkDefaultProofOfWorkNonceTrialsPerByte if payloadLengthExtraBytes == 0: - payloadLengthExtraBytes = BMConfigParser().getint( + payloadLengthExtraBytes = config.getint( 'bitmessagesettings', 'defaultpayloadlengthextrabytes') if payloadLengthExtraBytes < \ defaults.networkDefaultPayloadLengthExtraBytes: @@ -119,7 +117,7 @@ class addressGenerator(StoppableThread): if command == 'createRandomAddress': queues.UISignalQueue.put(( 'updateStatusBar', - tr._translate( + _translate( "MainWindow", "Generating one new address") )) # This next section is a little bit strange. We're going @@ -129,18 +127,13 @@ class addressGenerator(StoppableThread): # the \x00 or \x00\x00 bytes thus making the address shorter. startTime = time.time() numberOfAddressesWeHadToMakeBeforeWeFoundOneWithTheCorrectRipePrefix = 0 - potentialPrivSigningKey = OpenSSL.rand(32) - potentialPubSigningKey = highlevelcrypto.pointMult( - potentialPrivSigningKey) + privSigningKey, pubSigningKey = highlevelcrypto.random_keys() while True: numberOfAddressesWeHadToMakeBeforeWeFoundOneWithTheCorrectRipePrefix += 1 - potentialPrivEncryptionKey = OpenSSL.rand(32) - potentialPubEncryptionKey = highlevelcrypto.pointMult( - potentialPrivEncryptionKey) - sha = hashlib.new('sha512') - sha.update( - potentialPubSigningKey + potentialPubEncryptionKey) - ripe = RIPEMD160Hash(sha.digest()).digest() + potentialPrivEncryptionKey, potentialPubEncryptionKey = \ + highlevelcrypto.random_keys() + ripe = highlevelcrypto.to_ripe( + pubSigningKey, potentialPubEncryptionKey) if ( ripe[:numberOfNullBytesDemandedOnFrontOfRipeHash] == b'\x00' * numberOfNullBytesDemandedOnFrontOfRipeHash @@ -163,34 +156,25 @@ class addressGenerator(StoppableThread): address = encodeAddress( addressVersionNumber, streamNumber, ripe) - # An excellent way for us to store our keys - # is in Wallet Import Format. Let us convert now. - # https://en.bitcoin.it/wiki/Wallet_import_format - privSigningKey = b'\x80' + potentialPrivSigningKey - checksum = hashlib.sha256(hashlib.sha256( - privSigningKey).digest()).digest()[0:4] - privSigningKeyWIF = arithmetic.changebase( - privSigningKey + checksum, 256, 58) + privSigningKeyWIF = highlevelcrypto.encodeWalletImportFormat( + privSigningKey) + privEncryptionKeyWIF = highlevelcrypto.encodeWalletImportFormat( + potentialPrivEncryptionKey) - privEncryptionKey = b'\x80' + potentialPrivEncryptionKey - checksum = hashlib.sha256(hashlib.sha256( - privEncryptionKey).digest()).digest()[0:4] - privEncryptionKeyWIF = arithmetic.changebase( - privEncryptionKey + checksum, 256, 58) - - BMConfigParser().add_section(address) - BMConfigParser().set(address, 'label', label) - BMConfigParser().set(address, 'enabled', 'true') - BMConfigParser().set(address, 'decoy', 'false') - BMConfigParser().set(address, 'noncetrialsperbyte', str( + config.add_section(address) + config.set(address, 'label', label) + config.set(address, 'enabled', 'true') + config.set(address, 'decoy', 'false') + config.set(address, 'noncetrialsperbyte', str( nonceTrialsPerByte)) - BMConfigParser().set(address, 'payloadlengthextrabytes', str( + config.set(address, 'payloadlengthextrabytes', str( payloadLengthExtraBytes)) - BMConfigParser().set( - address, 'privsigningkey', privSigningKeyWIF) - BMConfigParser().set( - address, 'privencryptionkey', privEncryptionKeyWIF) - BMConfigParser().save() + config.set( + address, 'privsigningkey', privSigningKeyWIF.decode()) + config.set( + address, 'privencryptionkey', + privEncryptionKeyWIF.decode()) + config.save() # The API and the join and create Chan functionality # both need information back from the address generator. @@ -198,7 +182,7 @@ class addressGenerator(StoppableThread): queues.UISignalQueue.put(( 'updateStatusBar', - tr._translate( + _translate( "MainWindow", "Done generating address. Doing work necessary" " to broadcast it...") @@ -214,10 +198,10 @@ class addressGenerator(StoppableThread): 'sendOutOrStoreMyV4Pubkey', address)) elif command in ( - 'createDeterministicAddresses', - 'getDeterministicAddress', 'createChan', 'joinChan' + 'createDeterministicAddresses', 'createChan', + 'getDeterministicAddress', 'joinChan' ): - if len(deterministicPassphrase) == 0: + if not deterministicPassphrase: self.logger.warning( 'You are creating deterministic' ' address(es) using a blank passphrase.' @@ -225,8 +209,9 @@ class addressGenerator(StoppableThread): if command == 'createDeterministicAddresses': queues.UISignalQueue.put(( 'updateStatusBar', - tr._translate( - "MainWindow", "Generating {0} new addresses." + _translate( + "MainWindow", + "Generating {0} new addresses." ).format(str(numberOfAddressesToMake)) )) signingKeyNonce = 0 @@ -246,24 +231,19 @@ class addressGenerator(StoppableThread): numberOfAddressesWeHadToMakeBeforeWeFoundOneWithTheCorrectRipePrefix = 0 while True: numberOfAddressesWeHadToMakeBeforeWeFoundOneWithTheCorrectRipePrefix += 1 - potentialPrivSigningKey = hashlib.sha512( - deterministicPassphrase - + encodeVarint(signingKeyNonce) - ).digest()[:32] - potentialPrivEncryptionKey = hashlib.sha512( - deterministicPassphrase - + encodeVarint(encryptionKeyNonce) - ).digest()[:32] - potentialPubSigningKey = highlevelcrypto.pointMult( - potentialPrivSigningKey) - potentialPubEncryptionKey = highlevelcrypto.pointMult( - potentialPrivEncryptionKey) + potentialPrivSigningKey, potentialPubSigningKey = \ + highlevelcrypto.deterministic_keys( + deterministicPassphrase, + encodeVarint(signingKeyNonce)) + potentialPrivEncryptionKey, potentialPubEncryptionKey = \ + highlevelcrypto.deterministic_keys( + deterministicPassphrase, + encodeVarint(encryptionKeyNonce)) + signingKeyNonce += 2 encryptionKeyNonce += 2 - sha = hashlib.new('sha512') - sha.update( - potentialPubSigningKey + potentialPubEncryptionKey) - ripe = RIPEMD160Hash(sha.digest()).digest() + ripe = highlevelcrypto.to_ripe( + potentialPubSigningKey, potentialPubEncryptionKey) if ( ripe[:numberOfNullBytesDemandedOnFrontOfRipeHash] == b'\x00' * numberOfNullBytesDemandedOnFrontOfRipeHash @@ -300,24 +280,15 @@ class addressGenerator(StoppableThread): saveAddressToDisk = False if saveAddressToDisk and live: - # An excellent way for us to store our keys is - # in Wallet Import Format. Let us convert now. - # https://en.bitcoin.it/wiki/Wallet_import_format - privSigningKey = b'\x80' + potentialPrivSigningKey - checksum = hashlib.sha256(hashlib.sha256( - privSigningKey).digest()).digest()[0:4] - privSigningKeyWIF = arithmetic.changebase( - privSigningKey + checksum, 256, 58) - - privEncryptionKey = b'\x80' + \ - potentialPrivEncryptionKey - checksum = hashlib.sha256(hashlib.sha256( - privEncryptionKey).digest()).digest()[0:4] - privEncryptionKeyWIF = arithmetic.changebase( - privEncryptionKey + checksum, 256, 58) + privSigningKeyWIF = \ + highlevelcrypto.encodeWalletImportFormat( + potentialPrivSigningKey) + privEncryptionKeyWIF = \ + highlevelcrypto.encodeWalletImportFormat( + potentialPrivEncryptionKey) try: - BMConfigParser().add_section(address) + config.add_section(address) addressAlreadyExists = False except configparser.DuplicateSectionError: addressAlreadyExists = True @@ -329,7 +300,7 @@ class addressGenerator(StoppableThread): ) queues.UISignalQueue.put(( 'updateStatusBar', - tr._translate( + _translate( "MainWindow", "{0} is already in 'Your Identities'." " Not adding it again." @@ -337,24 +308,24 @@ class addressGenerator(StoppableThread): )) else: self.logger.debug('label: %s', label) - BMConfigParser().set(address, 'label', label) - BMConfigParser().set(address, 'enabled', 'true') - BMConfigParser().set(address, 'decoy', 'false') - if command in ('joinChan', 'createChan'): - BMConfigParser().set(address, 'chan', 'true') - BMConfigParser().set( + config.set(address, 'label', label) + config.set(address, 'enabled', 'true') + config.set(address, 'decoy', 'false') + if command in ('createChan', 'joinChan'): + config.set(address, 'chan', 'true') + config.set( address, 'noncetrialsperbyte', str(nonceTrialsPerByte)) - BMConfigParser().set( + config.set( address, 'payloadlengthextrabytes', str(payloadLengthExtraBytes)) - BMConfigParser().set( - address, 'privSigningKey', - privSigningKeyWIF) - BMConfigParser().set( - address, 'privEncryptionKey', - privEncryptionKeyWIF) - BMConfigParser().save() + config.set( + address, 'privsigningkey', + privSigningKeyWIF.decode()) + config.set( + address, 'privencryptionkey', + privEncryptionKeyWIF.decode()) + config.save() queues.UISignalQueue.put(( 'writeNewAddressToTable', @@ -366,10 +337,10 @@ class addressGenerator(StoppableThread): highlevelcrypto.makeCryptor( hexlify(potentialPrivEncryptionKey)) shared.myAddressesByHash[ripe] = address - tag = hashlib.sha512(hashlib.sha512( + tag = highlevelcrypto.double_sha512( encodeVarint(addressVersionNumber) + encodeVarint(streamNumber) + ripe - ).digest()).digest()[32:] + )[32:] shared.myAddressesByTag[tag] = address if addressVersionNumber == 3: # If this is a chan address, @@ -382,18 +353,17 @@ class addressGenerator(StoppableThread): 'sendOutOrStoreMyV4Pubkey', address)) queues.UISignalQueue.put(( 'updateStatusBar', - tr._translate( + _translate( "MainWindow", "Done generating address") )) elif saveAddressToDisk and not live \ - and not BMConfigParser().has_section(address): + and not config.has_section(address): listOfNewAddressesToSendOutThroughTheAPI.append( address) # Done generating addresses. if command in ( - 'createDeterministicAddresses', - 'joinChan', 'createChan' + 'createDeterministicAddresses', 'createChan', 'joinChan' ): queues.apiAddressGeneratorReturnQueue.put( listOfNewAddressesToSendOutThroughTheAPI) diff --git a/src/class_objectProcessor.py b/src/class_objectProcessor.py index fcc63cc6..658bad9c 100644 --- a/src/class_objectProcessor.py +++ b/src/class_objectProcessor.py @@ -6,8 +6,9 @@ processes the network objects # pylint: disable=too-many-branches,too-many-statements import hashlib import logging +import os import random -import subprocess # nosec +import subprocess # nosec B404 import threading import time from binascii import hexlify @@ -23,13 +24,13 @@ import queues import shared import state from addresses import ( - calculateInventoryHash, decodeAddress, decodeVarint, + decodeAddress, decodeVarint, encodeAddress, encodeVarint, varintDecodeError ) -from bmconfigparser import BMConfigParser -from fallback import RIPEMD160Hash -from helper_sql import sql_ready, SqlBulkExecute, sqlExecute, sqlQuery -from network import bmproto, knownnodes +from bmconfigparser import config +from helper_sql import ( + sql_ready, sql_timeout, SqlBulkExecute, sqlExecute, sqlQuery) +from network import knownnodes from network.node import Peer from tr import _translate @@ -44,12 +45,16 @@ class objectProcessor(threading.Thread): def __init__(self): threading.Thread.__init__(self, name="objectProcessor") random.seed() + if sql_ready.wait(sql_timeout) is False: + logger.fatal('SQL thread is not started in %s sec', sql_timeout) + os._exit(1) # pylint: disable=protected-access + shared.reloadMyAddressHashes() + shared.reloadBroadcastSendersForWhichImWatching() # It may be the case that the last time Bitmessage was running, # the user closed it before it finished processing everything in the # objectProcessorQueue. Assuming that Bitmessage wasn't closed # forcefully, it should have saved the data in the queue into the # objectprocessorqueue table. Let's pull it out. - sql_ready.wait() queryreturn = sqlQuery( 'SELECT objecttype, data FROM objectprocessorqueue') for objectType, data in queryreturn: @@ -58,7 +63,6 @@ class objectProcessor(threading.Thread): logger.debug( 'Loaded %s objects from disk into the objectProcessorQueue.', len(queryreturn)) - self._ack_obj = bmproto.BMStringParser() self.successfullyDecryptMessageTimings = [] def run(self): @@ -98,7 +102,7 @@ class objectProcessor(threading.Thread): 'The object is too big after decompression (stopped' ' decompressing at %ib, your configured limit %ib).' ' Ignoring', - e.size, BMConfigParser().safeGetInt('zlib', 'maxsize')) + e.size, config.safeGetInt('zlib', 'maxsize')) except varintDecodeError as e: logger.debug( 'There was a problem with a varint while processing an' @@ -241,12 +245,12 @@ class objectProcessor(threading.Thread): ' one of my pubkeys but the stream number on which we' ' heard this getpubkey object doesn\'t match this' ' address\' stream number. Ignoring.') - if BMConfigParser().safeGetBoolean(myAddress, 'chan'): + if config.safeGetBoolean(myAddress, 'chan'): return logger.info( 'Ignoring getpubkey request because it is for one of my' ' chan addresses. The other party should already have' ' the pubkey.') - lastPubkeySendTime = BMConfigParser().safeGetInt( + lastPubkeySendTime = config.safeGetInt( myAddress, 'lastpubkeysendtime') # If the last time we sent our pubkey was more recent than # 28 days ago... @@ -294,23 +298,20 @@ class objectProcessor(threading.Thread): '(within processpubkey) payloadLength less than 146.' ' Sanity check failed.') readPosition += 4 - publicSigningKey = data[readPosition:readPosition + 64] + pubSigningKey = '\x04' + data[readPosition:readPosition + 64] # Is it possible for a public key to be invalid such that trying to # encrypt or sign with it will cause an error? If it is, it would # be easiest to test them here. readPosition += 64 - publicEncryptionKey = data[readPosition:readPosition + 64] - if len(publicEncryptionKey) < 64: + pubEncryptionKey = '\x04' + data[readPosition:readPosition + 64] + if len(pubEncryptionKey) < 65: return logger.debug( 'publicEncryptionKey length less than 64. Sanity check' ' failed.') readPosition += 64 # The data we'll store in the pubkeys table. dataToStore = data[20:readPosition] - sha = hashlib.new('sha512') - sha.update( - '\x04' + publicSigningKey + '\x04' + publicEncryptionKey) - ripe = RIPEMD160Hash(sha.digest()).digest() + ripe = highlevelcrypto.to_ripe(pubSigningKey, pubEncryptionKey) if logger.isEnabledFor(logging.DEBUG): logger.debug( @@ -318,7 +319,7 @@ class objectProcessor(threading.Thread): '\nripe %s\npublicSigningKey in hex: %s' '\npublicEncryptionKey in hex: %s', addressVersion, streamNumber, hexlify(ripe), - hexlify(publicSigningKey), hexlify(publicEncryptionKey) + hexlify(pubSigningKey), hexlify(pubEncryptionKey) ) address = encodeAddress(addressVersion, streamNumber, ripe) @@ -348,9 +349,9 @@ class objectProcessor(threading.Thread): ' Sanity check failed.') return readPosition += 4 - publicSigningKey = '\x04' + data[readPosition:readPosition + 64] + pubSigningKey = '\x04' + data[readPosition:readPosition + 64] readPosition += 64 - publicEncryptionKey = '\x04' + data[readPosition:readPosition + 64] + pubEncryptionKey = '\x04' + data[readPosition:readPosition + 64] readPosition += 64 specifiedNonceTrialsPerByteLength = decodeVarint( data[readPosition:readPosition + 10])[1] @@ -367,15 +368,13 @@ class objectProcessor(threading.Thread): signature = data[readPosition:readPosition + signatureLength] if highlevelcrypto.verify( data[8:endOfSignedDataPosition], - signature, hexlify(publicSigningKey)): + signature, hexlify(pubSigningKey)): logger.debug('ECDSA verify passed (within processpubkey)') else: logger.warning('ECDSA verify failed (within processpubkey)') return - sha = hashlib.new('sha512') - sha.update(publicSigningKey + publicEncryptionKey) - ripe = RIPEMD160Hash(sha.digest()).digest() + ripe = highlevelcrypto.to_ripe(pubSigningKey, pubEncryptionKey) if logger.isEnabledFor(logging.DEBUG): logger.debug( @@ -383,7 +382,7 @@ class objectProcessor(threading.Thread): '\nripe %s\npublicSigningKey in hex: %s' '\npublicEncryptionKey in hex: %s', addressVersion, streamNumber, hexlify(ripe), - hexlify(publicSigningKey), hexlify(publicEncryptionKey) + hexlify(pubSigningKey), hexlify(pubEncryptionKey) ) address = encodeAddress(addressVersion, streamNumber, ripe) @@ -449,7 +448,7 @@ class objectProcessor(threading.Thread): streamNumberAsClaimedByMsg, streamNumberAsClaimedByMsgLength = \ decodeVarint(data[readPosition:readPosition + 9]) readPosition += streamNumberAsClaimedByMsgLength - inventoryHash = calculateInventoryHash(data) + inventoryHash = highlevelcrypto.calculateInventoryHash(data) initialDecryptionSuccessful = False # This is not an acknowledgement bound for me. See if it is a message @@ -457,7 +456,7 @@ class objectProcessor(threading.Thread): for key, cryptorObject in sorted( shared.myECCryptorObjects.items(), - key=lambda x: random.random()): + key=lambda x: random.random()): # nosec B311 try: # continue decryption attempts to avoid timing attacks if initialDecryptionSuccessful: @@ -472,7 +471,7 @@ class objectProcessor(threading.Thread): logger.info( 'EC decryption successful using key associated' ' with ripe hash: %s.', hexlify(key)) - except Exception: + except Exception: # nosec B110 pass if not initialDecryptionSuccessful: # This is not a message bound for me. @@ -579,13 +578,10 @@ class objectProcessor(threading.Thread): helper_bitcoin.calculateTestnetAddressFromPubkey(pubSigningKey) ) # Used to detect and ignore duplicate messages in our inbox - sigHash = hashlib.sha512( - hashlib.sha512(signature).digest()).digest()[32:] + sigHash = highlevelcrypto.double_sha512(signature)[32:] # calculate the fromRipe. - sha = hashlib.new('sha512') - sha.update(pubSigningKey + pubEncryptionKey) - ripe = RIPEMD160Hash(sha.digest()).digest() + ripe = highlevelcrypto.to_ripe(pubSigningKey, pubEncryptionKey) fromAddress = encodeAddress( sendersAddressVersionNumber, sendersStreamNumber, ripe) @@ -612,13 +608,13 @@ class objectProcessor(threading.Thread): # If the toAddress version number is 3 or higher and not one of # my chan addresses: if decodeAddress(toAddress)[1] >= 3 \ - and not BMConfigParser().safeGetBoolean(toAddress, 'chan'): + and not config.safeGetBoolean(toAddress, 'chan'): # If I'm not friendly with this person: if not shared.isAddressInMyAddressBookSubscriptionsListOrWhitelist( fromAddress): - requiredNonceTrialsPerByte = BMConfigParser().getint( + requiredNonceTrialsPerByte = config.getint( toAddress, 'noncetrialsperbyte') - requiredPayloadLengthExtraBytes = BMConfigParser().getint( + requiredPayloadLengthExtraBytes = config.getint( toAddress, 'payloadlengthextrabytes') if not protocol.isProofOfWorkSufficient( data, requiredNonceTrialsPerByte, @@ -630,7 +626,7 @@ class objectProcessor(threading.Thread): # to black or white lists. blockMessage = False # If we are using a blacklist - if BMConfigParser().get( + if config.get( 'bitmessagesettings', 'blackwhitelist') == 'black': queryreturn = sqlQuery( "SELECT label FROM blacklist where address=? and enabled='1'", @@ -648,7 +644,7 @@ class objectProcessor(threading.Thread): 'Message ignored because address not in whitelist.') blockMessage = True - # toLabel = BMConfigParser().safeGet(toAddress, 'label', toAddress) + # toLabel = config.safeGet(toAddress, 'label', toAddress) try: decodedMessage = helper_msgcoding.MsgDecode( messageEncodingType, message) @@ -674,18 +670,18 @@ class objectProcessor(threading.Thread): # If we are behaving as an API then we might need to run an # outside command to let some program know that a new message # has arrived. - if BMConfigParser().safeGetBoolean( + if config.safeGetBoolean( 'bitmessagesettings', 'apienabled'): - apiNotifyPath = BMConfigParser().safeGet( + apiNotifyPath = config.safeGet( 'bitmessagesettings', 'apinotifypath') if apiNotifyPath: - subprocess.call([apiNotifyPath, "newMessage"]) + subprocess.call([apiNotifyPath, "newMessage"]) # nosec B603 # Let us now check and see whether our receiving address is # behaving as a mailing list - if BMConfigParser().safeGetBoolean(toAddress, 'mailinglist') \ + if config.safeGetBoolean(toAddress, 'mailinglist') \ and messageEncodingType != 0: - mailingListName = BMConfigParser().safeGet( + mailingListName = config.safeGet( toAddress, 'mailinglistname', '') # Let us send out this message as a broadcast subject = self.addMailingListNameToSubject( @@ -723,10 +719,16 @@ class objectProcessor(threading.Thread): if ( self.ackDataHasAValidHeader(ackData) and not blockMessage and messageEncodingType != 0 - and not BMConfigParser().safeGetBoolean(toAddress, 'dontsendack') - and not BMConfigParser().safeGetBoolean(toAddress, 'chan') + and not config.safeGetBoolean(toAddress, 'dontsendack') + and not config.safeGetBoolean(toAddress, 'chan') ): - self._ack_obj.send_data(ackData[24:]) + ackPayload = ackData[24:] + objectType, toStreamNumber, expiresTime = \ + protocol.decodeObjectParameters(ackPayload) + inventoryHash = highlevelcrypto.calculateInventoryHash(ackPayload) + state.Inventory[inventoryHash] = ( + objectType, toStreamNumber, ackPayload, expiresTime, b'') + queues.invQueue.put((toStreamNumber, inventoryHash)) # Display timing data timeRequiredToAttemptToDecryptMessage = time.time( @@ -750,7 +752,7 @@ class objectProcessor(threading.Thread): state.numberOfBroadcastsProcessed += 1 queues.UISignalQueue.put(( 'updateNumberOfBroadcastsProcessed', 'no data')) - inventoryHash = calculateInventoryHash(data) + inventoryHash = highlevelcrypto.calculateInventoryHash(data) readPosition = 20 # bypass the nonce, time, and object type broadcastVersion, broadcastVersionLength = decodeVarint( data[readPosition:readPosition + 9]) @@ -775,7 +777,7 @@ class objectProcessor(threading.Thread): initialDecryptionSuccessful = False for key, cryptorObject in sorted( shared.MyECSubscriptionCryptorObjects.items(), - key=lambda x: random.random()): + key=lambda x: random.random()): # nosec B311 try: # continue decryption attempts to avoid timing attacks if initialDecryptionSuccessful: @@ -872,9 +874,8 @@ class objectProcessor(threading.Thread): requiredPayloadLengthExtraBytes) endOfPubkeyPosition = readPosition - sha = hashlib.new('sha512') - sha.update(sendersPubSigningKey + sendersPubEncryptionKey) - calculatedRipe = RIPEMD160Hash(sha.digest()).digest() + calculatedRipe = highlevelcrypto.to_ripe( + sendersPubSigningKey, sendersPubEncryptionKey) if broadcastVersion == 4: if toRipe != calculatedRipe: @@ -884,10 +885,10 @@ class objectProcessor(threading.Thread): ' itself. Ignoring message.' ) elif broadcastVersion == 5: - calculatedTag = hashlib.sha512(hashlib.sha512( + calculatedTag = highlevelcrypto.double_sha512( encodeVarint(sendersAddressVersion) + encodeVarint(sendersStream) + calculatedRipe - ).digest()).digest()[32:] + )[32:] if calculatedTag != embeddedTag: return logger.debug( 'The tag and encryption key used to encrypt this' @@ -917,8 +918,7 @@ class objectProcessor(threading.Thread): return logger.debug('ECDSA verify passed') # Used to detect and ignore duplicate messages in our inbox - sigHash = hashlib.sha512( - hashlib.sha512(signature).digest()).digest()[32:] + sigHash = highlevelcrypto.double_sha512(signature)[32:] fromAddress = encodeAddress( sendersAddressVersion, sendersStream, calculatedRipe) @@ -959,11 +959,11 @@ class objectProcessor(threading.Thread): # If we are behaving as an API then we might need to run an # outside command to let some program know that a new message # has arrived. - if BMConfigParser().safeGetBoolean('bitmessagesettings', 'apienabled'): - apiNotifyPath = BMConfigParser().safeGet( + if config.safeGetBoolean('bitmessagesettings', 'apienabled'): + apiNotifyPath = config.safeGet( 'bitmessagesettings', 'apinotifypath') if apiNotifyPath: - subprocess.call([apiNotifyPath, "newBroadcast"]) + subprocess.call([apiNotifyPath, "newBroadcast"]) # nosec B603 # Display timing data logger.info( @@ -992,10 +992,10 @@ class objectProcessor(threading.Thread): # Let us create the tag from the address and see if we were waiting # for it. elif addressVersion >= 4: - tag = hashlib.sha512(hashlib.sha512( + tag = highlevelcrypto.double_sha512( encodeVarint(addressVersion) + encodeVarint(streamNumber) + ripe - ).digest()).digest()[32:] + )[32:] if tag in state.neededPubkeys: del state.neededPubkeys[tag] self.sendMessages(address) @@ -1025,7 +1025,7 @@ class objectProcessor(threading.Thread): magic, command, payloadLength, checksum = protocol.Header.unpack( ackData[:protocol.Header.size]) - if magic != 0xE9BEB4D9: + if magic != protocol.magic: logger.info('Ackdata magic bytes were wrong. Not sending ackData.') return False payload = ackData[protocol.Header.size:] diff --git a/src/class_singleCleaner.py b/src/class_singleCleaner.py index 3f3f8ec0..06153dcf 100644 --- a/src/class_singleCleaner.py +++ b/src/class_singleCleaner.py @@ -25,10 +25,9 @@ import time import queues import state -from bmconfigparser import BMConfigParser +from bmconfigparser import config from helper_sql import sqlExecute, sqlQuery -from inventory import Inventory -from network import BMConnectionPool, knownnodes, StoppableThread +from network import connectionpool, knownnodes, StoppableThread from tr import _translate @@ -50,11 +49,11 @@ class singleCleaner(StoppableThread): timeWeLastClearedInventoryAndPubkeysTables = 0 try: state.maximumLengthOfTimeToBotherResendingMessages = ( - BMConfigParser().getfloat( + config.getfloat( 'bitmessagesettings', 'stopresendingafterxdays') * 24 * 60 * 60 ) + ( - BMConfigParser().getfloat( + config.getfloat( 'bitmessagesettings', 'stopresendingafterxmonths') * (60 * 60 * 24 * 365) / 12) except: # noqa:E722 @@ -69,7 +68,7 @@ class singleCleaner(StoppableThread): 'updateStatusBar', 'Doing housekeeping (Flushing inventory in memory to disk...)' )) - Inventory().flush() + state.Inventory.flush() queues.UISignalQueue.put(('updateStatusBar', '')) # If we are running as a daemon then we are going to fill up the UI @@ -82,7 +81,7 @@ class singleCleaner(StoppableThread): tick = int(time.time()) if timeWeLastClearedInventoryAndPubkeysTables < tick - 7380: timeWeLastClearedInventoryAndPubkeysTables = tick - Inventory().clean() + state.Inventory.clean() queues.workerQueue.put(('sendOnionPeerObj', '')) # pubkeys sqlExecute( @@ -108,7 +107,8 @@ class singleCleaner(StoppableThread): try: # Cleanup knownnodes and handle possible severe exception # while writing it to disk - knownnodes.cleanupKnownNodes() + if state.enableNetwork: + knownnodes.cleanupKnownNodes(connectionpool.pool) except Exception as err: if "Errno 28" in str(err): self.logger.fatal( @@ -129,7 +129,7 @@ class singleCleaner(StoppableThread): os._exit(1) # pylint: disable=protected-access # inv/object tracking - for connection in BMConnectionPool().connections(): + for connection in connectionpool.pool.connections(): connection.clean() # discovery tracking diff --git a/src/class_singleWorker.py b/src/class_singleWorker.py index 6e2fb94d..2d16c869 100644 --- a/src/class_singleWorker.py +++ b/src/class_singleWorker.py @@ -12,8 +12,6 @@ from binascii import hexlify, unhexlify from struct import pack from subprocess import call # nosec -from six.moves import configparser, queue - import defaults import helper_inbox import helper_msgcoding @@ -26,12 +24,11 @@ import protocol import queues import shared import state -from addresses import ( - calculateInventoryHash, decodeAddress, decodeVarint, encodeVarint) -from bmconfigparser import BMConfigParser +from addresses import decodeAddress, decodeVarint, encodeVarint +from bmconfigparser import config from helper_sql import sqlExecute, sqlQuery -from inventory import Inventory from network import knownnodes, StoppableThread +from six.moves import configparser, queue from tr import _translate @@ -50,6 +47,8 @@ class singleWorker(StoppableThread): def __init__(self): super(singleWorker, self).__init__(name="singleWorker") + self.digestAlg = config.safeGet( + 'bitmessagesettings', 'digestalg', 'sha256') proofofwork.init() def stopThread(self): @@ -73,18 +72,16 @@ class singleWorker(StoppableThread): queryreturn = sqlQuery( '''SELECT DISTINCT toaddress FROM sent''' ''' WHERE (status='awaitingpubkey' AND folder='sent')''') - for row in queryreturn: - toAddress, = row - # toStatus - _, toAddressVersionNumber, toStreamNumber, toRipe = \ - decodeAddress(toAddress) + for toAddress, in queryreturn: + toAddressVersionNumber, toStreamNumber, toRipe = \ + decodeAddress(toAddress)[1:] if toAddressVersionNumber <= 3: state.neededPubkeys[toAddress] = 0 elif toAddressVersionNumber >= 4: - doubleHashOfAddressData = hashlib.sha512(hashlib.sha512( + doubleHashOfAddressData = highlevelcrypto.double_sha512( encodeVarint(toAddressVersionNumber) + encodeVarint(toStreamNumber) + toRipe - ).digest()).digest() + ) # Note that this is the first half of the sha512 hash. privEncryptionKey = doubleHashOfAddressData[:32] tag = doubleHashOfAddressData[32:] @@ -119,7 +116,7 @@ class singleWorker(StoppableThread): # For the case if user deleted knownnodes # but is still having onionpeer objects in inventory if not knownnodes.knownNodesActual: - for item in Inventory().by_type_and_tag(protocol.OBJECT_ONIONPEER): + for item in state.Inventory.by_type_and_tag(protocol.OBJECT_ONIONPEER): queues.objectProcessorQueue.put(( protocol.OBJECT_ONIONPEER, item.payload )) @@ -195,15 +192,19 @@ class singleWorker(StoppableThread): self.logger.info("Quitting...") def _getKeysForAddress(self, address): - privSigningKeyBase58 = BMConfigParser().get( - address, 'privsigningkey') - privEncryptionKeyBase58 = BMConfigParser().get( - address, 'privencryptionkey') + try: + privSigningKeyBase58 = config.get(address, 'privsigningkey') + privEncryptionKeyBase58 = config.get(address, 'privencryptionkey') + except (configparser.NoSectionError, configparser.NoOptionError): + self.logger.error( + 'Could not read or decode privkey for address %s', address) + raise ValueError - privSigningKeyHex = hexlify(shared.decodeWalletImportFormat( - privSigningKeyBase58)) - privEncryptionKeyHex = hexlify(shared.decodeWalletImportFormat( - privEncryptionKeyBase58)) + privSigningKeyHex = hexlify(highlevelcrypto.decodeWalletImportFormat( + privSigningKeyBase58.encode())) + privEncryptionKeyHex = hexlify( + highlevelcrypto.decodeWalletImportFormat( + privEncryptionKeyBase58.encode())) # The \x04 on the beginning of the public keys are not sent. # This way there is only one acceptable way to encode @@ -255,9 +256,7 @@ class singleWorker(StoppableThread): """ # Look up my stream number based on my address hash myAddress = shared.myAddressesByHash[adressHash] - # status - _, addressVersionNumber, streamNumber, adressHash = ( - decodeAddress(myAddress)) + addressVersionNumber, streamNumber = decodeAddress(myAddress)[1:3] # 28 days from now plus or minus five minutes TTL = int(28 * 24 * 60 * 60 + helper_random.randomrandrange(-300, 300)) @@ -270,17 +269,15 @@ class singleWorker(StoppableThread): payload += protocol.getBitfield(myAddress) try: - # privSigningKeyHex, privEncryptionKeyHex - _, _, pubSigningKey, pubEncryptionKey = \ - self._getKeysForAddress(myAddress) - except (configparser.NoSectionError, configparser.NoOptionError) as err: - self.logger.warning("Section or Option did not found: %s", err) - except Exception as err: + pubSigningKey, pubEncryptionKey = self._getKeysForAddress( + myAddress)[2:] + except ValueError: + return + except Exception: # pylint:disable=broad-exception-caught self.logger.error( 'Error within doPOWForMyV2Pubkey. Could not read' ' the keys from the keys.dat file for a requested' - ' address. %s\n', err - ) + ' address. %s\n', exc_info=True) return payload += pubSigningKey + pubEncryptionKey @@ -289,9 +286,9 @@ class singleWorker(StoppableThread): payload = self._doPOWDefaults( payload, TTL, log_prefix='(For pubkey message)') - inventoryHash = calculateInventoryHash(payload) + inventoryHash = highlevelcrypto.calculateInventoryHash(payload) objectType = 1 - Inventory()[inventoryHash] = ( + state.Inventory[inventoryHash] = ( objectType, streamNumber, payload, embeddedTime, '') self.logger.info( @@ -300,15 +297,15 @@ class singleWorker(StoppableThread): queues.invQueue.put((streamNumber, inventoryHash)) queues.UISignalQueue.put(('updateStatusBar', '')) try: - BMConfigParser().set( + config.set( myAddress, 'lastpubkeysendtime', str(int(time.time()))) - BMConfigParser().save() + config.save() except configparser.NoSectionError: # The user deleted the address out of the keys.dat file # before this finished. pass except: # noqa:E722 - self.logger.warning("BMConfigParser().set didn't work") + self.logger.warning("config.set didn't work") def sendOutOrStoreMyV3Pubkey(self, adressHash): """ @@ -320,10 +317,10 @@ class singleWorker(StoppableThread): try: myAddress = shared.myAddressesByHash[adressHash] except KeyError: - # The address has been deleted. - self.logger.warning("Can't find %s in myAddressByHash", hexlify(adressHash)) + self.logger.warning( # The address has been deleted. + "Can't find %s in myAddressByHash", hexlify(adressHash)) return - if BMConfigParser().safeGetBoolean(myAddress, 'chan'): + if config.safeGetBoolean(myAddress, 'chan'): self.logger.info('This is a chan address. Not sending pubkey.') return _, addressVersionNumber, streamNumber, adressHash = decodeAddress( @@ -353,24 +350,24 @@ class singleWorker(StoppableThread): # , privEncryptionKeyHex privSigningKeyHex, _, pubSigningKey, pubEncryptionKey = \ self._getKeysForAddress(myAddress) - except (configparser.NoSectionError, configparser.NoOptionError) as err: - self.logger.warning("Section or Option did not found: %s", err) - except Exception as err: + except ValueError: + return + except Exception: # pylint:disable=broad-exception-caught self.logger.error( 'Error within sendOutOrStoreMyV3Pubkey. Could not read' ' the keys from the keys.dat file for a requested' - ' address. %s\n', err - ) + ' address. %s\n', exc_info=True) return payload += pubSigningKey + pubEncryptionKey - payload += encodeVarint(BMConfigParser().getint( + payload += encodeVarint(config.getint( myAddress, 'noncetrialsperbyte')) - payload += encodeVarint(BMConfigParser().getint( + payload += encodeVarint(config.getint( myAddress, 'payloadlengthextrabytes')) - signature = highlevelcrypto.sign(payload, privSigningKeyHex) + signature = highlevelcrypto.sign( + payload, privSigningKeyHex, self.digestAlg) payload += encodeVarint(len(signature)) payload += signature @@ -378,9 +375,9 @@ class singleWorker(StoppableThread): payload = self._doPOWDefaults( payload, TTL, log_prefix='(For pubkey message)') - inventoryHash = calculateInventoryHash(payload) + inventoryHash = highlevelcrypto.calculateInventoryHash(payload) objectType = 1 - Inventory()[inventoryHash] = ( + state.Inventory[inventoryHash] = ( objectType, streamNumber, payload, embeddedTime, '') self.logger.info( @@ -389,9 +386,9 @@ class singleWorker(StoppableThread): queues.invQueue.put((streamNumber, inventoryHash)) queues.UISignalQueue.put(('updateStatusBar', '')) try: - BMConfigParser().set( + config.set( myAddress, 'lastpubkeysendtime', str(int(time.time()))) - BMConfigParser().save() + config.save() except configparser.NoSectionError: # The user deleted the address out of the keys.dat file # before this finished. @@ -406,10 +403,10 @@ class singleWorker(StoppableThread): past it directly appended it to the outgoing buffer, I think. Same with all the other methods in this class. """ - if not BMConfigParser().has_section(myAddress): + if not config.has_section(myAddress): # The address has been deleted. return - if shared.BMConfigParser().safeGetBoolean(myAddress, 'chan'): + if config.safeGetBoolean(myAddress, 'chan'): self.logger.info('This is a chan address. Not sending pubkey.') return _, addressVersionNumber, streamNumber, addressHash = decodeAddress( @@ -428,21 +425,20 @@ class singleWorker(StoppableThread): # , privEncryptionKeyHex privSigningKeyHex, _, pubSigningKey, pubEncryptionKey = \ self._getKeysForAddress(myAddress) - except (configparser.NoSectionError, configparser.NoOptionError) as err: - self.logger.warning("Section or Option did not found: %s", err) - except Exception as err: + except ValueError: + return + except Exception: # pylint:disable=broad-exception-caught self.logger.error( 'Error within sendOutOrStoreMyV4Pubkey. Could not read' ' the keys from the keys.dat file for a requested' - ' address. %s\n', err - ) + ' address. %s\n', exc_info=True) return dataToEncrypt += pubSigningKey + pubEncryptionKey - dataToEncrypt += encodeVarint(BMConfigParser().getint( + dataToEncrypt += encodeVarint(config.getint( myAddress, 'noncetrialsperbyte')) - dataToEncrypt += encodeVarint(BMConfigParser().getint( + dataToEncrypt += encodeVarint(config.getint( myAddress, 'payloadlengthextrabytes')) # When we encrypt, we'll use a hash of the data @@ -452,14 +448,13 @@ class singleWorker(StoppableThread): # unencrypted, the pubkey with part of the hash so that nodes # know which pubkey object to try to decrypt # when they want to send a message. - doubleHashOfAddressData = hashlib.sha512(hashlib.sha512( + doubleHashOfAddressData = highlevelcrypto.double_sha512( encodeVarint(addressVersionNumber) + encodeVarint(streamNumber) + addressHash - ).digest()).digest() + ) payload += doubleHashOfAddressData[32:] # the tag signature = highlevelcrypto.sign( - payload + dataToEncrypt, privSigningKeyHex - ) + payload + dataToEncrypt, privSigningKeyHex, self.digestAlg) dataToEncrypt += encodeVarint(len(signature)) dataToEncrypt += signature @@ -472,9 +467,9 @@ class singleWorker(StoppableThread): payload = self._doPOWDefaults( payload, TTL, log_prefix='(For pubkey message)') - inventoryHash = calculateInventoryHash(payload) + inventoryHash = highlevelcrypto.calculateInventoryHash(payload) objectType = 1 - Inventory()[inventoryHash] = ( + state.Inventory[inventoryHash] = ( objectType, streamNumber, payload, embeddedTime, doubleHashOfAddressData[32:] ) @@ -485,9 +480,9 @@ class singleWorker(StoppableThread): queues.invQueue.put((streamNumber, inventoryHash)) queues.UISignalQueue.put(('updateStatusBar', '')) try: - BMConfigParser().set( + config.set( myAddress, 'lastpubkeysendtime', str(int(time.time()))) - BMConfigParser().save() + config.save() except Exception as err: self.logger.error( 'Error: Couldn\'t add the lastpubkeysendtime' @@ -508,9 +503,9 @@ class singleWorker(StoppableThread): objectType = protocol.OBJECT_ONIONPEER # FIXME: ideally the objectPayload should be signed objectPayload = encodeVarint(peer.port) + protocol.encodeHost(peer.host) - tag = calculateInventoryHash(objectPayload) + tag = highlevelcrypto.calculateInventoryHash(objectPayload) - if Inventory().by_type_and_tag(objectType, tag): + if state.Inventory.by_type_and_tag(objectType, tag): return # not expired payload = pack('>Q', embeddedTime) @@ -522,10 +517,10 @@ class singleWorker(StoppableThread): payload = self._doPOWDefaults( payload, TTL, log_prefix='(For onionpeer object)') - inventoryHash = calculateInventoryHash(payload) - Inventory()[inventoryHash] = ( - objectType, streamNumber, buffer(payload), - embeddedTime, buffer(tag) + inventoryHash = highlevelcrypto.calculateInventoryHash(payload) + state.Inventory[inventoryHash] = ( + objectType, streamNumber, buffer(payload), # noqa: F821 + embeddedTime, buffer(tag) # noqa: F821 ) self.logger.info( 'sending inv (within sendOnionPeerObj function) for object: %s', @@ -564,8 +559,7 @@ class singleWorker(StoppableThread): # , privEncryptionKeyHex privSigningKeyHex, _, pubSigningKey, pubEncryptionKey = \ self._getKeysForAddress(fromaddress) - except (configparser.NoSectionError, configparser.NoOptionError) as err: - self.logger.warning("Section or Option did not found: %s", err) + except ValueError: queues.UISignalQueue.put(( 'updateSentItemStatusByAckdata', ( ackdata, _translate( @@ -573,6 +567,7 @@ class singleWorker(StoppableThread): "Error! Could not find sender address" " (your address) in the keys.dat file.")) )) + continue except Exception as err: self.logger.error( 'Error within sendBroadcast. Could not read' @@ -618,10 +613,10 @@ class singleWorker(StoppableThread): payload += encodeVarint(streamNumber) if addressVersionNumber >= 4: - doubleHashOfAddressData = hashlib.sha512(hashlib.sha512( + doubleHashOfAddressData = highlevelcrypto.double_sha512( encodeVarint(addressVersionNumber) + encodeVarint(streamNumber) + ripe - ).digest()).digest() + ) tag = doubleHashOfAddressData[32:] payload += tag else: @@ -633,9 +628,9 @@ class singleWorker(StoppableThread): dataToEncrypt += protocol.getBitfield(fromaddress) dataToEncrypt += pubSigningKey + pubEncryptionKey if addressVersionNumber >= 3: - dataToEncrypt += encodeVarint(BMConfigParser().getint( + dataToEncrypt += encodeVarint(config.getint( fromaddress, 'noncetrialsperbyte')) - dataToEncrypt += encodeVarint(BMConfigParser().getint( + dataToEncrypt += encodeVarint(config.getint( fromaddress, 'payloadlengthextrabytes')) # message encoding type dataToEncrypt += encodeVarint(encoding) @@ -646,7 +641,7 @@ class singleWorker(StoppableThread): dataToSign = payload + dataToEncrypt signature = highlevelcrypto.sign( - dataToSign, privSigningKeyHex) + dataToSign, privSigningKeyHex, self.digestAlg) dataToEncrypt += encodeVarint(len(signature)) dataToEncrypt += signature @@ -690,9 +685,9 @@ class singleWorker(StoppableThread): ) continue - inventoryHash = calculateInventoryHash(payload) + inventoryHash = highlevelcrypto.calculateInventoryHash(payload) objectType = 3 - Inventory()[inventoryHash] = ( + state.Inventory[inventoryHash] = ( objectType, streamNumber, payload, embeddedTime, tag) self.logger.info( 'sending inv (within sendBroadcast function)' @@ -761,7 +756,7 @@ class singleWorker(StoppableThread): # then we won't need an entry in the pubkeys table; # we can calculate the needed pubkey using the private keys # in our keys.dat file. - elif BMConfigParser().has_section(toaddress): + elif config.has_section(toaddress): if not sqlExecute( '''UPDATE sent SET status='doingmsgpow' ''' ''' WHERE toaddress=? AND status='msgqueued' AND folder='sent' ''', @@ -800,10 +795,10 @@ class singleWorker(StoppableThread): if toAddressVersionNumber <= 3: toTag = '' else: - toTag = hashlib.sha512(hashlib.sha512( + toTag = highlevelcrypto.double_sha512( encodeVarint(toAddressVersionNumber) + encodeVarint(toStreamNumber) + toRipe - ).digest()).digest()[32:] + )[32:] if toaddress in state.neededPubkeys or \ toTag in state.neededPubkeys: # We already sent a request for the pubkey @@ -836,11 +831,11 @@ class singleWorker(StoppableThread): # already contains the toAddress and cryptor # object associated with the tag for this toAddress. if toAddressVersionNumber >= 4: - doubleHashOfToAddressData = hashlib.sha512( - hashlib.sha512( - encodeVarint(toAddressVersionNumber) + encodeVarint(toStreamNumber) + toRipe - ).digest() - ).digest() + doubleHashOfToAddressData = \ + highlevelcrypto.double_sha512( + encodeVarint(toAddressVersionNumber) + + encodeVarint(toStreamNumber) + toRipe + ) # The first half of the sha512 hash. privEncryptionKey = doubleHashOfToAddressData[:32] # The second half of the sha512 hash. @@ -851,7 +846,7 @@ class singleWorker(StoppableThread): hexlify(privEncryptionKey)) ) - for value in Inventory().by_type_and_tag(1, toTag): + for value in state.Inventory.by_type_and_tag(1, toTag): # if valid, this function also puts it # in the pubkeys table. if protocol.decryptAndCheckPubkeyPayload( @@ -908,7 +903,7 @@ class singleWorker(StoppableThread): embeddedTime = int(time.time() + TTL) # if we aren't sending this to ourselves or a chan - if not BMConfigParser().has_section(toaddress): + if not config.has_section(toaddress): state.ackdataForWhichImWatching[ackdata] = 0 queues.UISignalQueue.put(( 'updateSentItemStatusByAckdata', ( @@ -958,7 +953,7 @@ class singleWorker(StoppableThread): if protocol.isBitSetWithinBitfield(behaviorBitfield, 30): # if we are Not willing to include the receiver's # RIPE hash on the message.. - if not shared.BMConfigParser().safeGetBoolean( + if not config.safeGetBoolean( 'bitmessagesettings', 'willinglysendtomobile' ): self.logger.info( @@ -1045,9 +1040,9 @@ class singleWorker(StoppableThread): )) )) if status != 'forcepow': - maxacceptablenoncetrialsperbyte = BMConfigParser().getint( + maxacceptablenoncetrialsperbyte = config.getint( 'bitmessagesettings', 'maxacceptablenoncetrialsperbyte') - maxacceptablepayloadlengthextrabytes = BMConfigParser().getint( + maxacceptablepayloadlengthextrabytes = config.getint( 'bitmessagesettings', 'maxacceptablepayloadlengthextrabytes') cond1 = maxacceptablenoncetrialsperbyte and \ requiredAverageProofOfWorkNonceTrialsPerByte > maxacceptablenoncetrialsperbyte @@ -1084,7 +1079,7 @@ class singleWorker(StoppableThread): behaviorBitfield = protocol.getBitfield(fromaddress) try: - privEncryptionKeyBase58 = BMConfigParser().get( + privEncryptionKeyBase58 = config.get( toaddress, 'privencryptionkey') except (configparser.NoSectionError, configparser.NoOptionError) as err: queues.UISignalQueue.put(( @@ -1103,8 +1098,9 @@ class singleWorker(StoppableThread): ' from the keys.dat file for our own address. %s\n', err) continue - privEncryptionKeyHex = hexlify(shared.decodeWalletImportFormat( - privEncryptionKeyBase58)) + privEncryptionKeyHex = hexlify( + highlevelcrypto.decodeWalletImportFormat( + privEncryptionKeyBase58.encode())) pubEncryptionKeyBase256 = unhexlify(highlevelcrypto.privToPub( privEncryptionKeyHex))[1:] requiredAverageProofOfWorkNonceTrialsPerByte = \ @@ -1132,8 +1128,7 @@ class singleWorker(StoppableThread): privSigningKeyHex, privEncryptionKeyHex, \ pubSigningKey, pubEncryptionKey = self._getKeysForAddress( fromaddress) - except (configparser.NoSectionError, configparser.NoOptionError) as err: - self.logger.warning("Section or Option did not found: %s", err) + except ValueError: queues.UISignalQueue.put(( 'updateSentItemStatusByAckdata', ( ackdata, _translate( @@ -1141,6 +1136,7 @@ class singleWorker(StoppableThread): "Error! Could not find sender address" " (your address) in the keys.dat file.")) )) + continue except Exception as err: self.logger.error( 'Error within sendMsg. Could not read' @@ -1170,9 +1166,9 @@ class singleWorker(StoppableThread): payload += encodeVarint( defaults.networkDefaultPayloadLengthExtraBytes) else: - payload += encodeVarint(BMConfigParser().getint( + payload += encodeVarint(config.getint( fromaddress, 'noncetrialsperbyte')) - payload += encodeVarint(BMConfigParser().getint( + payload += encodeVarint(config.getint( fromaddress, 'payloadlengthextrabytes')) # This hash will be checked by the receiver of the message @@ -1185,7 +1181,7 @@ class singleWorker(StoppableThread): ) payload += encodeVarint(encodedMessage.length) payload += encodedMessage.data - if BMConfigParser().has_section(toaddress): + if config.has_section(toaddress): self.logger.info( 'Not bothering to include ackdata because we are' ' sending to ourselves or a chan.' @@ -1207,7 +1203,8 @@ class singleWorker(StoppableThread): payload += fullAckPayload dataToSign = pack('>Q', embeddedTime) + '\x00\x00\x00\x02' + \ encodeVarint(1) + encodeVarint(toStreamNumber) + payload - signature = highlevelcrypto.sign(dataToSign, privSigningKeyHex) + signature = highlevelcrypto.sign( + dataToSign, privSigningKeyHex, self.digestAlg) payload += encodeVarint(len(signature)) payload += signature @@ -1284,11 +1281,11 @@ class singleWorker(StoppableThread): ) continue - inventoryHash = calculateInventoryHash(encryptedPayload) + inventoryHash = highlevelcrypto.calculateInventoryHash(encryptedPayload) objectType = 2 - Inventory()[inventoryHash] = ( + state.Inventory[inventoryHash] = ( objectType, toStreamNumber, encryptedPayload, embeddedTime, '') - if BMConfigParser().has_section(toaddress) or \ + if config.has_section(toaddress) or \ not protocol.checkBitfield(behaviorBitfield, protocol.BITFIELD_DOESACK): queues.UISignalQueue.put(( 'updateSentItemStatusByAckdata', ( @@ -1314,7 +1311,7 @@ class singleWorker(StoppableThread): # Update the sent message in the sent table with the # necessary information. - if BMConfigParser().has_section(toaddress) or \ + if config.has_section(toaddress) or \ not protocol.checkBitfield(behaviorBitfield, protocol.BITFIELD_DOESACK): newStatus = 'msgsentnoackexpected' else: @@ -1330,10 +1327,9 @@ class singleWorker(StoppableThread): # If we are sending to ourselves or a chan, let's put # the message in our own inbox. - if BMConfigParser().has_section(toaddress): + if config.has_section(toaddress): # Used to detect and ignore duplicate messages in our inbox - sigHash = hashlib.sha512(hashlib.sha512( - signature).digest()).digest()[32:] + sigHash = highlevelcrypto.double_sha512(signature)[32:] t = (inventoryHash, toaddress, fromaddress, subject, int( time.time()), message, 'inbox', encoding, 0, sigHash) helper_inbox.insert(t) @@ -1344,14 +1340,16 @@ class singleWorker(StoppableThread): # If we are behaving as an API then we might need to run an # outside command to let some program know that a new message # has arrived. - if BMConfigParser().safeGetBoolean( + if config.safeGetBoolean( 'bitmessagesettings', 'apienabled'): - apiNotifyPath = BMConfigParser().safeGet( + apiNotifyPath = config.safeGet( 'bitmessagesettings', 'apinotifypath') if apiNotifyPath: - call([apiNotifyPath, "newMessage"]) + # There is no additional risk of remote exploitation or + # privilege escalation + call([apiNotifyPath, "newMessage"]) # nosec B603 def requestPubKey(self, toAddress): """Send a getpubkey object""" @@ -1388,16 +1386,13 @@ class singleWorker(StoppableThread): # neededPubkeys dictionary. But if we are recovering # from a restart of the client then we have to put it in now. - # Note that this is the first half of the sha512 hash. - privEncryptionKey = hashlib.sha512(hashlib.sha512( + doubleHashOfAddressData = highlevelcrypto.double_sha512( encodeVarint(addressVersionNumber) + encodeVarint(streamNumber) + ripe - ).digest()).digest()[:32] + ) + privEncryptionKey = doubleHashOfAddressData[:32] # Note that this is the second half of the sha512 hash. - tag = hashlib.sha512(hashlib.sha512( - encodeVarint(addressVersionNumber) - + encodeVarint(streamNumber) + ripe - ).digest()).digest()[32:] + tag = doubleHashOfAddressData[32:] if tag not in state.neededPubkeys: # We'll need this for when we receive a pubkey reply: # it will be encrypted and we'll need to decrypt it. @@ -1439,9 +1434,9 @@ class singleWorker(StoppableThread): payload = self._doPOWDefaults(payload, TTL) - inventoryHash = calculateInventoryHash(payload) + inventoryHash = highlevelcrypto.calculateInventoryHash(payload) objectType = 1 - Inventory()[inventoryHash] = ( + state.Inventory[inventoryHash] = ( objectType, streamNumber, payload, embeddedTime, '') self.logger.info('sending inv (for the getpubkey message)') queues.invQueue.put((streamNumber, inventoryHash)) diff --git a/src/class_smtpDeliver.py b/src/class_smtpDeliver.py index 08cb35ab..9e3b8ab3 100644 --- a/src/class_smtpDeliver.py +++ b/src/class_smtpDeliver.py @@ -10,7 +10,7 @@ from email.mime.text import MIMEText import queues import state -from bmconfigparser import BMConfigParser +from bmconfigparser import config from network.threads import StoppableThread SMTPDOMAIN = "bmaddr.lan" @@ -22,11 +22,8 @@ class smtpDeliver(StoppableThread): _instance = None def stopThread(self): - # pylint: disable=no-member - try: - queues.UISignallerQueue.put(("stopThread", "data")) - except: # noqa:E722 - pass + """Relay shutdown instruction""" + queues.UISignalQueue.put(("stopThread", "data")) super(smtpDeliver, self).stopThread() @classmethod @@ -51,7 +48,7 @@ class smtpDeliver(StoppableThread): ackData, message = data elif command == 'displayNewInboxMessage': inventoryHash, toAddress, fromAddress, subject, body = data - dest = BMConfigParser().safeGet("bitmessagesettings", "smtpdeliver", '') + dest = config.safeGet("bitmessagesettings", "smtpdeliver", '') if dest == '': continue try: @@ -62,9 +59,9 @@ class smtpDeliver(StoppableThread): msg['Subject'] = Header(subject, 'utf-8') msg['From'] = fromAddress + '@' + SMTPDOMAIN toLabel = map( - lambda y: BMConfigParser().safeGet(y, "label"), + lambda y: config.safeGet(y, "label"), filter( - lambda x: x == toAddress, BMConfigParser().addresses()) + lambda x: x == toAddress, config.addresses()) ) if toLabel: msg['To'] = "\"%s\" <%s>" % (Header(toLabel[0], 'utf-8'), toAddress + '@' + SMTPDOMAIN) diff --git a/src/class_smtpServer.py b/src/class_smtpServer.py index f5b63c2e..44ea7c9c 100644 --- a/src/class_smtpServer.py +++ b/src/class_smtpServer.py @@ -15,7 +15,7 @@ from email.parser import Parser import queues from addresses import decodeAddress -from bmconfigparser import BMConfigParser +from bmconfigparser import config from helper_ackPayload import genAckPayload from helper_sql import sqlExecute from network.threads import StoppableThread @@ -51,8 +51,8 @@ class smtpServerChannel(smtpd.SMTPChannel): authstring = arg[6:] try: decoded = base64.b64decode(authstring) - correctauth = "\x00" + BMConfigParser().safeGet( - "bitmessagesettings", "smtpdusername", "") + "\x00" + BMConfigParser().safeGet( + correctauth = "\x00" + config.safeGet( + "bitmessagesettings", "smtpdusername", "") + "\x00" + config.safeGet( "bitmessagesettings", "smtpdpassword", "") logger.debug('authstring: %s / %s', correctauth, decoded) if correctauth == decoded: @@ -84,7 +84,7 @@ class smtpServerPyBitmessage(smtpd.SMTPServer): """Send a bitmessage""" # pylint: disable=arguments-differ streamNumber, ripe = decodeAddress(toAddress)[2:] - stealthLevel = BMConfigParser().safeGetInt('bitmessagesettings', 'ackstealthlevel') + stealthLevel = config.safeGetInt('bitmessagesettings', 'ackstealthlevel') ackdata = genAckPayload(streamNumber, stealthLevel) sqlExecute( '''INSERT INTO sent VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''', @@ -103,7 +103,7 @@ class smtpServerPyBitmessage(smtpd.SMTPServer): 'sent', # folder 2, # encodingtype # not necessary to have a TTL higher than 2 days - min(BMConfigParser().getint('bitmessagesettings', 'ttl'), 86400 * 2) + min(config.getint('bitmessagesettings', 'ttl'), 86400 * 2) ) queues.workerQueue.put(('sendmessage', toAddress)) @@ -136,7 +136,7 @@ class smtpServerPyBitmessage(smtpd.SMTPServer): sender, domain = p.sub(r'\1', mailfrom).split("@") if domain != SMTPDOMAIN: raise Exception("Bad domain %s" % domain) - if sender not in BMConfigParser().addresses(): + if sender not in config.addresses(): raise Exception("Nonexisting user %s" % sender) except Exception as err: logger.debug('Bad envelope from %s: %r', mailfrom, err) @@ -146,7 +146,7 @@ class smtpServerPyBitmessage(smtpd.SMTPServer): sender, domain = msg_from.split("@") if domain != SMTPDOMAIN: raise Exception("Bad domain %s" % domain) - if sender not in BMConfigParser().addresses(): + if sender not in config.addresses(): raise Exception("Nonexisting user %s" % sender) except Exception as err: logger.error('Bad headers from %s: %r', msg_from, err) diff --git a/src/class_sqlThread.py b/src/class_sqlThread.py index d22ffadb..7df9e253 100644 --- a/src/class_sqlThread.py +++ b/src/class_sqlThread.py @@ -16,13 +16,13 @@ try: import queues import state from addresses import encodeAddress - from bmconfigparser import BMConfigParser + from bmconfigparser import config, config_ready from debug import logger from tr import _translate except ImportError: from . import helper_sql, helper_startup, paths, queues, state from .addresses import encodeAddress - from .bmconfigparser import BMConfigParser + from .bmconfigparser import config, config_ready from .debug import logger from .tr import _translate @@ -36,6 +36,7 @@ class sqlThread(threading.Thread): def run(self): # pylint: disable=too-many-locals, too-many-branches, too-many-statements """Process SQL queries from `.helper_sql.sqlSubmitQueue`""" helper_sql.sql_available = True + config_ready.wait() self.conn = sqlite3.connect(state.appdata + 'messages.dat') self.conn.text_factory = str self.cur = self.conn.cursor() @@ -93,7 +94,7 @@ class sqlThread(threading.Thread): # If the settings version is equal to 2 or 3 then the # sqlThread will modify the pubkeys table and change # the settings version to 4. - settingsversion = BMConfigParser().getint( + settingsversion = config.getint( 'bitmessagesettings', 'settingsversion') # People running earlier versions of PyBitmessage do not have the @@ -125,9 +126,9 @@ class sqlThread(threading.Thread): settingsversion = 4 - BMConfigParser().set( + config.set( 'bitmessagesettings', 'settingsversion', str(settingsversion)) - BMConfigParser().save() + config.save() helper_startup.updateConfig() diff --git a/src/debug.py b/src/debug.py index a70cb543..639be123 100644 --- a/src/debug.py +++ b/src/debug.py @@ -50,7 +50,7 @@ helper_startup.loadConfig() # Now can be overriden from a config file, which uses standard python # logging.config.fileConfig interface # examples are here: -# https://bitmessage.org/forum/index.php/topic,4820.msg11163.html#msg11163 +# https://web.archive.org/web/20170712122006/https://bitmessage.org/forum/index.php/topic,4820.msg11163.html#msg11163 log_level = 'WARNING' diff --git a/src/default.ini b/src/default.ini new file mode 100644 index 00000000..d4420ba5 --- /dev/null +++ b/src/default.ini @@ -0,0 +1,46 @@ +[bitmessagesettings] +maxaddrperstreamsend = 500 +maxbootstrapconnections = 20 +maxdownloadrate = 0 +maxoutboundconnections = 8 +maxtotalconnections = 200 +maxuploadrate = 0 +apiinterface = 127.0.0.1 +apiport = 8442 +udp = True +port = 8444 +timeformat = %%c +blackwhitelist = black +startonlogon = False +showtraynotifications = True +startintray = False +socksproxytype = none +sockshostname = localhost +socksport = 9050 +socksauthentication = False +socksusername = +sockspassword = +keysencrypted = False +messagesencrypted = False +minimizeonclose = False +replybelow = False +stopresendingafterxdays = +stopresendingafterxmonths = +opencl = + +[threads] +receive = 3 + +[network] +bind = +dandelion = 90 + +[inventory] +storage = sqlite +acceptmismatch = False + +[knownnodes] +maxnodes = 20000 + +[zlib] +maxsize = 1048576 diff --git a/src/depends.py b/src/depends.py index 4cbd2ef7..1c42f441 100755 --- a/src/depends.py +++ b/src/depends.py @@ -17,8 +17,9 @@ if not hasattr(sys, 'hexversion') or sys.hexversion < 0x20300F0: ) import logging # noqa:E402 -import subprocess from distutils import version +import subprocess # nosec B404 + from importlib import import_module # We can now use logging so set up a simple configuration @@ -287,7 +288,7 @@ def check_openssl(): path = ctypes.util.find_library('ssl') if path not in paths: paths.append(path) - except: # noqa:E722 + except: # nosec B110 # pylint:disable=bare-except pass openssl_version = None @@ -361,7 +362,7 @@ def check_curses(): return False try: - subprocess.check_call(['which', 'dialog']) + subprocess.check_call(['which', 'dialog']) # nosec B603, B607 except subprocess.CalledProcessError: logger.error( 'Curses requires the `dialog` command to be installed as well as' diff --git a/src/fallback/__init__.py b/src/fallback/__init__.py index 06998e2a..a0b8b6b4 100644 --- a/src/fallback/__init__.py +++ b/src/fallback/__init__.py @@ -18,11 +18,11 @@ try: hashlib.new('ripemd160') except ValueError: try: - from Crypto.Hash import RIPEMD + from Crypto.Hash import RIPEMD160 except ImportError: RIPEMD160Hash = None else: - RIPEMD160Hash = RIPEMD.RIPEMD160Hash + RIPEMD160Hash = RIPEMD160.new else: def RIPEMD160Hash(data=None): """hashlib based RIPEMD160Hash""" diff --git a/src/helper_ackPayload.py b/src/helper_ackPayload.py index d30f4c0d..1c5ddf98 100644 --- a/src/helper_ackPayload.py +++ b/src/helper_ackPayload.py @@ -22,26 +22,26 @@ def genAckPayload(streamNumber=1, stealthLevel=0): - level 1: a getpubkey request for a (random) dummy key hash - level 2: a standard message, encrypted to a random pubkey """ - if stealthLevel == 2: # Generate privacy-enhanced payload + if stealthLevel == 2: # Generate privacy-enhanced payload # Generate a dummy privkey and derive the pubkey dummyPubKeyHex = highlevelcrypto.privToPub( - hexlify(helper_random.randomBytes(32))) + hexlify(highlevelcrypto.randomBytes(32))) # Generate a dummy message of random length # (the smallest possible standard-formatted message is 234 bytes) - dummyMessage = helper_random.randomBytes( + dummyMessage = highlevelcrypto.randomBytes( helper_random.randomrandrange(234, 801)) # Encrypt the message using standard BM encryption (ECIES) ackdata = highlevelcrypto.encrypt(dummyMessage, dummyPubKeyHex) acktype = 2 # message version = 1 - elif stealthLevel == 1: # Basic privacy payload (random getpubkey) - ackdata = helper_random.randomBytes(32) + elif stealthLevel == 1: # Basic privacy payload (random getpubkey) + ackdata = highlevelcrypto.randomBytes(32) acktype = 0 # getpubkey version = 4 else: # Minimum viable payload (non stealth) - ackdata = helper_random.randomBytes(32) + ackdata = highlevelcrypto.randomBytes(32) acktype = 2 # message version = 1 diff --git a/src/helper_addressbook.py b/src/helper_addressbook.py index fb572150..6d354113 100644 --- a/src/helper_addressbook.py +++ b/src/helper_addressbook.py @@ -2,13 +2,13 @@ Insert value into addressbook """ -from bmconfigparser import BMConfigParser +from bmconfigparser import config from helper_sql import sqlExecute def insert(address, label): """perform insert into addressbook""" - if address not in BMConfigParser().addresses(): + if address not in config.addresses(): return sqlExecute('''INSERT INTO addressbook VALUES (?,?)''', label, address) == 1 return False diff --git a/src/helper_inbox.py b/src/helper_inbox.py index d99e9544..555795df 100644 --- a/src/helper_inbox.py +++ b/src/helper_inbox.py @@ -18,6 +18,11 @@ def trash(msgid): queues.UISignalQueue.put(('removeInboxRowByMsgid', msgid)) +def delete(ack_data): + """Permanent delete message from trash""" + sqlExecute("DELETE FROM inbox WHERE msgid = ?", ack_data) + + def undeleteMessage(msgid): """Undelte the message""" sqlExecute('''UPDATE inbox SET folder='inbox' WHERE msgid=?''', msgid) diff --git a/src/helper_msgcoding.py b/src/helper_msgcoding.py index 76447884..abc9a228 100644 --- a/src/helper_msgcoding.py +++ b/src/helper_msgcoding.py @@ -6,7 +6,7 @@ import string import zlib import messagetypes -from bmconfigparser import BMConfigParser +from bmconfigparser import config from debug import logger from tr import _translate @@ -100,10 +100,10 @@ class MsgDecode(object): """Handle extended encoding""" dc = zlib.decompressobj() tmp = "" - while len(tmp) <= BMConfigParser().safeGetInt("zlib", "maxsize"): + while len(tmp) <= config.safeGetInt("zlib", "maxsize"): try: got = dc.decompress( - data, BMConfigParser().safeGetInt("zlib", "maxsize") + data, config.safeGetInt("zlib", "maxsize") + 1 - len(tmp)) # EOF if got == "": diff --git a/src/helper_random.py b/src/helper_random.py index 2e6a151b..e6da707e 100644 --- a/src/helper_random.py +++ b/src/helper_random.py @@ -1,12 +1,7 @@ """Convenience functions for random operations. Not suitable for security / cryptography operations.""" -import os import random -try: - from pyelliptic.openssl import OpenSSL -except ImportError: - from .pyelliptic.openssl import OpenSSL NoneType = type(None) @@ -16,14 +11,6 @@ def seed(): random.seed() -def randomBytes(n): - """Method randomBytes.""" - try: - return os.urandom(n) - except NotImplementedError: - return OpenSSL.rand(n) - - def randomshuffle(population): """Method randomShuffle. diff --git a/src/helper_sent.py b/src/helper_sent.py index d83afce6..aa76e756 100644 --- a/src/helper_sent.py +++ b/src/helper_sent.py @@ -5,9 +5,9 @@ Insert values into sent table import time import uuid from addresses import decodeAddress -from bmconfigparser import BMConfigParser +from bmconfigparser import config from helper_ackPayload import genAckPayload -from helper_sql import sqlExecute +from helper_sql import sqlExecute, sqlQuery # pylint: disable=too-many-arguments @@ -27,7 +27,7 @@ def insert(msgid=None, toAddress='[Broadcast subscribers]', fromAddress=None, su ripe = new_ripe if not ackdata: - stealthLevel = BMConfigParser().safeGetInt( + stealthLevel = config.safeGetInt( 'bitmessagesettings', 'ackstealthlevel') new_ackdata = genAckPayload(streamNumber, stealthLevel) ackdata = new_ackdata @@ -36,7 +36,7 @@ def insert(msgid=None, toAddress='[Broadcast subscribers]', fromAddress=None, su sentTime = sentTime if sentTime else int(time.time()) # sentTime (this doesn't change) lastActionTime = lastActionTime if lastActionTime else int(time.time()) - ttl = ttl if ttl else BMConfigParser().getint('bitmessagesettings', 'ttl') + ttl = ttl if ttl else config.getint('bitmessagesettings', 'ttl') t = (msgid, toAddress, ripe, fromAddress, subject, message, ackdata, sentTime, lastActionTime, sleeptill, status, retryNumber, folder, @@ -46,3 +46,24 @@ def insert(msgid=None, toAddress='[Broadcast subscribers]', fromAddress=None, su return ackdata else: return None + + +def delete(ack_data): + """Perform Delete query""" + sqlExecute("DELETE FROM sent WHERE ackdata = ?", ack_data) + + +def retrieve_message_details(ack_data): + """Retrieving Message details""" + data = sqlQuery( + "select toaddress, fromaddress, subject, message, received from inbox where msgid = ?", ack_data + ) + return data + + +def trash(ackdata): + """Mark a message in the `sent` as `trash`""" + rowcount = sqlExecute( + '''UPDATE sent SET folder='trash' WHERE ackdata=?''', ackdata + ) + return rowcount diff --git a/src/helper_sql.py b/src/helper_sql.py index cba98884..8dee9e0c 100644 --- a/src/helper_sql.py +++ b/src/helper_sql.py @@ -33,6 +33,8 @@ sql_available = False sql_ready = threading.Event() """set by `.threads.sqlThread` when ready for processing (after initialization is done)""" +sql_timeout = 60 +"""timeout for waiting for sql_ready in seconds""" def sqlQuery(sql_statement, *args): diff --git a/src/helper_startup.py b/src/helper_startup.py index b4951668..52e1bf7a 100644 --- a/src/helper_startup.py +++ b/src/helper_startup.py @@ -12,16 +12,17 @@ import sys import time from distutils.version import StrictVersion from struct import pack +from six.moves import configparser try: import defaults import helper_random import paths import state - from bmconfigparser import BMConfigParser + from bmconfigparser import config, config_ready except ImportError: from . import defaults, helper_random, paths, state - from .bmconfigparser import BMConfigParser + from .bmconfigparser import config, config_ready try: from plugins.plugin import get_plugin @@ -38,7 +39,6 @@ StoreConfigFilesInSameDirectoryAsProgramByDefault = False def loadConfig(): """Load the config""" - config = BMConfigParser() if state.appdata: config.read(state.appdata + 'keys.dat') # state.appdata must have been specified as a startup option. @@ -70,12 +70,9 @@ def loadConfig(): # This appears to be the first time running the program; there is # no config file (or it cannot be accessed). Create config file. - config.add_section('bitmessagesettings') + # config.add_section('bitmessagesettings') + config.read() config.set('bitmessagesettings', 'settingsversion', '10') - config.set('bitmessagesettings', 'port', '8444') - config.set('bitmessagesettings', 'timeformat', '%%c') - config.set('bitmessagesettings', 'blackwhitelist', 'black') - config.set('bitmessagesettings', 'startonlogon', 'false') if 'linux' in sys.platform: config.set('bitmessagesettings', 'minimizetotray', 'false') # This isn't implimented yet and when True on @@ -83,31 +80,16 @@ def loadConfig(): # running when minimized. else: config.set('bitmessagesettings', 'minimizetotray', 'true') - config.set('bitmessagesettings', 'showtraynotifications', 'true') - config.set('bitmessagesettings', 'startintray', 'false') - config.set('bitmessagesettings', 'socksproxytype', 'none') - config.set('bitmessagesettings', 'sockshostname', 'localhost') - config.set('bitmessagesettings', 'socksport', '9050') - config.set('bitmessagesettings', 'socksauthentication', 'false') - config.set('bitmessagesettings', 'socksusername', '') - config.set('bitmessagesettings', 'sockspassword', '') - config.set('bitmessagesettings', 'keysencrypted', 'false') - config.set('bitmessagesettings', 'messagesencrypted', 'false') config.set( 'bitmessagesettings', 'defaultnoncetrialsperbyte', str(defaults.networkDefaultProofOfWorkNonceTrialsPerByte)) config.set( 'bitmessagesettings', 'defaultpayloadlengthextrabytes', str(defaults.networkDefaultPayloadLengthExtraBytes)) - config.set('bitmessagesettings', 'minimizeonclose', 'false') config.set('bitmessagesettings', 'dontconnect', 'true') - config.set('bitmessagesettings', 'replybelow', 'False') - config.set('bitmessagesettings', 'maxdownloadrate', '0') - config.set('bitmessagesettings', 'maxuploadrate', '0') - # UI setting to stop trying to send messages after X days/months - config.set('bitmessagesettings', 'stopresendingafterxdays', '') - config.set('bitmessagesettings', 'stopresendingafterxmonths', '') + # config.set('bitmessagesettings', 'stopresendingafterxdays', '') + # config.set('bitmessagesettings', 'stopresendingafterxmonths', '') # Are you hoping to add a new option to the keys.dat file? You're in # the right place for adding it to users who install the software for @@ -130,11 +112,11 @@ def loadConfig(): config.save() else: updateConfig() + config_ready.set() def updateConfig(): """Save the config""" - config = BMConfigParser() settingsversion = config.getint('bitmessagesettings', 'settingsversion') if settingsversion == 1: config.set('bitmessagesettings', 'socksproxytype', 'none') @@ -237,7 +219,8 @@ def updateConfig(): config.set( addressInKeysFile, 'payloadlengthextrabytes', str(int(previousSmallMessageDifficulty * 1000))) - except Exception: + except (ValueError, TypeError, configparser.NoSectionError, + configparser.NoOptionError): continue config.set('bitmessagesettings', 'maxdownloadrate', '0') config.set('bitmessagesettings', 'maxuploadrate', '0') @@ -259,8 +242,6 @@ def updateConfig(): * defaults.networkDefaultPayloadLengthExtraBytes) ) - if not config.has_option('bitmessagesettings', 'onionhostname'): - config.set('bitmessagesettings', 'onionhostname', '') if not config.has_option('bitmessagesettings', 'onionport'): config.set('bitmessagesettings', 'onionport', '8444') if not config.has_option('bitmessagesettings', 'onionbindip'): @@ -286,7 +267,7 @@ def updateConfig(): def adjustHalfOpenConnectionsLimit(): """Check and satisfy half-open connections limit (mainly XP and Vista)""" - if BMConfigParser().safeGet( + if config.safeGet( 'bitmessagesettings', 'socksproxytype', 'none') != 'none': state.maximumNumberOfHalfOpenConnections = 4 return @@ -373,7 +354,7 @@ def start_proxyconfig(): """Check socksproxytype and start any proxy configuration plugin""" if not get_plugin: return - config = BMConfigParser() + config_ready.wait() proxy_type = config.safeGet('bitmessagesettings', 'socksproxytype') if proxy_type and proxy_type not in ('none', 'SOCKS4a', 'SOCKS5'): try: diff --git a/src/highlevelcrypto.py b/src/highlevelcrypto.py index 82743acf..d59a721d 100644 --- a/src/highlevelcrypto.py +++ b/src/highlevelcrypto.py @@ -7,28 +7,98 @@ High level cryptographic functions based on `.pyelliptic` OpenSSL bindings. `More discussion. `_ """ +import hashlib +import os from binascii import hexlify import pyelliptic from pyelliptic import OpenSSL from pyelliptic import arithmetic as a -from bmconfigparser import BMConfigParser +from fallback import RIPEMD160Hash -__all__ = ['encrypt', 'makeCryptor', 'pointMult', 'privToPub', 'sign', 'verify'] +__all__ = [ + 'decodeWalletImportFormat', 'deterministic_keys', + 'double_sha512', 'calculateInventoryHash', 'encodeWalletImportFormat', + 'encrypt', 'makeCryptor', 'pointMult', 'privToPub', 'randomBytes', + 'random_keys', 'sign', 'to_ripe', 'verify'] -def makeCryptor(privkey): - """Return a private `.pyelliptic.ECC` instance""" - private_key = a.changebase(privkey, 16, 256, minlen=32) - public_key = pointMult(private_key) - privkey_bin = b'\x02\xca\x00\x20' + private_key - pubkey_bin = ( - b'\x02\xca\x00\x20' + public_key[1:-32] + b'\x00\x20' + public_key[-32:] - ) - cryptor = pyelliptic.ECC( - curve='secp256k1', privkey=privkey_bin, pubkey=pubkey_bin) - return cryptor +# WIF (uses arithmetic ): +def decodeWalletImportFormat(WIFstring): + """ + Convert private key from base58 that's used in the config file to + 8-bit binary string. + """ + fullString = a.changebase(WIFstring, 58, 256) + privkey = fullString[:-4] + if fullString[-4:] != \ + hashlib.sha256(hashlib.sha256(privkey).digest()).digest()[:4]: + raise ValueError('Checksum failed') + elif privkey[0:1] == b'\x80': # checksum passed + return privkey[1:] + + raise ValueError('No hex 80 prefix') + + +# An excellent way for us to store our keys +# is in Wallet Import Format. Let us convert now. +# https://en.bitcoin.it/wiki/Wallet_import_format +def encodeWalletImportFormat(privKey): + """ + Convert private key from binary 8-bit string into base58check WIF string. + """ + privKey = b'\x80' + privKey + checksum = hashlib.sha256(hashlib.sha256(privKey).digest()).digest()[0:4] + return a.changebase(privKey + checksum, 256, 58) + + +# Random + +def randomBytes(n): + """Get n random bytes""" + try: + return os.urandom(n) + except NotImplementedError: + return OpenSSL.rand(n) + + +# Hashes + +def _bm160(data): + """RIPEME160(SHA512(data)) -> bytes""" + return RIPEMD160Hash(hashlib.sha512(data).digest()).digest() + + +def to_ripe(signing_key, encryption_key): + """Convert two public keys to a ripe hash""" + return _bm160(signing_key + encryption_key) + + +def double_sha512(data): + """Binary double SHA512 digest""" + return hashlib.sha512(hashlib.sha512(data).digest()).digest() + + +def calculateInventoryHash(data): + """Calculate inventory hash from object data""" + return double_sha512(data)[:32] + + +# Keys + +def random_keys(): + """Return a pair of keys, private and public""" + priv = randomBytes(32) + pub = pointMult(priv) + return priv, pub + + +def deterministic_keys(passphrase, nonce): + """Generate keys from *passphrase* and *nonce* (encoded as varint)""" + priv = hashlib.sha512(passphrase + nonce).digest()[:32] + pub = pointMult(priv) + return priv, pub def hexToPubkey(pubkey): @@ -38,12 +108,6 @@ def hexToPubkey(pubkey): return pubkey_bin -def makePubCryptor(pubkey): - """Return a public `.pyelliptic.ECC` instance""" - pubkey_bin = hexToPubkey(pubkey) - return pyelliptic.ECC(curve='secp256k1', pubkey=pubkey_bin) - - def privToPub(privkey): """Converts hex private key into hex public key""" private_key = a.changebase(privkey, 16, 256, minlen=32) @@ -51,63 +115,6 @@ def privToPub(privkey): return hexlify(public_key) -def encrypt(msg, hexPubkey): - """Encrypts message with hex public key""" - return pyelliptic.ECC(curve='secp256k1').encrypt( - msg, hexToPubkey(hexPubkey)) - - -def decrypt(msg, hexPrivkey): - """Decrypts message with hex private key""" - return makeCryptor(hexPrivkey).decrypt(msg) - - -def decryptFast(msg, cryptor): - """Decrypts message with an existing `.pyelliptic.ECC` object""" - return cryptor.decrypt(msg) - - -def sign(msg, hexPrivkey): - """ - Signs with hex private key using SHA1 or SHA256 depending on - "digestalg" setting - """ - digestAlg = BMConfigParser().safeGet( - 'bitmessagesettings', 'digestalg', 'sha256') - if digestAlg == "sha1": - # SHA1, this will eventually be deprecated - return makeCryptor(hexPrivkey).sign( - msg, digest_alg=OpenSSL.digest_ecdsa_sha1) - elif digestAlg == "sha256": - # SHA256. Eventually this will become the default - return makeCryptor(hexPrivkey).sign(msg, digest_alg=OpenSSL.EVP_sha256) - else: - raise ValueError("Unknown digest algorithm %s" % digestAlg) - - -def verify(msg, sig, hexPubkey): - """Verifies with hex public key using SHA1 or SHA256""" - # As mentioned above, we must upgrade gracefully to use SHA256. So - # let us check the signature using both SHA1 and SHA256 and if one - # of them passes then we will be satisfied. Eventually this can - # be simplified and we'll only check with SHA256. - try: - # old SHA1 algorithm. - sigVerifyPassed = makePubCryptor(hexPubkey).verify( - sig, msg, digest_alg=OpenSSL.digest_ecdsa_sha1) - except: - sigVerifyPassed = False - if sigVerifyPassed: - # The signature check passed using SHA1 - return True - # The signature check using SHA1 failed. Let us try it with SHA256. - try: - return makePubCryptor(hexPubkey).verify( - sig, msg, digest_alg=OpenSSL.EVP_sha256) - except: - return False - - def pointMult(secret): """ Does an EC point multiplication; turns a private key into a public key. @@ -134,9 +141,6 @@ def pointMult(secret): mb = OpenSSL.create_string_buffer(size) OpenSSL.i2o_ECPublicKey(k, OpenSSL.byref(OpenSSL.pointer(mb))) - OpenSSL.EC_POINT_free(pub_key) - OpenSSL.BN_free(priv_key) - OpenSSL.EC_KEY_free(k) return mb.raw except Exception: @@ -144,3 +148,85 @@ def pointMult(secret): import time traceback.print_exc() time.sleep(0.2) + finally: + OpenSSL.EC_POINT_free(pub_key) + OpenSSL.BN_free(priv_key) + OpenSSL.EC_KEY_free(k) + + +# Encryption + +def makeCryptor(privkey, curve='secp256k1'): + """Return a private `.pyelliptic.ECC` instance""" + private_key = a.changebase(privkey, 16, 256, minlen=32) + public_key = pointMult(private_key) + cryptor = pyelliptic.ECC( + pubkey_x=public_key[1:-32], pubkey_y=public_key[-32:], + raw_privkey=private_key, curve=curve) + return cryptor + + +def makePubCryptor(pubkey): + """Return a public `.pyelliptic.ECC` instance""" + pubkey_bin = hexToPubkey(pubkey) + return pyelliptic.ECC(curve='secp256k1', pubkey=pubkey_bin) + + +def encrypt(msg, hexPubkey): + """Encrypts message with hex public key""" + return pyelliptic.ECC(curve='secp256k1').encrypt( + msg, hexToPubkey(hexPubkey)) + + +def decrypt(msg, hexPrivkey): + """Decrypts message with hex private key""" + return makeCryptor(hexPrivkey).decrypt(msg) + + +def decryptFast(msg, cryptor): + """Decrypts message with an existing `.pyelliptic.ECC` object""" + return cryptor.decrypt(msg) + + +# Signatures + +def _choose_digest_alg(name): + """ + Choose openssl digest constant by name raises ValueError if not appropriate + """ + if name not in ("sha1", "sha256"): + raise ValueError("Unknown digest algorithm %s" % name) + return ( + # SHA1, this will eventually be deprecated + OpenSSL.digest_ecdsa_sha1 if name == "sha1" else OpenSSL.EVP_sha256) + + +def sign(msg, hexPrivkey, digestAlg="sha256"): + """ + Signs with hex private key using SHA1 or SHA256 depending on + *digestAlg* keyword. + """ + return makeCryptor(hexPrivkey).sign( + msg, digest_alg=_choose_digest_alg(digestAlg)) + + +def verify(msg, sig, hexPubkey, digestAlg=None): + """Verifies with hex public key using SHA1 or SHA256""" + # As mentioned above, we must upgrade gracefully to use SHA256. So + # let us check the signature using both SHA1 and SHA256 and if one + # of them passes then we will be satisfied. Eventually this can + # be simplified and we'll only check with SHA256. + if digestAlg is None: + # old SHA1 algorithm. + sigVerifyPassed = verify(msg, sig, hexPubkey, "sha1") + if sigVerifyPassed: + # The signature check passed using SHA1 + return True + # The signature check using SHA1 failed. Let us try it with SHA256. + return verify(msg, sig, hexPubkey, "sha256") + + try: + return makePubCryptor(hexPubkey).verify( + sig, msg, digest_alg=_choose_digest_alg(digestAlg)) + except: + return False diff --git a/src/images/kivy/down-arrow.png b/src/images/kivy/down-arrow.png new file mode 100644 index 00000000..bf3e864c Binary files /dev/null and b/src/images/kivy/down-arrow.png differ diff --git a/src/images/kivy/draft-icon.png b/src/images/kivy/draft-icon.png new file mode 100644 index 00000000..9fc38f31 Binary files /dev/null and b/src/images/kivy/draft-icon.png differ diff --git a/src/images/kivy/drawer_logo1.png b/src/images/kivy/drawer_logo1.png new file mode 100644 index 00000000..256f9be6 Binary files /dev/null and b/src/images/kivy/drawer_logo1.png differ diff --git a/src/images/kivy/loader.gif b/src/images/kivy/loader.gif new file mode 100644 index 00000000..34ab1943 Binary files /dev/null and b/src/images/kivy/loader.gif differ diff --git a/src/images/kivy/payment/btc.png b/src/images/kivy/payment/btc.png new file mode 100644 index 00000000..33302ff8 Binary files /dev/null and b/src/images/kivy/payment/btc.png differ diff --git a/src/images/kivy/payment/buy.png b/src/images/kivy/payment/buy.png new file mode 100644 index 00000000..3a63af11 Binary files /dev/null and b/src/images/kivy/payment/buy.png differ diff --git a/src/images/kivy/payment/buynew1.png b/src/images/kivy/payment/buynew1.png new file mode 100644 index 00000000..f02090f8 Binary files /dev/null and b/src/images/kivy/payment/buynew1.png differ diff --git a/src/images/kivy/payment/gplay.png b/src/images/kivy/payment/gplay.png new file mode 100644 index 00000000..69550edd Binary files /dev/null and b/src/images/kivy/payment/gplay.png differ diff --git a/src/images/kivy/payment/paypal.png b/src/images/kivy/payment/paypal.png new file mode 100644 index 00000000..f994130d Binary files /dev/null and b/src/images/kivy/payment/paypal.png differ diff --git a/src/images/kivy/right-arrow.png b/src/images/kivy/right-arrow.png new file mode 100644 index 00000000..8f136a77 Binary files /dev/null and b/src/images/kivy/right-arrow.png differ diff --git a/src/images/kivy/search.png b/src/images/kivy/search.png new file mode 100644 index 00000000..42a1e45a Binary files /dev/null and b/src/images/kivy/search.png differ diff --git a/src/images/kivy/text_images/!.png b/src/images/kivy/text_images/!.png new file mode 100644 index 00000000..bac2f246 Binary files /dev/null and b/src/images/kivy/text_images/!.png differ diff --git a/src/images/kivy/text_images/0.png b/src/images/kivy/text_images/0.png new file mode 100644 index 00000000..2b8b63e3 Binary files /dev/null and b/src/images/kivy/text_images/0.png differ diff --git a/src/images/kivy/text_images/1.png b/src/images/kivy/text_images/1.png new file mode 100644 index 00000000..3918f6d3 Binary files /dev/null and b/src/images/kivy/text_images/1.png differ diff --git a/src/images/kivy/text_images/2.png b/src/images/kivy/text_images/2.png new file mode 100644 index 00000000..0cf202e9 Binary files /dev/null and b/src/images/kivy/text_images/2.png differ diff --git a/src/images/kivy/text_images/3.png b/src/images/kivy/text_images/3.png new file mode 100644 index 00000000..f9d612dd Binary files /dev/null and b/src/images/kivy/text_images/3.png differ diff --git a/src/images/kivy/text_images/4.png b/src/images/kivy/text_images/4.png new file mode 100644 index 00000000..f2ab33e1 Binary files /dev/null and b/src/images/kivy/text_images/4.png differ diff --git a/src/images/kivy/text_images/5.png b/src/images/kivy/text_images/5.png new file mode 100644 index 00000000..09d6e56e Binary files /dev/null and b/src/images/kivy/text_images/5.png differ diff --git a/src/images/kivy/text_images/6.png b/src/images/kivy/text_images/6.png new file mode 100644 index 00000000..e385a954 Binary files /dev/null and b/src/images/kivy/text_images/6.png differ diff --git a/src/images/kivy/text_images/7.png b/src/images/kivy/text_images/7.png new file mode 100644 index 00000000..55fc4f77 Binary files /dev/null and b/src/images/kivy/text_images/7.png differ diff --git a/src/images/kivy/text_images/8.png b/src/images/kivy/text_images/8.png new file mode 100644 index 00000000..2a3fa76f Binary files /dev/null and b/src/images/kivy/text_images/8.png differ diff --git a/src/images/kivy/text_images/9.png b/src/images/kivy/text_images/9.png new file mode 100644 index 00000000..81ad9084 Binary files /dev/null and b/src/images/kivy/text_images/9.png differ diff --git a/src/images/kivy/text_images/A.png b/src/images/kivy/text_images/A.png new file mode 100644 index 00000000..64ed6110 Binary files /dev/null and b/src/images/kivy/text_images/A.png differ diff --git a/src/images/kivy/text_images/B.png b/src/images/kivy/text_images/B.png new file mode 100644 index 00000000..2db56c1f Binary files /dev/null and b/src/images/kivy/text_images/B.png differ diff --git a/src/images/kivy/text_images/C.png b/src/images/kivy/text_images/C.png new file mode 100644 index 00000000..47a4052c Binary files /dev/null and b/src/images/kivy/text_images/C.png differ diff --git a/src/images/kivy/text_images/D.png b/src/images/kivy/text_images/D.png new file mode 100644 index 00000000..2549ffc2 Binary files /dev/null and b/src/images/kivy/text_images/D.png differ diff --git a/src/images/kivy/text_images/E.png b/src/images/kivy/text_images/E.png new file mode 100644 index 00000000..5d631611 Binary files /dev/null and b/src/images/kivy/text_images/E.png differ diff --git a/src/images/kivy/text_images/F.png b/src/images/kivy/text_images/F.png new file mode 100644 index 00000000..43086f38 Binary files /dev/null and b/src/images/kivy/text_images/F.png differ diff --git a/src/images/kivy/text_images/G.png b/src/images/kivy/text_images/G.png new file mode 100644 index 00000000..32d1709d Binary files /dev/null and b/src/images/kivy/text_images/G.png differ diff --git a/src/images/kivy/text_images/H.png b/src/images/kivy/text_images/H.png new file mode 100644 index 00000000..279bd1ce Binary files /dev/null and b/src/images/kivy/text_images/H.png differ diff --git a/src/images/kivy/text_images/I.png b/src/images/kivy/text_images/I.png new file mode 100644 index 00000000..c88f048d Binary files /dev/null and b/src/images/kivy/text_images/I.png differ diff --git a/src/images/kivy/text_images/J.png b/src/images/kivy/text_images/J.png new file mode 100644 index 00000000..15331171 Binary files /dev/null and b/src/images/kivy/text_images/J.png differ diff --git a/src/images/kivy/text_images/K.png b/src/images/kivy/text_images/K.png new file mode 100644 index 00000000..9afcadd7 Binary files /dev/null and b/src/images/kivy/text_images/K.png differ diff --git a/src/images/kivy/text_images/L.png b/src/images/kivy/text_images/L.png new file mode 100644 index 00000000..e841b9d9 Binary files /dev/null and b/src/images/kivy/text_images/L.png differ diff --git a/src/images/kivy/text_images/M.png b/src/images/kivy/text_images/M.png new file mode 100644 index 00000000..10de35e9 Binary files /dev/null and b/src/images/kivy/text_images/M.png differ diff --git a/src/images/kivy/text_images/N.png b/src/images/kivy/text_images/N.png new file mode 100644 index 00000000..2d235d06 Binary files /dev/null and b/src/images/kivy/text_images/N.png differ diff --git a/src/images/kivy/text_images/O.png b/src/images/kivy/text_images/O.png new file mode 100644 index 00000000..c0cc972a Binary files /dev/null and b/src/images/kivy/text_images/O.png differ diff --git a/src/images/kivy/text_images/P.png b/src/images/kivy/text_images/P.png new file mode 100644 index 00000000..57ec5012 Binary files /dev/null and b/src/images/kivy/text_images/P.png differ diff --git a/src/images/kivy/text_images/Q.png b/src/images/kivy/text_images/Q.png new file mode 100644 index 00000000..27ffd18b Binary files /dev/null and b/src/images/kivy/text_images/Q.png differ diff --git a/src/images/kivy/text_images/R.png b/src/images/kivy/text_images/R.png new file mode 100644 index 00000000..090646f5 Binary files /dev/null and b/src/images/kivy/text_images/R.png differ diff --git a/src/images/kivy/text_images/S.png b/src/images/kivy/text_images/S.png new file mode 100644 index 00000000..444419cf Binary files /dev/null and b/src/images/kivy/text_images/S.png differ diff --git a/src/images/kivy/text_images/T.png b/src/images/kivy/text_images/T.png new file mode 100644 index 00000000..ace7b36b Binary files /dev/null and b/src/images/kivy/text_images/T.png differ diff --git a/src/images/kivy/text_images/U.png b/src/images/kivy/text_images/U.png new file mode 100644 index 00000000..a47f326e Binary files /dev/null and b/src/images/kivy/text_images/U.png differ diff --git a/src/images/kivy/text_images/V.png b/src/images/kivy/text_images/V.png new file mode 100644 index 00000000..da07d0ac Binary files /dev/null and b/src/images/kivy/text_images/V.png differ diff --git a/src/images/kivy/text_images/W.png b/src/images/kivy/text_images/W.png new file mode 100644 index 00000000..a00f9d7c Binary files /dev/null and b/src/images/kivy/text_images/W.png differ diff --git a/src/images/kivy/text_images/X.png b/src/images/kivy/text_images/X.png new file mode 100644 index 00000000..be919fc4 Binary files /dev/null and b/src/images/kivy/text_images/X.png differ diff --git a/src/images/kivy/text_images/Y.png b/src/images/kivy/text_images/Y.png new file mode 100644 index 00000000..4819bbd1 Binary files /dev/null and b/src/images/kivy/text_images/Y.png differ diff --git a/src/images/kivy/text_images/Z.png b/src/images/kivy/text_images/Z.png new file mode 100644 index 00000000..7d1c8e01 Binary files /dev/null and b/src/images/kivy/text_images/Z.png differ diff --git a/src/images/kivymd_logo.png b/src/images/kivymd_logo.png deleted file mode 100644 index ce39b0d4..00000000 Binary files a/src/images/kivymd_logo.png and /dev/null differ diff --git a/src/images/me.jpg b/src/images/me.jpg deleted file mode 100644 index f54c791f..00000000 Binary files a/src/images/me.jpg and /dev/null differ diff --git a/src/images/ngletteravatar/1.png b/src/images/ngletteravatar/1.png deleted file mode 100644 index 9436c7d2..00000000 Binary files a/src/images/ngletteravatar/1.png and /dev/null differ diff --git a/src/images/ngletteravatar/12.png b/src/images/ngletteravatar/12.png deleted file mode 100644 index de894b4a..00000000 Binary files a/src/images/ngletteravatar/12.png and /dev/null differ diff --git a/src/images/ngletteravatar/14.png b/src/images/ngletteravatar/14.png deleted file mode 100644 index 42b692dd..00000000 Binary files a/src/images/ngletteravatar/14.png and /dev/null differ diff --git a/src/images/ngletteravatar/3.png b/src/images/ngletteravatar/3.png deleted file mode 100644 index ad83cef2..00000000 Binary files a/src/images/ngletteravatar/3.png and /dev/null differ diff --git a/src/images/ngletteravatar/5.png b/src/images/ngletteravatar/5.png deleted file mode 100644 index 3875aace..00000000 Binary files a/src/images/ngletteravatar/5.png and /dev/null differ diff --git a/src/images/ngletteravatar/56.png b/src/images/ngletteravatar/56.png deleted file mode 100644 index 33f038cf..00000000 Binary files a/src/images/ngletteravatar/56.png and /dev/null differ diff --git a/src/images/ngletteravatar/65.png b/src/images/ngletteravatar/65.png deleted file mode 100644 index fb608098..00000000 Binary files a/src/images/ngletteravatar/65.png and /dev/null differ diff --git a/src/images/ngletteravatar/8.png b/src/images/ngletteravatar/8.png deleted file mode 100644 index dd2671e9..00000000 Binary files a/src/images/ngletteravatar/8.png and /dev/null differ diff --git a/src/images/ngletteravatar/90.png b/src/images/ngletteravatar/90.png deleted file mode 100644 index 7d1770ab..00000000 Binary files a/src/images/ngletteravatar/90.png and /dev/null differ diff --git a/src/images/ngletteravatar/Galleryr_rcirclelogo_Small.jpg b/src/images/ngletteravatar/Galleryr_rcirclelogo_Small.jpg deleted file mode 100644 index 8e3b1c35..00000000 Binary files a/src/images/ngletteravatar/Galleryr_rcirclelogo_Small.jpg and /dev/null differ diff --git a/src/images/ngletteravatar/a.png b/src/images/ngletteravatar/a.png deleted file mode 100644 index 1c3bb8bf..00000000 Binary files a/src/images/ngletteravatar/a.png and /dev/null differ diff --git a/src/images/ngletteravatar/b.png b/src/images/ngletteravatar/b.png deleted file mode 100644 index 462bf808..00000000 Binary files a/src/images/ngletteravatar/b.png and /dev/null differ diff --git a/src/images/ngletteravatar/c.png b/src/images/ngletteravatar/c.png deleted file mode 100644 index 180b7edc..00000000 Binary files a/src/images/ngletteravatar/c.png and /dev/null differ diff --git a/src/images/ngletteravatar/d.png b/src/images/ngletteravatar/d.png deleted file mode 100644 index 9e983e4d..00000000 Binary files a/src/images/ngletteravatar/d.png and /dev/null differ diff --git a/src/images/ngletteravatar/depositphotos_142729281-stock-illustration-letter-l-sign-design-template.jpg b/src/images/ngletteravatar/depositphotos_142729281-stock-illustration-letter-l-sign-design-template.jpg deleted file mode 100644 index 7630511f..00000000 Binary files a/src/images/ngletteravatar/depositphotos_142729281-stock-illustration-letter-l-sign-design-template.jpg and /dev/null differ diff --git a/src/images/ngletteravatar/e.png b/src/images/ngletteravatar/e.png deleted file mode 100644 index 60961992..00000000 Binary files a/src/images/ngletteravatar/e.png and /dev/null differ diff --git a/src/images/ngletteravatar/g.png b/src/images/ngletteravatar/g.png deleted file mode 100644 index 762cc6fa..00000000 Binary files a/src/images/ngletteravatar/g.png and /dev/null differ diff --git a/src/images/ngletteravatar/h.png b/src/images/ngletteravatar/h.png deleted file mode 100644 index 8f752952..00000000 Binary files a/src/images/ngletteravatar/h.png and /dev/null differ diff --git a/src/images/ngletteravatar/i.png b/src/images/ngletteravatar/i.png deleted file mode 100644 index a89710ef..00000000 Binary files a/src/images/ngletteravatar/i.png and /dev/null differ diff --git a/src/images/ngletteravatar/j.png b/src/images/ngletteravatar/j.png deleted file mode 100644 index 926de34c..00000000 Binary files a/src/images/ngletteravatar/j.png and /dev/null differ diff --git a/src/images/ngletteravatar/k.png b/src/images/ngletteravatar/k.png deleted file mode 100644 index 77da163e..00000000 Binary files a/src/images/ngletteravatar/k.png and /dev/null differ diff --git a/src/images/ngletteravatar/l.png b/src/images/ngletteravatar/l.png deleted file mode 100644 index e6112f2a..00000000 Binary files a/src/images/ngletteravatar/l.png and /dev/null differ diff --git a/src/images/ngletteravatar/m.png b/src/images/ngletteravatar/m.png deleted file mode 100644 index c93554a1..00000000 Binary files a/src/images/ngletteravatar/m.png and /dev/null differ diff --git a/src/images/ngletteravatar/n.png b/src/images/ngletteravatar/n.png deleted file mode 100644 index 8158a1e4..00000000 Binary files a/src/images/ngletteravatar/n.png and /dev/null differ diff --git a/src/images/ngletteravatar/o.png b/src/images/ngletteravatar/o.png deleted file mode 100644 index 729a82a6..00000000 Binary files a/src/images/ngletteravatar/o.png and /dev/null differ diff --git a/src/images/ngletteravatar/p.png b/src/images/ngletteravatar/p.png deleted file mode 100644 index b3adb0ce..00000000 Binary files a/src/images/ngletteravatar/p.png and /dev/null differ diff --git a/src/images/ngletteravatar/r.png b/src/images/ngletteravatar/r.png deleted file mode 100644 index 1b64b8ee..00000000 Binary files a/src/images/ngletteravatar/r.png and /dev/null differ diff --git a/src/images/ngletteravatar/s.png b/src/images/ngletteravatar/s.png deleted file mode 100644 index 8813d11a..00000000 Binary files a/src/images/ngletteravatar/s.png and /dev/null differ diff --git a/src/images/ngletteravatar/t.jpg b/src/images/ngletteravatar/t.jpg deleted file mode 100644 index 20932aad..00000000 Binary files a/src/images/ngletteravatar/t.jpg and /dev/null differ diff --git a/src/images/ngletteravatar/u.png b/src/images/ngletteravatar/u.png deleted file mode 100644 index 6af53add..00000000 Binary files a/src/images/ngletteravatar/u.png and /dev/null differ diff --git a/src/images/ngletteravatar/v.png b/src/images/ngletteravatar/v.png deleted file mode 100644 index aaaf191e..00000000 Binary files a/src/images/ngletteravatar/v.png and /dev/null differ diff --git a/src/images/ngletteravatar/w.png b/src/images/ngletteravatar/w.png deleted file mode 100644 index 20ff7ed9..00000000 Binary files a/src/images/ngletteravatar/w.png and /dev/null differ diff --git a/src/images/ngletteravatar/x.jpg b/src/images/ngletteravatar/x.jpg deleted file mode 100644 index 107f1732..00000000 Binary files a/src/images/ngletteravatar/x.jpg and /dev/null differ diff --git a/src/images/ngletteravatar/z.png b/src/images/ngletteravatar/z.png deleted file mode 100644 index efcda8fe..00000000 Binary files a/src/images/ngletteravatar/z.png and /dev/null differ diff --git a/src/inventory.py b/src/inventory.py index fc06e455..5b739e84 100644 --- a/src/inventory.py +++ b/src/inventory.py @@ -1,25 +1,29 @@ -"""The Inventory singleton""" +"""The Inventory""" # TODO make this dynamic, and watch out for frozen, like with messagetypes import storage.filesystem import storage.sqlite -from bmconfigparser import BMConfigParser -from singleton import Singleton +from bmconfigparser import config -@Singleton -class Inventory(): +def create_inventory_instance(backend="sqlite"): """ - Inventory singleton class which uses storage backends + Create an instance of the inventory class + defined in `storage.`. + """ + return getattr( + getattr(storage, backend), + "{}Inventory".format(backend.title()))() + + +class Inventory: + """ + Inventory class which uses storage backends to manage the inventory. """ def __init__(self): - self._moduleName = BMConfigParser().safeGet("inventory", "storage") - self._inventoryClass = getattr( - getattr(storage, self._moduleName), - "{}Inventory".format(self._moduleName.title()) - ) - self._realInventory = self._inventoryClass() + self._moduleName = config.safeGet("inventory", "storage") + self._realInventory = create_inventory_instance(self._moduleName) self.numberOfInventoryLookupsPerformed = 0 # cheap inheritance copied from asyncore @@ -39,3 +43,6 @@ class Inventory(): # hint for pylint: this is dictionary like object def __getitem__(self, key): return self._realInventory[key] + + def __setitem__(self, key, value): + self._realInventory[key] = value diff --git a/src/l10n.py b/src/l10n.py index 3b16f0b6..fe02d3f4 100644 --- a/src/l10n.py +++ b/src/l10n.py @@ -8,7 +8,7 @@ import time from six.moves import range -from bmconfigparser import BMConfigParser +from bmconfigparser import config logger = logging.getLogger('default') @@ -53,7 +53,7 @@ windowsLanguageMap = { } -time_format = BMConfigParser().safeGet( +time_format = config.safeGet( 'bitmessagesettings', 'timeformat', DEFAULT_TIME_FORMAT) if not re.search(r'\d', time.strftime(time_format)): @@ -125,7 +125,7 @@ def formatTimestamp(timestamp=None): def getTranslationLanguage(): """Return the user's language choice""" - userlocale = BMConfigParser().safeGet( + userlocale = config.safeGet( 'bitmessagesettings', 'userlocale', 'system') return userlocale if userlocale and userlocale != 'system' else language diff --git a/src/mock/kivy_main.py b/src/mock/kivy_main.py deleted file mode 100644 index badc1dc1..00000000 --- a/src/mock/kivy_main.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Mock kivy app with mock threads.""" - -from pybitmessage import state -from pybitmessage.bitmessagekivy.mpybit import NavigateApp -from class_addressGenerator import FakeAddressGenerator - - -def main(): - """main method for starting threads""" - # Start the address generation thread - addressGeneratorThread = FakeAddressGenerator() - # close the main program even if there are threads left - addressGeneratorThread.daemon = True - addressGeneratorThread.start() - - state.kivyapp = NavigateApp() - state.kivyapp.run() - - -if __name__ == '__main__': - main() diff --git a/src/mockbm/__init__.py b/src/mockbm/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/mock/class_addressGenerator.py b/src/mockbm/class_addressGenerator.py similarity index 89% rename from src/mock/class_addressGenerator.py rename to src/mockbm/class_addressGenerator.py index fbb34710..c84b92d5 100644 --- a/src/mock/class_addressGenerator.py +++ b/src/mockbm/class_addressGenerator.py @@ -11,7 +11,7 @@ from six.moves import queue from pybitmessage import state from pybitmessage import queues -from pybitmessage.bmconfigparser import BMConfigParser +from pybitmessage.bmconfigparser import config # from network.threads import StoppableThread @@ -83,14 +83,14 @@ class FakeAddressGenerator(StoppableThread): address = self.address_list.pop(0) label = queueValue[3] - BMConfigParser().add_section(address) - BMConfigParser().set(address, 'label', label) - BMConfigParser().set(address, 'enabled', 'true') - BMConfigParser().set( + config.add_section(address) + config.set(address, 'label', label) + config.set(address, 'enabled', 'true') + config.set( address, 'privsigningkey', fake_addresses[address]['privsigningkey']) - BMConfigParser().set( + config.set( address, 'privencryptionkey', fake_addresses[address]['privencryptionkey']) - BMConfigParser().save() + config.save() queues.addressGeneratorQueue.task_done() except IndexError: diff --git a/src/mockbm/helper_startup.py b/src/mockbm/helper_startup.py new file mode 100644 index 00000000..838302ae --- /dev/null +++ b/src/mockbm/helper_startup.py @@ -0,0 +1,13 @@ +import os +from pybitmessage.bmconfigparser import config + + +def loadConfig(): + """Loading mock test data""" + config.read(os.path.join(os.environ['BITMESSAGE_HOME'], 'keys.dat')) + + +def total_encrypted_messages_per_month(): + """Loading mock total encrypted message """ + encrypted_messages_per_month = 0 + return encrypted_messages_per_month diff --git a/src/mockbm/kivy_main.py b/src/mockbm/kivy_main.py new file mode 100644 index 00000000..4cf4da98 --- /dev/null +++ b/src/mockbm/kivy_main.py @@ -0,0 +1,40 @@ +# pylint: disable=unused-import, wrong-import-position, ungrouped-imports +# flake8: noqa:E401, E402 + +"""Mock kivy app with mock threads.""" + +import os +from kivy.config import Config +from pybitmessage.mockbm import multiqueue +from pybitmessage import state + +if os.environ.get("INSTALL_TESTS", False): + Config.set("graphics", "height", 1280) + Config.set("graphics", "width", 720) + Config.set("graphics", "position", "custom") + Config.set("graphics", "top", 0) + Config.set("graphics", "left", 0) + + +from pybitmessage.mockbm.class_addressGenerator import FakeAddressGenerator # noqa:E402 +from pybitmessage.bitmessagekivy.mpybit import NavigateApp # noqa:E402 +from pybitmessage.mockbm import network # noqa:E402 + +stats = network.stats +objectracker = network.objectracker + + +def main(): + """main method for starting threads""" + # Start the address generation thread + addressGeneratorThread = FakeAddressGenerator() + # close the main program even if there are threads left + addressGeneratorThread.daemon = True + addressGeneratorThread.start() + state.kivyapp = NavigateApp() + state.kivyapp.run() + addressGeneratorThread.stopThread() + + +if __name__ == "__main__": + main() diff --git a/src/mockbm/multiqueue.py b/src/mockbm/multiqueue.py new file mode 100644 index 00000000..8ec76920 --- /dev/null +++ b/src/mockbm/multiqueue.py @@ -0,0 +1,7 @@ +""" +Mock MultiQueue (just normal Queue) +""" + +from six.moves import queue + +MultiQueue = queue.Queue diff --git a/src/mockbm/network.py b/src/mockbm/network.py new file mode 100644 index 00000000..3f33c91b --- /dev/null +++ b/src/mockbm/network.py @@ -0,0 +1,25 @@ +# pylint: disable=too-few-public-methods + +""" +Mock Network +""" + + +class objectracker(object): + """Mock object tracker""" + + missingObjects = {} + + +class stats(object): + """Mock network statistics""" + + @staticmethod + def connectedHostsList(): + """Mock list of all the connected hosts""" + return ["conn1", "conn2", "conn3", "conn4"] + + @staticmethod + def pendingDownload(): + """Mock pending download count""" + return 0 diff --git a/src/namecoin.py b/src/namecoin.py index d1205261..87414570 100644 --- a/src/namecoin.py +++ b/src/namecoin.py @@ -12,7 +12,7 @@ import sys import defaults from addresses import decodeAddress -from bmconfigparser import BMConfigParser +from bmconfigparser import config from debug import logger from tr import _translate @@ -52,13 +52,15 @@ class namecoinConnection(object): actually changing the values (yet). """ if options is None: - self.nmctype = BMConfigParser().get( + self.nmctype = config.get( configSection, "namecoinrpctype") - self.host = BMConfigParser().get(configSection, "namecoinrpchost") - self.port = int(BMConfigParser().get( + self.host = config.get( + configSection, "namecoinrpchost") + self.port = int(config.get( configSection, "namecoinrpcport")) - self.user = BMConfigParser().get(configSection, "namecoinrpcuser") - self.password = BMConfigParser().get( + self.user = config.get( + configSection, "namecoinrpcuser") + self.password = config.get( configSection, "namecoinrpcpassword") else: self.nmctype = options["type"] @@ -310,14 +312,14 @@ def ensureNamecoinOptions(): that aren't there. """ - if not BMConfigParser().has_option(configSection, "namecoinrpctype"): - BMConfigParser().set(configSection, "namecoinrpctype", "namecoind") - if not BMConfigParser().has_option(configSection, "namecoinrpchost"): - BMConfigParser().set(configSection, "namecoinrpchost", "localhost") + if not config.has_option(configSection, "namecoinrpctype"): + config.set(configSection, "namecoinrpctype", "namecoind") + if not config.has_option(configSection, "namecoinrpchost"): + config.set(configSection, "namecoinrpchost", "localhost") - hasUser = BMConfigParser().has_option(configSection, "namecoinrpcuser") - hasPass = BMConfigParser().has_option(configSection, "namecoinrpcpassword") - hasPort = BMConfigParser().has_option(configSection, "namecoinrpcport") + hasUser = config.has_option(configSection, "namecoinrpcuser") + hasPass = config.has_option(configSection, "namecoinrpcpassword") + hasPort = config.has_option(configSection, "namecoinrpcport") # Try to read user/password from .namecoin configuration file. defaultUser = "" @@ -353,11 +355,10 @@ def ensureNamecoinOptions(): # If still nothing found, set empty at least. if not hasUser: - BMConfigParser().set(configSection, "namecoinrpcuser", defaultUser) + config.set(configSection, "namecoinrpcuser", defaultUser) if not hasPass: - BMConfigParser().set(configSection, "namecoinrpcpassword", defaultPass) + config.set(configSection, "namecoinrpcpassword", defaultPass) # Set default port now, possibly to found value. if not hasPort: - BMConfigParser().set( - configSection, "namecoinrpcport", defaults.namecoinDefaultRpcPort) + config.set(configSection, "namecoinrpcport", defaults.namecoinDefaultRpcPort) diff --git a/src/network/__init__.py b/src/network/__init__.py index 70613539..d89670a7 100644 --- a/src/network/__init__.py +++ b/src/network/__init__.py @@ -1,20 +1,44 @@ """ -Network subsystem packages +Network subsystem package """ -from addrthread import AddrThread -from announcethread import AnnounceThread -from connectionpool import BMConnectionPool -from dandelion import Dandelion -from downloadthread import DownloadThread -from invthread import InvThread -from networkthread import BMNetworkThread -from receivequeuethread import ReceiveQueueThread -from threads import StoppableThread -from uploadthread import UploadThread + +from .threads import StoppableThread -__all__ = [ - "BMConnectionPool", "Dandelion", - "AddrThread", "AnnounceThread", "BMNetworkThread", "DownloadThread", - "InvThread", "ReceiveQueueThread", "UploadThread", "StoppableThread" -] +__all__ = ["StoppableThread"] + + +def start(config, state): + """Start network threads""" + import state + from .announcethread import AnnounceThread + import connectionpool # pylint: disable=relative-import + from .addrthread import AddrThread + from .dandelion import Dandelion + from .downloadthread import DownloadThread + from .invthread import InvThread + from .networkthread import BMNetworkThread + from .knownnodes import readKnownNodes + from .receivequeuethread import ReceiveQueueThread + from .uploadthread import UploadThread + + readKnownNodes() + # init, needs to be early because other thread may access it early + state.Dandelion = Dandelion() + connectionpool.pool.connectToStream(1) + for thread in ( + BMNetworkThread(), InvThread(), AddrThread(), + DownloadThread(), UploadThread() + ): + thread.daemon = True + thread.start() + + # Optional components + for i in range(config.getint('threads', 'receive')): + thread = ReceiveQueueThread(i) + thread.daemon = True + thread.start() + if config.safeGetBoolean('bitmessagesettings', 'udp'): + state.announceThread = AnnounceThread() + state.announceThread.daemon = True + state.announceThread.start() diff --git a/src/network/addrthread.py b/src/network/addrthread.py index 8b46750f..a0e869e3 100644 --- a/src/network/addrthread.py +++ b/src/network/addrthread.py @@ -3,12 +3,12 @@ Announce addresses as they are received from other hosts """ from six.moves import queue - -import state +# magic imports! +import connectionpool from helper_random import randomshuffle -from network.assemble import assemble_addr -from network.connectionpool import BMConnectionPool -from queues import addrQueue +from protocol import assembleAddrMessage +from queues import addrQueue # FIXME: init with queue + from threads import StoppableThread @@ -17,7 +17,7 @@ class AddrThread(StoppableThread): name = "AddrBroadcaster" def run(self): - while not state.shutdown: + while not self._stopped: chunk = [] while True: try: @@ -28,7 +28,7 @@ class AddrThread(StoppableThread): if chunk: # Choose peers randomly - connections = BMConnectionPool().establishedConnections() + connections = connectionpool.pool.establishedConnections() randomshuffle(connections) for i in connections: randomshuffle(chunk) @@ -41,7 +41,7 @@ class AddrThread(StoppableThread): continue filtered.append((stream, peer, seen)) if filtered: - i.append_write_buf(assemble_addr(filtered)) + i.append_write_buf(assembleAddrMessage(filtered)) addrQueue.iterate() for i in range(len(chunk)): diff --git a/src/network/announcethread.py b/src/network/announcethread.py index e34ed963..7cb35e77 100644 --- a/src/network/announcethread.py +++ b/src/network/announcethread.py @@ -3,10 +3,11 @@ Announce myself (node address) """ import time -import state -from bmconfigparser import BMConfigParser -from network.assemble import assemble_addr -from network.connectionpool import BMConnectionPool +# magic imports! +import connectionpool +from bmconfigparser import config +from protocol import assembleAddrMessage + from node import Peer from threads import StoppableThread @@ -18,7 +19,7 @@ class AnnounceThread(StoppableThread): def run(self): lastSelfAnnounced = 0 - while not self._stopped and state.shutdown == 0: + while not self._stopped: processed = 0 if lastSelfAnnounced < time.time() - self.announceInterval: self.announceSelf() @@ -29,15 +30,14 @@ class AnnounceThread(StoppableThread): @staticmethod def announceSelf(): """Announce our presence""" - for connection in BMConnectionPool().udpSockets.values(): + for connection in connectionpool.pool.udpSockets.values(): if not connection.announcing: continue - for stream in state.streamsInWhichIAmParticipating: + for stream in connectionpool.pool.streams: addr = ( stream, Peer( '127.0.0.1', - BMConfigParser().safeGetInt( - 'bitmessagesettings', 'port')), - time.time()) - connection.append_write_buf(assemble_addr([addr])) + config.safeGetInt('bitmessagesettings', 'port')), + int(time.time())) + connection.append_write_buf(assembleAddrMessage([addr])) diff --git a/src/network/assemble.py b/src/network/assemble.py deleted file mode 100644 index 32fad3e4..00000000 --- a/src/network/assemble.py +++ /dev/null @@ -1,31 +0,0 @@ -""" -Create bitmessage protocol command packets -""" -import struct - -import addresses -from network.constants import MAX_ADDR_COUNT -from network.node import Peer -from protocol import CreatePacket, encodeHost - - -def assemble_addr(peerList): - """Create address command""" - if isinstance(peerList, Peer): - peerList = [peerList] - if not peerList: - return b'' - retval = b'' - for i in range(0, len(peerList), MAX_ADDR_COUNT): - payload = addresses.encodeVarint(len(peerList[i:i + MAX_ADDR_COUNT])) - for stream, peer, timestamp in peerList[i:i + MAX_ADDR_COUNT]: - # 64-bit time - payload += struct.pack('>Q', timestamp) - payload += struct.pack('>I', stream) - # service bit flags offered by this node - payload += struct.pack('>q', 1) - payload += encodeHost(peer.host) - # remote port - payload += struct.pack('>H', peer.port) - retval += CreatePacket('addr', payload) - return retval diff --git a/src/network/bmobject.py b/src/network/bmobject.py index 12b997d7..c91bf1b3 100644 --- a/src/network/bmobject.py +++ b/src/network/bmobject.py @@ -6,9 +6,8 @@ import time import protocol import state -from addresses import calculateInventoryHash -from inventory import Inventory -from network.dandelion import Dandelion +import connectionpool +from highlevelcrypto import calculateInventoryHash logger = logging.getLogger('default') @@ -19,12 +18,6 @@ class BMObjectInsufficientPOWError(Exception): errorCodes = ("Insufficient proof of work") -class BMObjectInvalidDataError(Exception): - """Exception indicating the data being parsed - does not match the specification.""" - errorCodes = ("Data invalid") - - class BMObjectExpiredError(Exception): """Exception indicating the object's lifetime has expired.""" errorCodes = ("Object expired") @@ -101,7 +94,12 @@ class BMObject(object): # pylint: disable=too-many-instance-attributes def checkStream(self): """Check if object's stream matches streams we are interested in""" - if self.streamNumber not in state.streamsInWhichIAmParticipating: + if self.streamNumber < protocol.MIN_VALID_STREAM \ + or self.streamNumber > protocol.MAX_VALID_STREAM: + logger.warning( + 'The object has invalid stream: %s', self.streamNumber) + raise BMObjectInvalidError() + if self.streamNumber not in connectionpool.pool.streams: logger.debug( 'The streamNumber %i isn\'t one we are interested in.', self.streamNumber) @@ -114,9 +112,9 @@ class BMObject(object): # pylint: disable=too-many-instance-attributes or advertise it unnecessarily) """ # if it's a stem duplicate, pretend we don't have it - if Dandelion().hasHash(self.inventoryHash): + if state.Dandelion.hasHash(self.inventoryHash): return - if self.inventoryHash in Inventory(): + if self.inventoryHash in state.Inventory: raise BMObjectAlreadyHaveError() def checkObjectByType(self): diff --git a/src/network/bmproto.py b/src/network/bmproto.py index 008eadb0..ed1d48c4 100644 --- a/src/network/bmproto.py +++ b/src/network/bmproto.py @@ -9,31 +9,27 @@ import re import socket import struct import time -from binascii import hexlify +# magic imports! import addresses -import connectionpool import knownnodes import protocol import state -from bmconfigparser import BMConfigParser -from inventory import Inventory +import connectionpool +from bmconfigparser import config +from queues import invQueue, objectProcessorQueue, portCheckerQueue +from randomtrackingdict import RandomTrackingDict from network.advanceddispatcher import AdvancedDispatcher from network.bmobject import ( BMObject, BMObjectAlreadyHaveError, BMObjectExpiredError, - BMObjectInsufficientPOWError, BMObjectInvalidDataError, - BMObjectInvalidError, BMObjectUnwantedStreamError + BMObjectInsufficientPOWError, BMObjectInvalidError, + BMObjectUnwantedStreamError ) -from network.constants import ( - ADDRESS_ALIVE, MAX_MESSAGE_SIZE, MAX_OBJECT_COUNT, - MAX_OBJECT_PAYLOAD_SIZE, MAX_TIME_OFFSET -) -from network.dandelion import Dandelion from network.proxy import ProxyError + from node import Node, Peer from objectracker import ObjectTracker, missingObjects -from queues import invQueue, objectProcessorQueue, portCheckerQueue -from randomtrackingdict import RandomTrackingDict + logger = logging.getLogger('default') @@ -87,7 +83,7 @@ class BMProto(AdvancedDispatcher, ObjectTracker): self.magic, self.command, self.payloadLength, self.checksum = \ protocol.Header.unpack(self.read_buf[:protocol.Header.size]) self.command = self.command.rstrip('\x00') - if self.magic != 0xE9BEB4D9: + if self.magic != protocol.magic: # skip 1 byte in order to sync self.set_state("bm_header", length=1) self.bm_proto_reset() @@ -96,7 +92,7 @@ class BMProto(AdvancedDispatcher, ObjectTracker): self.close_reason = "Bad magic" self.set_state("close") return False - if self.payloadLength > MAX_MESSAGE_SIZE: + if self.payloadLength > protocol.MAX_MESSAGE_SIZE: self.invalid = True self.set_state( "bm_command", @@ -129,8 +125,6 @@ class BMProto(AdvancedDispatcher, ObjectTracker): logger.debug('too much data, skipping') except BMObjectInsufficientPOWError: logger.debug('insufficient PoW, skipping') - except BMObjectInvalidDataError: - logger.debug('object invalid data, skipping') except BMObjectExpiredError: logger.debug('object expired, skipping') except BMObjectUnwantedStreamError: @@ -350,20 +344,20 @@ class BMProto(AdvancedDispatcher, ObjectTracker): """ items = self.decode_payload_content("l32s") - if len(items) > MAX_OBJECT_COUNT: + if len(items) > protocol.MAX_OBJECT_COUNT: logger.error( 'Too many items in %sinv message!', 'd' if dandelion else '') raise BMProtoExcessiveDataError() # ignore dinv if dandelion turned off - if dandelion and not state.dandelion: + if dandelion and not state.dandelion_enabled: return True for i in map(str, items): - if i in Inventory() and not Dandelion().hasHash(i): + if i in state.Inventory and not state.Dandelion.hasHash(i): continue - if dandelion and not Dandelion().hasHash(i): - Dandelion().addHash(i, self) + if dandelion and not state.Dandelion.hasHash(i): + state.Dandelion.addHash(i, self) self.handleReceivedInventory(i) return True @@ -386,7 +380,7 @@ class BMProto(AdvancedDispatcher, ObjectTracker): self.payload, self.payloadOffset) payload_len = len(self.payload) - self.payloadOffset - if payload_len > MAX_OBJECT_PAYLOAD_SIZE: + if payload_len > protocol.MAX_OBJECT_PAYLOAD_SIZE: logger.info( 'The payload length of this object is too large' ' (%d bytes). Ignoring it.', payload_len) @@ -403,17 +397,20 @@ class BMProto(AdvancedDispatcher, ObjectTracker): try: self.object.checkStream() except BMObjectUnwantedStreamError: - acceptmismatch = BMConfigParser().get( + acceptmismatch = config.getboolean( "inventory", "acceptmismatch") BMProto.stopDownloadingObject( self.object.inventoryHash, acceptmismatch) if not acceptmismatch: raise + except BMObjectInvalidError: + BMProto.stopDownloadingObject(self.object.inventoryHash) + raise try: self.object.checkObjectByType() objectProcessorQueue.put(( - self.object.objectType, buffer(self.object.data))) + self.object.objectType, buffer(self.object.data))) # noqa: F821 except BMObjectInvalidError: BMProto.stopDownloadingObject(self.object.inventoryHash, True) else: @@ -422,15 +419,15 @@ class BMProto(AdvancedDispatcher, ObjectTracker): except KeyError: pass - if self.object.inventoryHash in Inventory() and Dandelion().hasHash( + if self.object.inventoryHash in state.Inventory and state.Dandelion.hasHash( self.object.inventoryHash): - Dandelion().removeHash( + state.Dandelion.removeHash( self.object.inventoryHash, "cycle detection") - Inventory()[self.object.inventoryHash] = ( + state.Inventory[self.object.inventoryHash] = ( self.object.objectType, self.object.streamNumber, - buffer(self.payload[objectOffset:]), self.object.expiresTime, - buffer(self.object.tag) + buffer(self.payload[objectOffset:]), self.object.expiresTime, # noqa: F821 + buffer(self.object.tag) # noqa: F821 ) self.handleReceivedObject( self.object.streamNumber, self.object.inventoryHash) @@ -448,7 +445,7 @@ class BMProto(AdvancedDispatcher, ObjectTracker): for seenTime, stream, _, ip, port in self._decode_addr(): ip = str(ip) if ( - stream not in state.streamsInWhichIAmParticipating + stream not in connectionpool.pool.streams # FIXME: should check against complete list or ip.startswith('bootstrap') ): @@ -456,7 +453,7 @@ class BMProto(AdvancedDispatcher, ObjectTracker): decodedIP = protocol.checkIPAddress(ip) if ( decodedIP and time.time() - seenTime > 0 - and seenTime > time.time() - ADDRESS_ALIVE + and seenTime > time.time() - protocol.ADDRESS_ALIVE and port > 0 ): peer = Peer(decodedIP, port) @@ -543,7 +540,7 @@ class BMProto(AdvancedDispatcher, ObjectTracker): if not self.isOutbound: self.append_write_buf(protocol.assembleVersionMessage( self.destination.host, self.destination.port, - connectionpool.BMConnectionPool().streams, True, + connectionpool.pool.streams, True, nodeid=self.nodeid)) logger.debug( '%(host)s:%(port)i sending version', @@ -569,7 +566,7 @@ class BMProto(AdvancedDispatcher, ObjectTracker): 'Closing connection to old protocol version %s, node: %s', self.remoteProtocolVersion, self.destination) return False - if self.timeOffset > MAX_TIME_OFFSET: + if self.timeOffset > protocol.MAX_TIME_OFFSET: self.append_write_buf(protocol.assembleErrorMessage( errorText="Your time is too far in the future" " compared to mine. Closing connection.", fatal=2)) @@ -579,7 +576,7 @@ class BMProto(AdvancedDispatcher, ObjectTracker): self.destination, self.timeOffset) BMProto.timeOffsetWrongCount += 1 return False - elif self.timeOffset < -MAX_TIME_OFFSET: + elif self.timeOffset < -protocol.MAX_TIME_OFFSET: self.append_write_buf(protocol.assembleErrorMessage( errorText="Your time is too far in the past compared to mine." " Closing connection.", fatal=2)) @@ -599,7 +596,7 @@ class BMProto(AdvancedDispatcher, ObjectTracker): 'Closed connection to %s because there is no overlapping' ' interest in streams.', self.destination) return False - if connectionpool.BMConnectionPool().inboundConnections.get( + if connectionpool.pool.inboundConnections.get( self.destination): try: if not protocol.checkSocksIP(self.destination.host): @@ -610,18 +607,18 @@ class BMProto(AdvancedDispatcher, ObjectTracker): 'Closed connection to %s because we are already' ' connected to that IP.', self.destination) return False - except Exception: # TODO: exception types + except Exception: # nosec B110 # pylint:disable=broad-exception-caught pass if not self.isOutbound: # incoming from a peer we're connected to as outbound, # or server full report the same error to counter deanonymisation if ( Peer(self.destination.host, self.peerNode.port) - in connectionpool.BMConnectionPool().inboundConnections - or len(connectionpool.BMConnectionPool()) - > BMConfigParser().safeGetInt( + in connectionpool.pool.inboundConnections + or len(connectionpool.pool) + > config.safeGetInt( 'bitmessagesettings', 'maxtotalconnections') - + BMConfigParser().safeGetInt( + + config.safeGetInt( 'bitmessagesettings', 'maxbootstrapconnections') ): self.append_write_buf(protocol.assembleErrorMessage( @@ -630,7 +627,7 @@ class BMProto(AdvancedDispatcher, ObjectTracker): 'Closed connection to %s due to server full' ' or duplicate inbound/outbound.', self.destination) return False - if connectionpool.BMConnectionPool().isAlreadyConnected(self.nonce): + if connectionpool.pool.isAlreadyConnected(self.nonce): self.append_write_buf(protocol.assembleErrorMessage( errorText="I'm connected to myself. Closing connection.", fatal=2)) @@ -644,7 +641,7 @@ class BMProto(AdvancedDispatcher, ObjectTracker): @staticmethod def stopDownloadingObject(hashId, forwardAnyway=False): """Stop downloading object *hashId*""" - for connection in connectionpool.BMConnectionPool().connections(): + for connection in connectionpool.pool.connections(): try: del connection.objectsNewToMe[hashId] except KeyError: @@ -678,32 +675,3 @@ class BMProto(AdvancedDispatcher, ObjectTracker): except AttributeError: logger.debug('Disconnected socket closing') AdvancedDispatcher.handle_close(self) - - -class BMStringParser(BMProto): - """ - A special case of BMProto used by objectProcessor to send ACK - """ - def __init__(self): - super(BMStringParser, self).__init__() - self.destination = Peer('127.0.0.1', 8444) - self.payload = None - ObjectTracker.__init__(self) - - def send_data(self, data): - """Send object given by the data string""" - # This class is introduced specially for ACK sending, please - # change log strings if you are going to use it for something else - self.bm_proto_reset() - self.payload = data - try: - self.bm_command_object() - except BMObjectAlreadyHaveError: - pass # maybe the same msg received on different nodes - except BMObjectExpiredError: - logger.debug( - 'Sending ACK failure (expired): %s', hexlify(data)) - except Exception as e: - logger.debug( - 'Exception of type %s while sending ACK', - type(e), exc_info=True) diff --git a/src/network/connectionchooser.py b/src/network/connectionchooser.py index c31bbb6a..d7062d24 100644 --- a/src/network/connectionchooser.py +++ b/src/network/connectionchooser.py @@ -3,12 +3,12 @@ Select which node to connect to """ # pylint: disable=too-many-branches import logging -import random # nosec +import random import knownnodes import protocol import state -from bmconfigparser import BMConfigParser +from bmconfigparser import config from queues import queue, portCheckerQueue logger = logging.getLogger('default') @@ -17,7 +17,7 @@ logger = logging.getLogger('default') def getDiscoveredPeer(): """Get a peer from the local peer discovery list""" try: - peer = random.choice(state.discoveredPeers.keys()) + peer = random.choice(state.discoveredPeers.keys()) # nosec B311 except (IndexError, KeyError): raise ValueError try: @@ -29,9 +29,9 @@ def getDiscoveredPeer(): def chooseConnection(stream): """Returns an appropriate connection""" - haveOnion = BMConfigParser().safeGet( + haveOnion = config.safeGet( "bitmessagesettings", "socksproxytype")[0:5] == 'SOCKS' - onionOnly = BMConfigParser().safeGetBoolean( + onionOnly = config.safeGetBoolean( "bitmessagesettings", "onionservicesonly") try: retval = portCheckerQueue.get(False) @@ -40,11 +40,12 @@ def chooseConnection(stream): except queue.Empty: pass # with a probability of 0.5, connect to a discovered peer - if random.choice((False, True)) and not haveOnion: + if random.choice((False, True)) and not haveOnion: # nosec B311 # discovered peers are already filtered by allowed streams return getDiscoveredPeer() for _ in range(50): - peer = random.choice(knownnodes.knownNodes[stream].keys()) + peer = random.choice( # nosec B311 + knownnodes.knownNodes[stream].keys()) try: peer_info = knownnodes.knownNodes[stream][peer] if peer_info.get('self'): @@ -70,7 +71,7 @@ def chooseConnection(stream): if rating > 1: rating = 1 try: - if 0.05 / (1.0 - rating) > random.random(): + if 0.05 / (1.0 - rating) > random.random(): # nosec B311 return peer except ZeroDivisionError: return peer diff --git a/src/network/connectionpool.py b/src/network/connectionpool.py index fffc0bc3..36c91c18 100644 --- a/src/network/connectionpool.py +++ b/src/network/connectionpool.py @@ -13,11 +13,10 @@ import helper_random import knownnodes import protocol import state -from bmconfigparser import BMConfigParser +from bmconfigparser import config from connectionchooser import chooseConnection from node import Peer from proxy import Proxy -from singleton import Singleton from tcp import ( bootstrap, Socks4aBMConnection, Socks5BMConnection, TCPConnection, TCPServer) @@ -26,7 +25,6 @@ from udp import UDPSocket logger = logging.getLogger('default') -@Singleton class BMConnectionPool(object): """Pool of all existing connections""" # pylint: disable=too-many-instance-attributes @@ -46,9 +44,9 @@ class BMConnectionPool(object): def __init__(self): asyncore.set_rates( - BMConfigParser().safeGetInt( + config.safeGetInt( "bitmessagesettings", "maxdownloadrate"), - BMConfigParser().safeGetInt( + config.safeGetInt( "bitmessagesettings", "maxuploadrate") ) self.outboundConnections = {} @@ -60,7 +58,7 @@ class BMConnectionPool(object): self._spawnWait = 2 self._bootstrapped = False - trustedPeer = BMConfigParser().safeGet( + trustedPeer = config.safeGet( 'bitmessagesettings', 'trustedpeer') try: if trustedPeer: @@ -90,7 +88,6 @@ class BMConnectionPool(object): def connectToStream(self, streamNumber): """Connect to a bitmessage stream""" self.streams.append(streamNumber) - state.streamsInWhichIAmParticipating.append(streamNumber) def getConnectionByAddr(self, addr): """ @@ -163,27 +160,27 @@ class BMConnectionPool(object): @staticmethod def getListeningIP(): """What IP are we supposed to be listening on?""" - if BMConfigParser().safeGet( - "bitmessagesettings", "onionhostname").endswith(".onion"): - host = BMConfigParser().safeGet( + if config.safeGet( + "bitmessagesettings", "onionhostname", "").endswith(".onion"): + host = config.safeGet( "bitmessagesettings", "onionbindip") else: host = '127.0.0.1' if ( - BMConfigParser().safeGetBoolean("bitmessagesettings", "sockslisten") - or BMConfigParser().safeGet("bitmessagesettings", "socksproxytype") + config.safeGetBoolean("bitmessagesettings", "sockslisten") + or config.safeGet("bitmessagesettings", "socksproxytype") == "none" ): # python doesn't like bind + INADDR_ANY? # host = socket.INADDR_ANY - host = BMConfigParser().get("network", "bind") + host = config.get("network", "bind") return host def startListening(self, bind=None): """Open a listening socket and start accepting connections on it""" if bind is None: bind = self.getListeningIP() - port = BMConfigParser().safeGetInt("bitmessagesettings", "port") + port = config.safeGetInt("bitmessagesettings", "port") # correct port even if it changed ls = TCPServer(host=bind, port=port) self.listeningSockets[ls.destination] = ls @@ -205,7 +202,7 @@ class BMConnectionPool(object): def startBootstrappers(self): """Run the process of resolving bootstrap hostnames""" - proxy_type = BMConfigParser().safeGet( + proxy_type = config.safeGet( 'bitmessagesettings', 'socksproxytype') # A plugins may be added here hostname = None @@ -237,21 +234,21 @@ class BMConnectionPool(object): # defaults to empty loop if outbound connections are maxed spawnConnections = False acceptConnections = True - if BMConfigParser().safeGetBoolean( + if config.safeGetBoolean( 'bitmessagesettings', 'dontconnect'): acceptConnections = False - elif BMConfigParser().safeGetBoolean( + elif config.safeGetBoolean( 'bitmessagesettings', 'sendoutgoingconnections'): spawnConnections = True - socksproxytype = BMConfigParser().safeGet( + socksproxytype = config.safeGet( 'bitmessagesettings', 'socksproxytype', '') - onionsocksproxytype = BMConfigParser().safeGet( + onionsocksproxytype = config.safeGet( 'bitmessagesettings', 'onionsocksproxytype', '') if ( socksproxytype[:5] == 'SOCKS' - and not BMConfigParser().safeGetBoolean( + and not config.safeGetBoolean( 'bitmessagesettings', 'sockslisten') - and '.onion' not in BMConfigParser().safeGet( + and '.onion' not in config.safeGet( 'bitmessagesettings', 'onionhostname', '') ): acceptConnections = False @@ -264,9 +261,9 @@ class BMConnectionPool(object): if not self._bootstrapped: self._bootstrapped = True Proxy.proxy = ( - BMConfigParser().safeGet( + config.safeGet( 'bitmessagesettings', 'sockshostname'), - BMConfigParser().safeGetInt( + config.safeGetInt( 'bitmessagesettings', 'socksport') ) # TODO AUTH @@ -275,9 +272,9 @@ class BMConnectionPool(object): if not onionsocksproxytype.startswith("SOCKS"): raise ValueError Proxy.onion_proxy = ( - BMConfigParser().safeGet( + config.safeGet( 'network', 'onionsockshostname', None), - BMConfigParser().safeGet( + config.safeGet( 'network', 'onionsocksport', None) ) except ValueError: @@ -286,7 +283,7 @@ class BMConnectionPool(object): 1 for c in self.outboundConnections.values() if (c.connected and c.fullyEstablished)) pending = len(self.outboundConnections) - established - if established < BMConfigParser().safeGetInt( + if established < config.safeGetInt( 'bitmessagesettings', 'maxoutboundconnections'): for i in range( state.maximumNumberOfHalfOpenConnections - pending): @@ -334,28 +331,28 @@ class BMConnectionPool(object): self._lastSpawned = time.time() else: - for i in self.connections(): + for i in self.outboundConnections.values(): # FIXME: rating will be increased after next connection i.handle_close() if acceptConnections: if not self.listeningSockets: - if BMConfigParser().safeGet('network', 'bind') == '': + if config.safeGet('network', 'bind') == '': self.startListening() else: for bind in re.sub( r'[^\w.]+', ' ', - BMConfigParser().safeGet('network', 'bind') + config.safeGet('network', 'bind') ).split(): self.startListening(bind) logger.info('Listening for incoming connections.') if not self.udpSockets: - if BMConfigParser().safeGet('network', 'bind') == '': + if config.safeGet('network', 'bind') == '': self.startUDPSocket() else: for bind in re.sub( r'[^\w.]+', ' ', - BMConfigParser().safeGet('network', 'bind') + config.safeGet('network', 'bind') ).split(): self.startUDPSocket(bind) self.startUDPSocket(False) @@ -403,3 +400,6 @@ class BMConnectionPool(object): pass for i in reaper: self.removeConnection(i) + + +pool = BMConnectionPool() diff --git a/src/network/constants.py b/src/network/constants.py deleted file mode 100644 index f8f4120f..00000000 --- a/src/network/constants.py +++ /dev/null @@ -1,17 +0,0 @@ -""" -Network protocol constants -""" - - -#: address is online if online less than this many seconds ago -ADDRESS_ALIVE = 10800 -#: protocol specification says max 1000 addresses in one addr command -MAX_ADDR_COUNT = 1000 -#: ~1.6 MB which is the maximum possible size of an inv message. -MAX_MESSAGE_SIZE = 1600100 -#: 2**18 = 256kB is the maximum size of an object payload -MAX_OBJECT_PAYLOAD_SIZE = 2**18 -#: protocol specification says max 50000 objects in one inv command -MAX_OBJECT_COUNT = 50000 -#: maximum time offset -MAX_TIME_OFFSET = 3600 diff --git a/src/network/dandelion.py b/src/network/dandelion.py index 03f45bd7..35e70c95 100644 --- a/src/network/dandelion.py +++ b/src/network/dandelion.py @@ -10,7 +10,6 @@ from time import time import connectionpool import state from queues import invQueue -from singleton import Singleton # randomise routes after 600 seconds REASSIGN_INTERVAL = 600 @@ -26,7 +25,6 @@ Stem = namedtuple('Stem', ['child', 'stream', 'timeout']) logger = logging.getLogger('default') -@Singleton class Dandelion: # pylint: disable=old-style-class """Dandelion class for tracking stem/fluff stages.""" def __init__(self): @@ -51,7 +49,7 @@ class Dandelion: # pylint: disable=old-style-class def addHash(self, hashId, source=None, stream=1): """Add inventory vector to dandelion stem""" - if not state.dandelion: + if not state.dandelion_enabled: return with self.lock: self.hashMap[hashId] = Stem( @@ -140,7 +138,7 @@ class Dandelion: # pylint: disable=old-style-class """ try: # pick a random from available stems - stem = choice(range(len(self.stem))) + stem = choice(range(len(self.stem))) # nosec B311 if self.stem[stem] == parent: # one stem available and it's the parent if len(self.stem) == 1: diff --git a/src/network/downloadthread.py b/src/network/downloadthread.py index 0ae83b5b..4f108c72 100644 --- a/src/network/downloadthread.py +++ b/src/network/downloadthread.py @@ -2,13 +2,11 @@ `DownloadThread` class definition """ import time - +import state import addresses import helper_random import protocol -from dandelion import Dandelion -from inventory import Inventory -from network.connectionpool import BMConnectionPool +import connectionpool from objectracker import missingObjects from threads import StoppableThread @@ -43,7 +41,7 @@ class DownloadThread(StoppableThread): while not self._stopped: requested = 0 # Choose downloading peers randomly - connections = BMConnectionPool().establishedConnections() + connections = connectionpool.pool.establishedConnections() helper_random.randomshuffle(connections) requestChunk = max(int( min(self.maxRequestChunk, len(missingObjects)) @@ -61,7 +59,7 @@ class DownloadThread(StoppableThread): payload = bytearray() chunkCount = 0 for chunk in request: - if chunk in Inventory() and not Dandelion().hasHash(chunk): + if chunk in state.Inventory and not state.Dandelion.hasHash(chunk): try: del i.objectsNewToMe[chunk] except KeyError: diff --git a/src/network/invthread.py b/src/network/invthread.py index e68b7692..b55408d4 100644 --- a/src/network/invthread.py +++ b/src/network/invthread.py @@ -8,8 +8,7 @@ from time import time import addresses import protocol import state -from network.connectionpool import BMConnectionPool -from network.dandelion import Dandelion +import connectionpool from queues import invQueue from threads import StoppableThread @@ -19,7 +18,7 @@ def handleExpiredDandelion(expired): the object""" if not expired: return - for i in BMConnectionPool().connections(): + for i in connectionpool.pool.connections(): if not i.fullyEstablished: continue for x in expired: @@ -40,10 +39,10 @@ class InvThread(StoppableThread): @staticmethod def handleLocallyGenerated(stream, hashId): """Locally generated inventory items require special handling""" - Dandelion().addHash(hashId, stream=stream) - for connection in BMConnectionPool().connections(): - if state.dandelion and connection != \ - Dandelion().objectChildStem(hashId): + state.Dandelion.addHash(hashId, stream=stream) + for connection in connectionpool.pool.connections(): + if state.dandelion_enabled and connection != \ + state.Dandelion.objectChildStem(hashId): continue connection.objectsNewToThem[hashId] = time() @@ -52,7 +51,7 @@ class InvThread(StoppableThread): chunk = [] while True: # Dandelion fluff trigger by expiration - handleExpiredDandelion(Dandelion().expire()) + handleExpiredDandelion(state.Dandelion.expire()) try: data = invQueue.get(False) chunk.append((data[0], data[1])) @@ -63,7 +62,7 @@ class InvThread(StoppableThread): break if chunk: - for connection in BMConnectionPool().connections(): + for connection in connectionpool.pool.connections(): fluffs = [] stems = [] for inv in chunk: @@ -75,10 +74,10 @@ class InvThread(StoppableThread): except KeyError: continue try: - if connection == Dandelion().objectChildStem(inv[1]): + if connection == state.Dandelion.objectChildStem(inv[1]): # Fluff trigger by RNG # auto-ignore if config set to 0, i.e. dandelion is off - if random.randint(1, 100) >= state.dandelion: + if random.randint(1, 100) >= state.dandelion_enabled: # nosec B311 fluffs.append(inv[1]) # send a dinv only if the stem node supports dandelion elif connection.services & protocol.NODE_DANDELION > 0: @@ -105,7 +104,7 @@ class InvThread(StoppableThread): for _ in range(len(chunk)): invQueue.task_done() - if Dandelion().refresh < time(): - Dandelion().reRandomiseStems() + if state.Dandelion.refresh < time(): + state.Dandelion.reRandomiseStems() self.stop.wait(1) diff --git a/src/network/knownnodes.py b/src/network/knownnodes.py index 4840aad9..c53be2cd 100644 --- a/src/network/knownnodes.py +++ b/src/network/knownnodes.py @@ -7,7 +7,7 @@ Manipulations with knownNodes dictionary. import json import logging import os -import pickle +import pickle # nosec B403 import threading import time try: @@ -16,7 +16,7 @@ except ImportError: from collections import Iterable import state -from bmconfigparser import BMConfigParser +from bmconfigparser import config from network.node import Peer state.Peer = Peer @@ -85,7 +85,7 @@ def pickle_deserialize_old_knownnodes(source): the new format is {Peer:{"lastseen":i, "rating":f}} """ global knownNodes - knownNodes = pickle.load(source) + knownNodes = pickle.load(source) # nosec B301 for stream in knownNodes.keys(): for node, params in knownNodes[stream].iteritems(): if isinstance(params, (float, int)): @@ -130,7 +130,7 @@ def addKnownNode(stream, peer, lastseen=None, is_self=False): return if not is_self: - if len(knownNodes[stream]) > BMConfigParser().safeGetInt( + if len(knownNodes[stream]) > config.safeGetInt( "knownnodes", "maxnodes"): return @@ -165,8 +165,6 @@ def readKnownNodes(): 'Failed to read nodes from knownnodes.dat', exc_info=True) createDefaultKnownNodes() - config = BMConfigParser() - # your own onion address, if setup onionhostname = config.safeGet('bitmessagesettings', 'onionhostname') if onionhostname and ".onion" in onionhostname: @@ -210,7 +208,7 @@ def decreaseRating(peer): def trimKnownNodes(recAddrStream=1): """Triming Knownnodes""" if len(knownNodes[recAddrStream]) < \ - BMConfigParser().safeGetInt("knownnodes", "maxnodes"): + config.safeGetInt("knownnodes", "maxnodes"): return with knownNodesLock: oldestList = sorted( @@ -228,7 +226,7 @@ def dns(): 1, Peer('bootstrap%s.bitmessage.org' % port, port)) -def cleanupKnownNodes(): +def cleanupKnownNodes(pool): """ Cleanup knownnodes: remove old nodes and nodes with low rating """ @@ -238,7 +236,7 @@ def cleanupKnownNodes(): with knownNodesLock: for stream in knownNodes: - if stream not in state.streamsInWhichIAmParticipating: + if stream not in pool.streams: continue keys = knownNodes[stream].keys() for node in keys: diff --git a/src/network/networkthread.py b/src/network/networkthread.py index 61ff6c09..640d47a1 100644 --- a/src/network/networkthread.py +++ b/src/network/networkthread.py @@ -2,8 +2,7 @@ A thread to handle network concerns """ import network.asyncore_pollchoose as asyncore -import state -from network.connectionpool import BMConnectionPool +import connectionpool from queues import excQueue from threads import StoppableThread @@ -14,28 +13,28 @@ class BMNetworkThread(StoppableThread): def run(self): try: - while not self._stopped and state.shutdown == 0: - BMConnectionPool().loop() + while not self._stopped: + connectionpool.pool.loop() except Exception as e: excQueue.put((self.name, e)) raise def stopThread(self): super(BMNetworkThread, self).stopThread() - for i in BMConnectionPool().listeningSockets.values(): + for i in connectionpool.pool.listeningSockets.values(): try: i.close() - except: + except: # nosec B110 # pylint:disable=bare-except pass - for i in BMConnectionPool().outboundConnections.values(): + for i in connectionpool.pool.outboundConnections.values(): try: i.close() - except: + except: # nosec B110 # pylint:disable=bare-except pass - for i in BMConnectionPool().inboundConnections.values(): + for i in connectionpool.pool.inboundConnections.values(): try: i.close() - except: + except: # nosec B110 # pylint:disable=bare-except pass # just in case diff --git a/src/network/objectracker.py b/src/network/objectracker.py index 65e06de4..a458e5d2 100644 --- a/src/network/objectracker.py +++ b/src/network/objectracker.py @@ -4,8 +4,8 @@ Module for tracking objects import time from threading import RLock -import network.connectionpool -from network.dandelion import Dandelion +import state +import connectionpool from randomtrackingdict import RandomTrackingDict haveBloom = False @@ -100,21 +100,21 @@ class ObjectTracker(object): def handleReceivedObject(self, streamNumber, hashid): """Handling received object""" - for i in network.connectionpool.BMConnectionPool().connections(): + for i in connectionpool.pool.connections(): if not i.fullyEstablished: continue try: del i.objectsNewToMe[hashid] except KeyError: if streamNumber in i.streams and ( - not Dandelion().hasHash(hashid) - or Dandelion().objectChildStem(hashid) == i): + not state.Dandelion.hasHash(hashid) + or state.Dandelion.objectChildStem(hashid) == i): with i.objectsNewToThemLock: i.objectsNewToThem[hashid] = time.time() # update stream number, # which we didn't have when we just received the dinv # also resets expiration of the stem mode - Dandelion().setHashStream(hashid, streamNumber) + state.Dandelion.setHashStream(hashid, streamNumber) if i == self: try: diff --git a/src/network/proxy.py b/src/network/proxy.py index 3bd3cc66..ed1af127 100644 --- a/src/network/proxy.py +++ b/src/network/proxy.py @@ -8,7 +8,7 @@ import time import asyncore_pollchoose as asyncore from advanceddispatcher import AdvancedDispatcher -from bmconfigparser import BMConfigParser +from bmconfigparser import config from node import Peer logger = logging.getLogger('default') @@ -114,12 +114,12 @@ class Proxy(AdvancedDispatcher): self.isOutbound = True self.fullyEstablished = False self.create_socket(socket.AF_INET, socket.SOCK_STREAM) - if BMConfigParser().safeGetBoolean( + if config.safeGetBoolean( "bitmessagesettings", "socksauthentication"): self.auth = ( - BMConfigParser().safeGet( + config.safeGet( "bitmessagesettings", "socksusername"), - BMConfigParser().safeGet( + config.safeGet( "bitmessagesettings", "sockspassword")) else: self.auth = None diff --git a/src/network/receivequeuethread.py b/src/network/receivequeuethread.py index 56c01b77..10f2acea 100644 --- a/src/network/receivequeuethread.py +++ b/src/network/receivequeuethread.py @@ -5,9 +5,8 @@ import errno import Queue import socket -import state +import connectionpool from network.advanceddispatcher import UnknownStateError -from network.connectionpool import BMConnectionPool from queues import receiveDataQueue from threads import StoppableThread @@ -19,13 +18,13 @@ class ReceiveQueueThread(StoppableThread): super(ReceiveQueueThread, self).__init__(name="ReceiveQueue_%i" % num) def run(self): - while not self._stopped and state.shutdown == 0: + while not self._stopped: try: dest = receiveDataQueue.get(block=True, timeout=1) except Queue.Empty: continue - if self._stopped or state.shutdown: + if self._stopped: break # cycle as long as there is data @@ -36,7 +35,7 @@ class ReceiveQueueThread(StoppableThread): # enough data, or the connection is to be aborted try: - connection = BMConnectionPool().getConnectionByAddr(dest) + connection = connectionpool.pool.getConnectionByAddr(dest) # connection object not found except KeyError: receiveDataQueue.task_done() diff --git a/src/network/stats.py b/src/network/stats.py index 82e6c87f..0ab1ae0f 100644 --- a/src/network/stats.py +++ b/src/network/stats.py @@ -4,7 +4,7 @@ Network statistics import time import asyncore_pollchoose as asyncore -from network.connectionpool import BMConnectionPool +import connectionpool from objectracker import missingObjects @@ -18,7 +18,7 @@ currentSentSpeed = 0 def connectedHostsList(): """List of all the connected hosts""" - return BMConnectionPool().establishedConnections() + return connectionpool.pool.establishedConnections() def sentBytes(): @@ -69,8 +69,8 @@ def pendingDownload(): def pendingUpload(): """Getting pending uploads""" # tmp = {} - # for connection in BMConnectionPool().inboundConnections.values() + \ - # BMConnectionPool().outboundConnections.values(): + # for connection in connectionpool.pool.inboundConnections.values() + \ + # connectionpool.pool.outboundConnections.values(): # for k in connection.objectsNewToThem.keys(): # tmp[k] = True # This probably isn't the correct logic so it's disabled diff --git a/src/network/tcp.py b/src/network/tcp.py index 77bb21c3..47517528 100644 --- a/src/network/tcp.py +++ b/src/network/tcp.py @@ -2,35 +2,35 @@ TCP protocol handler """ # pylint: disable=too-many-ancestors -import l10n + import logging import math import random import socket import time +# magic imports! import addresses -import asyncore_pollchoose as asyncore -import connectionpool import helper_random -import knownnodes +import l10n import protocol import state -from bmconfigparser import BMConfigParser -from helper_random import randomBytes -from inventory import Inventory +import connectionpool +from bmconfigparser import config +from highlevelcrypto import randomBytes +from queues import invQueue, receiveDataQueue, UISignalQueue +from tr import _translate + +import asyncore_pollchoose as asyncore +import knownnodes from network.advanceddispatcher import AdvancedDispatcher -from network.assemble import assemble_addr from network.bmproto import BMProto -from network.constants import MAX_OBJECT_COUNT -from network.dandelion import Dandelion from network.objectracker import ObjectTracker from network.socks4a import Socks4aConnection from network.socks5 import Socks5Connection from network.tls import TLSDispatcher from node import Peer -from queues import invQueue, receiveDataQueue, UISignalQueue -from tr import _translate + logger = logging.getLogger('default') @@ -169,7 +169,7 @@ class TCPConnection(BMProto, TLSDispatcher): knownnodes.increaseRating(self.destination) knownnodes.addKnownNode( self.streams, self.destination, time.time()) - Dandelion().maybeAddStem(self) + state.Dandelion.maybeAddStem(self) self.sendAddr() self.sendBigInv() @@ -178,7 +178,7 @@ class TCPConnection(BMProto, TLSDispatcher): # We are going to share a maximum number of 1000 addrs (per overlapping # stream) with our peer. 500 from overlapping streams, 250 from the # left child stream, and 250 from the right child stream. - maxAddrCount = BMConfigParser().safeGetInt( + maxAddrCount = config.safeGetInt( "bitmessagesettings", "maxaddrperstreamsend", 500) templist = [] @@ -195,7 +195,7 @@ class TCPConnection(BMProto, TLSDispatcher): (k, v) for k, v in nodes.iteritems() if v["lastseen"] > int(time.time()) - maximumAgeOfNodesThatIAdvertiseToOthers - and v["rating"] >= 0 and len(k.host) <= 22 + and v["rating"] >= 0 and not k.host.endswith('.onion') ] # sent 250 only if the remote isn't interested in it elemCount = min( @@ -206,7 +206,7 @@ class TCPConnection(BMProto, TLSDispatcher): for peer, params in addrs[substream]: templist.append((substream, peer, params["lastseen"])) if templist: - self.append_write_buf(assemble_addr(templist)) + self.append_write_buf(protocol.assembleAddrMessage(templist)) def sendBigInv(self): """ @@ -229,9 +229,9 @@ class TCPConnection(BMProto, TLSDispatcher): # may lock for a long time, but I think it's better than # thousands of small locks with self.objectsNewToThemLock: - for objHash in Inventory().unexpired_hashes_by_stream(stream): + for objHash in state.Inventory.unexpired_hashes_by_stream(stream): # don't advertise stem objects on bigInv - if Dandelion().hasHash(objHash): + if state.Dandelion.hasHash(objHash): continue bigInvList[objHash] = 0 objectCount = 0 @@ -245,7 +245,7 @@ class TCPConnection(BMProto, TLSDispatcher): # Remove -1 below when sufficient time has passed for users to # upgrade to versions of PyBitmessage that accept inv with 50,000 # items - if objectCount >= MAX_OBJECT_COUNT - 1: + if objectCount >= protocol.MAX_OBJECT_COUNT - 1: sendChunk() payload = b'' objectCount = 0 @@ -268,7 +268,7 @@ class TCPConnection(BMProto, TLSDispatcher): self.append_write_buf( protocol.assembleVersionMessage( self.destination.host, self.destination.port, - connectionpool.BMConnectionPool().streams, + connectionpool.pool.streams, False, nodeid=self.nodeid)) self.connectedAt = time.time() receiveDataQueue.put(self.destination) @@ -293,7 +293,7 @@ class TCPConnection(BMProto, TLSDispatcher): if host_is_global: knownnodes.addKnownNode( self.streams, self.destination, time.time()) - Dandelion().maybeRemoveStem(self) + state.Dandelion.maybeRemoveStem(self) else: self.checkTimeOffsetNotification() if host_is_global: @@ -319,7 +319,7 @@ class Socks5BMConnection(Socks5Connection, TCPConnection): self.append_write_buf( protocol.assembleVersionMessage( self.destination.host, self.destination.port, - connectionpool.BMConnectionPool().streams, + connectionpool.pool.streams, False, nodeid=self.nodeid)) self.set_state("bm_header", expectBytes=protocol.Header.size) return True @@ -343,7 +343,7 @@ class Socks4aBMConnection(Socks4aConnection, TCPConnection): self.append_write_buf( protocol.assembleVersionMessage( self.destination.host, self.destination.port, - connectionpool.BMConnectionPool().streams, + connectionpool.pool.streams, False, nodeid=self.nodeid)) self.set_state("bm_header", expectBytes=protocol.Header.size) return True @@ -399,7 +399,7 @@ class TCPServer(AdvancedDispatcher): try: if attempt > 0: logger.warning('Failed to bind on port %s', port) - port = random.randint(32767, 65535) + port = random.randint(32767, 65535) # nosec B311 self.bind((host, port)) except socket.error as e: if e.errno in (asyncore.EADDRINUSE, asyncore.WSAEADDRINUSE): @@ -407,9 +407,9 @@ class TCPServer(AdvancedDispatcher): else: if attempt > 0: logger.warning('Setting port to %s', port) - BMConfigParser().set( + config.set( 'bitmessagesettings', 'port', str(port)) - BMConfigParser().save() + config.save() break self.destination = Peer(host, port) self.bound = True @@ -431,10 +431,10 @@ class TCPServer(AdvancedDispatcher): state.ownAddresses[Peer(*sock.getsockname())] = True if ( - len(connectionpool.BMConnectionPool()) - > BMConfigParser().safeGetInt( + len(connectionpool.pool) + > config.safeGetInt( 'bitmessagesettings', 'maxtotalconnections') - + BMConfigParser().safeGetInt( + + config.safeGetInt( 'bitmessagesettings', 'maxbootstrapconnections') + 10 ): # 10 is a sort of buffer, in between it will go through @@ -443,7 +443,7 @@ class TCPServer(AdvancedDispatcher): sock.close() return try: - connectionpool.BMConnectionPool().addConnection( + connectionpool.pool.addConnection( TCPConnection(sock=sock)) except socket.error: pass diff --git a/src/network/udp.py b/src/network/udp.py index 3f999332..b16146f9 100644 --- a/src/network/udp.py +++ b/src/network/udp.py @@ -5,13 +5,16 @@ import logging import socket import time +# magic imports! import protocol import state +import connectionpool +from queues import receiveDataQueue + from bmproto import BMProto -from constants import MAX_TIME_OFFSET from node import Peer from objectracker import ObjectTracker -from queues import receiveDataQueue + logger = logging.getLogger('default') @@ -79,10 +82,10 @@ class UDPSocket(BMProto): # pylint: disable=too-many-instance-attributes remoteport = False for seenTime, stream, _, ip, port in addresses: decodedIP = protocol.checkIPAddress(str(ip)) - if stream not in state.streamsInWhichIAmParticipating: + if stream not in connectionpool.pool.streams: continue - if (seenTime < time.time() - MAX_TIME_OFFSET - or seenTime > time.time() + MAX_TIME_OFFSET): + if (seenTime < time.time() - protocol.MAX_TIME_OFFSET + or seenTime > time.time() + protocol.MAX_TIME_OFFSET): continue if decodedIP is False: # if the address isn't local, interpret it as diff --git a/src/network/uploadthread.py b/src/network/uploadthread.py index 7d80d789..90048c0a 100644 --- a/src/network/uploadthread.py +++ b/src/network/uploadthread.py @@ -5,9 +5,8 @@ import time import helper_random import protocol -from inventory import Inventory -from network.connectionpool import BMConnectionPool -from network.dandelion import Dandelion +import state +import connectionpool from randomtrackingdict import RandomTrackingDict from threads import StoppableThread @@ -23,7 +22,7 @@ class UploadThread(StoppableThread): while not self._stopped: uploaded = 0 # Choose uploading peers randomly - connections = BMConnectionPool().establishedConnections() + connections = connectionpool.pool.establishedConnections() helper_random.randomshuffle(connections) for i in connections: now = time.time() @@ -41,8 +40,8 @@ class UploadThread(StoppableThread): chunk_count = 0 for chunk in request: del i.pendingUpload[chunk] - if Dandelion().hasHash(chunk) and \ - i != Dandelion().objectChildStem(chunk): + if state.Dandelion.hasHash(chunk) and \ + i != state.Dandelion.objectChildStem(chunk): i.antiIntersectionDelay() self.logger.info( '%s asked for a stem object we didn\'t offer to it.', @@ -50,7 +49,7 @@ class UploadThread(StoppableThread): break try: payload.extend(protocol.CreatePacket( - 'object', Inventory()[chunk].payload)) + 'object', state.Inventory[chunk].payload)) chunk_count += 1 except KeyError: i.antiIntersectionDelay() diff --git a/src/openclpow.py b/src/openclpow.py index 1091f555..5391590c 100644 --- a/src/openclpow.py +++ b/src/openclpow.py @@ -6,7 +6,7 @@ import os from struct import pack import paths -from bmconfigparser import BMConfigParser +from bmconfigparser import config from state import shutdown try: @@ -42,12 +42,12 @@ def initCL(): try: for platform in cl.get_platforms(): gpus.extend(platform.get_devices(device_type=cl.device_type.GPU)) - if BMConfigParser().safeGet("bitmessagesettings", "opencl") == platform.vendor: + if config.safeGet("bitmessagesettings", "opencl") == platform.vendor: enabledGpus.extend(platform.get_devices( device_type=cl.device_type.GPU)) if platform.vendor not in vendors: vendors.append(platform.vendor) - except: # noqa:E722 + except: # nosec B110 # noqa:E722 # pylint:disable=bare-except pass if enabledGpus: ctx = cl.Context(devices=enabledGpus) diff --git a/src/paths.py b/src/paths.py index e2f8c97e..e0d43334 100644 --- a/src/paths.py +++ b/src/paths.py @@ -21,14 +21,15 @@ def lookupExeFolder(): if frozen: exeFolder = ( # targetdir/Bitmessage.app/Contents/MacOS/Bitmessage - os.path.dirname(sys.executable).split(os.path.sep)[0] + os.path.sep - if frozen == "macosx_app" else - os.path.dirname(sys.executable) + os.path.sep) + os.path.dirname(sys.executable).split(os.path.sep)[0] + if frozen == "macosx_app" else os.path.dirname(sys.executable)) + elif os.getenv('APPIMAGE'): + exeFolder = os.path.dirname(os.getenv('APPIMAGE')) elif __file__: - exeFolder = os.path.dirname(__file__) + os.path.sep + exeFolder = os.path.dirname(__file__) else: - exeFolder = '' - return exeFolder + return '' + return exeFolder + os.path.sep def lookupAppdataFolder(): @@ -49,11 +50,8 @@ def lookupAppdataFolder(): sys.exit( 'Could not find home folder, please report this message' ' and your OS X version to the BitMessage Github.') - elif 'win32' in sys.platform or 'win64' in sys.platform: - dataFolder = os.path.join( - os.environ['APPDATA'].decode( - sys.getfilesystemencoding(), 'ignore'), APPNAME - ) + os.path.sep + elif sys.platform.startswith('win'): + dataFolder = os.path.join(os.environ['APPDATA'], APPNAME) + os.path.sep else: try: dataFolder = os.path.join(os.environ['XDG_CONFIG_HOME'], APPNAME) diff --git a/src/plugins/desktop_xdg.py b/src/plugins/desktop_xdg.py index 3dbd212f..0b551e1c 100644 --- a/src/plugins/desktop_xdg.py +++ b/src/plugins/desktop_xdg.py @@ -2,21 +2,23 @@ import os -from xdg import BaseDirectory, Menu +from xdg import BaseDirectory, Menu, Exceptions class DesktopXDG(object): """pyxdg Freedesktop desktop implementation""" def __init__(self): - menu_entry = Menu.parse().getMenu('Office').getMenuEntry( - 'pybitmessage.desktop') - self.desktop = menu_entry.DesktopEntry if menu_entry else None + try: + self.desktop = Menu.parse().getMenu('Office').getMenuEntry( + 'pybitmessage.desktop').DesktopEntry + except (AttributeError, Exceptions.ParsingError): + raise TypeError # TypeError disables startonlogon + appimage = os.getenv('APPIMAGE') + if appimage: + self.desktop.set('Exec', appimage) def adjust_startonlogon(self, autostart=False): """Configure autostart according to settings""" - if not self.desktop: - return - autostart_path = os.path.join( BaseDirectory.xdg_config_home, 'autostart', 'pybitmessage.desktop') if autostart: diff --git a/src/plugins/proxyconfig_stem.py b/src/plugins/proxyconfig_stem.py index d18a2e5f..25f75f69 100644 --- a/src/plugins/proxyconfig_stem.py +++ b/src/plugins/proxyconfig_stem.py @@ -13,7 +13,7 @@ Configure tor proxy and hidden service with """ import logging import os -import random # noseq +import random import sys import tempfile @@ -79,7 +79,7 @@ def connect_plugin(config): port = config.safeGetInt('bitmessagesettings', 'socksport', 9050) for attempt in range(50): if attempt > 0: - port = random.randint(32767, 65535) + port = random.randint(32767, 65535) # nosec B311 tor_config['SocksPort'] = str(port) if tor_config.get('DataDirectory'): control_port = port + 1 diff --git a/src/plugins/sound_playfile.py b/src/plugins/sound_playfile.py index e36d9922..c6b70f66 100644 --- a/src/plugins/sound_playfile.py +++ b/src/plugins/sound_playfile.py @@ -11,14 +11,14 @@ try: winsound.PlaySound(sound_file, winsound.SND_FILENAME) except ImportError: import os - import subprocess + import subprocess # nosec B404 play_cmd = {} def _subprocess(*args): FNULL = open(os.devnull, 'wb') subprocess.call( - args, stdout=FNULL, stderr=subprocess.STDOUT, close_fds=True) + args, stdout=FNULL, stderr=subprocess.STDOUT, close_fds=True) # nosec B603 def connect_plugin(sound_file): """This function implements the entry point.""" diff --git a/src/proofofwork.py b/src/proofofwork.py index 148d6734..5e157db9 100644 --- a/src/proofofwork.py +++ b/src/proofofwork.py @@ -4,20 +4,20 @@ Proof of work calculation """ import ctypes -import hashlib import os +import subprocess # nosec B404 import sys import tempfile import time from struct import pack, unpack -from subprocess import call +import highlevelcrypto import openclpow import paths import queues import state import tr -from bmconfigparser import BMConfigParser +from bmconfigparser import config from debug import logger bitmsglib = 'bitmsghash.so' @@ -82,18 +82,25 @@ def _set_idle(): pid = win32api.GetCurrentProcessId() handle = win32api.OpenProcess(win32con.PROCESS_ALL_ACCESS, True, pid) win32process.SetPriorityClass(handle, win32process.IDLE_PRIORITY_CLASS) - except: # noqa:E722 + except: # nosec B110 # noqa:E722 # pylint:disable=bare-except # Windows 64-bit pass +def trial_value(nonce, initialHash): + """Calculate PoW trial value""" + trialValue, = unpack( + '>Q', highlevelcrypto.double_sha512( + pack('>Q', nonce) + initialHash)[0:8]) + return trialValue + + def _pool_worker(nonce, initialHash, target, pool_size): _set_idle() trialValue = float('inf') while trialValue > target: nonce += pool_size - trialValue, = unpack('>Q', hashlib.sha512(hashlib.sha512( - pack('>Q', nonce) + initialHash).digest()).digest()[0:8]) + trialValue = trial_value(nonce, initialHash) return [trialValue, nonce] @@ -103,10 +110,9 @@ def _doSafePoW(target, initialHash): trialValue = float('inf') while trialValue > target and state.shutdown == 0: nonce += 1 - trialValue, = unpack('>Q', hashlib.sha512(hashlib.sha512( - pack('>Q', nonce) + initialHash).digest()).digest()[0:8]) + trialValue = trial_value(nonce, initialHash) if state.shutdown != 0: - raise StopIteration("Interrupted") # pylint: misplaced-bare-raise + raise StopIteration("Interrupted") logger.debug("Safe PoW done") return [trialValue, nonce] @@ -119,7 +125,7 @@ def _doFastPoW(target, initialHash): except: # noqa:E722 pool_size = 4 try: - maxCores = BMConfigParser().getint('bitmessagesettings', 'maxcores') + maxCores = config.getint('bitmessagesettings', 'maxcores') except: # noqa:E722 maxCores = 99999 if pool_size > maxCores: @@ -135,7 +141,7 @@ def _doFastPoW(target, initialHash): try: pool.terminate() pool.join() - except: # noqa:E722 + except: # nosec B110 # noqa:E722 # pylint:disable=bare-except pass raise StopIteration("Interrupted") for i in range(pool_size): @@ -163,7 +169,7 @@ def _doCPoW(target, initialHash): logger.debug("C PoW start") nonce = bmpow(out_h, out_m) - trialValue, = unpack('>Q', hashlib.sha512(hashlib.sha512(pack('>Q', nonce) + initialHash).digest()).digest()[0:8]) + trialValue = trial_value(nonce, initialHash) if state.shutdown != 0: raise StopIteration("Interrupted") logger.debug("C PoW done") @@ -173,7 +179,7 @@ def _doCPoW(target, initialHash): def _doGPUPoW(target, initialHash): logger.debug("GPU PoW start") nonce = openclpow.do_opencl_pow(initialHash.encode("hex"), target) - trialValue, = unpack('>Q', hashlib.sha512(hashlib.sha512(pack('>Q', nonce) + initialHash).digest()).digest()[0:8]) + trialValue = trial_value(nonce, initialHash) if trialValue > target: deviceNames = ", ".join(gpu.name for gpu in openclpow.enabledGpus) queues.UISignalQueue.put(( @@ -272,16 +278,26 @@ def buildCPoW(): try: if "bsd" in sys.platform: # BSD make - call(["make", "-C", os.path.join(paths.codePath(), "bitmsghash"), '-f', 'Makefile.bsd']) + subprocess.check_call([ # nosec B607, B603 + "make", "-C", os.path.join(paths.codePath(), "bitmsghash"), + '-f', 'Makefile.bsd']) else: # GNU make - call(["make", "-C", os.path.join(paths.codePath(), "bitmsghash")]) - if os.path.exists(os.path.join(paths.codePath(), "bitmsghash", "bitmsghash.so")): + subprocess.check_call([ # nosec B607, B603 + "make", "-C", os.path.join(paths.codePath(), "bitmsghash")]) + if os.path.exists( + os.path.join(paths.codePath(), "bitmsghash", "bitmsghash.so") + ): init() notifyBuild(True) else: notifyBuild(True) + except (OSError, subprocess.CalledProcessError): + notifyBuild(True) except: # noqa:E722 + logger.warning( + 'Unexpected exception rised when tried to build bitmsghash lib', + exc_info=True) notifyBuild(True) @@ -296,14 +312,14 @@ def run(target, initialHash): return _doGPUPoW(target, initialHash) except StopIteration: raise - except: # noqa:E722 + except: # nosec B110 # noqa:E722 # pylint:disable=bare-except pass # fallback if bmpow: try: return _doCPoW(target, initialHash) except StopIteration: raise - except: # noqa:E722 + except: # nosec B110 # noqa:E722 # pylint:disable=bare-except pass # fallback if paths.frozen == "macosx_app" or not paths.frozen: # on my (Peter Surda) Windows 10, Windows Defender @@ -315,13 +331,13 @@ def run(target, initialHash): except StopIteration: logger.error("Fast PoW got StopIteration") raise - except: # noqa:E722 + except: # noqa:E722 # pylint:disable=bare-except logger.error("Fast PoW got exception:", exc_info=True) try: return _doSafePoW(target, initialHash) except StopIteration: raise - except: # noqa:E722 + except: # nosec B110 # noqa:E722 # pylint:disable=bare-except pass # fallback diff --git a/src/protocol.py b/src/protocol.py index 1934d9cc..7f9830e5 100644 --- a/src/protocol.py +++ b/src/protocol.py @@ -18,12 +18,27 @@ import highlevelcrypto import state from addresses import ( encodeVarint, decodeVarint, decodeAddress, varintDecodeError) -from bmconfigparser import BMConfigParser +from bmconfigparser import config from debug import logger -from fallback import RIPEMD160Hash from helper_sql import sqlExecute +from network.node import Peer from version import softwareVersion +# Network constants +magic = 0xE9BEB4D9 +#: protocol specification says max 1000 addresses in one addr command +MAX_ADDR_COUNT = 1000 +#: address is online if online less than this many seconds ago +ADDRESS_ALIVE = 10800 +#: ~1.6 MB which is the maximum possible size of an inv message. +MAX_MESSAGE_SIZE = 1600100 +#: 2**18 = 256kB is the maximum size of an object payload +MAX_OBJECT_PAYLOAD_SIZE = 2**18 +#: protocol specification says max 50000 objects in one inv command +MAX_OBJECT_COUNT = 50000 +#: maximum time offset +MAX_TIME_OFFSET = 3600 + # Service flags #: This is a normal network node NODE_NETWORK = 1 @@ -56,7 +71,7 @@ OBJECT_I2P = 0x493250 OBJECT_ADDR = 0x61646472 eightBytesOfRandomDataUsedToDetectConnectionsToSelf = pack( - '>Q', random.randrange(1, 18446744073709551615)) + '>Q', random.randrange(1, 18446744073709551615)) # nosec B311 # Compiled struct for packing/unpacking headers # New code should use CreatePacket instead of Header.pack @@ -72,7 +87,7 @@ def getBitfield(address): # bitfield of features supported by me (see the wiki). bitfield = 0 # send ack - if not BMConfigParser().safeGetBoolean(address, 'dontsendack'): + if not config.safeGetBoolean(address, 'dontsendack'): bitfield |= BITFIELD_DOESACK return pack('>I', bitfield) @@ -90,13 +105,18 @@ def isBitSetWithinBitfield(fourByteString, n): x, = unpack('>L', fourByteString) return x & 2**n != 0 +# Streams + + +MIN_VALID_STREAM = 1 +MAX_VALID_STREAM = 2**63 - 1 # IP addresses def encodeHost(host): """Encode a given host to be used in low-level socket operations""" - if host.find('.onion') > -1: + if host.endswith('.onion'): return b'\xfd\x87\xd8\x7e\xeb\x43' + base64.b32decode( host.split(".")[0], True) elif host.find(':') == -1: @@ -107,7 +127,7 @@ def encodeHost(host): def networkType(host): """Determine if a host is IPv4, IPv6 or an onion address""" - if host.find('.onion') > -1: + if host.endswith('.onion'): return 'onion' elif host.find(':') == -1: return 'IPv4' @@ -238,7 +258,7 @@ def haveSSL(server=False): def checkSocksIP(host): """Predicate to check if we're using a SOCKS proxy""" - sockshostname = BMConfigParser().safeGet( + sockshostname = config.safeGet( 'bitmessagesettings', 'sockshostname') try: if not state.socksIP: @@ -254,7 +274,7 @@ def isProofOfWorkSufficient( data, nonceTrialsPerByte=0, payloadLengthExtraBytes=0, recvTime=0): """ Validate an object's Proof of Work using method described - `here `_ + :doc:`here ` Arguments: int nonceTrialsPerByte (default: from `.defaults`) @@ -269,12 +289,11 @@ def isProofOfWorkSufficient( if payloadLengthExtraBytes < defaults.networkDefaultPayloadLengthExtraBytes: payloadLengthExtraBytes = defaults.networkDefaultPayloadLengthExtraBytes endOfLifeTime, = unpack('>Q', data[8:16]) - TTL = endOfLifeTime - (int(recvTime) if recvTime else int(time.time())) + TTL = endOfLifeTime - int(recvTime if recvTime else time.time()) if TTL < 300: TTL = 300 - POW, = unpack('>Q', hashlib.sha512(hashlib.sha512( - data[:8] + hashlib.sha512(data[8:]).digest() - ).digest()).digest()[0:8]) + POW, = unpack('>Q', highlevelcrypto.double_sha512( + data[:8] + hashlib.sha512(data[8:]).digest())[0:8]) return POW <= 2 ** 64 / ( nonceTrialsPerByte * ( len(data) + payloadLengthExtraBytes @@ -290,11 +309,33 @@ def CreatePacket(command, payload=b''): checksum = hashlib.sha512(payload).digest()[0:4] b = bytearray(Header.size + payload_length) - Header.pack_into(b, 0, 0xE9BEB4D9, command, payload_length, checksum) + Header.pack_into(b, 0, magic, command, payload_length, checksum) b[Header.size:] = payload return bytes(b) +def assembleAddrMessage(peerList): + """Create address command""" + if isinstance(peerList, Peer): + peerList = [peerList] + if not peerList: + return b'' + retval = b'' + for i in range(0, len(peerList), MAX_ADDR_COUNT): + payload = encodeVarint(len(peerList[i:i + MAX_ADDR_COUNT])) + for stream, peer, timestamp in peerList[i:i + MAX_ADDR_COUNT]: + # 64-bit time + payload += pack('>Q', timestamp) + payload += pack('>I', stream) + # service bit flags offered by this node + payload += pack('>q', 1) + payload += encodeHost(peer.host) + # remote port + payload += pack('>H', peer.port) + retval += CreatePacket(b'addr', payload) + return retval + + def assembleVersionMessage( remoteHost, remotePort, participatingStreams, server=False, nodeid=None ): @@ -309,7 +350,7 @@ def assembleVersionMessage( '>q', NODE_NETWORK | (NODE_SSL if haveSSL(server) else 0) - | (NODE_DANDELION if state.dandelion else 0) + | (NODE_DANDELION if state.dandelion_enabled else 0) ) payload += pack('>q', int(time.time())) @@ -333,7 +374,7 @@ def assembleVersionMessage( '>q', NODE_NETWORK | (NODE_SSL if haveSSL(server) else 0) - | (NODE_DANDELION if state.dandelion else 0) + | (NODE_DANDELION if state.dandelion_enabled else 0) ) # = 127.0.0.1. This will be ignored by the remote host. # The actual remote connected IP will be used. @@ -341,19 +382,19 @@ def assembleVersionMessage( '>L', 2130706433) # we have a separate extPort and incoming over clearnet # or outgoing through clearnet - extport = BMConfigParser().safeGetInt('bitmessagesettings', 'extport') + extport = config.safeGetInt('bitmessagesettings', 'extport') if ( extport and ((server and not checkSocksIP(remoteHost)) or ( - BMConfigParser().get('bitmessagesettings', 'socksproxytype') + config.get('bitmessagesettings', 'socksproxytype') == 'none' and not server)) ): payload += pack('>H', extport) elif checkSocksIP(remoteHost) and server: # incoming connection over Tor payload += pack( - '>H', BMConfigParser().getint('bitmessagesettings', 'onionport')) + '>H', config.getint('bitmessagesettings', 'onionport')) else: # no extport and not incoming over Tor payload += pack( - '>H', BMConfigParser().getint('bitmessagesettings', 'port')) + '>H', config.getint('bitmessagesettings', 'port')) if nodeid is not None: payload += nodeid[0:8] @@ -393,6 +434,17 @@ def assembleErrorMessage(fatal=0, banTime=0, inventoryVector='', errorText=''): # Packet decoding +def decodeObjectParameters(data): + """Decode the parameters of a raw object needed to put it in inventory""" + # BMProto.decode_payload_content("QQIvv") + expiresTime = unpack('>Q', data[8:16])[0] + objectType = unpack('>I', data[16:20])[0] + parserPos = 20 + decodeVarint(data[20:30])[1] + toStreamNumber = decodeVarint(data[parserPos:parserPos + 10])[0] + + return objectType, toStreamNumber, expiresTime + + def decryptAndCheckPubkeyPayload(data, address): """ Version 4 pubkeys are encrypted. This function is run when we @@ -459,9 +511,9 @@ def decryptAndCheckPubkeyPayload(data, address): readPosition = 0 # bitfieldBehaviors = decryptedData[readPosition:readPosition + 4] readPosition += 4 - publicSigningKey = '\x04' + decryptedData[readPosition:readPosition + 64] + pubSigningKey = '\x04' + decryptedData[readPosition:readPosition + 64] readPosition += 64 - publicEncryptionKey = '\x04' + decryptedData[readPosition:readPosition + 64] + pubEncryptionKey = '\x04' + decryptedData[readPosition:readPosition + 64] readPosition += 64 specifiedNonceTrialsPerByteLength = decodeVarint( decryptedData[readPosition:readPosition + 10])[1] @@ -477,7 +529,7 @@ def decryptAndCheckPubkeyPayload(data, address): signature = decryptedData[readPosition:readPosition + signatureLength] if not highlevelcrypto.verify( - signedData, signature, hexlify(publicSigningKey)): + signedData, signature, hexlify(pubSigningKey)): logger.info( 'ECDSA verify failed (within decryptAndCheckPubkeyPayload)') return 'failed' @@ -485,9 +537,7 @@ def decryptAndCheckPubkeyPayload(data, address): logger.info( 'ECDSA verify passed (within decryptAndCheckPubkeyPayload)') - sha = hashlib.new('sha512') - sha.update(publicSigningKey + publicEncryptionKey) - embeddedRipe = RIPEMD160Hash(sha.digest()).digest() + embeddedRipe = highlevelcrypto.to_ripe(pubSigningKey, pubEncryptionKey) if embeddedRipe != ripe: # Although this pubkey object had the tag were were looking for @@ -505,7 +555,7 @@ def decryptAndCheckPubkeyPayload(data, address): 'addressVersion: %s, streamNumber: %s\nripe %s\n' 'publicSigningKey in hex: %s\npublicEncryptionKey in hex: %s', addressVersion, streamNumber, hexlify(ripe), - hexlify(publicSigningKey), hexlify(publicEncryptionKey) + hexlify(pubSigningKey), hexlify(pubEncryptionKey) ) t = (address, addressVersion, storedData, int(time.time()), 'yes') diff --git a/src/pyelliptic/ecc.py b/src/pyelliptic/ecc.py index 388227c7..c670d023 100644 --- a/src/pyelliptic/ecc.py +++ b/src/pyelliptic/ecc.py @@ -18,28 +18,31 @@ class ECC(object): Asymmetric encryption with Elliptic Curve Cryptography (ECC) ECDH, ECDSA and ECIES + >>> from binascii import hexlify >>> import pyelliptic >>> alice = pyelliptic.ECC() # default curve: sect283r1 >>> bob = pyelliptic.ECC(curve='sect571r1') >>> ciphertext = alice.encrypt("Hello Bob", bob.get_pubkey()) - >>> print bob.decrypt(ciphertext) + >>> print(bob.decrypt(ciphertext)) >>> signature = bob.sign("Hello Alice") >>> # alice's job : - >>> print pyelliptic.ECC( - >>> pubkey=bob.get_pubkey()).verify(signature, "Hello Alice") + >>> print(pyelliptic.ECC( + >>> pubkey=bob.get_pubkey()).verify(signature, "Hello Alice")) >>> # ERROR !!! >>> try: >>> key = alice.get_ecdh_key(bob.get_pubkey()) >>> except: - >>> print("For ECDH key agreement, the keys must be defined on the same curve !") + >>> print( + "For ECDH key agreement, the keys must be defined" + " on the same curve!") >>> alice = pyelliptic.ECC(curve='sect571r1') - >>> print alice.get_ecdh_key(bob.get_pubkey()).encode('hex') - >>> print bob.get_ecdh_key(alice.get_pubkey()).encode('hex') + >>> print(hexlify(alice.get_ecdh_key(bob.get_pubkey()))) + >>> print(hexlify(bob.get_ecdh_key(alice.get_pubkey()))) """ @@ -53,7 +56,7 @@ class ECC(object): curve='sect283r1', ): # pylint: disable=too-many-arguments """ - For a normal and High level use, specifie pubkey, + For a normal and high level use, specifie pubkey, privkey (if you need) and the curve """ if isinstance(curve, str): @@ -80,20 +83,19 @@ class ECC(object): self.pubkey_y = None self.privkey = None raise Exception("Bad ECC keys ...") - else: - self.pubkey_x = pubkey_x - self.pubkey_y = pubkey_y - self.privkey = privkey + self.pubkey_x = pubkey_x + self.pubkey_y = pubkey_y + self.privkey = privkey @staticmethod def get_curves(): """ - static method, returns the list of all the curves available + Static method, returns the list of all the curves available """ return OpenSSL.curves.keys() def get_curve(self): - """Encryption object from curve name""" + """The name of currently used curve""" return OpenSSL.get_curve_by_id(self.curve) def get_curve_id(self): @@ -105,12 +107,19 @@ class ECC(object): High level function which returns : curve(2) + len_of_pubkeyX(2) + pubkeyX + len_of_pubkeyY + pubkeyY """ + ctx = OpenSSL.BN_CTX_new() + n = OpenSSL.BN_new() + group = OpenSSL.EC_GROUP_new_by_curve_name(self.curve) + OpenSSL.EC_GROUP_get_order(group, n, ctx) + key_len = OpenSSL.BN_num_bytes(n) + pubkey_x = self.pubkey_x.rjust(key_len, b'\x00') + pubkey_y = self.pubkey_y.rjust(key_len, b'\x00') return b''.join(( pack('!H', self.curve), - pack('!H', len(self.pubkey_x)), - self.pubkey_x, - pack('!H', len(self.pubkey_y)), - self.pubkey_y, + pack('!H', len(pubkey_x)), + pubkey_x, + pack('!H', len(pubkey_y)), + pubkey_y, )) def get_privkey(self): @@ -158,9 +167,9 @@ class ECC(object): key = OpenSSL.EC_KEY_new_by_curve_name(self.curve) if key == 0: raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") - if (OpenSSL.EC_KEY_generate_key(key)) == 0: + if OpenSSL.EC_KEY_generate_key(key) == 0: raise Exception("[OpenSSL] EC_KEY_generate_key FAIL ...") - if (OpenSSL.EC_KEY_check_key(key)) == 0: + if OpenSSL.EC_KEY_check_key(key) == 0: raise Exception("[OpenSSL] EC_KEY_check_key FAIL ...") priv_key = OpenSSL.EC_KEY_get0_private_key(key) @@ -193,7 +202,7 @@ class ECC(object): def get_ecdh_key(self, pubkey): """ High level function. Compute public key with the local private key - and returns a 512bits shared key + and returns a 512bits shared key. """ curve, pubkey_x, pubkey_y, _ = ECC._decode_pubkey(pubkey) if curve != self.curve: @@ -209,31 +218,31 @@ class ECC(object): if other_key == 0: raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") - other_pub_key_x = OpenSSL.BN_bin2bn(pubkey_x, len(pubkey_x), 0) - other_pub_key_y = OpenSSL.BN_bin2bn(pubkey_y, len(pubkey_y), 0) + other_pub_key_x = OpenSSL.BN_bin2bn(pubkey_x, len(pubkey_x), None) + other_pub_key_y = OpenSSL.BN_bin2bn(pubkey_y, len(pubkey_y), None) other_group = OpenSSL.EC_KEY_get0_group(other_key) other_pub_key = OpenSSL.EC_POINT_new(other_group) - if (OpenSSL.EC_POINT_set_affine_coordinates_GFp(other_group, - other_pub_key, - other_pub_key_x, - other_pub_key_y, - 0)) == 0: + if OpenSSL.EC_POINT_set_affine_coordinates_GFp(other_group, + other_pub_key, + other_pub_key_x, + other_pub_key_y, + 0) == 0: raise Exception( "[OpenSSL] EC_POINT_set_affine_coordinates_GFp FAIL ...") - if (OpenSSL.EC_KEY_set_public_key(other_key, other_pub_key)) == 0: + if OpenSSL.EC_KEY_set_public_key(other_key, other_pub_key) == 0: raise Exception("[OpenSSL] EC_KEY_set_public_key FAIL ...") - if (OpenSSL.EC_KEY_check_key(other_key)) == 0: + if OpenSSL.EC_KEY_check_key(other_key) == 0: raise Exception("[OpenSSL] EC_KEY_check_key FAIL ...") own_key = OpenSSL.EC_KEY_new_by_curve_name(self.curve) if own_key == 0: raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") own_priv_key = OpenSSL.BN_bin2bn( - self.privkey, len(self.privkey), 0) + self.privkey, len(self.privkey), None) - if (OpenSSL.EC_KEY_set_private_key(own_key, own_priv_key)) == 0: + if OpenSSL.EC_KEY_set_private_key(own_key, own_priv_key) == 0: raise Exception("[OpenSSL] EC_KEY_set_private_key FAIL ...") if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: @@ -259,7 +268,7 @@ class ECC(object): def check_key(self, privkey, pubkey): """ Check the public key and the private key. - The private key is optional (replace by None) + The private key is optional (replace by None). """ curve, pubkey_x, pubkey_y, _ = ECC._decode_pubkey(pubkey) if privkey is None: @@ -277,34 +286,32 @@ class ECC(object): curve = self.curve elif isinstance(curve, str): curve = OpenSSL.get_curve(curve) - else: - curve = curve try: key = OpenSSL.EC_KEY_new_by_curve_name(curve) if key == 0: raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") if privkey is not None: - priv_key = OpenSSL.BN_bin2bn(privkey, len(privkey), 0) - pub_key_x = OpenSSL.BN_bin2bn(pubkey_x, len(pubkey_x), 0) - pub_key_y = OpenSSL.BN_bin2bn(pubkey_y, len(pubkey_y), 0) + priv_key = OpenSSL.BN_bin2bn(privkey, len(privkey), None) + pub_key_x = OpenSSL.BN_bin2bn(pubkey_x, len(pubkey_x), None) + pub_key_y = OpenSSL.BN_bin2bn(pubkey_y, len(pubkey_y), None) if privkey is not None: - if (OpenSSL.EC_KEY_set_private_key(key, priv_key)) == 0: + if OpenSSL.EC_KEY_set_private_key(key, priv_key) == 0: raise Exception( "[OpenSSL] EC_KEY_set_private_key FAIL ...") group = OpenSSL.EC_KEY_get0_group(key) pub_key = OpenSSL.EC_POINT_new(group) - if (OpenSSL.EC_POINT_set_affine_coordinates_GFp(group, pub_key, - pub_key_x, - pub_key_y, - 0)) == 0: + if OpenSSL.EC_POINT_set_affine_coordinates_GFp(group, pub_key, + pub_key_x, + pub_key_y, + 0) == 0: raise Exception( "[OpenSSL] EC_POINT_set_affine_coordinates_GFp FAIL ...") - if (OpenSSL.EC_KEY_set_public_key(key, pub_key)) == 0: + if OpenSSL.EC_KEY_set_public_key(key, pub_key) == 0: raise Exception("[OpenSSL] EC_KEY_set_public_key FAIL ...") - if (OpenSSL.EC_KEY_check_key(key)) == 0: + if OpenSSL.EC_KEY_check_key(key) == 0: raise Exception("[OpenSSL] EC_KEY_check_key FAIL ...") return 0 @@ -336,25 +343,27 @@ class ECC(object): if key == 0: raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") - priv_key = OpenSSL.BN_bin2bn(self.privkey, len(self.privkey), 0) - pub_key_x = OpenSSL.BN_bin2bn(self.pubkey_x, len(self.pubkey_x), 0) - pub_key_y = OpenSSL.BN_bin2bn(self.pubkey_y, len(self.pubkey_y), 0) + priv_key = OpenSSL.BN_bin2bn(self.privkey, len(self.privkey), None) + pub_key_x = OpenSSL.BN_bin2bn(self.pubkey_x, len(self.pubkey_x), + None) + pub_key_y = OpenSSL.BN_bin2bn(self.pubkey_y, len(self.pubkey_y), + None) - if (OpenSSL.EC_KEY_set_private_key(key, priv_key)) == 0: + if OpenSSL.EC_KEY_set_private_key(key, priv_key) == 0: raise Exception("[OpenSSL] EC_KEY_set_private_key FAIL ...") group = OpenSSL.EC_KEY_get0_group(key) pub_key = OpenSSL.EC_POINT_new(group) - if (OpenSSL.EC_POINT_set_affine_coordinates_GFp(group, pub_key, - pub_key_x, - pub_key_y, - 0)) == 0: + if OpenSSL.EC_POINT_set_affine_coordinates_GFp(group, pub_key, + pub_key_x, + pub_key_y, + 0) == 0: raise Exception( "[OpenSSL] EC_POINT_set_affine_coordinates_GFp FAIL ...") - if (OpenSSL.EC_KEY_set_public_key(key, pub_key)) == 0: + if OpenSSL.EC_KEY_set_public_key(key, pub_key) == 0: raise Exception("[OpenSSL] EC_KEY_set_public_key FAIL ...") - if (OpenSSL.EC_KEY_check_key(key)) == 0: + if OpenSSL.EC_KEY_check_key(key) == 0: raise Exception("[OpenSSL] EC_KEY_check_key FAIL ...") if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: @@ -363,12 +372,13 @@ class ECC(object): OpenSSL.EVP_MD_CTX_init(md_ctx) OpenSSL.EVP_DigestInit_ex(md_ctx, digest_alg(), None) - if (OpenSSL.EVP_DigestUpdate(md_ctx, buff, size)) == 0: + if OpenSSL.EVP_DigestUpdate(md_ctx, buff, size) == 0: raise Exception("[OpenSSL] EVP_DigestUpdate FAIL ...") OpenSSL.EVP_DigestFinal_ex(md_ctx, digest, dgst_len) OpenSSL.ECDSA_sign(0, digest, dgst_len.contents, sig, siglen, key) - if (OpenSSL.ECDSA_verify(0, digest, dgst_len.contents, sig, - siglen.contents, key)) != 1: + if OpenSSL.ECDSA_verify( + 0, digest, dgst_len.contents, sig, siglen.contents, key + ) != 1: raise Exception("[OpenSSL] ECDSA_verify FAIL ...") return sig.raw[:siglen.contents.value] @@ -387,7 +397,7 @@ class ECC(object): def verify(self, sig, inputb, digest_alg=OpenSSL.digest_ecdsa_sha1): """ Verify the signature with the input and the local public key. - Returns a boolean + Returns a boolean. """ try: bsig = OpenSSL.malloc(sig, len(sig)) @@ -403,27 +413,29 @@ class ECC(object): if key == 0: raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") - pub_key_x = OpenSSL.BN_bin2bn(self.pubkey_x, len(self.pubkey_x), 0) - pub_key_y = OpenSSL.BN_bin2bn(self.pubkey_y, len(self.pubkey_y), 0) + pub_key_x = OpenSSL.BN_bin2bn(self.pubkey_x, len(self.pubkey_x), + None) + pub_key_y = OpenSSL.BN_bin2bn(self.pubkey_y, len(self.pubkey_y), + None) group = OpenSSL.EC_KEY_get0_group(key) pub_key = OpenSSL.EC_POINT_new(group) - if (OpenSSL.EC_POINT_set_affine_coordinates_GFp(group, pub_key, - pub_key_x, - pub_key_y, - 0)) == 0: + if OpenSSL.EC_POINT_set_affine_coordinates_GFp(group, pub_key, + pub_key_x, + pub_key_y, + 0) == 0: raise Exception( "[OpenSSL] EC_POINT_set_affine_coordinates_GFp FAIL ...") - if (OpenSSL.EC_KEY_set_public_key(key, pub_key)) == 0: + if OpenSSL.EC_KEY_set_public_key(key, pub_key) == 0: raise Exception("[OpenSSL] EC_KEY_set_public_key FAIL ...") - if (OpenSSL.EC_KEY_check_key(key)) == 0: + if OpenSSL.EC_KEY_check_key(key) == 0: raise Exception("[OpenSSL] EC_KEY_check_key FAIL ...") if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: OpenSSL.EVP_MD_CTX_new(md_ctx) else: OpenSSL.EVP_MD_CTX_init(md_ctx) OpenSSL.EVP_DigestInit_ex(md_ctx, digest_alg(), None) - if (OpenSSL.EVP_DigestUpdate(md_ctx, binputb, len(inputb))) == 0: + if OpenSSL.EVP_DigestUpdate(md_ctx, binputb, len(inputb)) == 0: raise Exception("[OpenSSL] EVP_DigestUpdate FAIL ...") OpenSSL.EVP_DigestFinal_ex(md_ctx, digest, dgst_len) @@ -467,7 +479,7 @@ class ECC(object): ephemcurve=None, ciphername='aes-256-cbc', ): # pylint: disable=too-many-arguments - """ECHD encryption, keys supplied in binary data format""" + """ECDH encryption, keys supplied in binary data format""" if ephemcurve is None: ephemcurve = curve @@ -475,9 +487,9 @@ class ECC(object): key = sha512(ephem.raw_get_ecdh_key(pubkey_x, pubkey_y)).digest() key_e, key_m = key[:32], key[32:] pubkey = ephem.get_pubkey() - iv = OpenSSL.rand(OpenSSL.get_cipher(ciphername).get_blocksize()) - ctx = Cipher(key_e, iv, 1, ciphername) - ciphertext = iv + pubkey + ctx.ciphering(data) + _iv = Cipher.gen_IV(ciphername) + ctx = Cipher(key_e, _iv, 1, ciphername) + ciphertext = _iv + pubkey + ctx.ciphering(data) mac = hmac_sha256(key_m, ciphertext) return ciphertext + mac @@ -486,10 +498,10 @@ class ECC(object): Decrypt data with ECIES method using the local private key """ blocksize = OpenSSL.get_cipher(ciphername).get_blocksize() - iv = data[:blocksize] + _iv = data[:blocksize] i = blocksize - _, pubkey_x, pubkey_y, i2 = ECC._decode_pubkey(data[i:]) - i += i2 + _, pubkey_x, pubkey_y, _i2 = ECC._decode_pubkey(data[i:]) + i += _i2 ciphertext = data[i:len(data) - 32] i += len(ciphertext) mac = data[i:] @@ -497,5 +509,6 @@ class ECC(object): key_e, key_m = key[:32], key[32:] if not equals(hmac_sha256(key_m, data[:len(data) - 32]), mac): raise RuntimeError("Fail to verify data") - ctx = Cipher(key_e, iv, 0, ciphername) - return ctx.ciphering(ciphertext) + ctx = Cipher(key_e, _iv, 0, ciphername) + retval = ctx.ciphering(ciphertext) + return retval diff --git a/src/pyelliptic/eccblind.py b/src/pyelliptic/eccblind.py index 83bc7632..df987824 100644 --- a/src/pyelliptic/eccblind.py +++ b/src/pyelliptic/eccblind.py @@ -109,7 +109,7 @@ class ECCBlind(object): # pylint: disable=too-many-instance-attributes """ ECC inversion """ - inverse = OpenSSL.BN_mod_inverse(0, a, self.n, self.ctx) + inverse = OpenSSL.BN_mod_inverse(None, a, self.n, self.ctx) return inverse def ec_gen_keypair(self): @@ -119,7 +119,7 @@ class ECCBlind(object): # pylint: disable=too-many-instance-attributes """ d = self.ec_get_random() Q = OpenSSL.EC_POINT_new(self.group) - OpenSSL.EC_POINT_mul(self.group, Q, d, 0, 0, 0) + OpenSSL.EC_POINT_mul(self.group, Q, d, None, None, None) return (d, Q) def ec_Ftor(self, F): @@ -139,7 +139,7 @@ class ECCBlind(object): # pylint: disable=too-many-instance-attributes x = OpenSSL.BN_new() y = OpenSSL.BN_new() OpenSSL.EC_POINT_get_affine_coordinates( - self.group, point, x, y, 0) + self.group, point, x, y, None) y_byte = (OpenSSL.BN_is_odd(y) & Y_BIT) | COMPRESSED_BIT l_ = OpenSSL.BN_num_bytes(self.n) try: @@ -160,7 +160,7 @@ class ECCBlind(object): # pylint: disable=too-many-instance-attributes def _ec_point_deserialize(self, data): """Make a string into an EC point""" y_bit, x_raw = unpack(EC, data) - x = OpenSSL.BN_bin2bn(x_raw, OpenSSL.BN_num_bytes(self.n), 0) + x = OpenSSL.BN_bin2bn(x_raw, OpenSSL.BN_num_bytes(self.n), None) y_bit &= Y_BIT retval = OpenSSL.EC_POINT_new(self.group) OpenSSL.EC_POINT_set_compressed_coordinates(self.group, @@ -184,7 +184,7 @@ class ECCBlind(object): # pylint: disable=too-many-instance-attributes def _bn_deserialize(self, data): """Make a BigNum out of string""" - x = OpenSSL.BN_bin2bn(data, OpenSSL.BN_num_bytes(self.n), 0) + x = OpenSSL.BN_bin2bn(data, OpenSSL.BN_num_bytes(self.n), None) return x def _init_privkey(self, privkey): @@ -261,7 +261,7 @@ class ECCBlind(object): # pylint: disable=too-many-instance-attributes # R = kG self.R = OpenSSL.EC_POINT_new(self.group) - OpenSSL.EC_POINT_mul(self.group, self.R, self.k, 0, 0, 0) + OpenSSL.EC_POINT_mul(self.group, self.R, self.k, None, None, None) return self._ec_point_serialize(self.R) @@ -286,17 +286,18 @@ class ECCBlind(object): # pylint: disable=too-many-instance-attributes # F = b^-1 * R... self.binv = self.ec_invert(self.b) - OpenSSL.EC_POINT_mul(self.group, temp, 0, self.R, self.binv, 0) + OpenSSL.EC_POINT_mul(self.group, temp, None, self.R, self.binv, + None) OpenSSL.EC_POINT_copy(self.F, temp) # ... + a*b^-1 * Q... OpenSSL.BN_mul(abinv, self.a, self.binv, self.ctx) - OpenSSL.EC_POINT_mul(self.group, temp, 0, self.Q, abinv, 0) - OpenSSL.EC_POINT_add(self.group, self.F, self.F, temp, 0) + OpenSSL.EC_POINT_mul(self.group, temp, None, self.Q, abinv, None) + OpenSSL.EC_POINT_add(self.group, self.F, self.F, temp, None) # ... + c*G - OpenSSL.EC_POINT_mul(self.group, temp, 0, self.G, self.c, 0) - OpenSSL.EC_POINT_add(self.group, self.F, self.F, temp, 0) + OpenSSL.EC_POINT_mul(self.group, temp, None, self.G, self.c, None) + OpenSSL.EC_POINT_add(self.group, self.F, self.F, temp, None) # F = (x0, y0) self.r = self.ec_Ftor(self.F) @@ -355,10 +356,10 @@ class ECCBlind(object): # pylint: disable=too-many-instance-attributes lhs = OpenSSL.EC_POINT_new(self.group) rhs = OpenSSL.EC_POINT_new(self.group) - OpenSSL.EC_POINT_mul(self.group, lhs, s, 0, 0, 0) + OpenSSL.EC_POINT_mul(self.group, lhs, s, None, None, None) - OpenSSL.EC_POINT_mul(self.group, rhs, 0, self.Q, self.m, 0) - OpenSSL.EC_POINT_mul(self.group, rhs, 0, rhs, self.r, 0) + OpenSSL.EC_POINT_mul(self.group, rhs, None, self.Q, self.m, None) + OpenSSL.EC_POINT_mul(self.group, rhs, None, rhs, self.r, None) OpenSSL.EC_POINT_add(self.group, rhs, rhs, self.F, self.ctx) retval = OpenSSL.EC_POINT_cmp(self.group, lhs, rhs, self.ctx) diff --git a/src/pyelliptic/openssl.py b/src/pyelliptic/openssl.py index abc6ac13..deb81644 100644 --- a/src/pyelliptic/openssl.py +++ b/src/pyelliptic/openssl.py @@ -72,6 +72,29 @@ def get_version(library): return (version, hexversion, cflags) +class BIGNUM(ctypes.Structure): # pylint: disable=too-few-public-methods + """OpenSSL's BIGNUM struct""" + _fields_ = [ + ('d', ctypes.POINTER(ctypes.c_ulong)), + ('top', ctypes.c_int), + ('dmax', ctypes.c_int), + ('neg', ctypes.c_int), + ('flags', ctypes.c_int), + ] + + +class EC_POINT(ctypes.Structure): # pylint: disable=too-few-public-methods + """OpenSSL's EC_POINT struct""" + _fields_ = [ + ('meth', ctypes.c_void_p), + ('curve_name', ctypes.c_int), + ('X', ctypes.POINTER(BIGNUM)), + ('Y', ctypes.POINTER(BIGNUM)), + ('Z', ctypes.POINTER(BIGNUM)), + ('Z_is_one', ctypes.c_int), + ] + + class _OpenSSL(object): """ Wrapper for OpenSSL using ctypes @@ -91,38 +114,38 @@ class _OpenSSL(object): self.create_string_buffer = ctypes.create_string_buffer self.BN_new = self._lib.BN_new - self.BN_new.restype = ctypes.c_void_p + self.BN_new.restype = ctypes.POINTER(BIGNUM) self.BN_new.argtypes = [] self.BN_free = self._lib.BN_free self.BN_free.restype = None - self.BN_free.argtypes = [ctypes.c_void_p] + self.BN_free.argtypes = [ctypes.POINTER(BIGNUM)] self.BN_clear_free = self._lib.BN_clear_free self.BN_clear_free.restype = None - self.BN_clear_free.argtypes = [ctypes.c_void_p] + self.BN_clear_free.argtypes = [ctypes.POINTER(BIGNUM)] self.BN_num_bits = self._lib.BN_num_bits self.BN_num_bits.restype = ctypes.c_int - self.BN_num_bits.argtypes = [ctypes.c_void_p] + self.BN_num_bits.argtypes = [ctypes.POINTER(BIGNUM)] self.BN_bn2bin = self._lib.BN_bn2bin self.BN_bn2bin.restype = ctypes.c_int - self.BN_bn2bin.argtypes = [ctypes.c_void_p, ctypes.c_void_p] + self.BN_bn2bin.argtypes = [ctypes.POINTER(BIGNUM), ctypes.c_void_p] try: self.BN_bn2binpad = self._lib.BN_bn2binpad self.BN_bn2binpad.restype = ctypes.c_int - self.BN_bn2binpad.argtypes = [ctypes.c_void_p, ctypes.c_void_p, + self.BN_bn2binpad.argtypes = [ctypes.POINTER(BIGNUM), ctypes.c_void_p, ctypes.c_int] except AttributeError: # optional, we have a workaround pass self.BN_bin2bn = self._lib.BN_bin2bn - self.BN_bin2bn.restype = ctypes.c_void_p + self.BN_bin2bn.restype = ctypes.POINTER(BIGNUM) self.BN_bin2bn.argtypes = [ctypes.c_void_p, ctypes.c_int, - ctypes.c_void_p] + ctypes.POINTER(BIGNUM)] self.EC_KEY_free = self._lib.EC_KEY_free self.EC_KEY_free.restype = None @@ -141,11 +164,11 @@ class _OpenSSL(object): self.EC_KEY_check_key.argtypes = [ctypes.c_void_p] self.EC_KEY_get0_private_key = self._lib.EC_KEY_get0_private_key - self.EC_KEY_get0_private_key.restype = ctypes.c_void_p + self.EC_KEY_get0_private_key.restype = ctypes.POINTER(BIGNUM) self.EC_KEY_get0_private_key.argtypes = [ctypes.c_void_p] self.EC_KEY_get0_public_key = self._lib.EC_KEY_get0_public_key - self.EC_KEY_get0_public_key.restype = ctypes.c_void_p + self.EC_KEY_get0_public_key.restype = ctypes.POINTER(EC_POINT) self.EC_KEY_get0_public_key.argtypes = [ctypes.c_void_p] self.EC_KEY_get0_group = self._lib.EC_KEY_get0_group @@ -156,9 +179,9 @@ class _OpenSSL(object): self._lib.EC_POINT_get_affine_coordinates_GFp self.EC_POINT_get_affine_coordinates_GFp.restype = ctypes.c_int self.EC_POINT_get_affine_coordinates_GFp.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_void_p, + ctypes.POINTER(EC_POINT), + ctypes.POINTER(BIGNUM), + ctypes.POINTER(BIGNUM), ctypes.c_void_p] try: @@ -170,20 +193,20 @@ class _OpenSSL(object): self._lib.EC_POINT_get_affine_coordinates_GF2m self.EC_POINT_get_affine_coordinates.restype = ctypes.c_int self.EC_POINT_get_affine_coordinates.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_void_p, + ctypes.POINTER(EC_POINT), + ctypes.POINTER(BIGNUM), + ctypes.POINTER(BIGNUM), ctypes.c_void_p] self.EC_KEY_set_private_key = self._lib.EC_KEY_set_private_key self.EC_KEY_set_private_key.restype = ctypes.c_int self.EC_KEY_set_private_key.argtypes = [ctypes.c_void_p, - ctypes.c_void_p] + ctypes.POINTER(BIGNUM)] self.EC_KEY_set_public_key = self._lib.EC_KEY_set_public_key self.EC_KEY_set_public_key.restype = ctypes.c_int self.EC_KEY_set_public_key.argtypes = [ctypes.c_void_p, - ctypes.c_void_p] + ctypes.POINTER(EC_POINT)] self.EC_KEY_set_group = self._lib.EC_KEY_set_group self.EC_KEY_set_group.restype = ctypes.c_int @@ -194,9 +217,9 @@ class _OpenSSL(object): self._lib.EC_POINT_set_affine_coordinates_GFp self.EC_POINT_set_affine_coordinates_GFp.restype = ctypes.c_int self.EC_POINT_set_affine_coordinates_GFp.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_void_p, + ctypes.POINTER(EC_POINT), + ctypes.POINTER(BIGNUM), + ctypes.POINTER(BIGNUM), ctypes.c_void_p] try: @@ -208,9 +231,9 @@ class _OpenSSL(object): self._lib.EC_POINT_set_affine_coordinates_GF2m self.EC_POINT_set_affine_coordinates.restype = ctypes.c_int self.EC_POINT_set_affine_coordinates.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_void_p, + ctypes.POINTER(EC_POINT), + ctypes.POINTER(BIGNUM), + ctypes.POINTER(BIGNUM), ctypes.c_void_p] try: @@ -219,38 +242,39 @@ class _OpenSSL(object): except AttributeError: # OpenSSL docs say only use this for backwards compatibility self.EC_POINT_set_compressed_coordinates = \ - self._lib.EC_POINT_set_compressed_coordinates_GF2m + self._lib.EC_POINT_set_compressed_coordinates_GFp self.EC_POINT_set_compressed_coordinates.restype = ctypes.c_int self.EC_POINT_set_compressed_coordinates.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_void_p, + ctypes.POINTER(EC_POINT), + ctypes.POINTER(BIGNUM), ctypes.c_int, ctypes.c_void_p] self.EC_POINT_new = self._lib.EC_POINT_new - self.EC_POINT_new.restype = ctypes.c_void_p + self.EC_POINT_new.restype = ctypes.POINTER(EC_POINT) self.EC_POINT_new.argtypes = [ctypes.c_void_p] self.EC_POINT_free = self._lib.EC_POINT_free self.EC_POINT_free.restype = None - self.EC_POINT_free.argtypes = [ctypes.c_void_p] + self.EC_POINT_free.argtypes = [ctypes.POINTER(EC_POINT)] self.BN_CTX_free = self._lib.BN_CTX_free self.BN_CTX_free.restype = None self.BN_CTX_free.argtypes = [ctypes.c_void_p] self.EC_POINT_mul = self._lib.EC_POINT_mul - self.EC_POINT_mul.restype = None + self.EC_POINT_mul.restype = ctypes.c_int self.EC_POINT_mul.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_void_p, + ctypes.POINTER(EC_POINT), + ctypes.POINTER(BIGNUM), + ctypes.POINTER(EC_POINT), + ctypes.POINTER(BIGNUM), ctypes.c_void_p] self.EC_KEY_set_private_key = self._lib.EC_KEY_set_private_key self.EC_KEY_set_private_key.restype = ctypes.c_int self.EC_KEY_set_private_key.argtypes = [ctypes.c_void_p, - ctypes.c_void_p] + ctypes.POINTER(BIGNUM)] if self._hexversion >= 0x10100000 and not self._libreSSL: self.EC_KEY_OpenSSL = self._lib.EC_KEY_OpenSSL @@ -368,7 +392,7 @@ class _OpenSSL(object): self.EVP_DigestUpdate = self._lib.EVP_DigestUpdate self.EVP_DigestUpdate.restype = ctypes.c_int self.EVP_DigestUpdate.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, ctypes.c_int] + ctypes.c_void_p, ctypes.c_size_t] self.EVP_DigestFinal = self._lib.EVP_DigestFinal self.EVP_DigestFinal.restype = ctypes.c_int @@ -448,7 +472,7 @@ class _OpenSSL(object): self.HMAC = self._lib.HMAC self.HMAC.restype = ctypes.c_void_p self.HMAC.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int, - ctypes.c_void_p, ctypes.c_int, + ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p, ctypes.c_void_p] try: @@ -469,70 +493,71 @@ class _OpenSSL(object): self.BN_CTX_new.argtypes = [] self.BN_dup = self._lib.BN_dup - self.BN_dup.restype = ctypes.c_void_p - self.BN_dup.argtypes = [ctypes.c_void_p] + self.BN_dup.restype = ctypes.POINTER(BIGNUM) + self.BN_dup.argtypes = [ctypes.POINTER(BIGNUM)] self.BN_rand = self._lib.BN_rand self.BN_rand.restype = ctypes.c_int - self.BN_rand.argtypes = [ctypes.c_void_p, + self.BN_rand.argtypes = [ctypes.POINTER(BIGNUM), + ctypes.c_int, ctypes.c_int, ctypes.c_int] self.BN_set_word = self._lib.BN_set_word self.BN_set_word.restype = ctypes.c_int - self.BN_set_word.argtypes = [ctypes.c_void_p, + self.BN_set_word.argtypes = [ctypes.POINTER(BIGNUM), ctypes.c_ulong] self.BN_mul = self._lib.BN_mul self.BN_mul.restype = ctypes.c_int - self.BN_mul.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_void_p, + self.BN_mul.argtypes = [ctypes.POINTER(BIGNUM), + ctypes.POINTER(BIGNUM), + ctypes.POINTER(BIGNUM), ctypes.c_void_p] self.BN_mod_add = self._lib.BN_mod_add self.BN_mod_add.restype = ctypes.c_int - self.BN_mod_add.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_void_p, + self.BN_mod_add.argtypes = [ctypes.POINTER(BIGNUM), + ctypes.POINTER(BIGNUM), + ctypes.POINTER(BIGNUM), + ctypes.POINTER(BIGNUM), ctypes.c_void_p] self.BN_mod_inverse = self._lib.BN_mod_inverse - self.BN_mod_inverse.restype = ctypes.c_void_p - self.BN_mod_inverse.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_void_p, + self.BN_mod_inverse.restype = ctypes.POINTER(BIGNUM) + self.BN_mod_inverse.argtypes = [ctypes.POINTER(BIGNUM), + ctypes.POINTER(BIGNUM), + ctypes.POINTER(BIGNUM), ctypes.c_void_p] self.BN_mod_mul = self._lib.BN_mod_mul self.BN_mod_mul.restype = ctypes.c_int - self.BN_mod_mul.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_void_p, + self.BN_mod_mul.argtypes = [ctypes.POINTER(BIGNUM), + ctypes.POINTER(BIGNUM), + ctypes.POINTER(BIGNUM), + ctypes.POINTER(BIGNUM), ctypes.c_void_p] self.BN_lshift = self._lib.BN_lshift self.BN_lshift.restype = ctypes.c_int - self.BN_lshift.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, + self.BN_lshift.argtypes = [ctypes.POINTER(BIGNUM), + ctypes.POINTER(BIGNUM), ctypes.c_int] self.BN_sub_word = self._lib.BN_sub_word self.BN_sub_word.restype = ctypes.c_int - self.BN_sub_word.argtypes = [ctypes.c_void_p, + self.BN_sub_word.argtypes = [ctypes.POINTER(BIGNUM), ctypes.c_ulong] self.BN_cmp = self._lib.BN_cmp self.BN_cmp.restype = ctypes.c_int - self.BN_cmp.argtypes = [ctypes.c_void_p, - ctypes.c_void_p] + self.BN_cmp.argtypes = [ctypes.POINTER(BIGNUM), + ctypes.POINTER(BIGNUM)] try: self.BN_is_odd = self._lib.BN_is_odd self.BN_is_odd.restype = ctypes.c_int - self.BN_is_odd.argtypes = [ctypes.c_void_p] + self.BN_is_odd.argtypes = [ctypes.POINTER(BIGNUM)] except AttributeError: # OpenSSL 1.1.0 implements this as a function, but earlier # versions as macro, so we need to workaround @@ -540,7 +565,7 @@ class _OpenSSL(object): self.BN_bn2dec = self._lib.BN_bn2dec self.BN_bn2dec.restype = ctypes.c_char_p - self.BN_bn2dec.argtypes = [ctypes.c_void_p] + self.BN_bn2dec.argtypes = [ctypes.POINTER(BIGNUM)] self.EC_GROUP_new_by_curve_name = self._lib.EC_GROUP_new_by_curve_name self.EC_GROUP_new_by_curve_name.restype = ctypes.c_void_p @@ -549,43 +574,43 @@ class _OpenSSL(object): self.EC_GROUP_get_order = self._lib.EC_GROUP_get_order self.EC_GROUP_get_order.restype = ctypes.c_int self.EC_GROUP_get_order.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, + ctypes.POINTER(BIGNUM), ctypes.c_void_p] self.EC_GROUP_get_cofactor = self._lib.EC_GROUP_get_cofactor self.EC_GROUP_get_cofactor.restype = ctypes.c_int self.EC_GROUP_get_cofactor.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, + ctypes.POINTER(BIGNUM), ctypes.c_void_p] self.EC_GROUP_get0_generator = self._lib.EC_GROUP_get0_generator - self.EC_GROUP_get0_generator.restype = ctypes.c_void_p + self.EC_GROUP_get0_generator.restype = ctypes.POINTER(EC_POINT) self.EC_GROUP_get0_generator.argtypes = [ctypes.c_void_p] self.EC_POINT_copy = self._lib.EC_POINT_copy self.EC_POINT_copy.restype = ctypes.c_int - self.EC_POINT_copy.argtypes = [ctypes.c_void_p, - ctypes.c_void_p] + self.EC_POINT_copy.argtypes = [ctypes.POINTER(EC_POINT), + ctypes.POINTER(EC_POINT)] self.EC_POINT_add = self._lib.EC_POINT_add self.EC_POINT_add.restype = ctypes.c_int self.EC_POINT_add.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_void_p, + ctypes.POINTER(EC_POINT), + ctypes.POINTER(EC_POINT), + ctypes.POINTER(EC_POINT), ctypes.c_void_p] self.EC_POINT_cmp = self._lib.EC_POINT_cmp self.EC_POINT_cmp.restype = ctypes.c_int self.EC_POINT_cmp.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_void_p, + ctypes.POINTER(EC_POINT), + ctypes.POINTER(EC_POINT), ctypes.c_void_p] self.EC_POINT_set_to_infinity = self._lib.EC_POINT_set_to_infinity self.EC_POINT_set_to_infinity.restype = ctypes.c_int self.EC_POINT_set_to_infinity.argtypes = [ctypes.c_void_p, - ctypes.c_void_p] + ctypes.POINTER(EC_POINT)] self._set_ciphers() self._set_curves() @@ -794,7 +819,7 @@ def loadOpenSSL(): try: OpenSSL = _OpenSSL(library) return - except Exception: + except Exception: # nosec B110 pass raise Exception( "Couldn't find and load the OpenSSL library. You must install it.") diff --git a/src/pyelliptic/tests/__init__.py b/src/pyelliptic/tests/__init__.py index e69de29b..b53ef881 100644 --- a/src/pyelliptic/tests/__init__.py +++ b/src/pyelliptic/tests/__init__.py @@ -0,0 +1,9 @@ +import sys + +if getattr(sys, 'frozen', None): + from test_arithmetic import TestArithmetic + from test_blindsig import TestBlindSig + from test_ecc import TestECC + from test_openssl import TestOpenSSL + + __all__ = ["TestArithmetic", "TestBlindSig", "TestECC", "TestOpenSSL"] diff --git a/src/pyelliptic/tests/samples.py b/src/pyelliptic/tests/samples.py new file mode 100644 index 00000000..0348d3f0 --- /dev/null +++ b/src/pyelliptic/tests/samples.py @@ -0,0 +1,111 @@ +"""Testing samples""" + +from binascii import unhexlify + + +# These keys are from addresses test script +sample_pubsigningkey = ( + b'044a367f049ec16cb6b6118eb734a9962d10b8db59c890cd08f210c43ff08bdf09d' + b'16f502ca26cd0713f38988a1237f1fc8fa07b15653c996dc4013af6d15505ce') +sample_pubencryptionkey = ( + b'044597d59177fc1d89555d38915f581b5ff2286b39d022ca0283d2bdd5c36be5d3c' + b'e7b9b97792327851a562752e4b79475d1f51f5a71352482b241227f45ed36a9') +sample_privsigningkey = \ + b'93d0b61371a54b53df143b954035d612f8efa8a3ed1cf842c2186bfd8f876665' +sample_privencryptionkey = \ + b'4b0b73a54e19b059dc274ab69df095fe699f43b17397bca26fdf40f4d7400a3a' + +# [chan] bitmessage +sample_privsigningkey_wif = \ + b'5K42shDERM5g7Kbi3JT5vsAWpXMqRhWZpX835M2pdSoqQQpJMYm' +sample_privencryptionkey_wif = \ + b'5HwugVWm31gnxtoYcvcK7oywH2ezYTh6Y4tzRxsndAeMi6NHqpA' +sample_wif_privsigningkey = \ + b'a2e8b841a531c1c558ee0680c396789c7a2ea3ac4795ae3f000caf9fe367d144' +sample_wif_privencryptionkey = \ + b'114ec0e2dca24a826a0eed064b0405b0ac148abc3b1d52729697f4d7b873fdc6' + +sample_factor = \ + 66858749573256452658262553961707680376751171096153613379801854825275240965733 +# G * sample_factor +sample_point = ( + 33567437183004486938355437500683826356288335339807546987348409590129959362313, + 94730058721143827257669456336351159718085716196507891067256111928318063085006 +) + +sample_deterministic_addr3 = b'2DBPTgeSawWYZceFD69AbDT5q4iUWtj1ZN' +sample_deterministic_addr4 = b'2cWzSnwjJ7yRP3nLEWUV5LisTZyREWSzUK' +sample_daddr3_512 = 18875720106589866286514488037355423395410802084648916523381 +sample_daddr4_512 = 25152821841976547050350277460563089811513157529113201589004 + + +# pubkey K +sample_pubkey = unhexlify( + '0409d4e5c0ab3d25fe' + '048c64c9da1a242c' + '7f19417e9517cd26' + '6950d72c75571358' + '5c6178e97fe092fc' + '897c9a1f1720d577' + '0ae8eaad2fa8fcbd' + '08e9324a5dde1857' +) + +sample_iv = unhexlify( + 'bddb7c2829b08038' + '753084a2f3991681' +) + +# Private key r +sample_ephem_privkey = unhexlify( + '5be6facd941b76e9' + 'd3ead03029fbdb6b' + '6e0809293f7fb197' + 'd0c51f84e96b8ba4' +) +# Public key R +sample_ephem_pubkey = unhexlify( + '040293213dcf1388b6' + '1c2ae5cf80fee6ff' + 'ffc049a2f9fe7365' + 'fe3867813ca81292' + 'df94686c6afb565a' + 'c6149b153d61b3b2' + '87ee2c7f997c1423' + '8796c12b43a3865a' +) + +# First 32 bytes of H called key_e +sample_enkey = unhexlify( + '1705438282678671' + '05263d4828efff82' + 'd9d59cbf08743b69' + '6bcc5d69fa1897b4' +) + +# Last 32 bytes of H called key_m +sample_mackey = unhexlify( + 'f83f1e9cc5d6b844' + '8d39dc6a9d5f5b7f' + '460e4a78e9286ee8' + 'd91ce1660a53eacd' +) + +# No padding of input! +sample_data = b'The quick brown fox jumps over the lazy dog.' + +sample_ciphertext = unhexlify( + '64203d5b24688e25' + '47bba345fa139a5a' + '1d962220d4d48a0c' + 'f3b1572c0d95b616' + '43a6f9a0d75af7ea' + 'cc1bd957147bf723' +) + +sample_mac = unhexlify( + 'f2526d61b4851fb2' + '3409863826fd2061' + '65edc021368c7946' + '571cead69046e619' +) diff --git a/src/pyelliptic/tests/test_arithmetic.py b/src/pyelliptic/tests/test_arithmetic.py index 7b5c59b1..1d1aecaf 100644 --- a/src/pyelliptic/tests/test_arithmetic.py +++ b/src/pyelliptic/tests/test_arithmetic.py @@ -10,25 +10,13 @@ try: except ImportError: from pybitmessage.pyelliptic import arithmetic - -# These keys are from addresses test script -sample_pubsigningkey = ( - b'044a367f049ec16cb6b6118eb734a9962d10b8db59c890cd08f210c43ff08bdf09d' - b'16f502ca26cd0713f38988a1237f1fc8fa07b15653c996dc4013af6d15505ce') -sample_pubencryptionkey = ( - b'044597d59177fc1d89555d38915f581b5ff2286b39d022ca0283d2bdd5c36be5d3c' - b'e7b9b97792327851a562752e4b79475d1f51f5a71352482b241227f45ed36a9') -sample_privsigningkey = \ - b'93d0b61371a54b53df143b954035d612f8efa8a3ed1cf842c2186bfd8f876665' -sample_privencryptionkey = \ - b'4b0b73a54e19b059dc274ab69df095fe699f43b17397bca26fdf40f4d7400a3a' - -sample_factor = \ - 66858749573256452658262553961707680376751171096153613379801854825275240965733 -# G * sample_factor -sample_point = ( - 33567437183004486938355437500683826356288335339807546987348409590129959362313, - 94730058721143827257669456336351159718085716196507891067256111928318063085006 +from .samples import ( + sample_deterministic_addr3, sample_deterministic_addr4, + sample_daddr3_512, sample_daddr4_512, + sample_factor, sample_point, sample_pubsigningkey, sample_pubencryptionkey, + sample_privsigningkey, sample_privencryptionkey, + sample_privsigningkey_wif, sample_privencryptionkey_wif, + sample_wif_privsigningkey, sample_wif_privencryptionkey ) @@ -40,6 +28,34 @@ class TestArithmetic(unittest.TestCase): sample_point, arithmetic.base10_multiply(arithmetic.G, sample_factor)) + def test_base58(self): + """Test encoding/decoding base58 using arithmetic functions""" + self.assertEqual( + arithmetic.decode(arithmetic.changebase( + sample_deterministic_addr4, 58, 256), 256), sample_daddr4_512) + self.assertEqual( + arithmetic.decode(arithmetic.changebase( + sample_deterministic_addr3, 58, 256), 256), sample_daddr3_512) + self.assertEqual( + arithmetic.changebase( + arithmetic.encode(sample_daddr4_512, 256), 256, 58), + sample_deterministic_addr4) + self.assertEqual( + arithmetic.changebase( + arithmetic.encode(sample_daddr3_512, 256), 256, 58), + sample_deterministic_addr3) + + def test_wif(self): + """Decode WIFs of [chan] bitmessage and check the keys""" + self.assertEqual( + sample_wif_privsigningkey, + arithmetic.changebase(arithmetic.changebase( + sample_privsigningkey_wif, 58, 256)[1:-4], 256, 16)) + self.assertEqual( + sample_wif_privencryptionkey, + arithmetic.changebase(arithmetic.changebase( + sample_privencryptionkey_wif, 58, 256)[1:-4], 256, 16)) + def test_decode(self): """Decode sample privsigningkey from hex to int and compare to factor""" self.assertEqual( diff --git a/src/pyelliptic/tests/test_blindsig.py b/src/pyelliptic/tests/test_blindsig.py index 9ed72081..8c4b2b9d 100644 --- a/src/pyelliptic/tests/test_blindsig.py +++ b/src/pyelliptic/tests/test_blindsig.py @@ -58,7 +58,7 @@ class TestBlindSig(unittest.TestCase): x = OpenSSL.BN_new() y = OpenSSL.BN_new() OpenSSL.EC_POINT_get_affine_coordinates( - obj.group, obj.Q, x, y, 0) + obj.group, obj.Q, x, y, None) self.assertEqual(OpenSSL.BN_is_odd(y), OpenSSL.BN_is_odd_compatible(y)) @@ -85,7 +85,7 @@ class TestBlindSig(unittest.TestCase): self.assertEqual(OpenSSL.BN_cmp(y0, y1), 0) self.assertEqual(OpenSSL.BN_cmp(x0, x1), 0) self.assertEqual(OpenSSL.EC_POINT_cmp(obj.group, randompoint, - secondpoint, 0), 0) + secondpoint, None), 0) finally: OpenSSL.BN_free(x0) OpenSSL.BN_free(x1) diff --git a/src/pyelliptic/tests/test_ecc.py b/src/pyelliptic/tests/test_ecc.py new file mode 100644 index 00000000..e87d1c21 --- /dev/null +++ b/src/pyelliptic/tests/test_ecc.py @@ -0,0 +1,102 @@ +"""Tests for ECC object""" + +import os +import unittest +from hashlib import sha512 + +try: + import pyelliptic +except ImportError: + from pybitmessage import pyelliptic + +from .samples import ( + sample_pubkey, sample_iv, sample_ephem_privkey, sample_ephem_pubkey, + sample_enkey, sample_mackey, sample_data, sample_ciphertext, sample_mac) + + +sample_pubkey_x = sample_ephem_pubkey[1:-32] +sample_pubkey_y = sample_ephem_pubkey[-32:] +sample_pubkey_bin = ( + b'\x02\xca\x00\x20' + sample_pubkey_x + b'\x00\x20' + sample_pubkey_y) +sample_privkey_bin = b'\x02\xca\x00\x20' + sample_ephem_privkey + + +class TestECC(unittest.TestCase): + """The test case for ECC""" + + def test_random_keys(self): + """A dummy test for random keys in ECC object""" + eccobj = pyelliptic.ECC(curve='secp256k1') + self.assertTrue(len(eccobj.privkey) <= 32) + pubkey = eccobj.get_pubkey() + self.assertEqual(pubkey[:4], b'\x02\xca\x00\x20') + + def test_short_keys(self): + """Check formatting of the keys with leading zeroes""" + # pylint: disable=protected-access + def sample_key(_): + """Fake ECC keypair""" + return os.urandom(32), os.urandom(31), os.urandom(30) + + try: + gen_orig = pyelliptic.ECC._generate + pyelliptic.ECC._generate = sample_key + eccobj = pyelliptic.ECC(curve='secp256k1') + pubkey = eccobj.get_pubkey() + self.assertEqual(pubkey[:4], b'\x02\xca\x00\x20') + self.assertEqual(pubkey[36:38], b'\x00\x20') + self.assertEqual(len(pubkey[38:]), 32) + finally: + pyelliptic.ECC._generate = gen_orig + + def test_decode_keys(self): + """Check keys decoding""" + # pylint: disable=protected-access + curve_secp256k1 = pyelliptic.OpenSSL.get_curve('secp256k1') + curve, raw_privkey, _ = pyelliptic.ECC._decode_privkey( + sample_privkey_bin) + self.assertEqual(curve, curve_secp256k1) + self.assertEqual( + pyelliptic.OpenSSL.get_curve_by_id(curve), 'secp256k1') + self.assertEqual(sample_ephem_privkey, raw_privkey) + + curve, pubkey_x, pubkey_y, _ = pyelliptic.ECC._decode_pubkey( + sample_pubkey_bin) + self.assertEqual(curve, curve_secp256k1) + self.assertEqual(sample_pubkey_x, pubkey_x) + self.assertEqual(sample_pubkey_y, pubkey_y) + + def test_encode_keys(self): + """Check keys encoding""" + cryptor = pyelliptic.ECC( + pubkey_x=sample_pubkey_x, + pubkey_y=sample_pubkey_y, + raw_privkey=sample_ephem_privkey, curve='secp256k1') + self.assertEqual(cryptor.get_privkey(), sample_privkey_bin) + self.assertEqual(cryptor.get_pubkey(), sample_pubkey_bin) + + def test_encryption_parts(self): + """Check results of the encryption steps against samples in the Spec""" + ephem = pyelliptic.ECC( + pubkey_x=sample_pubkey_x, + pubkey_y=sample_pubkey_y, + raw_privkey=sample_ephem_privkey, curve='secp256k1') + key = sha512(ephem.raw_get_ecdh_key( + sample_pubkey[1:-32], sample_pubkey[-32:])).digest() + self.assertEqual(sample_enkey, key[:32]) + self.assertEqual(sample_mackey, key[32:]) + + ctx = pyelliptic.Cipher(sample_enkey, sample_iv, 1) + self.assertEqual(ctx.ciphering(sample_data), sample_ciphertext) + self.assertEqual( + sample_mac, + pyelliptic.hash.hmac_sha256( + sample_mackey, + sample_iv + sample_pubkey_bin + sample_ciphertext)) + + def test_decryption(self): + """Check decription of a message by random cryptor""" + random_recipient = pyelliptic.ECC(curve='secp256k1') + payload = pyelliptic.ECC.encrypt( + sample_data, random_recipient.get_pubkey()) + self.assertEqual(random_recipient.decrypt(payload), sample_data) diff --git a/src/qidenticon.py b/src/qidenticon.py index 5294da86..049c0b2a 100644 --- a/src/qidenticon.py +++ b/src/qidenticon.py @@ -135,6 +135,7 @@ class IdenticonRendererBase(object): if penwidth > 0: pen_color = QtGui.QColor(255, 255, 255) pen = QtGui.QPen(pen_color) + pen.setBrush(QtCore.Qt.SolidPattern) pen.setWidth(penwidth) painter = QtGui.QPainter() diff --git a/src/shared.py b/src/shared.py index 4a654932..b85ddb20 100644 --- a/src/shared.py +++ b/src/shared.py @@ -11,7 +11,7 @@ from __future__ import division import hashlib import os import stat -import subprocess +import subprocess # nosec B404 import sys from binascii import hexlify @@ -19,12 +19,10 @@ from binascii import hexlify import highlevelcrypto import state from addresses import decodeAddress, encodeVarint -from bmconfigparser import BMConfigParser +from bmconfigparser import config from debug import logger from helper_sql import sqlQuery -from pyelliptic import arithmetic - myECCryptorObjects = {} MyECSubscriptionCryptorObjects = {} @@ -76,35 +74,6 @@ def isAddressInMyAddressBookSubscriptionsListOrWhitelist(address): return False -def decodeWalletImportFormat(WIFstring): - # pylint: disable=inconsistent-return-statements - """ - Convert private key from base58 that's used in the config file to - 8-bit binary string - """ - fullString = arithmetic.changebase(WIFstring, 58, 256) - privkey = fullString[:-4] - if fullString[-4:] != \ - hashlib.sha256(hashlib.sha256(privkey).digest()).digest()[:4]: - logger.critical( - 'Major problem! When trying to decode one of your' - ' private keys, the checksum failed. Here are the first' - ' 6 characters of the PRIVATE key: %s', - str(WIFstring)[:6] - ) - os._exit(0) # pylint: disable=protected-access - # return "" - elif privkey[0] == '\x80': # checksum passed - return privkey[1:] - - logger.critical( - 'Major problem! When trying to decode one of your private keys,' - ' the checksum passed but the key doesn\'t begin with hex 80.' - ' Here is the PRIVATE key: %s', WIFstring - ) - os._exit(0) # pylint: disable=protected-access - - def reloadMyAddressHashes(): """Reload keys for user's addresses from the config file""" logger.debug('reloading keys from keys.dat file') @@ -116,31 +85,40 @@ def reloadMyAddressHashes(): keyfileSecure = checkSensitiveFilePermissions(os.path.join( state.appdata, 'keys.dat')) hasEnabledKeys = False - for addressInKeysFile in BMConfigParser().addresses(): - isEnabled = BMConfigParser().getboolean(addressInKeysFile, 'enabled') - if isEnabled: - hasEnabledKeys = True - # status - addressVersionNumber, streamNumber, hashobj = decodeAddress(addressInKeysFile)[1:] - if addressVersionNumber in (2, 3, 4): - # Returns a simple 32 bytes of information encoded - # in 64 Hex characters, or null if there was an error. - privEncryptionKey = hexlify(decodeWalletImportFormat( - BMConfigParser().get(addressInKeysFile, 'privencryptionkey'))) - # It is 32 bytes encoded as 64 hex characters - if len(privEncryptionKey) == 64: - myECCryptorObjects[hashobj] = \ - highlevelcrypto.makeCryptor(privEncryptionKey) - myAddressesByHash[hashobj] = addressInKeysFile - tag = hashlib.sha512(hashlib.sha512( - encodeVarint(addressVersionNumber) - + encodeVarint(streamNumber) + hashobj).digest()).digest()[32:] - myAddressesByTag[tag] = addressInKeysFile - else: - logger.error( - 'Error in reloadMyAddressHashes: Can\'t handle' - ' address versions other than 2, 3, or 4.' - ) + for addressInKeysFile in config.addresses(): + if not config.getboolean(addressInKeysFile, 'enabled'): + continue + + hasEnabledKeys = True + + addressVersionNumber, streamNumber, hashobj = decodeAddress( + addressInKeysFile)[1:] + if addressVersionNumber not in (2, 3, 4): + logger.error( + 'Error in reloadMyAddressHashes: Can\'t handle' + ' address versions other than 2, 3, or 4.') + continue + + # Returns a simple 32 bytes of information encoded in 64 Hex characters + try: + privEncryptionKey = hexlify( + highlevelcrypto.decodeWalletImportFormat(config.get( + addressInKeysFile, 'privencryptionkey').encode() + )) + except ValueError: + logger.error( + 'Error in reloadMyAddressHashes: failed to decode' + ' one of the private keys for address %s', addressInKeysFile) + continue + # It is 32 bytes encoded as 64 hex characters + if len(privEncryptionKey) == 64: + myECCryptorObjects[hashobj] = \ + highlevelcrypto.makeCryptor(privEncryptionKey) + myAddressesByHash[hashobj] = addressInKeysFile + tag = highlevelcrypto.double_sha512( + encodeVarint(addressVersionNumber) + + encodeVarint(streamNumber) + hashobj)[32:] + myAddressesByTag[tag] = addressInKeysFile if not keyfileSecure: fixSensitiveFilePermissions(os.path.join( @@ -174,10 +152,10 @@ def reloadBroadcastSendersForWhichImWatching(): MyECSubscriptionCryptorObjects[hashobj] = \ highlevelcrypto.makeCryptor(hexlify(privEncryptionKey)) else: - doubleHashOfAddressData = hashlib.sha512(hashlib.sha512( + doubleHashOfAddressData = highlevelcrypto.double_sha512( encodeVarint(addressVersionNumber) + encodeVarint(streamNumber) + hashobj - ).digest()).digest() + ) tag = doubleHashOfAddressData[32:] privEncryptionKey = doubleHashOfAddressData[:32] MyECSubscriptionCryptorObjects[tag] = \ @@ -212,10 +190,9 @@ def checkSensitiveFilePermissions(filename): # Skip known problems for non-Win32 filesystems # without POSIX permissions. fstype = subprocess.check_output( - 'stat -f -c "%%T" %s' % (filename), - shell=True, + ['/usr/bin/stat', '-f', '-c', '%T', filename], stderr=subprocess.STDOUT - ) + ) # nosec B603 if 'fuseblk' in fstype: logger.info( 'Skipping file permissions check for %s.' diff --git a/src/shutdown.py b/src/shutdown.py index 3e2b8ca8..441d655e 100644 --- a/src/shutdown.py +++ b/src/shutdown.py @@ -9,7 +9,6 @@ from six.moves import queue import state from debug import logger from helper_sql import sqlQuery, sqlStoredProcedure -from inventory import Inventory from network import StoppableThread from network.knownnodes import saveKnownNodes from queues import ( @@ -41,7 +40,7 @@ def doCleanShutdown(): 'updateStatusBar', 'Flushing inventory in memory out to disk.' ' This should normally only take a second...')) - Inventory().flush() + state.Inventory.flush() # Verify that the objectProcessor has finished exiting. It should have # incremented the shutdown variable from 1 to 2. This must finish before diff --git a/src/singleinstance.py b/src/singleinstance.py index 660dcf54..cff9d794 100644 --- a/src/singleinstance.py +++ b/src/singleinstance.py @@ -93,7 +93,7 @@ class singleinstance(object): os.close(self.fd) else: fcntl.lockf(self.fp, fcntl.LOCK_UN) - except Exception: + except (IOError, OSError): pass return @@ -107,5 +107,5 @@ class singleinstance(object): fcntl.lockf(self.fp, fcntl.LOCK_UN) if os.path.isfile(self.lockfile): os.unlink(self.lockfile) - except Exception: + except (IOError, OSError): pass diff --git a/src/sql/config_setting_ver_2.sql b/src/sql/config_setting_ver_2.sql new file mode 100644 index 00000000..087d297a --- /dev/null +++ b/src/sql/config_setting_ver_2.sql @@ -0,0 +1 @@ +ALTER TABLE pubkeys ADD usedpersonally text DEFAULT 'no'; diff --git a/src/sql/config_setting_ver_3.sql b/src/sql/config_setting_ver_3.sql new file mode 100644 index 00000000..4bdcccc8 --- /dev/null +++ b/src/sql/config_setting_ver_3.sql @@ -0,0 +1,5 @@ +ALTER TABLE inbox ADD encodingtype int DEFAULT '2'; + +ALTER TABLE inbox ADD read bool DEFAULT '1'; + +ALTER TABLE sent ADD encodingtype int DEFAULT '2'; diff --git a/src/sql/init_version_10.sql b/src/sql/init_version_10.sql new file mode 100644 index 00000000..8bd8b0b3 --- /dev/null +++ b/src/sql/init_version_10.sql @@ -0,0 +1,15 @@ +-- -- +-- -- Update the address colunm to unique in addressbook table +-- -- + +ALTER TABLE addressbook RENAME TO old_addressbook; + +CREATE TABLE `addressbook` ( + `label` text , + `address` text , + UNIQUE(address) ON CONFLICT IGNORE +) ; + +INSERT INTO addressbook SELECT label, address FROM old_addressbook; + +DROP TABLE old_addressbook; diff --git a/src/sql/init_version_2.sql b/src/sql/init_version_2.sql new file mode 100644 index 00000000..ea42df4c --- /dev/null +++ b/src/sql/init_version_2.sql @@ -0,0 +1,29 @@ +-- +-- Let's get rid of the first20bytesofencryptedmessage field in the inventory table. +-- + +CREATE TEMP TABLE `inventory_backup` ( + `hash` blob , + `objecttype` text , + `streamnumber` int , + `payload` blob , + `receivedtime` int , + UNIQUE(hash) ON CONFLICT REPLACE +) ; + +INSERT INTO `inventory_backup` SELECT hash, objecttype, streamnumber, payload, receivedtime FROM inventory; + +DROP TABLE inventory; + +CREATE TABLE `inventory` ( + `hash` blob , + `objecttype` text , + `streamnumber` int , + `payload` blob , + `receivedtime` int , + UNIQUE(hash) ON CONFLICT REPLACE +) ; + +INSERT INTO inventory SELECT hash, objecttype, streamnumber, payload, receivedtime FROM inventory_backup; + +DROP TABLE inventory_backup; diff --git a/src/sql/init_version_3.sql b/src/sql/init_version_3.sql new file mode 100644 index 00000000..9de784a5 --- /dev/null +++ b/src/sql/init_version_3.sql @@ -0,0 +1,5 @@ +-- +-- Add a new column to the inventory table to store tags. +-- + +ALTER TABLE inventory ADD tag blob DEFAULT ''; diff --git a/src/sql/init_version_4.sql b/src/sql/init_version_4.sql new file mode 100644 index 00000000..d2fd393d --- /dev/null +++ b/src/sql/init_version_4.sql @@ -0,0 +1,17 @@ + -- + -- Add a new column to the pubkeys table to store the address version. + -- We're going to trash all of our pubkeys and let them be redownloaded. + -- + +DROP TABLE pubkeys; + +CREATE TABLE `pubkeys` ( + `hash` blob , + `addressversion` int , + `transmitdata` blob , + `time` int , + `usedpersonally` text , + UNIQUE(hash, addressversion) ON CONFLICT REPLACE +) ; + +DELETE FROM inventory WHERE objecttype = 'pubkey'; diff --git a/src/sql/init_version_5.sql b/src/sql/init_version_5.sql new file mode 100644 index 00000000..a13fa8cf --- /dev/null +++ b/src/sql/init_version_5.sql @@ -0,0 +1,12 @@ + -- + -- Add a new table: objectprocessorqueue with which to hold objects + -- that have yet to be processed if the user shuts down Bitmessage. + -- + +DROP TABLE knownnodes; + +CREATE TABLE `objectprocessorqueue` ( + `objecttype` text, + `data` blob, + UNIQUE(objecttype, data) ON CONFLICT REPLACE +) ; diff --git a/src/sql/init_version_6.sql b/src/sql/init_version_6.sql new file mode 100644 index 00000000..b9a03669 --- /dev/null +++ b/src/sql/init_version_6.sql @@ -0,0 +1,25 @@ +-- +-- changes related to protocol v3 +-- In table inventory and objectprocessorqueue, objecttype is now +-- an integer (it was a human-friendly string previously) +-- + +DROP TABLE inventory; + +CREATE TABLE `inventory` ( + `hash` blob, + `objecttype` int, + `streamnumber` int, + `payload` blob, + `expirestime` integer, + `tag` blob, + UNIQUE(hash) ON CONFLICT REPLACE +) ; + +DROP TABLE objectprocessorqueue; + +CREATE TABLE `objectprocessorqueue` ( + `objecttype` int, + `data` blob, + UNIQUE(objecttype, data) ON CONFLICT REPLACE +) ; diff --git a/src/sql/init_version_7.sql b/src/sql/init_version_7.sql new file mode 100644 index 00000000..a2f6f6e3 --- /dev/null +++ b/src/sql/init_version_7.sql @@ -0,0 +1,11 @@ +-- +-- The format of data stored in the pubkeys table has changed. Let's +-- clear it, and the pubkeys from inventory, so that they'll +-- be re-downloaded. +-- + +DELETE FROM inventory WHERE objecttype = 1; + +DELETE FROM pubkeys; + +UPDATE sent SET status='msgqueued' WHERE status='doingmsgpow' or status='badkey'; diff --git a/src/sql/init_version_8.sql b/src/sql/init_version_8.sql new file mode 100644 index 00000000..0c1813d3 --- /dev/null +++ b/src/sql/init_version_8.sql @@ -0,0 +1,7 @@ +-- +-- Add a new column to the inbox table to store the hash of +-- the message signature. We'll use this as temporary message UUID +-- in order to detect duplicates. +-- + +ALTER TABLE inbox ADD sighash blob DEFAULT ''; diff --git a/src/sql/init_version_9.sql b/src/sql/init_version_9.sql new file mode 100644 index 00000000..bc8296b9 --- /dev/null +++ b/src/sql/init_version_9.sql @@ -0,0 +1,74 @@ +CREATE TEMPORARY TABLE `sent_backup` ( + `msgid` blob, + `toaddress` text, + `toripe` blob, + `fromaddress` text, + `subject` text, + `message` text, + `ackdata` blob, + `lastactiontime` integer, + `status` text, + `retrynumber` integer, + `folder` text, + `encodingtype` int +) ; + +INSERT INTO sent_backup SELECT msgid, toaddress, toripe, fromaddress, subject, message, ackdata, lastactiontime, status, 0, folder, encodingtype FROM sent; + +DROP TABLE sent; + +CREATE TABLE `sent` ( + `msgid` blob, + `toaddress` text, + `toripe` blob, + `fromaddress` text, + `subject` text, + `message` text, + `ackdata` blob, + `senttime` integer, + `lastactiontime` integer, + `sleeptill` int, + `status` text, + `retrynumber` integer, + `folder` text, + `encodingtype` int, + `ttl` int +) ; + +INSERT INTO sent SELECT msgid, toaddress, toripe, fromaddress, subject, message, ackdata, lastactiontime, lastactiontime, 0, status, 0, folder, encodingtype, 216000 FROM sent_backup; + +DROP TABLE sent_backup; + +ALTER TABLE pubkeys ADD address text DEFAULT '' ; + +-- +-- replica for loop to update hashed address +-- + +UPDATE pubkeys SET address=(enaddr(pubkeys.addressversion, 1, hash)); + +CREATE TEMPORARY TABLE `pubkeys_backup` ( + `address` text, + `addressversion` int, + `transmitdata` blob, + `time` int, + `usedpersonally` text, + UNIQUE(address) ON CONFLICT REPLACE +) ; + +INSERT INTO pubkeys_backup SELECT address, addressversion, transmitdata, `time`, usedpersonally FROM pubkeys; + +DROP TABLE pubkeys; + +CREATE TABLE `pubkeys` ( + `address` text, + `addressversion` int, + `transmitdata` blob, + `time` int, + `usedpersonally` text, + UNIQUE(address) ON CONFLICT REPLACE +) ; + +INSERT INTO pubkeys SELECT address, addressversion, transmitdata, `time`, usedpersonally FROM pubkeys_backup; + +DROP TABLE pubkeys_backup; diff --git a/src/sql/initialize_schema.sql b/src/sql/initialize_schema.sql new file mode 100644 index 00000000..8413aa0a --- /dev/null +++ b/src/sql/initialize_schema.sql @@ -0,0 +1,100 @@ +CREATE TABLE `inbox` ( + `msgid` blob, + `toaddress` text, + `fromaddress` text, + `subject` text, + `received` text, + `message` text, + `folder` text, + `encodingtype` int, + `read` bool, + `sighash` blob, +UNIQUE(msgid) ON CONFLICT REPLACE +) ; + +CREATE TABLE `sent` ( + `msgid` blob, + `toaddress` text, + `toripe` blob, + `fromaddress` text, + `subject` text, + `message` text, + `ackdata` blob, + `senttime` integer, + `lastactiontime` integer, + `sleeptill` integer, + `status` text, + `retrynumber` integer, + `folder` text, + `encodingtype` int, + `ttl` int +) ; + + +CREATE TABLE `subscriptions` ( + `label` text, + `address` text, + `enabled` bool +) ; + + +CREATE TABLE `addressbook` ( + `label` text, + `address` text, + UNIQUE(address) ON CONFLICT IGNORE +) ; + + + CREATE TABLE `blacklist` ( + `label` text, + `address` text, + `enabled` bool + ) ; + + + CREATE TABLE `whitelist` ( + `label` text, + `address` text, + `enabled` bool + ) ; + + +CREATE TABLE `pubkeys` ( + `address` text, + `addressversion` int, + `transmitdata` blob, + `time` int, + `usedpersonally` text, + UNIQUE(address) ON CONFLICT REPLACE +) ; + + +CREATE TABLE `inventory` ( + `hash` blob, + `objecttype` int, + `streamnumber` int, + `payload` blob, + `expirestime` integer, + `tag` blob, + UNIQUE(hash) ON CONFLICT REPLACE +) ; + + +INSERT INTO subscriptions VALUES ('Bitmessage new releases/announcements', 'BM-GtovgYdgs7qXPkoYaRgrLFuFKz1SFpsw', 1); + + + CREATE TABLE `settings` ( + `key` blob, + `value` blob, + UNIQUE(key) ON CONFLICT REPLACE + ) ; + +INSERT INTO settings VALUES('version','11'); + +INSERT INTO settings VALUES('lastvacuumtime', CAST(strftime('%s', 'now') AS STR) ); + +CREATE TABLE `objectprocessorqueue` ( + `objecttype` int, + `data` blob, + UNIQUE(objecttype, data) ON CONFLICT REPLACE +) ; diff --git a/src/sql/upg_sc_if_old_ver_1.sql b/src/sql/upg_sc_if_old_ver_1.sql new file mode 100644 index 00000000..18a5ecfc --- /dev/null +++ b/src/sql/upg_sc_if_old_ver_1.sql @@ -0,0 +1,30 @@ +CREATE TEMPORARY TABLE `pubkeys_backup` ( + `hash` blob, + `transmitdata` blob, + `time` int, + `usedpersonally` text, + UNIQUE(hash) ON CONFLICT REPLACE +) ; + +INSERT INTO `pubkeys_backup` SELECT hash, transmitdata, `time`, usedpersonally FROM `pubkeys`; + +DROP TABLE `pubkeys` + +CREATE TABLE `pubkeys` ( + `hash` blob, + `transmitdata` blob, + `time` int, + `usedpersonally` text, + UNIQUE(hash) ON CONFLICT REPLACE +) ; + + +INSERT INTO `pubkeys` SELECT hash, transmitdata, `time`, usedpersonally FROM `pubkeys_backup`; + +DROP TABLE `pubkeys_backup`; + +DELETE FROM inventory WHERE objecttype = 'pubkey'; + +DELETE FROM subscriptions WHERE address='BM-BbkPSZbzPwpVcYZpU4yHwf9ZPEapN5Zx' + +INSERT INTO subscriptions VALUES('Bitmessage new releases/announcements','BM-GtovgYdgs7qXPkoYaRgrLFuFKz1SFpsw',1) diff --git a/src/sql/upg_sc_if_old_ver_2.sql b/src/sql/upg_sc_if_old_ver_2.sql new file mode 100644 index 00000000..1fde0098 --- /dev/null +++ b/src/sql/upg_sc_if_old_ver_2.sql @@ -0,0 +1,7 @@ +UPDATE `sent` SET status='doingmsgpow' WHERE status='doingpow'; + +UPDATE `sent` SET status='msgsent' WHERE status='sentmessage'; + +UPDATE `sent` SET status='doingpubkeypow' WHERE status='findingpubkey'; + +UPDATE `sent` SET status='broadcastqueued' WHERE status='broadcastpending'; diff --git a/src/state.py b/src/state.py index be81992d..e530425d 100644 --- a/src/state.py +++ b/src/state.py @@ -3,7 +3,6 @@ Global runtime variables. """ neededPubkeys = {} -streamsInWhichIAmParticipating = [] extPort = None """For UPnP""" @@ -11,7 +10,7 @@ extPort = None socksIP = None """for Tor hidden service""" -appdata = '' +appdata = "" """holds the location of the application data storage directory""" shutdown = 0 @@ -40,16 +39,15 @@ maximumNumberOfHalfOpenConnections = 0 maximumLengthOfTimeToBotherResendingMessages = 0 -invThread = None -addrThread = None -downloadThread = None -uploadThread = None - ownAddresses = {} discoveredPeers = {} -dandelion = 0 +dandelion_enabled = 0 + +kivy = False + +kivyapp = None testmode = False @@ -60,7 +58,7 @@ numberOfMessagesProcessed = 0 numberOfBroadcastsProcessed = 0 numberOfPubkeysProcessed = 0 -statusIconColor = 'red' +statusIconColor = "red" """ GUI status icon color .. note:: bad style, refactor it @@ -70,3 +68,34 @@ ackdataForWhichImWatching = {} thisapp = None """Singleton instance""" + +backend_py3_compatible = False + + +class Placeholder(object): # pylint:disable=too-few-public-methods + """Placeholder class""" + + def __init__(self, className): + self.className = className + + def __getattr__(self, name): + self._raise() + + def __setitem__(self, key, value): + self._raise() + + def __getitem__(self, key): + self._raise() + + def _raise(self): + raise NotImplementedError( + "Probabaly you forgot to initialize state variable for {}".format( + self.className + ) + ) + + +Inventory = Placeholder("Inventory") + + +Dandelion = Placeholder("Dandelion") diff --git a/src/storage/filesystem.py b/src/storage/filesystem.py index 150e8d9e..e756a820 100644 --- a/src/storage/filesystem.py +++ b/src/storage/filesystem.py @@ -2,21 +2,19 @@ Module for using filesystem (directory with files) for inventory storage """ import logging -import string +import os import time from binascii import hexlify, unhexlify -from os import listdir, makedirs, path, remove, rmdir from threading import RLock from paths import lookupAppdataFolder -from storage import InventoryItem, InventoryStorage +from .storage import InventoryItem, InventoryStorage logger = logging.getLogger('default') class FilesystemInventory(InventoryStorage): """Filesystem for inventory storage""" - # pylint: disable=too-many-ancestors, abstract-method topDir = "inventory" objectDir = "objects" metadataFilename = "metadata" @@ -24,15 +22,15 @@ class FilesystemInventory(InventoryStorage): def __init__(self): super(FilesystemInventory, self).__init__() - self.baseDir = path.join( + self.baseDir = os.path.join( lookupAppdataFolder(), FilesystemInventory.topDir) - for createDir in [self.baseDir, path.join(self.baseDir, "objects")]: - if path.exists(createDir): - if not path.isdir(createDir): + for createDir in [self.baseDir, os.path.join(self.baseDir, "objects")]: + if os.path.exists(createDir): + if not os.path.isdir(createDir): raise IOError( "%s exists but it's not a directory" % createDir) else: - makedirs(createDir) + os.makedirs(createDir) # Guarantees that two receiveDataThreads # don't receive and process the same message # concurrently (probably sent by a malicious individual) @@ -46,6 +44,9 @@ class FilesystemInventory(InventoryStorage): return True return False + def __delitem__(self, hash_): + raise NotImplementedError + def __getitem__(self, hashval): for streamDict in self._inventory.values(): try: @@ -66,18 +67,18 @@ class FilesystemInventory(InventoryStorage): with self.lock: value = InventoryItem(*value) try: - makedirs(path.join( + os.makedirs(os.path.join( self.baseDir, FilesystemInventory.objectDir, - hexlify(hashval))) + hexlify(hashval).decode())) except OSError: pass try: with open( - path.join( + os.path.join( self.baseDir, FilesystemInventory.objectDir, - hexlify(hashval), + hexlify(hashval).decode(), FilesystemInventory.metadataFilename, ), "w", @@ -86,15 +87,15 @@ class FilesystemInventory(InventoryStorage): value.type, value.stream, value.expires, - hexlify(value.tag))) + hexlify(value.tag).decode())) with open( - path.join( + os.path.join( self.baseDir, FilesystemInventory.objectDir, - hexlify(hashval), + hexlify(hashval).decode(), FilesystemInventory.dataFilename, ), - "w", + "wb", ) as f: f.write(value.payload) except IOError: @@ -114,28 +115,28 @@ class FilesystemInventory(InventoryStorage): pass with self.lock: try: - remove( - path.join( + os.remove( + os.path.join( self.baseDir, FilesystemInventory.objectDir, - hexlify(hashval), + hexlify(hashval).decode(), FilesystemInventory.metadataFilename)) except IOError: pass try: - remove( - path.join( + os.remove( + os.path.join( self.baseDir, FilesystemInventory.objectDir, - hexlify(hashval), + hexlify(hashval).decode(), FilesystemInventory.dataFilename)) except IOError: pass try: - rmdir(path.join( + os.rmdir(os.path.join( self.baseDir, FilesystemInventory.objectDir, - hexlify(hashval))) + hexlify(hashval).decode())) except IOError: pass @@ -168,8 +169,6 @@ class FilesystemInventory(InventoryStorage): logger.debug( 'error loading %s', hexlify(hashId), exc_info=True) self._inventory = newInventory -# for i, v in self._inventory.items(): -# print "loaded stream: %s, %i items" % (i, len(v)) def stream_list(self): """Return list of streams""" @@ -177,17 +176,17 @@ class FilesystemInventory(InventoryStorage): def object_list(self): """Return inventory vectors (hashes) from a directory""" - return [unhexlify(x) for x in listdir(path.join( + return [unhexlify(x) for x in os.listdir(os.path.join( self.baseDir, FilesystemInventory.objectDir))] def getData(self, hashId): """Get object data""" try: with open( - path.join( + os.path.join( self.baseDir, FilesystemInventory.objectDir, - hexlify(hashId), + hexlify(hashId).decode(), FilesystemInventory.dataFilename, ), "r", @@ -200,16 +199,16 @@ class FilesystemInventory(InventoryStorage): """Get object metadata""" try: with open( - path.join( + os.path.join( self.baseDir, FilesystemInventory.objectDir, - hexlify(hashId), + hexlify(hashId).decode(), FilesystemInventory.metadataFilename, ), "r", ) as f: - objectType, streamNumber, expiresTime, tag = string.split( - f.read(), ",", 4)[:4] + objectType, streamNumber, expiresTime, tag = f.read().split( + ",", 4)[:4] return [ int(objectType), int(streamNumber), @@ -246,10 +245,10 @@ class FilesystemInventory(InventoryStorage): def unexpired_hashes_by_stream(self, stream): """Return unexpired hashes in the inventory for a particular stream""" - t = int(time.time()) try: - return [x for x, value in self._inventory[stream].items() - if value.expires > t] + return [ + x for x, value in self._inventory[stream].items() + if value.expires > int(time.time())] except KeyError: return [] @@ -259,7 +258,7 @@ class FilesystemInventory(InventoryStorage): def clean(self): """Clean out old items from the inventory""" - minTime = int(time.time()) - (60 * 60 * 30) + minTime = int(time.time()) - 60 * 60 * 30 deletes = [] for streamDict in self._inventory.values(): for hashId, item in streamDict.items(): diff --git a/src/storage/sqlite.py b/src/storage/sqlite.py index 50a2034e..eb5df098 100644 --- a/src/storage/sqlite.py +++ b/src/storage/sqlite.py @@ -6,10 +6,10 @@ import time from threading import RLock from helper_sql import SqlBulkExecute, sqlExecute, sqlQuery -from storage import InventoryItem, InventoryStorage +from .storage import InventoryItem, InventoryStorage -class SqliteInventory(InventoryStorage): # pylint: disable=too-many-ancestors +class SqliteInventory(InventoryStorage): """Inventory using SQLite""" def __init__(self): super(SqliteInventory, self).__init__() diff --git a/src/storage/storage.py b/src/storage/storage.py index 0391979a..9b33eef7 100644 --- a/src/storage/storage.py +++ b/src/storage/storage.py @@ -1,73 +1,47 @@ """ Storing inventory items """ -import collections -InventoryItem = collections.namedtuple( - 'InventoryItem', 'type stream payload expires tag') +from abc import abstractmethod +from collections import namedtuple +try: + from collections import MutableMapping # pylint: disable=deprecated-class +except ImportError: + from collections.abc import MutableMapping -class Storage(object): # pylint: disable=too-few-public-methods - """Base class for storing inventory - (extendable for other items to store)""" - pass +InventoryItem = namedtuple('InventoryItem', 'type stream payload expires tag') -class InventoryStorage(Storage, collections.MutableMapping): - """Module used for inventory storage""" +class InventoryStorage(MutableMapping): + """ + Base class for storing inventory + (extendable for other items to store) + """ - def __init__(self): # pylint: disable=super-init-not-called + def __init__(self): self.numberOfInventoryLookupsPerformed = 0 - def __contains__(self, _): - raise NotImplementedError - - def __getitem__(self, _): - raise NotImplementedError - - def __setitem__(self, _, value): - raise NotImplementedError - - def __delitem__(self, _): - raise NotImplementedError - - def __iter__(self): - raise NotImplementedError - - def __len__(self): - raise NotImplementedError + @abstractmethod + def __contains__(self, item): + pass + @abstractmethod def by_type_and_tag(self, objectType, tag): """Return objects filtered by object type and tag""" - raise NotImplementedError + pass + @abstractmethod def unexpired_hashes_by_stream(self, stream): """Return unexpired inventory vectors filtered by stream""" - raise NotImplementedError + pass + @abstractmethod def flush(self): """Flush cache""" - raise NotImplementedError + pass + @abstractmethod def clean(self): """Free memory / perform garbage collection""" - raise NotImplementedError - - -class MailboxStorage(Storage, collections.MutableMapping): - """Method for storing mails""" - - def __delitem__(self, key): - raise NotImplementedError - - def __getitem__(self, key): - raise NotImplementedError - - def __iter__(self): - raise NotImplementedError - - def __len__(self): - raise NotImplementedError - - def __setitem__(self, key, value): - raise NotImplementedError + pass diff --git a/src/tests/__init__.py b/src/tests/__init__.py index e69de29b..1e5fb7b6 100644 --- a/src/tests/__init__.py +++ b/src/tests/__init__.py @@ -0,0 +1,13 @@ +import sys + +if getattr(sys, 'frozen', None): + from test_addresses import TestAddresses + from test_crypto import TestHighlevelcrypto + from test_l10n import TestL10n + from test_packets import TestSerialize + from test_protocol import TestProtocol + + __all__ = [ + "TestAddresses", "TestHighlevelcrypto", "TestL10n", + "TestProtocol", "TestSerialize" + ] diff --git a/src/tests/core.py b/src/tests/core.py index 52cea234..f1a11a06 100644 --- a/src/tests/core.py +++ b/src/tests/core.py @@ -21,12 +21,12 @@ import state import helper_sent import helper_addressbook -from bmconfigparser import BMConfigParser +from bmconfigparser import config from helper_msgcoding import MsgEncode, MsgDecode from helper_sql import sqlQuery from network import asyncore_pollchoose as asyncore, knownnodes from network.bmproto import BMProto -from network.connectionpool import BMConnectionPool +import network.connectionpool as connectionpool from network.node import Node, Peer from network.tcp import Socks4aBMConnection, Socks5BMConnection, TCPConnection from queues import excQueue @@ -40,6 +40,7 @@ try: except (OSError, socket.error): tor_port_free = False +frozen = getattr(sys, 'frozen', None) knownnodes_file = os.path.join(state.appdata, 'knownnodes.dat') @@ -66,9 +67,9 @@ class TestCore(unittest.TestCase): def tearDown(self): """Reset possible unexpected settings after test""" knownnodes.addKnownNode(1, Peer('127.0.0.1', 8444), is_self=True) - BMConfigParser().remove_option('bitmessagesettings', 'dontconnect') - BMConfigParser().remove_option('bitmessagesettings', 'onionservicesonly') - BMConfigParser().set('bitmessagesettings', 'socksproxytype', 'none') + config.remove_option('bitmessagesettings', 'dontconnect') + config.remove_option('bitmessagesettings', 'onionservicesonly') + config.set('bitmessagesettings', 'socksproxytype', 'none') def test_msgcoding(self): """test encoding and decoding (originally from helper_msgcoding)""" @@ -110,7 +111,7 @@ class TestCore(unittest.TestCase): @unittest.skip('Bad environment for asyncore.loop') def test_tcpconnection(self): """initial fill script from network.tcp""" - BMConfigParser().set('bitmessagesettings', 'dontconnect', 'true') + config.set('bitmessagesettings', 'dontconnect', 'true') try: for peer in (Peer("127.0.0.1", 8448),): direct = TCPConnection(peer) @@ -164,7 +165,7 @@ class TestCore(unittest.TestCase): """test knownnodes starvation leading to IndexError in Asyncore""" self._outdate_knownnodes() # time.sleep(303) # singleCleaner wakes up every 5 min - knownnodes.cleanupKnownNodes() + knownnodes.cleanupKnownNodes(connectionpool.pool) self.assertTrue(knownnodes.knownNodes[1]) while True: try: @@ -175,10 +176,10 @@ class TestCore(unittest.TestCase): self.fail("IndexError because of empty knownNodes!") def _initiate_bootstrap(self): - BMConfigParser().set('bitmessagesettings', 'dontconnect', 'true') + config.set('bitmessagesettings', 'dontconnect', 'true') self._wipe_knownnodes() knownnodes.addKnownNode(1, Peer('127.0.0.1', 8444), is_self=True) - knownnodes.cleanupKnownNodes() + knownnodes.cleanupKnownNodes(connectionpool.pool) time.sleep(5) def _check_connection(self, full=False): @@ -188,8 +189,8 @@ class TestCore(unittest.TestCase): fail otherwise. """ _started = time.time() - BMConfigParser().remove_option('bitmessagesettings', 'dontconnect') - proxy_type = BMConfigParser().safeGet( + config.remove_option('bitmessagesettings', 'dontconnect') + proxy_type = config.safeGet( 'bitmessagesettings', 'socksproxytype') if proxy_type == 'SOCKS5': connection_base = Socks5BMConnection @@ -201,15 +202,15 @@ class TestCore(unittest.TestCase): while c > 0: time.sleep(1) c -= 2 - for peer, con in BMConnectionPool().outboundConnections.iteritems(): + for peer, con in connectionpool.pool.outboundConnections.iteritems(): if ( peer.host.startswith('bootstrap') or peer.host == 'quzwelsuziwqgpt2.onion' ): if c < 60: self.fail( - 'Still connected to bootstrap node %s after % seconds' % - (peer, time.time() - _started)) + 'Still connected to bootstrap node %s after %.2f' + ' seconds' % (peer, time.time() - _started)) c += 1 break else: @@ -219,7 +220,7 @@ class TestCore(unittest.TestCase): continue return self.fail( - 'Failed to connect during %s sec' % (time.time() - _started)) + 'Failed to connect during %.2f sec' % (time.time() - _started)) def _check_knownnodes(self): for stream in knownnodes.knownNodes.itervalues(): @@ -231,7 +232,7 @@ class TestCore(unittest.TestCase): def test_dontconnect(self): """all connections are closed 5 seconds after setting dontconnect""" self._initiate_bootstrap() - self.assertEqual(len(BMConnectionPool().connections()), 0) + self.assertEqual(len(connectionpool.pool.connections()), 0) def test_connection(self): """test connection to bootstrap servers""" @@ -250,7 +251,7 @@ class TestCore(unittest.TestCase): def test_bootstrap(self): """test bootstrapping""" - BMConfigParser().set('bitmessagesettings', 'socksproxytype', 'none') + config.set('bitmessagesettings', 'socksproxytype', 'none') self._initiate_bootstrap() self._check_connection() self._check_knownnodes() @@ -262,7 +263,7 @@ class TestCore(unittest.TestCase): @unittest.skipIf(tor_port_free, 'no running tor detected') def test_bootstrap_tor(self): """test bootstrapping with tor""" - BMConfigParser().set('bitmessagesettings', 'socksproxytype', 'SOCKS5') + config.set('bitmessagesettings', 'socksproxytype', 'SOCKS5') self._initiate_bootstrap() self._check_connection() self._check_knownnodes() @@ -272,8 +273,8 @@ class TestCore(unittest.TestCase): """ensure bitmessage doesn't try to connect to non-onion nodes if onionservicesonly set, wait at least 3 onion nodes """ - BMConfigParser().set('bitmessagesettings', 'socksproxytype', 'SOCKS5') - BMConfigParser().set('bitmessagesettings', 'onionservicesonly', 'true') + config.set('bitmessagesettings', 'socksproxytype', 'SOCKS5') + config.set('bitmessagesettings', 'onionservicesonly', 'true') self._load_knownnodes(knownnodes_file + '.bak') if len([ node for node in knownnodes.knownNodes[1] @@ -282,11 +283,11 @@ class TestCore(unittest.TestCase): with knownnodes.knownNodesLock: for f in ('a', 'b', 'c', 'd'): knownnodes.addKnownNode(1, Peer(f * 16 + '.onion', 8444)) - BMConfigParser().remove_option('bitmessagesettings', 'dontconnect') + config.remove_option('bitmessagesettings', 'dontconnect') tried_hosts = set() for _ in range(360): time.sleep(1) - for peer in BMConnectionPool().outboundConnections: + for peer in connectionpool.pool.outboundConnections: if peer.host.endswith('.onion'): tried_hosts.add(peer.host) else: @@ -298,36 +299,17 @@ class TestCore(unittest.TestCase): return self.fail('Failed to find at least 3 nodes to connect within 360 sec') + @unittest.skipIf(frozen, 'skip fragile test') def test_udp(self): """check default udp setting and presence of Announcer thread""" self.assertTrue( - BMConfigParser().safeGetBoolean('bitmessagesettings', 'udp')) + config.safeGetBoolean('bitmessagesettings', 'udp')) for thread in threading.enumerate(): if thread.name == 'Announcer': # find Announcer thread break else: return self.fail('No Announcer thread found') - for _ in range(20): # wait for UDP socket - for sock in BMConnectionPool().udpSockets.values(): - thread.announceSelf() - break - else: - time.sleep(1) - continue - break - else: - self.fail('UDP socket is not started') - - for _ in range(20): - if state.discoveredPeers: - peer = state.discoveredPeers.keys()[0] - self.assertEqual(peer.port, 8444) - break - time.sleep(1) - else: - self.fail('No self in discovered peers') - @staticmethod def _decode_msg(data, pattern): proto = BMProto() @@ -342,7 +324,7 @@ class TestCore(unittest.TestCase): decoded = self._decode_msg(msg, "IQQiiQlsLv") peer, _, ua, streams = self._decode_msg(msg, "IQQiiQlsLv")[4:] self.assertEqual( - peer, Node(11 if state.dandelion else 3, '127.0.0.1', 8444)) + peer, Node(11 if state.dandelion_enabled else 3, '127.0.0.1', 8444)) self.assertEqual(ua, '/PyBitmessage:' + softwareVersion + '/') self.assertEqual(streams, [1]) # with multiple streams @@ -370,6 +352,7 @@ class TestCore(unittest.TestCase): '''select typeof(msgid) from sent where ackdata=?''', result) self.assertEqual(column_type[0][0] if column_type else '', 'text') + @unittest.skipIf(frozen, 'not packed test_pattern into the bundle') def test_old_knownnodes_pickle(self): """Testing old (v0.6.2) version knownnodes.dat file""" try: @@ -389,14 +372,17 @@ class TestCore(unittest.TestCase): def test_add_same_address_twice_in_addressbook(self): """checking same address is added twice in addressbook""" - self.assertTrue(helper_addressbook.insert(label='test1', address=self.addr)) - self.assertFalse(helper_addressbook.insert(label='test1', address=self.addr)) + self.assertTrue( + helper_addressbook.insert(label='test1', address=self.addr)) + self.assertFalse( + helper_addressbook.insert(label='test1', address=self.addr)) self.delete_address_from_addressbook(self.addr) def test_is_address_present_in_addressbook(self): """checking is address added in addressbook or not""" helper_addressbook.insert(label='test1', address=self.addr) - queryreturn = sqlQuery('''select count(*) from addressbook where address=?''', self.addr) + queryreturn = sqlQuery( + 'select count(*) from addressbook where address=?', self.addr) self.assertEqual(queryreturn[0][0], 1) self.delete_address_from_addressbook(self.addr) @@ -404,17 +390,30 @@ class TestCore(unittest.TestCase): """Testing same case sensitive address store in addressbook""" address1 = 'BM-2cVWtdUzPwF7UNGDrZftWuHWiJ6xxBpiSP' address2 = 'BM-2CvwTDuZpWf7ungdRzFTwUhwIj6XXbPIsp' - self.assertTrue(helper_addressbook.insert(label='test1', address=address1)) - self.assertTrue(helper_addressbook.insert(label='test2', address=address2)) + self.assertTrue( + helper_addressbook.insert(label='test1', address=address1)) + self.assertTrue( + helper_addressbook.insert(label='test2', address=address2)) self.delete_address_from_addressbook(address1) self.delete_address_from_addressbook(address2) def run(): - """Starts all tests defined in this module""" + """Starts all tests intended for core run""" loader = unittest.defaultTestLoader loader.sortTestMethodsUsing = None suite = loader.loadTestsFromTestCase(TestCore) + if frozen: + try: + from pybitmessage import tests + suite.addTests(loader.loadTestsFromModule(tests)) + except ImportError: + pass + try: + from pyelliptic import tests + suite.addTests(loader.loadTestsFromModule(tests)) + except ImportError: + pass try: import bitmessageqt.tests from xvfbwrapper import Xvfb diff --git a/src/tests/mockbm/__init__.py b/src/tests/mockbm/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/tests/mockbm/bitmessagemock.py b/src/tests/mockbm/bitmessagemock.py new file mode 100644 index 00000000..d9ee857b --- /dev/null +++ b/src/tests/mockbm/bitmessagemock.py @@ -0,0 +1,32 @@ +# pylint: disable=no-name-in-module, import-error + +""" +Bitmessage mock +""" + +from pybitmessage.class_addressGenerator import addressGenerator +from pybitmessage.inventory import Inventory +from pybitmessage.mpybit import NavigateApp +from pybitmessage import state + + +class MockMain(object): # pylint: disable=too-few-public-methods + """Mock main function""" + + def __init__(self): + """Start main application""" + addressGeneratorThread = addressGenerator() + # close the main program even if there are threads left + addressGeneratorThread.start() + Inventory() + state.kivyapp = NavigateApp() + state.kivyapp.run() + + +def main(): + """Triggers main module""" + MockMain() + + +if __name__ == "__main__": + main() diff --git a/src/tests/mockbm/images b/src/tests/mockbm/images new file mode 120000 index 00000000..847b03ed --- /dev/null +++ b/src/tests/mockbm/images @@ -0,0 +1 @@ +../../images/ \ No newline at end of file diff --git a/src/tests/mockbm/kivy_main.py b/src/tests/mockbm/kivy_main.py new file mode 100644 index 00000000..79bb413e --- /dev/null +++ b/src/tests/mockbm/kivy_main.py @@ -0,0 +1,8 @@ +"""Mock kivy app with mock threads.""" +from pybitmessage import state + +if __name__ == '__main__': + state.kivy = True + print("Kivy Loading......") + from bitmessagemock import main + main() diff --git a/src/tests/mockbm/pybitmessage/__init__.py b/src/tests/mockbm/pybitmessage/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/tests/mockbm/pybitmessage/addresses.py b/src/tests/mockbm/pybitmessage/addresses.py new file mode 120000 index 00000000..88fcee82 --- /dev/null +++ b/src/tests/mockbm/pybitmessage/addresses.py @@ -0,0 +1 @@ +../../../addresses.py \ No newline at end of file diff --git a/src/tests/mockbm/pybitmessage/bmconfigparser.py b/src/tests/mockbm/pybitmessage/bmconfigparser.py new file mode 120000 index 00000000..da05040e --- /dev/null +++ b/src/tests/mockbm/pybitmessage/bmconfigparser.py @@ -0,0 +1 @@ +../../../bmconfigparser.py \ No newline at end of file diff --git a/src/tests/mockbm/pybitmessage/class_addressGenerator.py b/src/tests/mockbm/pybitmessage/class_addressGenerator.py new file mode 100644 index 00000000..34258bbc --- /dev/null +++ b/src/tests/mockbm/pybitmessage/class_addressGenerator.py @@ -0,0 +1,80 @@ +""" +A thread for creating addresses +""" + +from six.moves import queue + +from pybitmessage import state +from pybitmessage import queues + +from pybitmessage.bmconfigparser import BMConfigParser + +from pybitmessage.network.threads import StoppableThread + + +fake_addresses = { + 'BM-2cUgQGcTLWAkC6dNsv2Bc8XB3Y1GEesVLV': { + 'privsigningkey': '5KWXwYq1oJMzghUSJaJoWPn8VdeBbhDN8zFot1cBd6ezKKReqBd', + 'privencryptionkey': '5JaeFJs8iPcQT3N8676r3gHKvJ5mTWXy1VLhGCEDqRs4vpvpxV8' + }, + 'BM-2cUd2dm8MVMokruMTcGhhteTpyRZCAMhnA': { + 'privsigningkey': '5JnJ79nkcwjo4Aj7iG8sFMkzYoQqWfpUjTcitTuFJZ1YKHZz98J', + 'privencryptionkey': '5JXgNzTRouFLqSRFJvuHMDHCYPBvTeMPBiHt4Jeb6smNjhUNTYq' + }, + 'BM-2cWyvL54WytfALrJHZqbsDHca5QkrtByAW': { + 'privsigningkey': '5KVE4gLmcfYVicLdgyD4GmnbBTFSnY7Yj2UCuytQqgBBsfwDhpi', + 'privencryptionkey': '5JTw48CGm5CP8fyJUJQMq8HQANQMHDHp2ETUe1dgm6EFpT1egD7' + }, + 'BM-2cTE65PK9Y4AQEkCZbazV86pcQACocnRXd': { + 'privsigningkey': '5KCuyReHx9MB4m5hhEyCWcLEXqc8rxhD1T2VWk8CicPFc8B6LaZ', + 'privencryptionkey': '5KBRpwXdX3n2tP7f583SbFgfzgs6Jemx7qfYqhdH7B1Vhe2jqY6' + }, + 'BM-2cX5z1EgmJ87f2oKAwXdv4VQtEVwr2V3BG': { + 'privsigningkey': '5K5UK7qED7F1uWCVsehudQrszLyMZxFVnP6vN2VDQAjtn5qnyRK', + 'privencryptionkey': '5J5coocoJBX6hy5DFTWKtyEgPmADpSwfQTazMpU7QPeART6oMAu' + } +} + + +class addressGenerator(StoppableThread): + """A thread for creating fake addresses""" + name = "addressGenerator" + address_list = list(fake_addresses.keys()) + + def stopThread(self): + """"To stop address generator thread""" + try: + queues.addressGeneratorQueue.put(("stopThread", "data")) + except queue.Full: + self.logger.warning('addressGeneratorQueue is Full') + super(addressGenerator, self).stopThread() # pylint: disable=super-with-arguments + + def run(self): + """ + Process the requests for addresses generation + from `.queues.addressGeneratorQueue` + """ + while state.shutdown == 0: + queueValue = queues.addressGeneratorQueue.get() + try: + address = self.address_list.pop(0) + except IndexError: + self.logger.error( + 'Program error: you can only create 5 fake addresses') + continue + + if len(queueValue) >= 3: + label = queueValue[3] + else: + label = '' + + BMConfigParser().add_section(address) + BMConfigParser().set(address, 'label', label) + BMConfigParser().set(address, 'enabled', 'true') + BMConfigParser().set( + address, 'privsigningkey', fake_addresses[address]['privsigningkey']) + BMConfigParser().set( + address, 'privencryptionkey', fake_addresses[address]['privencryptionkey']) + BMConfigParser().save() + + queues.addressGeneratorQueue.task_done() diff --git a/src/tests/mockbm/pybitmessage/inventory.py b/src/tests/mockbm/pybitmessage/inventory.py new file mode 100644 index 00000000..6173c3cd --- /dev/null +++ b/src/tests/mockbm/pybitmessage/inventory.py @@ -0,0 +1,15 @@ +"""The Inventory singleton""" + +# TODO make this dynamic, and watch out for frozen, like with messagetypes +from pybitmessage.singleton import Singleton + + +# pylint: disable=old-style-class,too-few-public-methods +@Singleton +class Inventory(): + """ + Inventory singleton class which uses storage backends + to manage the inventory. + """ + def __init__(self): + self.numberOfInventoryLookupsPerformed = 0 diff --git a/src/tests/mockbm/pybitmessage/network/__init__.py b/src/tests/mockbm/pybitmessage/network/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/tests/mockbm/pybitmessage/network/threads.py b/src/tests/mockbm/pybitmessage/network/threads.py new file mode 120000 index 00000000..c95b4c36 --- /dev/null +++ b/src/tests/mockbm/pybitmessage/network/threads.py @@ -0,0 +1 @@ +../../../../network/threads.py \ No newline at end of file diff --git a/src/tests/mockbm/pybitmessage/queues.py b/src/tests/mockbm/pybitmessage/queues.py new file mode 120000 index 00000000..8c556015 --- /dev/null +++ b/src/tests/mockbm/pybitmessage/queues.py @@ -0,0 +1 @@ +../../../queues.py \ No newline at end of file diff --git a/src/tests/mockbm/pybitmessage/shutdown.py b/src/tests/mockbm/pybitmessage/shutdown.py new file mode 100644 index 00000000..08c885d8 --- /dev/null +++ b/src/tests/mockbm/pybitmessage/shutdown.py @@ -0,0 +1,11 @@ +# pylint: disable=invalid-name +"""shutdown function""" + +from pybitmessage import state + + +def doCleanShutdown(): + """ + Used to exit Kivy UI. + """ + state.shutdown = 1 diff --git a/src/tests/mockbm/pybitmessage/singleton.py b/src/tests/mockbm/pybitmessage/singleton.py new file mode 120000 index 00000000..5e112567 --- /dev/null +++ b/src/tests/mockbm/pybitmessage/singleton.py @@ -0,0 +1 @@ +../../../singleton.py \ No newline at end of file diff --git a/src/tests/mockbm/pybitmessage/state.py b/src/tests/mockbm/pybitmessage/state.py new file mode 120000 index 00000000..117203f5 --- /dev/null +++ b/src/tests/mockbm/pybitmessage/state.py @@ -0,0 +1 @@ +../../../state.py \ No newline at end of file diff --git a/src/tests/partial.py b/src/tests/partial.py new file mode 100644 index 00000000..870f6626 --- /dev/null +++ b/src/tests/partial.py @@ -0,0 +1,41 @@ +"""A test case for partial run class definition""" + +import os +import sys +import unittest + +from pybitmessage import pathmagic + + +class TestPartialRun(unittest.TestCase): + """ + A base class for test cases running some parts of the app, + e.g. separate threads or packages. + """ + + @classmethod + def setUpClass(cls): + # pylint: disable=import-outside-toplevel,unused-import + cls.dirs = (os.path.abspath(os.curdir), pathmagic.setup()) + + import bmconfigparser + import state + + from debug import logger # noqa:F401 pylint: disable=unused-variable + if sys.hexversion >= 0x3000000: + # pylint: disable=no-name-in-module,relative-import + from mockbm import network as network_mock + import network + network.stats = network_mock.stats + + state.shutdown = 0 + cls.state = state + bmconfigparser.config = cls.config = bmconfigparser.BMConfigParser() + cls.config.read() + + @classmethod + def tearDownClass(cls): + cls.state.shutdown = 1 + # deactivate pathmagic + os.chdir(cls.dirs[0]) + sys.path.remove(cls.dirs[1]) diff --git a/src/tests/samples.py b/src/tests/samples.py index e1a3e676..dd862318 100644 --- a/src/tests/samples.py +++ b/src/tests/samples.py @@ -2,16 +2,30 @@ from binascii import unhexlify +# hello, page 1 of the Specification +sample_hash_data = b'hello' +sample_double_sha512 = unhexlify( + '0592a10584ffabf96539f3d780d776828c67da1ab5b169e9e8aed838aaecc9ed36d49ff14' + '23c55f019e050c66c6324f53588be88894fef4dcffdb74b98e2b200') -magic = 0xE9BEB4D9 +sample_bm160 = unhexlify('79a324faeebcbf9849f310545ed531556882487e') + +# 500 identical peers: +# 1626611891, 1, 1, 127.0.0.1, 8444 +sample_addr_data = unhexlify( + 'fd01f4' + ( + '0000000060f420b30000000' + '1000000000000000100000000000000000000ffff7f00000120fc' + ) * 500 +) # These keys are from addresses test script sample_pubsigningkey = unhexlify( - '044a367f049ec16cb6b6118eb734a9962d10b8db59c890cd08f210c43ff08bdf09d' - '16f502ca26cd0713f38988a1237f1fc8fa07b15653c996dc4013af6d15505ce') + '044a367f049ec16cb6b6118eb734a9962d10b8db59c890cd08f210c43ff08bdf09' + 'd16f502ca26cd0713f38988a1237f1fc8fa07b15653c996dc4013af6d15505ce') sample_pubencryptionkey = unhexlify( - '044597d59177fc1d89555d38915f581b5ff2286b39d022ca0283d2bdd5c36be5d3c' - 'e7b9b97792327851a562752e4b79475d1f51f5a71352482b241227f45ed36a9') + '044597d59177fc1d89555d38915f581b5ff2286b39d022ca0283d2bdd5c36be5d3' + 'ce7b9b97792327851a562752e4b79475d1f51f5a71352482b241227f45ed36a9') sample_privsigningkey = \ b'93d0b61371a54b53df143b954035d612f8efa8a3ed1cf842c2186bfd8f876665' sample_privencryptionkey = \ @@ -21,24 +35,59 @@ sample_ripe = b'003cd097eb7f35c87b5dc8b4538c22cb55312a9f' # stream: 1, version: 2 sample_address = 'BM-onkVu1KKL2UaUss5Upg9vXmqd3esTmV79' -sample_factor = 66858749573256452658262553961707680376751171096153613379801854825275240965733 +sample_factor = \ + 66858749573256452658262553961707680376751171096153613379801854825275240965733 # G * sample_factor sample_point = ( 33567437183004486938355437500683826356288335339807546987348409590129959362313, 94730058721143827257669456336351159718085716196507891067256111928318063085006 ) -sample_seed = 'TIGER, tiger, burning bright. In the forests of the night' -# Deterministic addresses with stream 1 and versions 3, 4 +sample_seed = b'TIGER, tiger, burning bright. In the forests of the night' +# RIPE hash on step 22 with signing key nonce 42 sample_deterministic_ripe = b'00cfb69416ae76f68a81c459de4e13460c7d17eb' +# Deterministic addresses with stream 1 and versions 3, 4 sample_deterministic_addr3 = 'BM-2DBPTgeSawWYZceFD69AbDT5q4iUWtj1ZN' sample_deterministic_addr4 = 'BM-2cWzSnwjJ7yRP3nLEWUV5LisTZyREWSzUK' sample_daddr3_512 = 18875720106589866286514488037355423395410802084648916523381 sample_daddr4_512 = 25152821841976547050350277460563089811513157529113201589004 -sample_statusbar_msg = "new status bar message" -sample_inbox_msg_ids = ['27e644765a3e4b2e973ee7ccf958ea20', '51fc5531-3989-4d69-bbb5-68d64b756f5b', - '2c975c515f8b414db5eea60ba57ba455', 'bc1f2d8a-681c-4cc0-9a12-6067c7e1ac24'] -# second address in sample_test_subscription_address is for the announcement broadcast -sample_test_subscription_address = ['BM-2cWQLCBGorT9pUGkYSuGGVr9LzE4mRnQaq', 'BM-GtovgYdgs7qXPkoYaRgrLFuFKz1SFpsw'] +sample_statusbar_msg = 'new status bar message' +sample_inbox_msg_ids = [ + '27e644765a3e4b2e973ee7ccf958ea20', '51fc5531-3989-4d69-bbb5-68d64b756f5b', + '2c975c515f8b414db5eea60ba57ba455', 'bc1f2d8a-681c-4cc0-9a12-6067c7e1ac24'] +# second address in sample_subscription_addresses is +# for the announcement broadcast, but is it matter? +sample_subscription_addresses = [ + 'BM-2cWQLCBGorT9pUGkYSuGGVr9LzE4mRnQaq', + 'BM-GtovgYdgs7qXPkoYaRgrLFuFKz1SFpsw'] sample_subscription_name = 'test sub' + + +sample_object_expires = 1712271487 +# from minode import structure +# obj = structure.Object( +# b'\x00' * 8, sample_object_expires, 42, 1, 2, b'HELLO') +# .. do pow and obj.to_bytes() +sample_object_data = unhexlify( + '00000000001be7fc00000000660f307f0000002a010248454c4c4f') + +sample_msg = unhexlify( + '0592a10584ffabf96539f3d780d776828c67da1ab5b169e9e8aed838aaecc9ed36d49ff' + '1423c55f019e050c66c6324f53588be88894fef4dcffdb74b98e2b200') +sample_sig = unhexlify( + '304402202302475351db6b822de15d922e29397541f10d8a19780ba2ca4a920b1035f075' + '02205e5bba40d5f07a24c23a89ba5f01a3828371dfbb685dd5375fa1c29095fd232b') +sample_sig_sha1 = unhexlify( + '30460221008ad234687d1bdc259932e28ea6ee091b88b0900d8134902aa8c2fd7f016b96e' + 'd022100dafb94e28322c2fa88878f9dcbf0c2d33270466ab3bbffaec3dca0a2d1ef9354') + +# [chan] bitmessage +sample_wif_privsigningkey = unhexlify( + b'a2e8b841a531c1c558ee0680c396789c7a2ea3ac4795ae3f000caf9fe367d144') +sample_wif_privencryptionkey = unhexlify( + b'114ec0e2dca24a826a0eed064b0405b0ac148abc3b1d52729697f4d7b873fdc6') +sample_privsigningkey_wif = \ + b'5K42shDERM5g7Kbi3JT5vsAWpXMqRhWZpX835M2pdSoqQQpJMYm' +sample_privencryptionkey_wif = \ + b'5HwugVWm31gnxtoYcvcK7oywH2ezYTh6Y4tzRxsndAeMi6NHqpA' diff --git a/src/tests/sql/init_version_10.sql b/src/tests/sql/init_version_10.sql new file mode 100644 index 00000000..b1764e76 --- /dev/null +++ b/src/tests/sql/init_version_10.sql @@ -0,0 +1 @@ +INSERT INTO `addressbook` VALUES ('test', "BM-2cWzMnxjJ7yRP3nLEWUV5LisTZyREWSxYz"), ('testone', "BM-2cWzMnxjJ7yRP3nLEWUV5LisTZyREWSxYz"); diff --git a/src/tests/sql/init_version_2.sql b/src/tests/sql/init_version_2.sql new file mode 100644 index 00000000..133284ec --- /dev/null +++ b/src/tests/sql/init_version_2.sql @@ -0,0 +1 @@ +INSERT INTO `inventory` VALUES ('hash', 1, 1,1, 1,'test'); diff --git a/src/tests/sql/init_version_3.sql b/src/tests/sql/init_version_3.sql new file mode 100644 index 00000000..875d859d --- /dev/null +++ b/src/tests/sql/init_version_3.sql @@ -0,0 +1 @@ +INSERT INTO `settings` VALUES ('version','3'); diff --git a/src/tests/sql/init_version_4.sql b/src/tests/sql/init_version_4.sql new file mode 100644 index 00000000..ea3f1768 --- /dev/null +++ b/src/tests/sql/init_version_4.sql @@ -0,0 +1 @@ +INSERT INTO `pubkeys` VALUES ('hash', 1, 1, 1,'test'); diff --git a/src/tests/sql/init_version_5.sql b/src/tests/sql/init_version_5.sql new file mode 100644 index 00000000..b894c038 --- /dev/null +++ b/src/tests/sql/init_version_5.sql @@ -0,0 +1 @@ +INSERT INTO `objectprocessorqueue` VALUES ('hash', 1); diff --git a/src/tests/sql/init_version_6.sql b/src/tests/sql/init_version_6.sql new file mode 100644 index 00000000..7cd30571 --- /dev/null +++ b/src/tests/sql/init_version_6.sql @@ -0,0 +1 @@ +INSERT INTO `inventory` VALUES ('hash', 1, 1, 1,'test','test'); diff --git a/src/tests/sql/init_version_7.sql b/src/tests/sql/init_version_7.sql new file mode 100644 index 00000000..bd87f8d8 --- /dev/null +++ b/src/tests/sql/init_version_7.sql @@ -0,0 +1,3 @@ +INSERT INTO `sent` VALUES +(1,'BM-2cWzMnxjJ7yRP3nLEWUV5LisTZyREWSxYz',1,'BM-2cWzSnwjJ7yRP3nLEWUV5LisTZyREWSzUK','Test1 subject','message test 1','ackdata',1638176409,1638176409,1638176423,'msgqueued',1,'testfolder',1,2), +(2,'BM-2cWzMnxjJ7yRP3nLEWUV5LisTZyREWSxYz',1,'BM-2cWzSnwjJ7yRP3nLEWUV5LisTZyREWSzUK','Test2 subject','message test 2','ackdata',1638176423,1638176423,1638176423,'msgqueued',1,'testfolder',1,2); diff --git a/src/tests/sql/init_version_8.sql b/src/tests/sql/init_version_8.sql new file mode 100644 index 00000000..9d9b6f3a --- /dev/null +++ b/src/tests/sql/init_version_8.sql @@ -0,0 +1 @@ +INSERT INTO `inbox` VALUES (1, "poland", "malasia", "test", "yes", "test message", "folder", 1, 1, 1); diff --git a/src/tests/sql/init_version_9.sql b/src/tests/sql/init_version_9.sql new file mode 100644 index 00000000..764634d2 --- /dev/null +++ b/src/tests/sql/init_version_9.sql @@ -0,0 +1,2 @@ +INSERT INTO `sent` VALUES +(1,'BM-2cWzMnxjJ7yRP3nLEWUV5LisTZyREWSxYz',1,'BM-2cWzSnwjJ7yRP3nLEWUV5LisTZyREWSzUK','Test1 subject','message test 1','ackdata',1638176409,1638176409,1638176423,'msgqueued',1,'testfolder',1,2); diff --git a/src/tests/test_addresses.py b/src/tests/test_addresses.py index 8f9c283d..dd989562 100644 --- a/src/tests/test_addresses.py +++ b/src/tests/test_addresses.py @@ -2,12 +2,14 @@ import unittest from binascii import unhexlify -from pybitmessage import addresses +from pybitmessage import addresses, highlevelcrypto from .samples import ( sample_address, sample_daddr3_512, sample_daddr4_512, sample_deterministic_addr4, sample_deterministic_addr3, - sample_deterministic_ripe, sample_ripe) + sample_deterministic_ripe, sample_ripe, + sample_privsigningkey_wif, sample_privencryptionkey_wif, + sample_wif_privsigningkey, sample_wif_privencryptionkey) sample_addr3 = sample_deterministic_addr3.split('-')[1] sample_addr4 = sample_deterministic_addr4.split('-')[1] @@ -59,3 +61,26 @@ class TestAddresses(unittest.TestCase): sample_addr4, addresses.encodeBase58(sample_daddr4_512)) self.assertEqual( sample_addr3, addresses.encodeBase58(sample_daddr3_512)) + + def test_wif(self): + """Decode WIFs of [chan] bitmessage and check the keys""" + self.assertEqual( + sample_wif_privsigningkey, + highlevelcrypto.decodeWalletImportFormat( + sample_privsigningkey_wif)) + self.assertEqual( + sample_wif_privencryptionkey, + highlevelcrypto.decodeWalletImportFormat( + sample_privencryptionkey_wif)) + self.assertEqual( + sample_privsigningkey_wif, + highlevelcrypto.encodeWalletImportFormat( + sample_wif_privsigningkey)) + self.assertEqual( + sample_privencryptionkey_wif, + highlevelcrypto.encodeWalletImportFormat( + sample_wif_privencryptionkey)) + + with self.assertRaises(ValueError): + highlevelcrypto.decodeWalletImportFormat( + sample_privencryptionkey_wif[:-2]) diff --git a/src/tests/test_addressgenerator.py b/src/tests/test_addressgenerator.py new file mode 100644 index 00000000..e48daef9 --- /dev/null +++ b/src/tests/test_addressgenerator.py @@ -0,0 +1,101 @@ +"""Tests for AddressGenerator (with thread or not)""" + +from binascii import unhexlify + +from six.moves import queue + +from .partial import TestPartialRun +from .samples import ( + sample_seed, sample_deterministic_addr3, sample_deterministic_addr4, + sample_deterministic_ripe) + +TEST_LABEL = 'test' + + +class TestAddressGenerator(TestPartialRun): + """Test case for AddressGenerator thread""" + + @classmethod + def setUpClass(cls): + super(TestAddressGenerator, cls).setUpClass() + + import defaults + import queues + from class_addressGenerator import addressGenerator + + cls.state.enableGUI = False + + cls.command_queue = queues.addressGeneratorQueue + cls.return_queue = queues.apiAddressGeneratorReturnQueue + cls.worker_queue = queues.workerQueue + + cls.config.set( + 'bitmessagesettings', 'defaultnoncetrialsperbyte', + str(defaults.networkDefaultProofOfWorkNonceTrialsPerByte)) + cls.config.set( + 'bitmessagesettings', 'defaultpayloadlengthextrabytes', + str(defaults.networkDefaultPayloadLengthExtraBytes)) + + thread = addressGenerator() + thread.daemon = True + thread.start() + + def _execute(self, command, *args): + self.command_queue.put((command,) + args) + try: + return self.return_queue.get(timeout=30)[0] + except (IndexError, queue.Empty): + self.fail('Failed to execute command %s' % command) + + def test_deterministic(self): + """Test deterministic commands""" + self.command_queue.put(( + 'getDeterministicAddress', 3, 1, + TEST_LABEL, 1, sample_seed, False)) + self.assertEqual(sample_deterministic_addr3, self.return_queue.get()) + + self.assertEqual( + sample_deterministic_addr3, + self._execute( + 'createDeterministicAddresses', 3, 1, TEST_LABEL, 2, + sample_seed, False, 0, 0)) + + try: + self.assertEqual( + self.worker_queue.get(timeout=30), + ('sendOutOrStoreMyV3Pubkey', + unhexlify(sample_deterministic_ripe))) + + self.worker_queue.get(timeout=30) # get the next addr's task + except queue.Empty: + self.fail('No commands in the worker queue') + + self.assertEqual( + sample_deterministic_addr4, + self._execute('createChan', 4, 1, TEST_LABEL, sample_seed, True)) + try: + self.assertEqual( + self.worker_queue.get(), + ('sendOutOrStoreMyV4Pubkey', sample_deterministic_addr4)) + except queue.Empty: + self.fail('No commands in the worker queue') + self.assertEqual( + self.config.get(sample_deterministic_addr4, 'label'), TEST_LABEL) + self.assertTrue( + self.config.getboolean(sample_deterministic_addr4, 'chan')) + self.assertTrue( + self.config.getboolean(sample_deterministic_addr4, 'enabled')) + + def test_random(self): + """Test random address""" + self.command_queue.put(( + 'createRandomAddress', 4, 1, 'test_random', 1, '', False, 0, 0)) + addr = self.return_queue.get() + self.assertRegexpMatches(addr, r'^BM-') + self.assertRegexpMatches(addr[3:], r'[a-zA-Z1-9]+$') + self.assertLessEqual(len(addr[3:]), 40) + + self.assertEqual( + self.worker_queue.get(), ('sendOutOrStoreMyV4Pubkey', addr)) + self.assertEqual(self.config.get(addr, 'label'), 'test_random') + self.assertTrue(self.config.getboolean(addr, 'enabled')) diff --git a/src/tests/test_api.py b/src/tests/test_api.py index 835b4afb..2a4640fa 100644 --- a/src/tests/test_api.py +++ b/src/tests/test_api.py @@ -12,8 +12,10 @@ from six.moves import xmlrpc_client # nosec import psutil from .samples import ( - sample_seed, sample_deterministic_addr3, sample_deterministic_addr4, sample_statusbar_msg, - sample_inbox_msg_ids, sample_test_subscription_address, sample_subscription_name) + sample_deterministic_addr3, sample_deterministic_addr4, sample_seed, + sample_inbox_msg_ids, + sample_subscription_addresses, sample_subscription_name +) from .test_process import TestProcessProto @@ -86,18 +88,6 @@ class TestAPI(TestAPIProto): 'API Error 0020: Invalid method: test' ) - def test_statusbar_method(self): - """Test statusbar method""" - self.api.clearUISignalQueue() - self.assertEqual( - self.api.statusBar(sample_statusbar_msg), - 'null' - ) - self.assertEqual( - self.api.getStatusBar(), - sample_statusbar_msg - ) - def test_message_inbox(self): """Test message inbox methods""" self.assertEqual( @@ -114,43 +104,38 @@ class TestAPI(TestAPIProto): ) self.assertEqual( len(json.loads( - self.api.getInboxMessageById(hexlify(sample_inbox_msg_ids[2])))["inboxMessage"]), + self.api.getInboxMessageById( + hexlify(sample_inbox_msg_ids[2])))["inboxMessage"]), 1 ) self.assertEqual( len(json.loads( - self.api.getInboxMessagesByReceiver(sample_deterministic_addr4))["inboxMessages"]), + self.api.getInboxMessagesByReceiver( + sample_deterministic_addr4))["inboxMessages"]), 4 ) def test_message_trash(self): """Test message inbox methods""" - messages_before_delete = len(json.loads(self.api.getAllInboxMessageIds())["inboxMessageIds"]) - self.assertEqual( - self.api.trashMessage(hexlify(sample_inbox_msg_ids[0])), - 'Trashed message (assuming message existed).' - ) - self.assertEqual( - self.api.trashInboxMessage(hexlify(sample_inbox_msg_ids[1])), - 'Trashed inbox message (assuming message existed).' - ) - self.assertEqual( - len(json.loads(self.api.getAllInboxMessageIds())["inboxMessageIds"]), - messages_before_delete - 2 - ) - self.assertEqual( - self.api.undeleteMessage(hexlify(sample_inbox_msg_ids[0])), - 'Undeleted message' - ) - self.assertEqual( - self.api.undeleteMessage(hexlify(sample_inbox_msg_ids[1])), - 'Undeleted message' - ) - self.assertEqual( - len(json.loads(self.api.getAllInboxMessageIds())["inboxMessageIds"]), - messages_before_delete - ) + messages_before_delete = len( + json.loads(self.api.getAllInboxMessageIds())["inboxMessageIds"]) + for msgid in sample_inbox_msg_ids[:2]: + self.assertEqual( + self.api.trashMessage(hexlify(msgid)), + 'Trashed message (assuming message existed).' + ) + self.assertEqual(len( + json.loads(self.api.getAllInboxMessageIds())["inboxMessageIds"] + ), messages_before_delete - 2) + for msgid in sample_inbox_msg_ids[:2]: + self.assertEqual( + self.api.undeleteMessage(hexlify(msgid)), + 'Undeleted message' + ) + self.assertEqual(len( + json.loads(self.api.getAllInboxMessageIds())["inboxMessageIds"] + ), messages_before_delete) def test_clientstatus_consistency(self): """If networkStatus is notConnected networkConnections should be 0""" @@ -160,6 +145,12 @@ class TestAPI(TestAPIProto): else: self.assertGreater(status["networkConnections"], 0) + def test_listconnections_consistency(self): + """Checking the return of API command 'listConnections'""" + result = json.loads(self.api.listConnections()) + self.assertGreaterEqual(len(result["inbound"]), 0) + self.assertGreaterEqual(len(result["outbound"]), 0) + def test_list_addresses(self): """Checking the return of API command 'listAddresses'""" self.assertEqual( @@ -265,7 +256,9 @@ class TestAPI(TestAPIProto): """Testing the API commands related to subscriptions""" self.assertEqual( - self.api.addSubscription(sample_test_subscription_address[0], sample_subscription_name.encode('base64')), + self.api.addSubscription( + sample_subscription_addresses[0], + sample_subscription_name.encode('base64')), 'Added subscription.' ) @@ -273,18 +266,23 @@ class TestAPI(TestAPIProto): # check_address for sub in json.loads(self.api.listSubscriptions())['subscriptions']: # special address, added when sqlThread starts - if sub['address'] == sample_test_subscription_address[0]: + if sub['address'] == sample_subscription_addresses[0]: added_subscription = sub + self.assertEqual( + base64.decodestring(sub['label']), sample_subscription_name + ) + self.assertTrue(sub['enabled']) break self.assertEqual( - base64.decodestring(added_subscription['label']) if added_subscription['label'] else None, + base64.decodestring(added_subscription['label']) + if added_subscription['label'] else None, sample_subscription_name) self.assertTrue(added_subscription['enabled']) for s in json.loads(self.api.listSubscriptions())['subscriptions']: # special address, added when sqlThread starts - if s['address'] == sample_test_subscription_address[1]: + if s['address'] == sample_subscription_addresses[1]: self.assertEqual( base64.decodestring(s['label']), 'Bitmessage new releases/announcements') @@ -295,17 +293,16 @@ class TestAPI(TestAPIProto): 'Could not find Bitmessage new releases/announcements' ' in subscriptions') self.assertEqual( - self.api.deleteSubscription(sample_test_subscription_address[0]), + self.api.deleteSubscription(sample_subscription_addresses[0]), 'Deleted subscription if it existed.') self.assertEqual( - self.api.deleteSubscription(sample_test_subscription_address[1]), + self.api.deleteSubscription(sample_subscription_addresses[1]), 'Deleted subscription if it existed.') self.assertEqual( json.loads(self.api.listSubscriptions())['subscriptions'], []) def test_send(self): """Test message sending""" - # self.api.createDeterministicAddresses(self._seed, 1, 4) addr = self._add_random_address('random_2') msg = base64.encodestring('test message') msg_subject = base64.encodestring('test_subject') @@ -330,13 +327,6 @@ class TestAPI(TestAPIProto): break else: raise KeyError - # Find the message in inbox - # for m in json.loads( - # self.api.getInboxMessagesByReceiver( - # 'BM-2cWzSnwjJ7yRP3nLEWUV5LisTZyREWSzUK'))['inboxMessages']: - # if m['subject'] == msg_subject: - # inbox_msg = m['message'] - # break except ValueError: self.fail('sendMessage returned error or ackData is not hex') except KeyError: @@ -379,7 +369,7 @@ class TestAPI(TestAPIProto): 'doingbroadcastpow', 'broadcastqueued', 'broadcastsent')) start = time.time() - while status == 'doingbroadcastpow': + while status != 'broadcastsent': spent = int(time.time() - start) if spent > 30: self.fail('PoW is taking too much time: %ss' % spent) @@ -417,9 +407,21 @@ class TestAPI(TestAPIProto): self.assertEqual(self.api.deleteAndVacuum(), 'done') self.assertIsNone(json.loads( self.api.getSentMessageById(sent_msgid))) + # Try sending from disabled address + self.assertEqual(self.api.enableAddress(addr, False), 'success') + result = self.api.sendBroadcast( + addr, base64.encodestring('test_subject'), msg) + self.assertRegexpMatches(result, r'^API Error 0014:') finally: self.assertEqual(self.api.deleteAddress(addr), 'success') + # sending from an address without private key + # (Bitmessage new releases/announcements) + result = self.api.sendBroadcast( + 'BM-GtovgYdgs7qXPkoYaRgrLFuFKz1SFpsw', + base64.encodestring('test_subject'), msg) + self.assertRegexpMatches(result, r'^API Error 0013:') + def test_chan(self): """Testing chan creation/joining""" # Create chan with known address diff --git a/src/tests/test_api_thread.py b/src/tests/test_api_thread.py new file mode 100644 index 00000000..6e453b19 --- /dev/null +++ b/src/tests/test_api_thread.py @@ -0,0 +1,96 @@ +"""TestAPIThread class definition""" + +import sys +import time +from binascii import hexlify, unhexlify +from struct import pack + +from six.moves import queue, xmlrpc_client + +from pybitmessage import protocol +from pybitmessage.highlevelcrypto import calculateInventoryHash + +from .partial import TestPartialRun +from .samples import sample_statusbar_msg, sample_object_data + + +class TestAPIThread(TestPartialRun): + """Test case running the API thread""" + + @classmethod + def setUpClass(cls): + super(TestAPIThread, cls).setUpClass() + + import helper_sql + import queues + + # pylint: disable=too-few-public-methods + class SqlReadyMock(object): + """Mock helper_sql.sql_ready event with dummy class""" + @staticmethod + def wait(): + """Don't wait, return immediately""" + return + + helper_sql.sql_ready = SqlReadyMock + cls.queues = queues + + cls.config.set('bitmessagesettings', 'apiusername', 'username') + cls.config.set('bitmessagesettings', 'apipassword', 'password') + cls.config.set('inventory', 'storage', 'filesystem') + + import api + cls.thread = api.singleAPI() + cls.thread.daemon = True + cls.thread.start() + time.sleep(3) + cls.api = xmlrpc_client.ServerProxy( + "http://username:password@127.0.0.1:8442/") + + def test_connection(self): + """API command 'helloWorld'""" + self.assertEqual( + self.api.helloWorld('hello', 'world'), 'hello-world') + + def test_statusbar(self): + """Check UISignalQueue after issuing the 'statusBar' command""" + self.queues.UISignalQueue.queue.clear() + self.assertEqual( + self.api.statusBar(sample_statusbar_msg), 'success') + try: + cmd, data = self.queues.UISignalQueue.get(block=False) + except queue.Empty: + self.fail('UISignalQueue is empty!') + + self.assertEqual(cmd, 'updateStatusBar') + self.assertEqual(data, sample_statusbar_msg) + + def test_client_status(self): + """Ensure the reply of clientStatus corresponds to mock""" + status = self.api.clientStatus() + if sys.hexversion >= 0x3000000: + self.assertEqual(status["networkConnections"], 4) + self.assertEqual(status["pendingDownload"], 0) + + def test_disseminate_preencrypted(self): + """Call disseminatePreEncryptedMsg API command and check inventory""" + import proofofwork + from inventory import Inventory + import state + state.Inventory = Inventory() + + proofofwork.init() + self.assertEqual( + unhexlify(self.api.disseminatePreparedObject( + hexlify(sample_object_data).decode())), + calculateInventoryHash(sample_object_data)) + update_object = b'\x00' * 8 + pack( + '>Q', int(time.time() + 7200)) + sample_object_data[16:] + invhash = unhexlify(self.api.disseminatePreEncryptedMsg( + hexlify(update_object).decode() + )) + obj_type, obj_stream, obj_data = state.Inventory[invhash][:3] + self.assertEqual(obj_type, 42) + self.assertEqual(obj_stream, 2) + self.assertEqual(sample_object_data[16:], obj_data[16:]) + self.assertTrue(protocol.isProofOfWorkSufficient(obj_data)) diff --git a/src/tests/test_config.py b/src/tests/test_config.py index 7b702016..44db7c8a 100644 --- a/src/tests/test_config.py +++ b/src/tests/test_config.py @@ -1,4 +1,3 @@ -# pylint: disable=no-member """ Various tests for config """ @@ -36,62 +35,101 @@ maxnodes = 15000 maxsize = 1048576""" +# pylint: disable=protected-access class TestConfig(unittest.TestCase): """A test case for bmconfigparser""" - configfile = StringIO('') def setUp(self): - """creates a backup of BMConfigparser current state""" - BMConfigParser().write(self.configfile) - self.configfile.seek(0) - - def tearDown(self): - """restore to the backup of BMConfigparser""" - # pylint: disable=protected-access - BMConfigParser()._reset() - BMConfigParser().readfp(self.configfile) + self.config = BMConfigParser() + self.config.add_section('bitmessagesettings') def test_safeGet(self): """safeGet retuns provided default for nonexistent option or None""" self.assertIs( - BMConfigParser().safeGet('nonexistent', 'nonexistent'), None) + self.config.safeGet('nonexistent', 'nonexistent'), None) self.assertEqual( - BMConfigParser().safeGet('nonexistent', 'nonexistent', 42), 42) + self.config.safeGet('nonexistent', 'nonexistent', 42), 42) def test_safeGetBoolean(self): """safeGetBoolean returns False for nonexistent option, no default""" self.assertIs( - BMConfigParser().safeGetBoolean('nonexistent', 'nonexistent'), - False - ) + self.config.safeGetBoolean('nonexistent', 'nonexistent'), False) # no arg for default # pylint: disable=too-many-function-args with self.assertRaises(TypeError): - BMConfigParser().safeGetBoolean( - 'nonexistent', 'nonexistent', True) + self.config.safeGetBoolean('nonexistent', 'nonexistent', True) def test_safeGetInt(self): """safeGetInt retuns provided default for nonexistent option or 0""" self.assertEqual( - BMConfigParser().safeGetInt('nonexistent', 'nonexistent'), 0) + self.config.safeGetInt('nonexistent', 'nonexistent'), 0) self.assertEqual( - BMConfigParser().safeGetInt('nonexistent', 'nonexistent', 42), 42) + self.config.safeGetInt('nonexistent', 'nonexistent', 42), 42) def test_safeGetFloat(self): - """safeGetFloat retuns provided default for nonexistent option or 0.0""" + """ + safeGetFloat retuns provided default for nonexistent option or 0.0 + """ self.assertEqual( - BMConfigParser().safeGetFloat('nonexistent', 'nonexistent'), 0.0) + self.config.safeGetFloat('nonexistent', 'nonexistent'), 0.0) self.assertEqual( - BMConfigParser().safeGetFloat('nonexistent', 'nonexistent', 42.0), 42.0) + self.config.safeGetFloat('nonexistent', 'nonexistent', 42.0), 42.0) + + def test_set(self): + """Check exceptions in set()""" + with self.assertRaises(TypeError): + self.config.set('bitmessagesettings', 'any', 42) + with self.assertRaises(ValueError): + self.config.set( + 'bitmessagesettings', 'maxoutboundconnections', 'none') + with self.assertRaises(ValueError): + self.config.set( + 'bitmessagesettings', 'maxoutboundconnections', '9') + + def test_validate(self): + """Check validation""" + self.assertTrue( + self.config.validate('nonexistent', 'nonexistent', 'any')) + for val in range(9): + self.assertTrue(self.config.validate( + 'bitmessagesettings', 'maxoutboundconnections', str(val))) + + def test_setTemp(self): + """Set a temporary value and ensure it's returned by get()""" + self.config.setTemp('bitmessagesettings', 'connect', 'true') + self.assertIs( + self.config.safeGetBoolean('bitmessagesettings', 'connect'), True) + written_fp = StringIO('') + self.config.write(written_fp) + self.config._reset() + self.config.read_file(written_fp) + self.assertIs( + self.config.safeGetBoolean('bitmessagesettings', 'connect'), False) + + def test_addresses(self): + """Check the addresses() method""" + self.config.read() + for num in range(1, 4): + addr = 'BM-%s' % num + self.config.add_section(addr) + self.config.set(addr, 'label', 'account %s' % (4 - num)) + self.assertEqual(self.config.addresses(), ['BM-1', 'BM-2', 'BM-3']) + self.assertEqual(self.config.addresses(True), ['BM-3', 'BM-2', 'BM-1']) def test_reset(self): - """safeGetInt retuns provided default for bitmessagesettings option or 0""" + """Some logic for testing _reset()""" test_config_object = StringIO(test_config) - BMConfigParser().readfp(test_config_object) + self.config.read_file(test_config_object) + self.assertEqual( + self.config.safeGetInt( + 'bitmessagesettings', 'maxaddrperstreamsend'), 100) + self.config._reset() + self.assertEqual(self.config.sections(), []) + def test_defaults(self): + """Loading defaults""" + self.config.set('bitmessagesettings', 'maxaddrperstreamsend', '100') + self.config.read() self.assertEqual( - BMConfigParser().safeGetInt('bitmessagesettings', 'maxaddrperstreamsend'), 100) - # pylint: disable=protected-access - BMConfigParser()._reset() - self.assertEqual( - BMConfigParser().safeGetInt('bitmessagesettings', 'maxaddrperstreamsend'), 500) + self.config.safeGetInt( + 'bitmessagesettings', 'maxaddrperstreamsend'), 500) diff --git a/src/tests/test_config_address.py b/src/tests/test_config_address.py new file mode 100644 index 00000000..b76df7ec --- /dev/null +++ b/src/tests/test_config_address.py @@ -0,0 +1,57 @@ +""" +Various tests to Enable and Disable the identity +""" + +import unittest +from six import StringIO +from six.moves import configparser +from pybitmessage.bmconfigparser import BMConfigParser + + +address_obj = """[BM-enabled_identity] +label = Test_address_1 +enabled = true + +[BM-disabled_identity] +label = Test_address_2 +enabled = false +""" + + +# pylint: disable=protected-access +class TestAddressEnableDisable(unittest.TestCase): + """A test case for bmconfigparser""" + + def setUp(self): + self.config = BMConfigParser() + self.config.read_file(StringIO(address_obj)) + + def test_enable_enabled_identity(self): + """Test enabling already enabled identity""" + self.config.enable_address('BM-enabled_identity') + self.assertEqual(self.config.safeGet('BM-enabled_identity', 'enabled'), 'true') + + def test_enable_disabled_identity(self): + """Test enabling the Disabled identity""" + self.config.enable_address('BM-disabled_identity') + self.assertEqual(self.config.safeGet('BM-disabled_identity', 'enabled'), 'true') + + def test_enable_non_existent_identity(self): + """Test enable non-existent address""" + with self.assertRaises(configparser.NoSectionError): + self.config.enable_address('non_existent_address') + + def test_disable_disabled_identity(self): + """Test disabling already disabled identity""" + self.config.disable_address('BM-disabled_identity') + self.assertEqual(self.config.safeGet('BM-disabled_identity', 'enabled'), 'false') + + def test_disable_enabled_identity(self): + """Test Disabling the Enabled identity""" + self.config.disable_address('BM-enabled_identity') + self.assertEqual(self.config.safeGet('BM-enabled_identity', 'enabled'), 'false') + + def test_disable_non_existent_identity(self): + """Test dsiable non-existent address""" + with self.assertRaises(configparser.NoSectionError): + self.config.disable_address('non_existent_address') diff --git a/src/tests/test_config_process.py b/src/tests/test_config_process.py index 173d323f..9322a2f0 100644 --- a/src/tests/test_config_process.py +++ b/src/tests/test_config_process.py @@ -4,7 +4,7 @@ Various tests for config import os import tempfile -from pybitmessage.bmconfigparser import BMConfigParser +from pybitmessage.bmconfigparser import config from .test_process import TestProcessProto from .common import skip_python3 @@ -17,7 +17,6 @@ class TestProcessConfig(TestProcessProto): def test_config_defaults(self): """Test settings in the generated config""" - config = BMConfigParser() self._stop_process() self._kill_process() config.read(os.path.join(self.home, 'keys.dat')) diff --git a/src/tests/test_crypto.py b/src/tests/test_crypto.py index 38410359..6dbb2f31 100644 --- a/src/tests/test_crypto.py +++ b/src/tests/test_crypto.py @@ -3,6 +3,7 @@ Test the alternatives for crypto primitives """ import hashlib +import ssl import unittest from abc import ABCMeta, abstractmethod from binascii import hexlify @@ -11,13 +12,15 @@ from pybitmessage import highlevelcrypto try: - from Crypto.Hash import RIPEMD + from Crypto.Hash import RIPEMD160 except ImportError: - RIPEMD = None + RIPEMD160 = None from .samples import ( - sample_pubsigningkey, sample_pubencryptionkey, - sample_privsigningkey, sample_privencryptionkey, sample_ripe + sample_bm160, sample_deterministic_ripe, sample_double_sha512, + sample_hash_data, sample_msg, sample_pubsigningkey, + sample_pubencryptionkey, sample_privsigningkey, sample_privencryptionkey, + sample_ripe, sample_seed, sample_sig, sample_sig_sha1 ) @@ -42,6 +45,8 @@ class RIPEMD160TestCase(object): self.assertEqual(hexlify(self._hashdigest(pubkey_sha)), sample_ripe) +@unittest.skipIf( + ssl.OPENSSL_VERSION.startswith('OpenSSL 3'), 'no ripemd160 in openssl 3') class TestHashlib(RIPEMD160TestCase, unittest.TestCase): """RIPEMD160 test case for hashlib""" @staticmethod @@ -51,17 +56,81 @@ class TestHashlib(RIPEMD160TestCase, unittest.TestCase): return hasher.digest() -@unittest.skipUnless(RIPEMD, 'pycrypto package not found') +@unittest.skipUnless(RIPEMD160, 'pycrypto package not found') class TestCrypto(RIPEMD160TestCase, unittest.TestCase): """RIPEMD160 test case for Crypto""" @staticmethod def _hashdigest(data): - return RIPEMD.RIPEMD160Hash(data).digest() + return RIPEMD160.new(data).digest() class TestHighlevelcrypto(unittest.TestCase): """Test highlevelcrypto public functions""" + def test_double_sha512(self): + """Reproduce the example on page 1 of the Specification""" + self.assertEqual( + highlevelcrypto.double_sha512(sample_hash_data), + sample_double_sha512) + + def test_bm160(self): + """Formally check highlevelcrypto._bm160()""" + # pylint: disable=protected-access + self.assertEqual( + highlevelcrypto._bm160(sample_hash_data), sample_bm160) + + def test_to_ripe(self): + """Formally check highlevelcrypto.to_ripe()""" + self.assertEqual( + hexlify(highlevelcrypto.to_ripe( + sample_pubsigningkey, sample_pubencryptionkey)), + sample_ripe) + + def test_randomBytes(self): + """Dummy checks for random bytes""" + for n in (8, 32, 64): + data = highlevelcrypto.randomBytes(n) + self.assertEqual(len(data), n) + self.assertNotEqual(len(set(data)), 1) + self.assertNotEqual(data, highlevelcrypto.randomBytes(n)) + + def test_random_keys(self): + """Dummy checks for random keys""" + priv, pub = highlevelcrypto.random_keys() + self.assertEqual(len(priv), 32) + self.assertEqual(highlevelcrypto.pointMult(priv), pub) + + def test_deterministic_keys(self): + """Generate deterministic keys, make ripe and compare it to sample""" + # encodeVarint(42) = b'*' + sigkey = highlevelcrypto.deterministic_keys(sample_seed, b'*')[1] + enkey = highlevelcrypto.deterministic_keys(sample_seed, b'+')[1] + self.assertEqual( + sample_deterministic_ripe, + hexlify(highlevelcrypto.to_ripe(sigkey, enkey))) + + def test_signatures(self): + """Verify sample signatures and newly generated ones""" + pubkey_hex = hexlify(sample_pubsigningkey) + # pregenerated signatures + self.assertTrue(highlevelcrypto.verify( + sample_msg, sample_sig, pubkey_hex, "sha256")) + self.assertFalse(highlevelcrypto.verify( + sample_msg, sample_sig, pubkey_hex, "sha1")) + self.assertTrue(highlevelcrypto.verify( + sample_msg, sample_sig_sha1, pubkey_hex, "sha1")) + self.assertTrue(highlevelcrypto.verify( + sample_msg, sample_sig_sha1, pubkey_hex)) + # new signatures + sig256 = highlevelcrypto.sign(sample_msg, sample_privsigningkey) + sig1 = highlevelcrypto.sign(sample_msg, sample_privsigningkey, "sha1") + self.assertTrue( + highlevelcrypto.verify(sample_msg, sig256, pubkey_hex)) + self.assertTrue( + highlevelcrypto.verify(sample_msg, sig256, pubkey_hex, "sha256")) + self.assertTrue( + highlevelcrypto.verify(sample_msg, sig1, pubkey_hex)) + def test_privtopub(self): """Generate public keys and check the result""" self.assertEqual( diff --git a/src/tests/test_helper_inbox.py b/src/tests/test_helper_inbox.py new file mode 100644 index 00000000..a0b6de1b --- /dev/null +++ b/src/tests/test_helper_inbox.py @@ -0,0 +1,76 @@ +"""Test cases for Helper Inbox""" + +import time +import unittest +from pybitmessage.helper_inbox import ( + insert, + trash, + delete, + isMessageAlreadyInInbox, + undeleteMessage, +) +from pybitmessage.helper_ackPayload import genAckPayload + +try: + # Python 3 + from unittest.mock import patch +except ImportError: + # Python 2 + from mock import patch + + +class TestHelperInbox(unittest.TestCase): + """Test class for Helper Inbox""" + + @patch("pybitmessage.helper_inbox.sqlExecute") + def test_insert(self, mock_sql_execute): # pylint: disable=no-self-use + """Test to perform an insert into the "inbox" table""" + mock_message_data = ( + "ruyv87bv", + "BM-2cUGaEcGz9Zft1SPAo8FJtfzyADTpEgU9U", + "BM-2cUGaEcGz9Zft1SPAo8FJtfzyADTp5g99U", + "Test subject", + int(time.time()), + "Test message", + "inbox", + 2, + 0, + "658gvjhtghv", + ) + insert(t=mock_message_data) + mock_sql_execute.assert_called_once() + + @patch("pybitmessage.helper_inbox.sqlExecute") + def test_trash(self, mock_sql_execute): # pylint: disable=no-self-use + """Test marking a message in the `inbox` as `trash`""" + mock_msg_id = "fefkosghsbse92" + trash(msgid=mock_msg_id) + mock_sql_execute.assert_called_once() + + @patch("pybitmessage.helper_inbox.sqlExecute") + def test_delete(self, mock_sql_execute): # pylint: disable=no-self-use + """Test for permanent deletion of message from trash""" + mock_ack_data = genAckPayload() + delete(mock_ack_data) + mock_sql_execute.assert_called_once() + + @patch("pybitmessage.helper_inbox.sqlExecute") + def test_undeleteMessage(self, mock_sql_execute): # pylint: disable=no-self-use + """Test for Undelete the message""" + mock_msg_id = "fefkosghsbse92" + undeleteMessage(msgid=mock_msg_id) + mock_sql_execute.assert_called_once() + + @patch("pybitmessage.helper_inbox.sqlQuery") + def test_isMessageAlreadyInInbox(self, mock_sql_query): + """Test for check for previous instances of this message""" + fake_sigHash = "h4dkn54546" + # if Message is already in Inbox + mock_sql_query.return_value = [(1,)] + result = isMessageAlreadyInInbox(sigHash=fake_sigHash) + self.assertTrue(result) + + # if Message is not in Inbox + mock_sql_query.return_value = [(0,)] + result = isMessageAlreadyInInbox(sigHash=fake_sigHash) + self.assertFalse(result) diff --git a/src/tests/test_helper_sent.py b/src/tests/test_helper_sent.py new file mode 100644 index 00000000..9227e43a --- /dev/null +++ b/src/tests/test_helper_sent.py @@ -0,0 +1,76 @@ +"""Test cases for helper_sent class""" + +import unittest +from pybitmessage.helper_sent import insert, delete, trash, retrieve_message_details + +try: + # Python 3 + from unittest.mock import patch +except ImportError: + # Python 2 + from mock import patch + + +class TestHelperSent(unittest.TestCase): + """Test class for helper_sent""" + + @patch("pybitmessage.helper_sent.sqlExecute") + def test_insert_valid_address(self, mock_sql_execute): + """Test insert with valid address""" + VALID_ADDRESS = "BM-2cUGaEcGz9Zft1SPAo8FJtfzyADTpEgU9U" + ackdata = insert( + msgid="123456", + toAddress="[Broadcast subscribers]", + fromAddress=VALID_ADDRESS, + subject="Test Subject", + message="Test Message", + status="msgqueued", + sentTime=1234567890, + lastActionTime=1234567890, + sleeptill=0, + retryNumber=0, + encoding=2, + ttl=3600, + folder="sent", + ) + mock_sql_execute.assert_called_once() + self.assertIsNotNone(ackdata) + + def test_insert_invalid_address(self): + """Test insert with invalid address""" + INVALID_ADDRESS = "TEST@1245.780" + ackdata = insert(toAddress=INVALID_ADDRESS) + self.assertIsNone(ackdata) + + @patch("pybitmessage.helper_sent.sqlExecute") + def test_delete(self, mock_sql_execute): + """Test delete function""" + delete("ack_data") + self.assertTrue(mock_sql_execute.called) + mock_sql_execute.assert_called_once_with( + "DELETE FROM sent WHERE ackdata = ?", "ack_data" + ) + + @patch("pybitmessage.helper_sent.sqlQuery") + def test_retrieve_valid_message_details(self, mock_sql_query): + """Test retrieving valid message details""" + return_data = [ + ( + "to@example.com", + "from@example.com", + "Test Subject", + "Test Message", + "2022-01-01", + ) + ] + mock_sql_query.return_value = return_data + result = retrieve_message_details("12345") + self.assertEqual(result, return_data) + + @patch("pybitmessage.helper_sent.sqlExecute") + def test_trash(self, mock_sql_execute): + """Test marking a message as 'trash'""" + ackdata = "ack_data" + mock_sql_execute.return_value = 1 + rowcount = trash(ackdata) + self.assertEqual(rowcount, 1) diff --git a/src/tests/test_helper_sql.py b/src/tests/test_helper_sql.py new file mode 100644 index 00000000..036bd2c9 --- /dev/null +++ b/src/tests/test_helper_sql.py @@ -0,0 +1,131 @@ +"""Test cases for helper_sql""" + +import unittest + +try: + # Python 3 + from unittest.mock import patch +except ImportError: + # Python 2 + from mock import patch + +import pybitmessage.helper_sql as helper_sql + + +class TestHelperSql(unittest.TestCase): + """Test class for helper_sql""" + + @classmethod + def setUpClass(cls): + helper_sql.sql_available = True + + @patch("pybitmessage.helper_sql.sqlSubmitQueue.put") + @patch("pybitmessage.helper_sql.sqlReturnQueue.get") + def test_sqlquery_no_args(self, mock_sqlreturnqueue_get, mock_sqlsubmitqueue_put): + """Test sqlQuery with no additional arguments""" + mock_sqlreturnqueue_get.return_value = ("dummy_result", None) + result = helper_sql.sqlQuery( + "SELECT msgid FROM inbox where folder='inbox' ORDER BY received" + ) + self.assertEqual(mock_sqlsubmitqueue_put.call_count, 2) + self.assertEqual(result, "dummy_result") + + @patch("pybitmessage.helper_sql.sqlSubmitQueue.put") + @patch("pybitmessage.helper_sql.sqlReturnQueue.get") + def test_sqlquery_with_args(self, mock_sqlreturnqueue_get, mock_sqlsubmitqueue_put): + """Test sqlQuery with additional arguments""" + mock_sqlreturnqueue_get.return_value = ("dummy_result", None) + result = helper_sql.sqlQuery( + "SELECT address FROM addressbook WHERE address=?", "PB-5yfds868gbkj" + ) + self.assertEqual(mock_sqlsubmitqueue_put.call_count, 2) + self.assertEqual(result, "dummy_result") + + @patch("pybitmessage.helper_sql.sqlSubmitQueue.put") + @patch("pybitmessage.helper_sql.sqlReturnQueue.get") + def test_sqlexecute(self, mock_sqlreturnqueue_get, mock_sqlsubmitqueue_put): + """Test sqlExecute with valid arguments""" + mock_sqlreturnqueue_get.return_value = (None, 1) + rowcount = helper_sql.sqlExecute( + "UPDATE sent SET status = 'msgqueued'" + "WHERE ackdata = ? AND folder = 'sent'", + "1710652313", + ) + self.assertEqual(mock_sqlsubmitqueue_put.call_count, 3) + self.assertEqual(rowcount, 1) + + @patch("pybitmessage.helper_sql.SqlBulkExecute.execute") + def test_sqlexecute_script(self, mock_execute): + """Test sqlExecuteScript with a SQL script""" + helper_sql.sqlExecuteScript( + "CREATE TABLE test (id INTEGER); INSERT INTO test VALUES (1);" + ) + self.assertTrue(mock_execute.assert_called) + + @patch("pybitmessage.helper_sql.sqlSubmitQueue.put") + @patch( + "pybitmessage.helper_sql.sqlReturnQueue.get", + ) + def test_sqlexecute_chunked(self, mock_sqlreturnqueue_get, mock_sqlsubmitqueue_put): + """Test sqlExecuteChunked with valid arguments""" + # side_effect is list of return value (_, rowcount) + # of sqlReturnQueue.get for each chunk + CHUNK_COUNT = 6 + CHUNK_SIZE = 999 + ID_COUNT = CHUNK_COUNT * CHUNK_SIZE + CHUNKS_ROWCOUNT_LIST = [50, 29, 28, 18, 678, 900] + TOTAL_ROW_COUNT = sum(CHUNKS_ROWCOUNT_LIST) + mock_sqlreturnqueue_get.side_effect = [(None, rowcount) for rowcount in CHUNKS_ROWCOUNT_LIST] + args = [] + for i in range(0, ID_COUNT): + args.append("arg{}".format(i)) + total_row_count_return = helper_sql.sqlExecuteChunked( + "INSERT INTO table VALUES {}", ID_COUNT, *args + ) + self.assertEqual(TOTAL_ROW_COUNT, total_row_count_return) + self.assertTrue(mock_sqlsubmitqueue_put.called) + self.assertTrue(mock_sqlreturnqueue_get.called) + + @patch("pybitmessage.helper_sql.sqlSubmitQueue.put") + @patch("pybitmessage.helper_sql.sqlReturnQueue.get") + def test_sqlexecute_chunked_with_idcount_zero( + self, mock_sqlreturnqueue_get, mock_sqlsubmitqueue_put + ): + """Test sqlExecuteChunked with id count 0""" + ID_COUNT = 0 + args = list() + for i in range(0, ID_COUNT): + args.append("arg{}".format(i)) + total_row_count = helper_sql.sqlExecuteChunked( + "INSERT INTO table VALUES {}", ID_COUNT, *args + ) + self.assertEqual(total_row_count, 0) + self.assertFalse(mock_sqlsubmitqueue_put.called) + self.assertFalse(mock_sqlreturnqueue_get.called) + + @patch("pybitmessage.helper_sql.sqlSubmitQueue.put") + @patch("pybitmessage.helper_sql.sqlReturnQueue.get") + def test_sqlexecute_chunked_with_args_less( + self, mock_sqlreturnqueue_get, mock_sqlsubmitqueue_put + ): + """Test sqlExecuteChunked with length of args less than idcount""" + ID_COUNT = 12 + args = ["args0", "arg1"] + total_row_count = helper_sql.sqlExecuteChunked( + "INSERT INTO table VALUES {}", ID_COUNT, *args + ) + self.assertEqual(total_row_count, 0) + self.assertFalse(mock_sqlsubmitqueue_put.called) + self.assertFalse(mock_sqlreturnqueue_get.called) + + @patch("pybitmessage.helper_sql.sqlSubmitQueue.put") + @patch("pybitmessage.helper_sql.sqlSubmitQueue.task_done") + def test_sqlstored_procedure(self, mock_task_done, mock_sqlsubmitqueue_put): + """Test sqlStoredProcedure with a stored procedure name""" + helper_sql.sqlStoredProcedure("exit") + self.assertTrue(mock_task_done.called_once) + mock_sqlsubmitqueue_put.assert_called_with("terminate") + + @classmethod + def tearDownClass(cls): + helper_sql.sql_available = False diff --git a/src/tests/test_inventory.py b/src/tests/test_inventory.py new file mode 100644 index 00000000..5978f9a5 --- /dev/null +++ b/src/tests/test_inventory.py @@ -0,0 +1,58 @@ +"""Tests for inventory""" + +import os +import shutil +import struct +import tempfile +import time +import unittest + +from pybitmessage import highlevelcrypto +from pybitmessage.storage import storage + +from .partial import TestPartialRun + + +class TestFilesystemInventory(TestPartialRun): + """A test case for the inventory using filesystem backend""" + + @classmethod + def setUpClass(cls): + cls.home = os.environ['BITMESSAGE_HOME'] = tempfile.mkdtemp() + super(TestFilesystemInventory, cls).setUpClass() + + from inventory import create_inventory_instance + cls.inventory = create_inventory_instance('filesystem') + + def test_consistency(self): + """Ensure the inventory is of proper class""" + if os.path.isfile(os.path.join(self.home, 'messages.dat')): + # this will likely never happen + self.fail("Failed to configure filesystem inventory!") + + def test_appending(self): + """Add a sample message to the inventory""" + TTL = 24 * 60 * 60 + embedded_time = int(time.time() + TTL) + msg = struct.pack('>Q', embedded_time) + os.urandom(166) + invhash = highlevelcrypto.calculateInventoryHash(msg) + self.inventory[invhash] = (2, 1, msg, embedded_time, b'') + + @classmethod + def tearDownClass(cls): + super(TestFilesystemInventory, cls).tearDownClass() + cls.inventory.flush() + shutil.rmtree(os.path.join(cls.home, cls.inventory.topDir)) + + +class TestStorageAbstract(unittest.TestCase): + """A test case for refactoring of the storage abstract classes""" + + def test_inventory_storage(self): + """Check inherited abstract methods""" + with self.assertRaisesRegexp( + TypeError, "^Can't instantiate abstract class.*" + "methods __contains__, __delitem__, __getitem__, __iter__," + " __len__, __setitem__" + ): # pylint: disable=abstract-class-instantiated + storage.InventoryStorage() diff --git a/src/tests/test_log.py b/src/tests/test_log.py index e62010a0..4e74e50d 100644 --- a/src/tests/test_log.py +++ b/src/tests/test_log.py @@ -14,8 +14,8 @@ class TestLog(unittest.TestCase): sys.hexversion < 0x3000000, 'assertLogs is new in version 3.4') def test_LogOutput(self): """Use proofofwork.LogOutput to log output of a shell command""" - def echo(): - """Call the echo shell command""" - with proofofwork.LogOutput(): + with self.assertLogs('default') as cm: # pylint: disable=no-member + with proofofwork.LogOutput('+'): subprocess.call(['echo', 'HELLO']) - self.assertLogs(echo(), 'HELLO') # pylint: disable=no-member + + self.assertEqual(cm.output, ['INFO:default:+: HELLO\n']) diff --git a/src/tests/test_multiqueue.py b/src/tests/test_multiqueue.py new file mode 100644 index 00000000..87149d56 --- /dev/null +++ b/src/tests/test_multiqueue.py @@ -0,0 +1,65 @@ +"""Test cases for multiqueue""" + +import unittest +from pybitmessage.multiqueue import MultiQueue + + +class TestMultiQueue(unittest.TestCase): + """Test cases for multiqueue""" + + def test_queue_creation(self): + """Check if the queueCount matches the specified value""" + mqsize = 3 + multiqueue = MultiQueue(count=mqsize) + self.assertEqual(multiqueue.queueCount, mqsize) + + def test_empty_queue(self): + """Check for empty queue""" + multiqueue = MultiQueue(count=5) + self.assertEqual(multiqueue.totalSize(), 0) + + def test_put_get_count(self): + """check if put & get count is equal""" + multiqueue = MultiQueue(count=5) + put_count = 6 + for i in range(put_count): + multiqueue.put(i) + + get_count = 0 + while multiqueue.totalSize() != 0: + if multiqueue.qsize() > 0: + multiqueue.get() + get_count += 1 + multiqueue.iterate() + + self.assertEqual(get_count, put_count) + + def test_put_and_get(self): + """Testing Put and Get""" + item = 400 + multiqueue = MultiQueue(count=3) + multiqueue.put(item) + result = None + for _ in multiqueue.queues: + if multiqueue.qsize() > 0: + result = multiqueue.get() + break + multiqueue.iterate() + self.assertEqual(result, item) + + def test_iteration(self): + """Check if the iteration wraps around correctly""" + mqsize = 3 + iteroffset = 1 + multiqueue = MultiQueue(count=mqsize) + for _ in range(mqsize + iteroffset): + multiqueue.iterate() + self.assertEqual(multiqueue.iter, iteroffset) + + def test_total_size(self): + """Check if the total size matches the expected value""" + multiqueue = MultiQueue(count=3) + put_count = 5 + for i in range(put_count): + multiqueue.put(i) + self.assertEqual(multiqueue.totalSize(), put_count) diff --git a/src/tests/test_network.py b/src/tests/test_network.py new file mode 100644 index 00000000..206117e0 --- /dev/null +++ b/src/tests/test_network.py @@ -0,0 +1,96 @@ +"""Test network module""" + +import threading +import time + +from .common import skip_python3 +from .partial import TestPartialRun + +skip_python3() + + +class TestNetwork(TestPartialRun): + """A test case for running the network subsystem""" + + @classmethod + def setUpClass(cls): + super(TestNetwork, cls).setUpClass() + + cls.state.maximumNumberOfHalfOpenConnections = 4 + + cls.config.set('bitmessagesettings', 'sendoutgoingconnections', 'True') + cls.config.set('bitmessagesettings', 'udp', 'True') + + # config variable is still used inside of the network ): + import network + from network import connectionpool, stats + + # beware of singleton + connectionpool.config = cls.config + cls.pool = connectionpool.pool + cls.stats = stats + + network.start(cls.config, cls.state) + + def test_threads(self): + """Ensure all the network threads started""" + threads = { + "AddrBroadcaster", "Announcer", "Asyncore", "Downloader", + "InvBroadcaster", "Uploader"} + extra = self.config.getint('threads', 'receive') + for thread in threading.enumerate(): + try: + threads.remove(thread.name) + except KeyError: + extra -= thread.name.startswith("ReceiveQueue_") + + self.assertEqual(len(threads), 0) + self.assertEqual(extra, 0) + + def test_stats(self): + """Check that network starts connections and updates stats""" + pl = 0 + for _ in range(30): + if pl == 0: + pl = len(self.pool) + if ( + self.stats.receivedBytes() > 0 and self.stats.sentBytes() > 0 + and pl > 0 + # and len(self.stats.connectedHostsList()) > 0 + ): + break + time.sleep(1) + else: + self.fail('Have not started any connection in 30 sec') + + def test_udp(self): + """Invoke AnnounceThread.announceSelf() and check discovered peers""" + for _ in range(20): + if self.pool.udpSockets: + break + time.sleep(1) + else: + self.fail('No UDP sockets found in 20 sec') + + for _ in range(10): + try: + self.state.announceThread.announceSelf() + except AttributeError: + self.fail('state.announceThread is not set properly') + time.sleep(1) + try: + peer = self.state.discoveredPeers.popitem()[0] + except KeyError: + continue + else: + self.assertEqual(peer.port, 8444) + break + else: + self.fail('No self in discovered peers') + + @classmethod + def tearDownClass(cls): + super(TestNetwork, cls).tearDownClass() + for thread in threading.enumerate(): + if thread.name == "Asyncore": + thread.stopThread() diff --git a/src/tests/test_openclpow.py b/src/tests/test_openclpow.py index 341beec9..4770072e 100644 --- a/src/tests/test_openclpow.py +++ b/src/tests/test_openclpow.py @@ -1,10 +1,10 @@ """ Tests for openclpow module """ -import hashlib + import unittest -from struct import pack, unpack -from pybitmessage import openclpow + +from pybitmessage import openclpow, proofofwork class TestOpenClPow(unittest.TestCase): @@ -25,7 +25,5 @@ class TestOpenClPow(unittest.TestCase): "b93f3ffeba0ef2fd08a8dc2f87b68ae5a0dc819ab57f22ad2c4c9c8618a43b3" ).decode("hex") nonce = openclpow.do_opencl_pow(initialHash.encode("hex"), target_) - trialValue, = unpack( - '>Q', hashlib.sha512(hashlib.sha512( - pack('>Q', nonce) + initialHash).digest()).digest()[0:8]) - self.assertLess((nonce - trialValue), target_) + self.assertLess( + nonce - proofofwork.trial_value(nonce, initialHash), target_) diff --git a/src/tests/test_packets.py b/src/tests/test_packets.py index 65ee0d44..9dfb1d23 100644 --- a/src/tests/test_packets.py +++ b/src/tests/test_packets.py @@ -1,14 +1,16 @@ +"""Test packets creation and parsing""" -import unittest from binascii import unhexlify from struct import pack from pybitmessage import addresses, protocol -from .samples import magic +from .samples import ( + sample_addr_data, sample_object_data, sample_object_expires) +from .test_protocol import TestSocketInet -class TestSerialize(unittest.TestCase): +class TestSerialize(TestSocketInet): """Test serializing and deserializing packet data""" def test_varint(self): @@ -44,10 +46,18 @@ class TestSerialize(unittest.TestCase): def test_packet(self): """Check the packet created by protocol.CreatePacket()""" - head = unhexlify(b'%x' % magic) + head = unhexlify(b'%x' % protocol.magic) self.assertEqual( protocol.CreatePacket(b'ping')[:len(head)], head) + def test_decode_obj_parameters(self): + """Check parameters decoded from a sample object""" + objectType, toStreamNumber, expiresTime = \ + protocol.decodeObjectParameters(sample_object_data) + self.assertEqual(objectType, 42) + self.assertEqual(toStreamNumber, 2) + self.assertEqual(expiresTime, sample_object_expires) + def test_encodehost(self): """Check the result of protocol.encodeHost()""" self.assertEqual( @@ -66,3 +76,12 @@ class TestSerialize(unittest.TestCase): self.assertEqual( protocol.encodeHost('quzwelsuziwqgpt2.onion'), unhexlify('fd87d87eeb438533622e54ca2d033e7a')) + + def test_assemble_addr(self): + """Assemble addr packet and compare it to pregenerated sample""" + self.assertEqual( + sample_addr_data, + protocol.assembleAddrMessage([ + (1, protocol.Peer('127.0.0.1', 8444), 1626611891) + for _ in range(500) + ])[protocol.Header.size:]) diff --git a/src/tests/test_process.py b/src/tests/test_process.py index c968c0ae..37b34541 100644 --- a/src/tests/test_process.py +++ b/src/tests/test_process.py @@ -205,6 +205,8 @@ class TestProcess(TestProcessProto): self.assertEqual( self.process.environ().get('BITMESSAGE_HOME'), self.home) + @unittest.skipIf( + os.getenv('WINEPREFIX'), "process.connections() doesn't work on wine") def test_listening(self): """Check that pybitmessage listens on port 8444""" for c in self.process.connections(): diff --git a/src/tests/test_protocol.py b/src/tests/test_protocol.py index d285d1df..e3137b25 100644 --- a/src/tests/test_protocol.py +++ b/src/tests/test_protocol.py @@ -9,14 +9,18 @@ from pybitmessage import protocol, state from pybitmessage.helper_startup import fixSocket -class TestProtocol(unittest.TestCase): - """Main protocol test case""" +class TestSocketInet(unittest.TestCase): + """Base class for test cases using protocol.encodeHost()""" @classmethod def setUpClass(cls): """Execute fixSocket() before start. Only for Windows?""" fixSocket() + +class TestProtocol(TestSocketInet): + """Main protocol test case""" + def test_checkIPv4Address(self): """Check the results of protocol.checkIPv4Address()""" token = 'HELLO' diff --git a/src/tests/test_shared.py b/src/tests/test_shared.py new file mode 100644 index 00000000..39bedf32 --- /dev/null +++ b/src/tests/test_shared.py @@ -0,0 +1,152 @@ +"""Test cases for shared.py""" + +import unittest +from pybitmessage.shared import ( + isAddressInMyAddressBook, + isAddressInMySubscriptionsList, + checkSensitiveFilePermissions, + reloadBroadcastSendersForWhichImWatching, + fixSensitiveFilePermissions, + MyECSubscriptionCryptorObjects, + stat, + os, +) + +from .samples import sample_address + +try: + # Python 3 + from unittest.mock import patch, PropertyMock +except ImportError: + # Python 2 + from mock import patch, PropertyMock + +# mock os.stat data for file +PERMISSION_MODE1 = stat.S_IRUSR # allow Read permission for the file owner. +PERMISSION_MODE2 = ( + stat.S_IRWXO +) # allow read, write, serach & execute permission for other users +INODE = 753 +DEV = 1795 +NLINK = 1 +UID = 1000 +GID = 0 +SIZE = 1021 +ATIME = 1711587560 +MTIME = 1709449249 +CTIME = 1709449603 + + +class TestShared(unittest.TestCase): + """Test class for shared.py""" + + @patch("pybitmessage.shared.sqlQuery") + def test_isaddress_in_myaddressbook(self, mock_sql_query): + """Test if address is in MyAddressbook""" + address = sample_address + + # if address is in MyAddressbook + mock_sql_query.return_value = [address] + return_val = isAddressInMyAddressBook(address) + mock_sql_query.assert_called_once() + self.assertTrue(return_val) + + # if address is not in MyAddressbook + mock_sql_query.return_value = [] + return_val = isAddressInMyAddressBook(address) + self.assertFalse(return_val) + self.assertEqual(mock_sql_query.call_count, 2) + + @patch("pybitmessage.shared.sqlQuery") + def test_isaddress_in_mysubscriptionslist(self, mock_sql_query): + """Test if address is in MySubscriptionsList""" + + address = sample_address + + # if address is in MySubscriptionsList + mock_sql_query.return_value = [address] + return_val = isAddressInMySubscriptionsList(address) + self.assertTrue(return_val) + + # if address is not in MySubscriptionsList + mock_sql_query.return_value = [] + return_val = isAddressInMySubscriptionsList(address) + self.assertFalse(return_val) + self.assertEqual(mock_sql_query.call_count, 2) + + @patch("pybitmessage.shared.sqlQuery") + def test_reloadBroadcastSendersForWhichImWatching(self, mock_sql_query): + """Test for reload Broadcast Senders For Which Im Watching""" + mock_sql_query.return_value = [ + (sample_address,), + ] + # before reload + self.assertEqual(len(MyECSubscriptionCryptorObjects), 0) + + # reloading with addressVersionNumber 1 + reloadBroadcastSendersForWhichImWatching() + self.assertGreater(len(MyECSubscriptionCryptorObjects), 0) + + @patch("pybitmessage.shared.os.stat") + @patch( + "pybitmessage.shared.sys", + new_callable=PropertyMock, # pylint: disable=used-before-assignment + ) + def test_check_sensitive_file_permissions(self, mock_sys, mock_os_stat): + """Test to check file permissions""" + fake_filename = "path/to/file" + + # test for windows system + mock_sys.platform = "win32" + result = checkSensitiveFilePermissions(fake_filename) + self.assertTrue(result) + + # test for freebsd system + mock_sys.platform = "freebsd7" + # returning file permission mode stat.S_IRUSR + MOCK_OS_STAT_RETURN = os.stat_result( + sequence=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + dict={ + "st_mode": PERMISSION_MODE1, + "st_ino": INODE, + "st_dev": DEV, + "st_nlink": NLINK, + "st_uid": UID, + "st_gid": GID, + "st_size": SIZE, + "st_atime": ATIME, + "st_mtime": MTIME, + "st_ctime": CTIME, + }, + ) + mock_os_stat.return_value = MOCK_OS_STAT_RETURN + result = checkSensitiveFilePermissions(fake_filename) + self.assertTrue(result) + + @patch("pybitmessage.shared.os.chmod") + @patch("pybitmessage.shared.os.stat") + def test_fix_sensitive_file_permissions( # pylint: disable=no-self-use + self, mock_os_stat, mock_chmod + ): + """Test to fix file permissions""" + fake_filename = "path/to/file" + + # returning file permission mode stat.S_IRWXO + MOCK_OS_STAT_RETURN = os.stat_result( + sequence=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + dict={ + "st_mode": PERMISSION_MODE2, + "st_ino": INODE, + "st_dev": DEV, + "st_nlink": NLINK, + "st_uid": UID, + "st_gid": GID, + "st_size": SIZE, + "st_atime": ATIME, + "st_mtime": MTIME, + "st_ctime": CTIME, + }, + ) + mock_os_stat.return_value = MOCK_OS_STAT_RETURN + fixSensitiveFilePermissions(fake_filename, False) + mock_chmod.assert_called_once() diff --git a/src/upnp.py b/src/upnp.py index 82608ddd..ab8bb9ff 100644 --- a/src/upnp.py +++ b/src/upnp.py @@ -5,19 +5,21 @@ Reference: http://mattscodecave.com/posts/using-python-and-upnp-to-forward-a-por """ import httplib +import re import socket import time import urllib2 from random import randint from urlparse import urlparse -from xml.dom.minidom import Document, parseString +from xml.dom.minidom import Document # nosec B408 +from defusedxml.minidom import parseString import queues import state import tr -from bmconfigparser import BMConfigParser +from bmconfigparser import config from debug import logger -from network import BMConnectionPool, knownnodes, StoppableThread +from network import connectionpool, knownnodes, StoppableThread from network.node import Peer @@ -119,7 +121,7 @@ class Router: # pylint: disable=old-style-class if service.childNodes[0].data.find('WANIPConnection') > 0 or \ service.childNodes[0].data.find('WANPPPConnection') > 0: self.path = service.parentNode.getElementsByTagName('controlURL')[0].childNodes[0].data - self.upnp_schema = service.childNodes[0].data.split(':')[-2] + self.upnp_schema = re.sub(r'[^A-Za-z0-9:-]', '', service.childNodes[0].data.split(':')[-2]) def AddPortMapping( self, @@ -207,7 +209,7 @@ class uPnPThread(StoppableThread): def __init__(self): super(uPnPThread, self).__init__(name="uPnPThread") - self.extPort = BMConfigParser().safeGetInt('bitmessagesettings', 'extport', default=None) + self.extPort = config.safeGetInt('bitmessagesettings', 'extport', default=None) self.localIP = self.getLocalIP() self.routers = [] self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) @@ -226,24 +228,24 @@ class uPnPThread(StoppableThread): # wait until asyncore binds so that we know the listening port bound = False while state.shutdown == 0 and not self._stopped and not bound: - for s in BMConnectionPool().listeningSockets.values(): + for s in connectionpool.pool.listeningSockets.values(): if s.is_bound(): bound = True if not bound: time.sleep(1) # pylint: disable=attribute-defined-outside-init - self.localPort = BMConfigParser().getint('bitmessagesettings', 'port') + self.localPort = config.getint('bitmessagesettings', 'port') - while state.shutdown == 0 and BMConfigParser().safeGetBoolean('bitmessagesettings', 'upnp'): + while state.shutdown == 0 and config.safeGetBoolean('bitmessagesettings', 'upnp'): if time.time() - lastSent > self.sendSleep and not self.routers: try: self.sendSearchRouter() - except: # noqa:E722 + except: # nosec B110 # noqa:E722 # pylint:disable=bare-except pass lastSent = time.time() try: - while state.shutdown == 0 and BMConfigParser().safeGetBoolean('bitmessagesettings', 'upnp'): + while state.shutdown == 0 and config.safeGetBoolean('bitmessagesettings', 'upnp'): resp, (ip, _) = self.sock.recvfrom(1000) if resp is None: continue @@ -282,11 +284,11 @@ class uPnPThread(StoppableThread): self.createPortMapping(router) try: self.sock.shutdown(socket.SHUT_RDWR) - except: # noqa:E722 + except (IOError, OSError): # noqa:E722 pass try: self.sock.close() - except: # noqa:E722 + except (IOError, OSError): # noqa:E722 pass deleted = False for router in self.routers: @@ -331,7 +333,7 @@ class uPnPThread(StoppableThread): elif i == 1 and self.extPort: extPort = self.extPort # try external port from last time next else: - extPort = randint(32767, 65535) + extPort = randint(32767, 65535) # nosec B311 logger.debug( "Attempt %i, requesting UPnP mapping for %s:%i on external port %i", i, @@ -340,8 +342,8 @@ class uPnPThread(StoppableThread): extPort) router.AddPortMapping(extPort, self.localPort, localIP, 'TCP', 'BitMessage') self.extPort = extPort - BMConfigParser().set('bitmessagesettings', 'extport', str(extPort)) - BMConfigParser().save() + config.set('bitmessagesettings', 'extport', str(extPort)) + config.save() break except UPnPError: logger.debug("UPnP error: ", exc_info=True) diff --git a/tests-kivy.py b/tests-kivy.py index 92b10b86..9bc08880 100644 --- a/tests-kivy.py +++ b/tests-kivy.py @@ -1,9 +1,13 @@ #!/usr/bin/env python """Custom tests runner script for tox and python3""" +import os import random # noseq +import subprocess import sys import unittest +from time import sleep + def unittest_discover(): """Explicit test suite creation""" @@ -13,5 +17,28 @@ def unittest_discover(): if __name__ == "__main__": + in_docker = os.path.exists("/.dockerenv") + + if in_docker: + try: + os.mkdir("../out") + except FileExistsError: # noqa:F821 + pass + + ffmpeg = subprocess.Popen([ # pylint: disable=consider-using-with + "ffmpeg", "-y", "-nostdin", "-f", "x11grab", "-video_size", "720x1280", + "-v", "quiet", "-nostats", + "-draw_mouse", "0", "-i", os.environ['DISPLAY'], + "-codec:v", "libvpx-vp9", "-lossless", "1", "-r", "60", + "../out/test.webm" + ]) + sleep(2) # let ffmpeg start result = unittest.TextTestRunner(verbosity=2).run(unittest_discover()) + sleep(1) + if in_docker: + ffmpeg.terminate() + try: + ffmpeg.wait(10) + except subprocess.TimeoutExpired: + ffmpeg.kill() sys.exit(not result.wasSuccessful()) diff --git a/tox.ini b/tox.ini index 632c7381..3524bb57 100644 --- a/tox.ini +++ b/tox.ini @@ -1,17 +1,42 @@ [tox] -envlist = reset,py{27,27-portable,36,38,39},stats +requires = virtualenv<20.22.0 +envlist = reset,py{27,27-portable,35,36,38,39,310},stats skip_missing_interpreters = true [testenv] setenv = BITMESSAGE_HOME = {envtmpdir} + HOME = {envtmpdir} PYTHONWARNINGS = default deps = -rrequirements.txt commands = python checkdeps.py - coverage run -a src/bitmessagemain.py -t + python src/bitmessagemain.py -t coverage run -a -m tests +[testenv:lint-basic] +skip_install = true +basepython = python3 +deps = + bandit + flake8 +commands = + bandit -r -s B101,B411,B413,B608 \ + -x checkdeps.*,bitmessagecurses,bitmessageqt,tests pybitmessage + flake8 pybitmessage --count --select=E9,F63,F7,F82 \ + --show-source --statistics + +[testenv:lint] +skip_install = true +basepython = python3 +deps = + -rrequirements.txt + pylint +commands = pylint --rcfile=tox.ini --exit-zero pybitmessage + +[testenv:py27] +sitepackages = true + [testenv:py27-doc] deps = .[docs] @@ -22,11 +47,16 @@ commands = python setup.py build_sphinx skip_install = true commands = python pybitmessage/bitmessagemain.py -t +[testenv:py35] +skip_install = true + [testenv:reset] +skip_install = true deps = coverage commands = coverage erase [testenv:stats] +skip_install = true deps = coverage commands = coverage report @@ -35,12 +65,22 @@ commands = [coverage:run] source = src omit = - */lib* tests.py */tests/* + src/bitmessagekivy/* src/version.py - */__init__.py src/fallback/umsgpack/* [coverage:report] ignore_errors = true + +[pylint.main] +disable = + invalid-name,consider-using-f-string,fixme,raise-missing-from, + super-with-arguments,unnecessary-pass,unknown-option-value, + unspecified-encoding,useless-object-inheritance,useless-option-value +ignore = bitmessagecurses,bitmessagekivy,bitmessageqt,messagetypes,mockbm, + network,plugins,umsgpack,bitmessagecli.py + +max-args = 8 +max-attributes = 8