diff --git a/.buildbot/android/Dockerfile b/.buildbot/android/Dockerfile new file mode 100755 index 0000000000..0fa81a4cd5 --- /dev/null +++ b/.buildbot/android/Dockerfile @@ -0,0 +1,105 @@ +# 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 + +# pyzbar dependencies +RUN apt-get -y install -qq --no-install-recommends libzbar0 libtool gettext + +RUN pip install buildozer cython==3.0.10 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 0000000000..f2c08ac5f1 --- /dev/null +++ b/.buildbot/android/build.sh @@ -0,0 +1,32 @@ +#!/bin/bash +export LC_ALL=en_US.UTF-8 +export LANG=en_US.UTF-8 + +# buildozer OOM workaround +mkdir -p ~/.gradle +echo "org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8" \ + > ~/.gradle/gradle.properties + +# workaround for symlink +rm -rf src/pybitmessage +mkdir -p src/pybitmessage +cp src/*.py src/pybitmessage +cp -r src/bitmessagekivy src/backend src/mockbm src/images src/pybitmessage + +pushd packages/android + +BUILDMODE=debug + +if [ "$BUILDBOT_JOBNAME" = "android" -a \ + "$BUILDBOT_REPOSITORY" = "https://github.com/Bitmessage/PyBitmessage" -a \ + "$BUILDBOT_BRANCH" = "v0.6" ]; then + sed -e 's/android.release_artifact *=.*/release_artifact = aab/' -i "" buildozer.spec + BUILDMODE=release +fi + +buildozer android $BUILDMODE || exit $? +popd + +mkdir -p ../out +RELEASE_ARTIFACT=$(grep release_artifact packages/android/buildozer.spec |cut -d= -f2|tr -Cd 'a-z') +cp packages/android/bin/*.${RELEASE_ARTIFACT} ../out diff --git a/.buildbot/android/test.sh b/.buildbot/android/test.sh new file mode 100755 index 0000000000..65a0fe7dfa --- /dev/null +++ b/.buildbot/android/test.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +RELEASE_ARTIFACT=$(grep release_artifact packages/android/buildozer.spec |cut -d= -f2|tr -Cd 'a-z') + +if [ $RELEASE_ARTIFACT = "aab" ]; then + exit +fi + +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 0000000000..b3d9ce7542 --- /dev/null +++ b/.buildbot/appimage/Dockerfile @@ -0,0 +1,28 @@ +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 . . + +CMD .buildbot/appimage/build.sh diff --git a/.buildbot/appimage/build.sh b/.buildbot/appimage/build.sh new file mode 100755 index 0000000000..9ce723303b --- /dev/null +++ b/.buildbot/appimage/build.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +export APPIMAGE_EXTRACT_AND_RUN=1 +BUILDER=appimage-builder-x86_64.AppImage +RECIPE=packages/AppImage/AppImageBuilder.yml + +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)" +export APP_VERSION=$(git describe --tags | cut -d- -f1,3 | tr -d v) +[ "$HEAD" != "$UPSTREAM" ] && APP_VERSION="${APP_VERSION}-alpha" + +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://artifacts.bitmessage.at/appimage/38930/appimage-builder-0.1.dev1035%2Bgce669f6-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 0000000000..871fc83aab --- /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 0000000000..c9c98b01d8 --- /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<71' pip diff --git a/.buildbot/kivy/build.sh b/.buildbot/kivy/build.sh new file mode 100755 index 0000000000..87aae8f757 --- /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 0000000000..3231f25064 --- /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 0000000000..7fde093d14 --- /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 0000000000..3a83ade77f --- /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 0000000000..1acf58dcd7 --- /dev/null +++ b/.buildbot/tox-bionic/Dockerfile @@ -0,0 +1,26 @@ +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 + +ADD . . + +CMD .buildbot/tox-bionic/test.sh diff --git a/.buildbot/tox-bionic/build.sh b/.buildbot/tox-bionic/build.sh new file mode 100755 index 0000000000..87f670ce54 --- /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 0000000000..b280953a42 --- /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 0000000000..fecc081985 --- /dev/null +++ b/.buildbot/tox-focal/Dockerfile @@ -0,0 +1,17 @@ +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 . . + +CMD .buildbot/tox-focal/test.sh diff --git a/.buildbot/tox-focal/test.sh b/.buildbot/tox-focal/test.sh new file mode 120000 index 0000000000..a9f8525c17 --- /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 0000000000..b15c3b8f27 --- /dev/null +++ b/.buildbot/tox-jammy/Dockerfile @@ -0,0 +1,16 @@ +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 + +ADD . . + +CMD .buildbot/tox-jammy/test.sh diff --git a/.buildbot/tox-jammy/test.sh b/.buildbot/tox-jammy/test.sh new file mode 100755 index 0000000000..ab6134c458 --- /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 0000000000..9b687f8f79 --- /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 0000000000..465fac95f9 --- /dev/null +++ b/.buildbot/winebuild/build.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +export SNAPSHOT="g$(git describe --contains --always)" + +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 0000000000..5c181dbca1 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,42 @@ +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 \ + python-six \ + 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 0000000000..d00fdd111d --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,41 @@ +{ + "name": "Codespaces Python3", + "customizations": { + "vscode": { + "extensions": [ + "GitHub.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" + ], + "settings": { + "flake8.args": ["--config=setup.cfg"], + "pylint.args": ["--rcfile=setup.cfg", "--init-hook", "import sys;sys.path.append('src')"], + "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 + } + } + } + }, + "remoteUser": "user", + "containerUser": "user", + "dockerFile": "Dockerfile", + "postCreateCommand": ".devcontainer/postCreateCommand.sh", + "updateContentCommand": "python2.7 setup.py install --user", + "remoteEnv": { + "PATH": "${containerEnv:PATH}:/home/user/.local/bin" + } +} diff --git a/.devcontainer/postCreateCommand.sh b/.devcontainer/postCreateCommand.sh new file mode 100755 index 0000000000..2350f686fe --- /dev/null +++ b/.devcontainer/postCreateCommand.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +pip3 install -r requirements.txt +pip3 install -r kivy-requirements.txt \ No newline at end of file diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..c21698ec2b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +bin +build +dist +__pycache__ +.buildozer +.tox +mprofile_* +**.so diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..776a13c110 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# Pickle files (for testing) should always have UNIX line endings. +# Windows issue like here https://stackoverflow.com/questions/556269 +knownnodes.dat text eol=lf diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..fb735a8419 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,30 @@ +## Repository contributions to the PyBitmessage project + +### 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. + +### Documentation + +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 +- To run tests locally use `tox` or `./run-tests-in-docker.sh` + +## Translations + +- For helping with translations, please use [Transifex](https://www.transifex.com/bitmessage-project/pybitmessage/). +- There is no need to submit pull requests for translations. +- For translating technical terms it is recommended to consult the [Microsoft Language Portal](https://www.microsoft.com/Language/en-US/Default.aspx). + +### Gitiquette + +- Make the pull request against the ["v0.6" branch](https://github.com/Bitmessage/PyBitmessage/tree/v0.6) +- PGP-sign the commits included in the pull request +- Use references to tickets, e.g. `addresses #123` or `fixes #234` in your commit messages +- Try to use a good editor that removes trailing whitespace, highlights potential python issues and uses unix line endings +- If for some reason you don't want to use github, you can submit the patch using Bitmessage to the "bitmessage" chan, or to one of the developers. + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..415583a95b --- /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 a11f6fadcb..fc331499e8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,27 @@ **pyc -**dat **.DS_Store src/build src/dist +src/.eggs src/.project src/.pydevproject src/.settings/ src/**/.dll src/**/*.o src/**/*.so +src/**/a.out build/lib.* build/temp.* +bin dist *.egg-info +docs/_*/* +docs/autodoc/ +build +pyan/ +**.coverage +coverage.xml +**htmlcov* +**coverage.json +.buildozer +.tox diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 0000000000..136ef6e92e --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,12 @@ +version: 2 + +build: + os: ubuntu-20.04 + tools: + python: "2.7" + +python: + install: + - requirements: docs/requirements.txt + - method: pip + path: . diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6534ee9ffa..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,12 +0,0 @@ -language: python -python: - - "2.7" -addons: - apt: - packages: - - build-essential - - libcap-dev -install: - - pip install -r requirements.txt - - python setup.py install -script: pybitmessage -t diff --git a/COPYING b/COPYING index b9f945f9f3..279cef2a38 100644 --- a/COPYING +++ b/COPYING @@ -1,5 +1,5 @@ Copyright (c) 2012-2016 Jonathan Warren -Copyright (c) 2013-2018 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 new file mode 100644 index 0000000000..b409d27aec --- /dev/null +++ b/Dockerfile @@ -0,0 +1,37 @@ +# A container for PyBitmessage daemon + +FROM ubuntu:bionic + +RUN apt-get update + +# Install dependencies +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 + +EXPOSE 8444 8442 + +ENV HOME /home/bitmessage +ENV BITMESSAGE_HOME ${HOME} + +WORKDIR ${HOME} +ADD . ${HOME} +COPY packages/docker/launcher.sh /usr/bin/ + +# Install +RUN pip2 install jsonrpclib . + +# Cleanup +RUN rm -rf /var/lib/apt/lists/* +RUN rm -rf ${HOME} + +# Create a user +RUN useradd -r bitmessage && chown -R bitmessage ${HOME} + +USER bitmessage + +# Generate default config +RUN pybitmessage -t + +ENTRYPOINT ["launcher.sh"] +CMD ["-d"] diff --git a/INSTALL.md b/INSTALL.md index 3eda3ae795..1993713d59 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,100 +1,117 @@ # PyBitmessage Installation Instructions +- Binary (64bit, no separate installation of dependencies required) + - Windows: https://artifacts.bitmessage.at/winebuild/ + - 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` + +## Notes on the AppImages + +The [AppImage](https://docs.appimage.org/introduction/index.html) +is a bundle, built by the +[appimage-builder](https://github.com/AppImageCrafters/appimage-builder) from +the Ubuntu Bionic deb files, the sources and `bitmsghash.so`, precompiled for +3 architectures, using the `packages/AppImage/AppImageBuilder.yml` recipe. + +When you run the appimage the bundle is loop mounted to a location like +`/tmp/.mount_PyBitm97wj4K` with `squashfs-tools`. + +The appimage name has several informational filds: +``` +PyBitmessage--g[-alpha]-.AppImage +``` -For an up-to-date version of these instructions, please visit the -[Bitmessage Wiki](https://bitmessage.org/wiki/Compiling_instructions). +E.g. `PyBitmessage-0.6.3.2-ge571ba8a-x86_64.AppImage` is an appimage, built from +the `v0.6` for x86_64 and `PyBitmessage-0.6.3.2-g9de2aaf1-alpha-aarch64.AppImage` +is one, built from some development branch for arm64. -PyBitmessage can be run either straight from source or from an installed -package. +You can also build the appimage with local code. For that you need installed +docker: -## Dependencies -Before running PyBitmessage, make sure you have all the necessary dependencies -installed on your system. +``` +$ docker build -t bm-appimage -f .buildbot/appimage/Dockerfile . +$ docker run -t --rm -v "$(pwd)"/dist:/out bm-appimage +``` -Here's a list of dependencies needed for PyBitmessage -- python2.7 -- python2-qt4 (python-qt4 on Debian/Ubuntu) -- openssl -- (Fedora & Redhat only) openssl-compat-bitcoin-libs +The appimages should be in the dist dir. -## Running PyBitmessage -PyBitmessage can be run two ways: straight from source or via a package which -is installed on your system. Since PyBitmessage is Beta, it is best to run -PyBitmessage from source, so that you may update as needed. -#### Updating -To update PyBitmessage from source (Linux/OS X), you can do these easy steps: +## Helper Script for building from source +Go to the directory with PyBitmessage source code and run: ``` -cd PyBitmessage/src/ -git fetch --all -git reset --hard origin/master -python bitmessagemain.py +python checkdeps.py ``` -Voilà! Bitmessage is updated! +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. -#### Linux -To run PyBitmessage from the command-line, you must download the source, then -run `src/bitmessagemain.py`. +### 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 + +For Debian-based (Ubuntu, Raspbian, PiBang, others) ``` -git clone git://github.com/Bitmessage/PyBitmessage.git -cd PyBitmessage/ && python src/bitmessagemain.py +python2.7 openssl libssl-dev python-msgpack python-qt4 python-six +``` +For Arch Linux +``` +python2 openssl python2-pyqt4 python-six +``` +For Fedora +``` +python python-qt4 openssl-compat-bitcoin-libs python-six +``` +For Red Hat Enterprise Linux (RHEL) +``` +python python-qt4 openssl-compat-bitcoin-libs python-six +``` +For GNU Guix +``` +python2-msgpack python2-pyqt@4.11.4 python2-sip openssl python-six ``` -That's it! *Honestly*! - -#### Windows -On Windows you can download an executable for Bitmessage -[here](https://github.com/Bitmessage/PyBitmessage/releases/download/0.6.3.2/Bitmessage_x86_0.6.3.2.exe). +## setuptools +This is now the recommended and in most cases the easiest way for +installing PyBitmessage. -However, if you would like to run PyBitmessage via Python in Windows, you can -go [here](https://bitmessage.org/wiki/Compiling_instructions#Windows) for -information on how to do so. +There are 2 options for installing with setuptools: root and user. -#### OS X -First off, install Homebrew. +### as root: ``` -ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" +python setup.py install +pybitmessage ``` -Now, install the required dependencies +### as user: ``` -brew install git python pyqt +python setup.py install --user +~/.local/bin/pybitmessage ``` -Download and run PyBitmessage: +## pip venv (daemon): +Create virtualenv with Python 2.x version ``` -git clone git://github.com/Bitmessage/PyBitmessage.git -cd PyBitmessage && python src/bitmessagemain.py +virtualenv -p python2 env ``` -## Creating a package for installation -If you really want, you can make a package for PyBitmessage, which you may -install yourself or distribute to friends. This isn't recommended, since -PyBitmessage is in Beta, and subject to frequent change. - -#### Linux -First off, since PyBitmessage uses something nifty called -[packagemonkey](https://github.com/fuzzgun/packagemonkey), go ahead and get -that installed. You may have to build it from source. - -Next, edit the generate.sh script to your liking. - -Now, run the appropriate script for the type of package you'd like to make +Activate env ``` -arch.sh - create a package for Arch Linux -debian.sh - create a package for Debian/Ubuntu -ebuild.sh - create a package for Gentoo -osx.sh - create a package for OS X -puppy.sh - create a package for Puppy Linux -rpm.sh - create a RPM package -slack.sh - create a package for Slackware +source env/bin/activate ``` -#### OS X -Please refer to -[this page](https://bitmessage.org/forum/index.php/topic,2761.0.html) on the -forums for instructions on how to create a package on OS X. - -Please note that some versions of OS X don't work. +Build & run pybitmessage +``` +pip install . +pybitmessage -d +``` -#### Windows -## TODO: Create Windows package creation instructions +## Alternative way to run PyBitmessage, without setuptools (this isn't recommended) +run `./start.sh`. diff --git a/LICENSE b/LICENSE index 06576583cf..fd77220146 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) Copyright (c) 2012-2016 Jonathan Warren -Copyright (c) 2013-2018 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 @@ -19,3 +19,76 @@ 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. + +===== qidenticon.py identicon python implementation with QPixmap output by sendiulo + +qidenticon.py is Licensed under FreeBSD License. +(http://www.freebsd.org/copyright/freebsd-license.html) + +Copyright 2013 "Sendiulo". All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +===== based on identicon.py identicon python implementation. by Shin Adachi + +identicon.py is Licensed under FreeBSD License. +(http://www.freebsd.org/copyright/freebsd-license.html) + +Copyright 1994-2009 Shin Adachi. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +===== based on asyncore_pollchoose.py asyncore_pollchoose python implementation. by Sam Rushing + +Copyright 1996 by Sam Rushing. All Rights Reserved + +Permission to use, copy, modify, and distribute this software and +its documentation for any purpose and without fee is hereby +granted, provided that the above copyright notice appear in all +copies and that both that copyright notice and this permission +notice appear in supporting documentation, and that the name of Sam +Rushing not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. + +SAM RUSHING DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN +NO EVENT SHALL SAM RUSHING BE LIABLE FOR ANY SPECIAL, INDIRECT OR +CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +===== based on namecoin.py namecoin.py python implementation by Daniel Kraft + +Copyright (C) 2013 by Daniel Kraft + +This file is part of the Bitmessage project. + +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 the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +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. diff --git a/MANIFEST.in b/MANIFEST.in index 2d29971b8e..15a6bf8111 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,5 @@ include COPYING include README.md +include requirements.txt recursive-include desktop * +recursive-include packages/apparmor * diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 91c3bc969e..0000000000 --- a/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,14 +0,0 @@ -## Code contributions to the Bitmessage project - -- try to explain what the code is about -- try to follow [PEP0008](https://www.python.org/dev/peps/pep-0008/) -- make the pull request against the ["v0.6" branch](https://github.com/Bitmessage/PyBitmessage/tree/v0.6) -- it should be possible to do a fast-forward merge of the pull requests -- PGP-sign the commits included in the pull request -- You can get paid for merged commits if you register at [Tip4Commit](https://tip4commit.com/github/Bitmessage/PyBitmessage) - -If for some reason you don't want to use github, you can submit the patch using Bitmessage to the "bitmessage" chan, or to one of the developers. -## Translations - -For helping with translations, please use [Transifex](https://www.transifex.com/bitmessage-project/pybitmessage/). There is no need to submit pull requests for translations. -For translating technical terms it is recommended to consult the [Microsoft Language Portal](https://www.microsoft.com/Language/en-US/Default.aspx). \ No newline at end of file diff --git a/README.md b/README.md index 06cba09840..06c97c011e 100644 --- a/README.md +++ b/README.md @@ -14,18 +14,17 @@ Development ---------- Bitmessage is a collaborative project. You are welcome to submit pull requests although if you plan to put a non-trivial amount of work into coding new -features, it is recommended that you first solicit feedback on the DevTalk -pseudo-mailing list: -BM-2D9QKN4teYRvoq2fyzpiftPh9WP9qggtzh +features, it is recommended that you first describe your ideas in the +separate issue. -Feel welcome to join chan "bitmessage", BM-2cWy7cvHoq3f1rYMerRJp8PT653jjSuEdY -which is on preview here: http://beamstat.com/chan/bitmessage +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) * [Chat on Gitter](https://gitter.im/Bitmessage/PyBitmessage) + diff --git a/build/changelang.sh b/build/changelang.sh deleted file mode 100755 index 915c5dead2..0000000000 --- a/build/changelang.sh +++ /dev/null @@ -1,16 +0,0 @@ -export LANG=de_DE.UTF-8 -export LANGUAGE=de_DE -export LC_CTYPE="de_DE.UTF-8" -export LC_NUMERIC=de_DE.UTF-8 -export LC_TIME=de_DE.UTF-8 -export LC_COLLATE="de_DE.UTF-8" -export LC_MONETARY=de_DE.UTF-8 -export LC_MESSAGES="de_DE.UTF-8" -export LC_PAPER=de_DE.UTF-8 -export LC_NAME=de_DE.UTF-8 -export LC_ADDRESS=de_DE.UTF-8 -export LC_TELEPHONE=de_DE.UTF-8 -export LC_MEASUREMENT=de_DE.UTF-8 -export LC_IDENTIFICATION=de_DE.UTF-8 -export LC_ALL= -python2.7 src/bitmessagemain.py diff --git a/build/compiletest.py b/build/compiletest.py deleted file mode 100755 index fdbf7db1b1..0000000000 --- a/build/compiletest.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/python2.7 - -import ctypes -import fnmatch -import os -import sys -import traceback - -matches = [] -for root, dirnames, filenames in os.walk('src'): - for filename in fnmatch.filter(filenames, '*.py'): - matches.append(os.path.join(root, filename)) - -for filename in matches: - source = open(filename, 'r').read() + '\n' - try: - compile(source, filename, 'exec') - except Exception as e: - if 'win' in sys.platform: - ctypes.windll.user32.MessageBoxA(0, traceback.format_exc(), "Exception in " + filename, 1) - else: - print "Exception in %s: %s" % (filename, traceback.format_exc()) - sys.exit(1) diff --git a/build/mergepullrequest.sh b/build/mergepullrequest.sh deleted file mode 100755 index 35e875664b..0000000000 --- a/build/mergepullrequest.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -if [ -z "$1" ]; then - echo "You must specify pull request number" - exit -fi - -git pull -git checkout v0.6 -git fetch origin pull/"$1"/head:"$1" -git merge --ff-only "$1" diff --git a/build/README.md b/buildscripts/README.md similarity index 100% rename from build/README.md rename to buildscripts/README.md diff --git a/buildscripts/androiddev.sh b/buildscripts/androiddev.sh new file mode 100755 index 0000000000..1634d4c003 --- /dev/null +++ b/buildscripts/androiddev.sh @@ -0,0 +1,112 @@ +#!/bin/sh + +ANDROID_HOME="/opt/android" +get_python_version=3 + +# INSTALL ANDROID PACKAGES +install_android_pkg () +{ + BUILDOZER_VERSION=1.2.0 + CYTHON_VERSION=0.29.15 + pip3 install buildozer==$BUILDOZER_VERSION + pip3 install --upgrade cython==$CYTHON_VERSION +} + +# SYSTEM DEPENDENCIES +system_dependencies () +{ + apt -y update -qq + apt -y install --no-install-recommends python3-pip pip3 python3 virtualenv python3-setuptools python3-wheel git wget unzip sudo patch bzip2 lzma + apt -y autoremove +} + +# build dependencies +# https://buildozer.readthedocs.io/en/latest/installation.html#android-on-ubuntu-16-04-64bit +build_dependencies () +{ + dpkg --add-architecture i386 + apt -y update -qq + apt -y install -qq --no-install-recommends build-essential ccache git python3 python3-dev libncurses5:i386 libstdc++6:i386 libgtk2.0-0:i386 libpangox-1.0-0:i386 libpangoxft-1.0-0:i386 libidn11:i386 zip zlib1g-dev zlib1g:i386 + apt -y autoremove + apt -y clean +} + +# RECIPES DEPENDENCIES +specific_recipes_dependencies () +{ + dpkg --add-architecture i386 + apt -y update -qq + apt -y install -qq --no-install-recommends libffi-dev autoconf automake cmake gettext libltdl-dev libtool pkg-config + apt -y autoremove + apt -y clean +} + +# INSTALL NDK +install_ndk() +{ + ANDROID_NDK_VERSION=23b + ANDROID_NDK_HOME="${ANDROID_HOME}/android-ndk" + ANDROID_NDK_HOME_V="${ANDROID_NDK_HOME}-r${ANDROID_NDK_VERSION}" + # get the latest version from https://developer.android.com/ndk/downloads/index.html + ANDROID_NDK_ARCHIVE="android-ndk-r${ANDROID_NDK_VERSION}-linux.zip" + ANDROID_NDK_DL_URL="https://dl.google.com/android/repository/${ANDROID_NDK_ARCHIVE}" + wget -nc ${ANDROID_NDK_DL_URL} + mkdir --parents "${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}" +} + +# INSTALL SDK +install_sdk() +{ + 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}" + wget -nc ${ANDROID_SDK_TOOLS_DL_URL} + mkdir --parents "${ANDROID_SDK_HOME}" + unzip -q "${ANDROID_SDK_TOOLS_ARCHIVE}" -d "${ANDROID_SDK_HOME}" + rm -rf "${ANDROID_SDK_TOOLS_ARCHIVE}" + # update Android SDK, install Android API, Build Tools... + mkdir --parents "${ANDROID_SDK_HOME}/.android/" + 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-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 + "${ANDROID_SDK_HOME}/tools/bin/sdkmanager" "platforms;android-24" > /dev/null + "${ANDROID_SDK_HOME}/tools/bin/sdkmanager" "platforms;android-28" > /dev/null + "${ANDROID_SDK_HOME}/tools/bin/sdkmanager" "build-tools;${ANDROID_SDK_BUILD_TOOLS_VERSION}" > /dev/null + "${ANDROID_SDK_HOME}/tools/bin/sdkmanager" "extras;android;m2repository" > /dev/null + find /opt/android/android-sdk -type f -perm /0111 -print0|xargs -0 chmod a+x + chown -R buildbot.buildbot /opt/android/android-sdk + chmod +x "${ANDROID_SDK_HOME}/tools/bin/avdmanager" +} + +# INSTALL APACHE-ANT +install_ant() +{ + 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}" + wget -nc ${APACHE_ANT_DL_URL} + tar -xf "${APACHE_ANT_ARCHIVE}" -C "${ANDROID_HOME}" + ln -sfn "${APACHE_ANT_HOME_V}" "${APACHE_ANT_HOME}" + rm -rf "${APACHE_ANT_ARCHIVE}" +} + +system_dependencies +build_dependencies +specific_recipes_dependencies +install_android_pkg +install_ndk +install_sdk +install_ant \ No newline at end of file diff --git a/buildscripts/appimage.sh b/buildscripts/appimage.sh new file mode 100755 index 0000000000..a56917835c --- /dev/null +++ b/buildscripts/appimage.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# Cleanup +rm -rf PyBitmessage +export VERSION=$(python setup.py --version) + +[ -f "pkg2appimage" ] || wget -O "pkg2appimage" https://github.com/AppImage/pkg2appimage/releases/download/continuous/pkg2appimage-1807-x86_64.AppImage +chmod a+x pkg2appimage + +echo "Building AppImage" + +if grep docker /proc/1/cgroup; then + export APPIMAGE_EXTRACT_AND_RUN=1 + mkdir PyBitmessage + wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -O PyBitmessage/appimagetool \ + && chmod +x PyBitmessage/appimagetool +fi + +./pkg2appimage packages/AppImage/PyBitmessage.yml + +./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_EXPANDED}.AppImage"; + out/PyBitmessage-${VERSION_EXPANDED}.AppImage -t +else + echo "Build Failed"; + exit 1 +fi diff --git a/build/osx.sh b/buildscripts/osx.sh similarity index 100% rename from build/osx.sh rename to buildscripts/osx.sh diff --git a/buildscripts/update_translation_source.sh b/buildscripts/update_translation_source.sh new file mode 100644 index 0000000000..205767cbd5 --- /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/build/updatetranslations.sh b/buildscripts/updatetranslations.sh similarity index 100% rename from build/updatetranslations.sh rename to buildscripts/updatetranslations.sh diff --git a/buildscripts/winbuild.sh b/buildscripts/winbuild.sh new file mode 100755 index 0000000000..4c52a1accb --- /dev/null +++ b/buildscripts/winbuild.sh @@ -0,0 +1,204 @@ +#!/bin/bash + +# INIT +MACHINE_TYPE=$(uname -m) +BASE_DIR=$(pwd) +PYTHON_VERSION=2.7.17 +PYQT_VERSION=4-4.11.4-gpl-Py2.7-Qt4.8.7 +OPENSSL_VERSION=1_0_2t +SRCPATH=~/Downloads + +#Functions +function download_sources_32 { + if [ ! -d ${SRCPATH} ]; then + mkdir -p ${SRCPATH} + fi + wget -P ${SRCPATH} -c -nc --content-disposition \ + https://www.python.org/ftp/python/${PYTHON_VERSION}/python-${PYTHON_VERSION}.msi \ + https://web.archive.org/web/20210420044701/https://download.microsoft.com/download/1/1/1/1116b75a-9ec3-481a-a3c8-1777b5381140/vcredist_x86.exe \ + https://github.com/Bitmessage/ThirdPartyLibraries/blob/master/PyQt${PYQT_VERSION}-x32.exe?raw=true \ + https://github.com/Bitmessage/ThirdPartyLibraries/blob/master/Win32OpenSSL-${OPENSSL_VERSION}.exe?raw=true \ + https://github.com/Bitmessage/ThirdPartyLibraries/blob/master/pyopencl-2015.1-cp27-none-win32.whl?raw=true +} + +function download_sources_64 { + if [ ! -d ${SRCPATH} ]; then + mkdir -p ${SRCPATH} + fi + wget -P ${SRCPATH} -c -nc --content-disposition \ + http://www.python.org/ftp/python/${PYTHON_VERSION}/python-${PYTHON_VERSION}.amd64.msi \ + https://download.microsoft.com/download/d/2/4/d242c3fb-da5a-4542-ad66-f9661d0a8d19/vcredist_x64.exe \ + https://github.com/Bitmessage/ThirdPartyLibraries/blob/master/PyQt${PYQT_VERSION}-x64.exe?raw=true \ + https://github.com/Bitmessage/ThirdPartyLibraries/blob/master/Win64OpenSSL-${OPENSSL_VERSION}.exe?raw=true \ + https://github.com/Bitmessage/ThirdPartyLibraries/blob/master/pyopencl-2015.1-cp27-none-win_amd64.whl?raw=true +} + +function download_sources { + if [ "${MACHINE_TYPE}" == 'x86_64' ]; then + download_sources_64 + else + download_sources_32 + fi +} + +function install_wine { + echo "Setting up wine" + if [ "${MACHINE_TYPE}" == 'x86_64' ]; then + export WINEPREFIX=${HOME}/.wine64 WINEARCH=win64 + else + export WINEPREFIX=${HOME}/.wine32 WINEARCH=win32 + fi + rm -rf "${WINEPREFIX}" + rm -rf packages/pyinstaller/{build,dist} +} + +function install_python(){ + cd ${SRCPATH} || exit 1 + if [ "${MACHINE_TYPE}" == 'x86_64' ]; then + echo "Installing Python ${PYTHON_VERSION} 64b" + wine msiexec -i python-${PYTHON_VERSION}.amd64.msi /q /norestart + echo "Installing vcredist for 64 bit" + wine vcredist_x64.exe /q /norestart + else + echo "Installing Python ${PYTHON_VERSION} 32b" + wine msiexec -i python-${PYTHON_VERSION}.msi /q /norestart + # MSVCR 2008 required for Windows XP + cd ${SRCPATH} || exit 1 + echo "Installing vc_redist (2008) for 32 bit " + wine vcredist_x86.exe /Q + fi + echo "Installing pytools 2020.2" + # last version compatible with python 2 + wine python -m pip install pytools==2020.2 + echo "Upgrading pip" + wine python -m pip install --upgrade pip +} + +function install_pyqt(){ + if [ "${MACHINE_TYPE}" == 'x86_64' ]; then + echo "Installing PyQt-${PYQT_VERSION} 64b" + wine PyQt${PYQT_VERSION}-x64.exe /S /WX + else + echo "Installing PyQt-${PYQT_VERSION} 32b" + wine PyQt${PYQT_VERSION}-x32.exe /S /WX + fi +} + +function install_openssl(){ + if [ "${MACHINE_TYPE}" == 'x86_64' ]; then + echo "Installing OpenSSL ${OPENSSL_VERSION} 64b" + wine Win64OpenSSL-${OPENSSL_VERSION}.exe /q /norestart /silent /verysilent /sp- /suppressmsgboxes + else + echo "Installing OpenSSL ${OPENSSL_VERSION} 32b" + wine Win32OpenSSL-${OPENSSL_VERSION}.exe /q /norestart /silent /verysilent /sp- /suppressmsgboxes + fi +} + +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 + # 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() +{ + cd "${BASE_DIR}" || exit 1 + echo "Installing pip depends" + wine python -m pip install msgpack-python .[json] .[qrcode] .[tor] .[xml] + python setup.py egg_info +} + +function install_pyopencl() +{ + cd "${SRCPATH}" || exit 1 + echo "Installing PyOpenCL" + if [ "${MACHINE_TYPE}" == 'x86_64' ]; then + wine python -m pip install pyopencl-2015.1-cp27-none-win_amd64.whl + else + wine python -m pip install pyopencl-2015.1-cp27-none-win32.whl + fi + sed -Ei 's/_DEFAULT_INCLUDE_OPTIONS = .*/_DEFAULT_INCLUDE_OPTIONS = [] /' \ + "$WINEPREFIX/drive_c/Python27/Lib/site-packages/pyopencl/__init__.py" +} + +function build_dll(){ + cd "${BASE_DIR}" || exit 1 + cd src/bitmsghash || exit 1 + if [ "${MACHINE_TYPE}" == 'x86_64' ]; then + echo "Create dll" + 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=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 \ + -fPIC -shared -lcrypt32 -leay32 -lwsock32 \ + -o bitmsghash64.dll -Wl,--out-implib,bitmsghash.a + else + echo "Create dll" + 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=i686 \ + "-I$HOME/.wine32/drive_c/OpenSSL-Win32/include" \ + "-L$HOME/.wine32/drive_c/OpenSSL-Win32/lib/MinGW" \ + -fPIC -shared -lcrypt32 -leay32 -lwsock32 \ + -o bitmsghash32.dll -Wl,--out-implib,bitmsghash.a + fi +} + +function build_exe(){ + cd "${BASE_DIR}" || exit 1 + cd packages/pyinstaller || exit 1 + wine pyinstaller bitmessagemain.spec +} + +function dryrun_exe(){ + 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 +# dpkg --add-architecture i386 +# apt update +# apt -y install wget wine-stable wine-development winetricks mingw-w64 wine32 wine64 xvfb + + +download_sources +if [ "$1" == "--download-only" ]; then + exit +fi + +install_wine +install_python +install_pyqt +install_openssl +install_pyopencl +install_pip_depends +install_pyinstaller +build_dll +build_exe +dryrun_exe diff --git a/checkdeps.py b/checkdeps.py old mode 100644 new mode 100755 index 05f0094424..0a28a6d256 --- a/checkdeps.py +++ b/checkdeps.py @@ -1,87 +1,33 @@ -"""Check dependendies and give recommendations about how to satisfy them""" +#!/usr/bin/env python +""" +Check dependencies and give recommendations about how to satisfy them +Limitations: + + * Does not detect whether packages are already installed. Solving this requires writing more of a configuration + management system. Or we could switch to an existing one. + * Not fully PEP508 compliant. Not slightly. It makes bold assumptions about the simplicity of the contents of + EXTRAS_REQUIRE. This is fine because most developers do, too. +""" + +import os +import sys from distutils.errors import CompileError try: from setuptools.dist import Distribution from setuptools.extension import Extension from setuptools.command.build_ext import build_ext HAVE_SETUPTOOLS = True + # another import from setuptools is in setup.py + from setup import EXTRAS_REQUIRE except ImportError: HAVE_SETUPTOOLS = False + EXTRAS_REQUIRE = {} + from importlib import import_module -import os -import sys -PACKAGE_MANAGER = { - "OpenBSD": "pkg_add", - "FreeBSD": "pkg install", - "Debian": "apt-get install", - "Ubuntu": "apt-get install", - "Ubuntu 12": "apt-get install", - "openSUSE": "zypper install", - "Fedora": "dnf install", - "Guix": "guix package -i", - "Gentoo": "emerge" -} +from src.depends import detectOS, PACKAGES, PACKAGE_MANAGER -PACKAGES = { - "PyQt4": { - "OpenBSD": "py-qt4", - "FreeBSD": "py27-qt4", - "Debian": "python-qt4", - "Ubuntu": "python-qt4", - "Ubuntu 12": "python-qt4", - "openSUSE": "python-qt", - "Fedora": "PyQt4", - "Guix": "python2-pyqt@4.11.4", - "Gentoo": "dev-python/PyQt4", - 'optional': True, - 'description': "You only need PyQt if you want to use the GUI. " \ - "When only running as a daemon, this can be skipped.\n" \ - "However, you would have to install it manually " \ - "because setuptools does not support PyQt." - }, - "msgpack": { - "OpenBSD": "py-msgpack", - "FreeBSD": "py27-msgpack-python", - "Debian": "python-msgpack", - "Ubuntu": "python-msgpack", - "Ubuntu 12": "msgpack-python", - "openSUSE": "python-msgpack-python", - "Fedora": "python2-msgpack", - "Guix": "python2-msgpack", - "Gentoo": "dev-python/msgpack", - "optional": True, - "description": "python-msgpack is recommended for improved performance of message encoding/decoding" - }, - "pyopencl": { - "FreeBSD": "py27-pyopencl", - "Debian": "python-pyopencl", - "Ubuntu": "python-pyopencl", - "Ubuntu 12": "python-pyopencl", - "Fedora": "python2-pyopencl", - "openSUSE": "", - "OpenBSD": "", - "Guix": "", - "Gentoo": "dev-python/pyopencl", - "optional": True, - 'description': "If you install pyopencl, you will be able to use " \ - "GPU acceleration for proof of work. \n" \ - "You also need a compatible GPU and drivers." - }, - "setuptools": { - "OpenBSD": "py-setuptools", - "FreeBSD": "py27-setuptools", - "Debian": "python-setuptools", - "Ubuntu": "python-setuptools", - "Ubuntu 12": "python-setuptools", - "Fedora": "python2-setuptools", - "openSUSE": "python-setuptools", - "Guix": "python2-setuptools", - "Gentoo": "", - "optional": False, - } -} COMPILING = { "Debian": "build-essential libssl-dev", @@ -91,46 +37,24 @@ "optional": False, } -def detectOSRelease(): - with open("/etc/os-release", 'r') as osRelease: - version = None - for line in osRelease: - if line.startswith("NAME="): - line = line.lower() - if "fedora" in line: - detectOS.result = "Fedora" - elif "opensuse" in line: - detectOS.result = "openSUSE" - elif "ubuntu" in line: - detectOS.result = "Ubuntu" - elif "debian" in line: - detectOS.result = "Debian" - elif "gentoo" in line or "calculate" in line: - detectOS.result = "Gentoo" - else: - detectOS.result = None - if line.startswith("VERSION_ID="): - try: - version = float(line.split("=")[1].replace("\"", "")) - except ValueError: - pass - if detectOS.result == "Ubuntu" and version < 14: - detectOS.result = "Ubuntu 12" - -def detectOS(): - if detectOS.result is not None: - return detectOS.result - if sys.platform.startswith('openbsd'): - detectOS.result = "OpenBSD" - elif sys.platform.startswith('freebsd'): - detectOS.result = "FreeBSD" - elif sys.platform.startswith('win'): - detectOS.result = "Windows" - elif os.path.isfile("/etc/os-release"): - detectOSRelease() - elif os.path.isfile("/etc/config.scm"): - detectOS.result = "Guix" - return detectOS.result +# OS-specific dependencies for optional components listed in EXTRAS_REQUIRE +EXTRAS_REQUIRE_DEPS = { + # The values from setup.EXTRAS_REQUIRE + 'python_prctl': { + # The packages needed for this requirement, by OS + "OpenBSD": [""], + "FreeBSD": [""], + "Debian": ["libcap-dev python-prctl"], + "Ubuntu": ["libcap-dev python-prctl"], + "Ubuntu 12": ["libcap-dev python-prctl"], + "Ubuntu 20": [""], + "openSUSE": [""], + "Fedora": ["prctl"], + "Guix": [""], + "Gentoo": ["dev-python/python-prctl"], + }, +} + def detectPrereqs(missing=True): available = [] @@ -144,18 +68,21 @@ def detectPrereqs(missing=True): available.append(module) return available + def prereqToPackages(): if not detectPrereqs(): return - print "%s %s" % ( + print("%s %s" % ( PACKAGE_MANAGER[detectOS()], " ".join( - PACKAGES[x][detectOS()] for x in detectPrereqs())) + PACKAGES[x][detectOS()] for x in detectPrereqs()))) + def compilerToPackages(): if not detectOS() in COMPILING: return - print "%s %s" % ( - PACKAGE_MANAGER[detectOS.result], COMPILING[detectOS.result]) + print("%s %s" % ( + PACKAGE_MANAGER[detectOS.result], COMPILING[detectOS.result])) + def testCompiler(): if not HAVE_SETUPTOOLS: @@ -182,34 +109,70 @@ def testCompiler(): fullPath = os.path.join(cmd.build_lib, cmd.get_ext_filename("bitmsghash")) return os.path.isfile(fullPath) -detectOS.result = None -prereqs = detectPrereqs() +prereqs = detectPrereqs() compiler = testCompiler() if (not compiler or prereqs) and detectOS() in PACKAGE_MANAGER: - print "It looks like you're using %s. " \ - "It is highly recommended to use the package manager\n" \ - "to install the missing dependencies." % (detectOS.result) + print( + "It looks like you're using %s. " + "It is highly recommended to use the package manager\n" + "to install the missing dependencies." % detectOS.result) if not compiler: - print "Building the bitmsghash module failed.\n" \ - "You may be missing a C++ compiler and/or the OpenSSL headers." + print( + "Building the bitmsghash module failed.\n" + "You may be missing a C++ compiler and/or the OpenSSL headers.") if prereqs: - mandatory = list(x for x in prereqs if "optional" not in PACKAGES[x] or not PACKAGES[x]["optional"]) - optional = list(x for x in prereqs if "optional" in PACKAGES[x] and PACKAGES[x]["optional"]) + mandatory = [x for x in prereqs if not PACKAGES[x].get("optional")] + optional = [x for x in prereqs if PACKAGES[x].get("optional")] if mandatory: - print "Missing mandatory dependencies: %s" % (" ".join(mandatory)) + print("Missing mandatory dependencies: %s" % " ".join(mandatory)) if optional: - print "Missing optional dependencies: %s" % (" ".join(optional)) + print("Missing optional dependencies: %s" % " ".join(optional)) for package in optional: - print PACKAGES[package].get('description') - -if (not compiler or prereqs) and detectOS() in PACKAGE_MANAGER: - print "You can install the missing dependencies by running, as root:" + print(PACKAGES[package].get('description')) + +# Install the system dependencies of optional extras_require components +OPSYS = detectOS() +CMD = PACKAGE_MANAGER[OPSYS] if OPSYS in PACKAGE_MANAGER else 'UNKNOWN_INSTALLER' +for lhs, rhs in EXTRAS_REQUIRE.items(): + if OPSYS is None: + break + if rhs and any([ + EXTRAS_REQUIRE_DEPS[x][OPSYS] + for x in rhs + if x in EXTRAS_REQUIRE_DEPS + ]): + try: + import_module(lhs) + except Exception as e: + rhs_cmd = ''.join([ + CMD, + ' ', + ' '.join([ + ''. join([ + xx for xx in EXTRAS_REQUIRE_DEPS[x][OPSYS] + ]) + for x in rhs + if x in EXTRAS_REQUIRE_DEPS + ]), + ]) + print( + "Optional dependency `pip install .[{}]` would require `{}`" + " to be run as root".format(lhs, rhs_cmd)) + +if detectOS.result == "Ubuntu 20": + print( + "Qt interface isn't supported in %s" % detectOS.result) + +if (not compiler or prereqs) and OPSYS in PACKAGE_MANAGER: + print("You can install the missing dependencies by running, as root:") if not compiler: compilerToPackages() prereqToPackages() + if prereqs and mandatory: + sys.exit(1) else: - print "All the dependencies satisfied, you can install PyBitmessage" + print("All the dependencies satisfied, you can install PyBitmessage") diff --git a/configure b/configure deleted file mode 100755 index 0519ecba6e..0000000000 --- a/configure +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/desktop/pybitmessage.desktop b/desktop/pybitmessage.desktop index 0597044033..c30276e4f3 100644 --- a/desktop/pybitmessage.desktop +++ b/desktop/pybitmessage.desktop @@ -6,4 +6,4 @@ Comment=Send encrypted messages Exec=pybitmessage %F Icon=pybitmessage Terminal=false -Categories=Office;Email; +Categories=Office;Email;Network; diff --git a/dev/bloomfiltertest.py b/dev/bloomfiltertest.py index 539d00f37f..19c93c7642 100644 --- a/dev/bloomfiltertest.py +++ b/dev/bloomfiltertest.py @@ -1,10 +1,16 @@ -from math import ceil -from os import stat, getenv, path -from pybloom import BloomFilter as BloomFilter1 -from pybloomfilter import BloomFilter as BloomFilter2 +""" +dev/bloomfiltertest.py +====================== + +""" + import sqlite3 +from os import getenv, path from time import time +from pybloom import BloomFilter as BloomFilter1 # pylint: disable=import-error +from pybloomfilter import BloomFilter as BloomFilter2 # pylint: disable=import-error + # Ubuntu: apt-get install python-pybloomfiltermmap conn = sqlite3.connect(path.join(getenv("HOME"), '.config/PyBitmessage/messages.dat')) @@ -41,20 +47,20 @@ except IndexError: pass -#f = open("/home/shurdeek/tmp/bloom.dat", "wb") -#sb1.tofile(f) -#f.close() +# f = open("/home/shurdeek/tmp/bloom.dat", "wb") +# sb1.tofile(f) +# f.close() -print "Item count: %i" % (itemcount) -print "Raw length: %i" % (rawlen) -print "Bloom filter 1 length: %i, reduction to: %.2f%%" % \ +print("Item count: %i" % (itemcount)) +print("Raw length: %i" % (rawlen)) +print("Bloom filter 1 length: %i, reduction to: %.2f%%" % (bf1.bitarray.buffer_info()[1], - 100.0 * bf1.bitarray.buffer_info()[1] / rawlen) -print "Bloom filter 1 capacity: %i and error rate: %.3f%%" % (bf1.capacity, 100.0 * bf1.error_rate) -print "Bloom filter 1 took %.2fs" % (bf1time) -print "Bloom filter 2 length: %i, reduction to: %.3f%%" % \ + 100.0 * bf1.bitarray.buffer_info()[1] / rawlen)) +print("Bloom filter 1 capacity: %i and error rate: %.3f%%" % (bf1.capacity, 100.0 * bf1.error_rate)) +print("Bloom filter 1 took %.2fs" % (bf1time)) +print("Bloom filter 2 length: %i, reduction to: %.3f%%" % (bf2.num_bits / 8, - 100.0 * bf2.num_bits / 8 / rawlen) -print "Bloom filter 2 capacity: %i and error rate: %.3f%%" % (bf2.capacity, 100.0 * bf2.error_rate) -print "Bloom filter 2 took %.2fs" % (bf2time) + 100.0 * bf2.num_bits / 8 / rawlen)) +print("Bloom filter 2 capacity: %i and error rate: %.3f%%" % (bf2.capacity, 100.0 * bf2.error_rate)) +print("Bloom filter 2 took %.2fs" % (bf2time)) diff --git a/dev/msgtest.py b/dev/msgtest.py deleted file mode 100644 index d5a8be8eb8..0000000000 --- a/dev/msgtest.py +++ /dev/null @@ -1,27 +0,0 @@ -import importlib -from os import listdir, path -from pprint import pprint -import sys -import traceback - -data = {"": "message", "subject": "subject", "body": "body"} -#data = {"": "vote", "msgid": "msgid"} -#data = {"fsck": 1} - -import messagetypes - -if __name__ == '__main__': - try: - msgType = data[""] - except KeyError: - print "Message type missing" - sys.exit(1) - else: - print "Message type: %s" % (msgType) - msgObj = messagetypes.constructObject(data) - if msgObj is None: - sys.exit(1) - try: - msgObj.process() - except: - pprint(sys.exc_info()) diff --git a/dev/powinterrupttest.py b/dev/powinterrupttest.py index cc4c21979d..bfb55d7867 100644 --- a/dev/powinterrupttest.py +++ b/dev/powinterrupttest.py @@ -11,7 +11,7 @@ def signal_handler(signal, frame): global shutdown - print "Got signal %i in %s/%s" % (signal, current_process().name, current_thread().name) + print("Got signal %i in %s/%s" % (signal, current_process().name, current_thread().name)) if current_process().name != "MainProcess": raise StopIteration("Interrupted") if current_thread().name != "PyBitmessage": @@ -20,21 +20,21 @@ def signal_handler(signal, frame): def _doCPoW(target, initialHash): -# global shutdown + # global shutdown h = initialHash m = target out_h = ctypes.pointer(ctypes.create_string_buffer(h, 64)) out_m = ctypes.c_ulonglong(m) - print "C PoW start" + print("C PoW start") for c in range(0, 200000): - print "Iter: %i" % (c) + print("Iter: %i" % (c)) nonce = bmpow(out_h, out_m) if shutdown: break trialValue, = unpack('>Q', hashlib.sha512(hashlib.sha512(pack('>Q', nonce) + initialHash).digest()).digest()[0:8]) if shutdown != 0: raise StopIteration("Interrupted") - print "C PoW done" + print("C PoW done") return [trialValue, nonce] diff --git a/dev/ssltest.py b/dev/ssltest.py index 4ddca8ca08..7268b65fc2 100644 --- a/dev/ssltest.py +++ b/dev/ssltest.py @@ -8,14 +8,15 @@ HOST = "127.0.0.1" PORT = 8912 + def sslProtocolVersion(): # sslProtocolVersion - if sys.version_info >= (2,7,13): + if sys.version_info >= (2, 7, 13): # this means TLSv1 or higher # in the future change to # ssl.PROTOCOL_TLS1.2 return ssl.PROTOCOL_TLS - elif sys.version_info >= (2,7,9): + elif sys.version_info >= (2, 7, 9): # this means any SSL/TLS. SSLv2 and 3 are excluded with an option after context is created return ssl.PROTOCOL_SSLv23 else: @@ -23,16 +24,19 @@ def sslProtocolVersion(): # "TLSv1.2" in < 2.7.9 return ssl.PROTOCOL_TLSv1 + def sslProtocolCiphers(): if ssl.OPENSSL_VERSION_NUMBER >= 0x10100000: return "AECDH-AES256-SHA@SECLEVEL=0" else: return "AECDH-AES256-SHA" + def connect(): sock = socket.create_connection((HOST, PORT)) return sock + def listen(): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) @@ -40,45 +44,51 @@ def listen(): sock.listen(0) return sock + def sslHandshake(sock, server=False): - if sys.version_info >= (2,7,9): + if sys.version_info >= (2, 7, 9): context = ssl.SSLContext(sslProtocolVersion()) context.set_ciphers(sslProtocolCiphers()) context.set_ecdh_curve("secp256k1") context.check_hostname = False context.verify_mode = ssl.CERT_NONE - context.options = ssl.OP_ALL | ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_SINGLE_ECDH_USE | ssl.OP_CIPHER_SERVER_PREFERENCE - sslSock = context.wrap_socket(sock, server_side = server, do_handshake_on_connect=False) + context.options = ssl.OP_ALL | ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3\ + | ssl.OP_SINGLE_ECDH_USE | ssl.OP_CIPHER_SERVER_PREFERENCE + sslSock = context.wrap_socket(sock, server_side=server, do_handshake_on_connect=False) else: - sslSock = ssl.wrap_socket(sock, keyfile = os.path.join('src', 'sslkeys', 'key.pem'), certfile = os.path.join('src', 'sslkeys', 'cert.pem'), server_side = server, ssl_version=sslProtocolVersion(), do_handshake_on_connect=False, ciphers='AECDH-AES256-SHA') + sslSock = ssl.wrap_socket(sock, keyfile=os.path.join('src', 'sslkeys', 'key.pem'), + certfile=os.path.join('src', 'sslkeys', 'cert.pem'), + server_side=server, ssl_version=sslProtocolVersion(), + do_handshake_on_connect=False, ciphers='AECDH-AES256-SHA') while True: try: sslSock.do_handshake() break except ssl.SSLWantReadError: - print "Waiting for SSL socket handhake read" + print("Waiting for SSL socket handhake read") select.select([sslSock], [], [], 10) except ssl.SSLWantWriteError: - print "Waiting for SSL socket handhake write" + print("Waiting for SSL socket handhake write") select.select([], [sslSock], [], 10) except Exception: - print "SSL socket handhake failed, shutting down connection" + print("SSL socket handhake failed, shutting down connection") traceback.print_exc() return - print "Success!" + print("Success!") return sslSock + if __name__ == "__main__": if len(sys.argv) != 2: - print "Usage: ssltest.py client|server" + print("Usage: ssltest.py client|server") sys.exit(0) elif sys.argv[1] == "server": serversock = listen() while True: - print "Waiting for connection" + print("Waiting for connection") sock, addr = serversock.accept() - print "Got connection from %s:%i" % (addr[0], addr[1]) + print("Got connection from %s:%i" % (addr[0], addr[1])) sslSock = sslHandshake(sock, True) if sslSock: sslSock.shutdown(socket.SHUT_RDWR) @@ -90,5 +100,5 @@ def sslHandshake(sock, server=False): sslSock.shutdown(socket.SHUT_RDWR) sslSock.close() else: - print "Usage: ssltest.py client|server" + print("Usage: ssltest.py client|server") sys.exit(0) diff --git a/docker-test.sh b/docker-test.sh new file mode 100755 index 0000000000..18b6569a08 --- /dev/null +++ b/docker-test.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +DOCKERFILE=.buildbot/tox-bionic/Dockerfile + +docker build -t pybm/tox -f $DOCKERFILE . + +if [ $? -gt 0 ]; then + docker build --no-cache -t pybm/tox -f $DOCKERFILE . +fi + +docker run --rm -it pybm/tox diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000000..3e91a7ea7c --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = PyBitmessage +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/docs/_static/custom.css b/docs/_static/custom.css new file mode 100644 index 0000000000..e0ba75c122 --- /dev/null +++ b/docs/_static/custom.css @@ -0,0 +1,18 @@ +/* Hide "On GitHub" section from versions menu */ +li.wy-breadcrumbs-aside > a.fa { + display: none; +} + +/* Override table width restrictions */ +/* @media screen and (min-width: 700px) { */ + +.wy-table-responsive table td { + /* !important prevents the common CSS stylesheets from overriding + this as on RTD they are loaded after this stylesheet */ + white-space: normal !important; +} + +.wy-table-responsive { + overflow: visible !important; +} +/* } */ diff --git a/docs/address.rst b/docs/address.rst new file mode 100644 index 0000000000..eec4bd2c00 --- /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 new file mode 100644 index 0000000000..b0cfef7b9d --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,276 @@ +# -*- coding: utf-8 -*- +""" +Configuration file for the Sphinx documentation builder. + +For a full list of options see the documentation: +http://www.sphinx-doc.org/en/master/config +""" + +import os +import sys + +sys.path.insert(0, os.path.abspath('../src')) + +from importlib import import_module + +import version # noqa:E402 + + +# -- Project information ----------------------------------------------------- + +project = u'PyBitmessage' +copyright = u'2019-2022, The Bitmessage Team' # pylint: disable=redefined-builtin +author = u'The Bitmessage Team' + +# The short X.Y version +version = unicode(version.softwareVersion) + +# The full version, including alpha/beta/rc tags +release = version + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.coverage', # FIXME: unused + 'sphinx.ext.imgmath', # legacy unused + 'sphinx.ext.intersphinx', + 'sphinx.ext.linkcode', + 'sphinx.ext.napoleon', + 'sphinx.ext.todo', + 'sphinxcontrib.apidoc', + 'm2r', +] + +default_role = 'obj' + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +source_suffix = ['.rst', '.md'] + +# The master toctree document. +master_doc = 'index' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +# language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path . +exclude_patterns = ['_build'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# Don't prepend every class or function name with full module path +add_module_names = False + +# A list of ignored prefixes for module index sorting. +modindex_common_prefix = ['pybitmessage.'] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +html_css_files = [ + 'custom.css', +] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} + +html_show_sourcelink = False + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = 'PyBitmessagedoc' + + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'PyBitmessage.tex', u'PyBitmessage Documentation', + u'The Bitmessage Team', 'manual'), +] + + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'pybitmessage', u'PyBitmessage Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ---------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'PyBitmessage', u'PyBitmessage Documentation', + author, 'PyBitmessage', 'One line description of project.', + 'Miscellaneous'), +] + + +# -- Options for Epub output ------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project +epub_author = author +epub_publisher = author +epub_copyright = copyright + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +# +# epub_identifier = '' + +# A unique identification for the text. +# +# epub_uid = '' + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ['search.html'] + + +# -- Extension configuration ------------------------------------------------- + +autodoc_mock_imports = [ + 'debug', + 'pybitmessage.bitmessagekivy', + 'pybitmessage.bitmessageqt.foldertree', + 'pybitmessage.helper_startup', + 'pybitmessage.mockbm', + 'pybitmessage.network.httpd', + 'pybitmessage.network.https', + 'ctypes', + 'dialog', + 'gi', + 'kivy', + 'logging', + 'msgpack', + 'numpy', + 'pkg_resources', + 'pycanberra', + 'pyopencl', + 'PyQt4', + 'PyQt5', + 'qrcode', + 'stem', + 'xdg', +] +autodoc_member_order = 'bysource' + +# Apidoc settings +apidoc_module_dir = '../pybitmessage' +apidoc_output_dir = 'autodoc' +apidoc_excluded_paths = [ + 'bitmessagekivy', 'build_osx.py', + 'bitmessageqt/addressvalidator.py', 'bitmessageqt/foldertree.py', + 'bitmessageqt/migrationwizard.py', 'bitmessageqt/newaddresswizard.py', + 'helper_startup.py', + 'kivymd', 'mockbm', 'main.py', 'navigationdrawer', 'network/http*', + 'src', 'tests', 'version.py' +] +apidoc_module_first = True +apidoc_separate_modules = True +apidoc_toc_file = False +apidoc_extra_args = ['-a'] + +# Napoleon settings +napoleon_google_docstring = True + + +# linkcode function +def linkcode_resolve(domain, info): + """This generates source URL's for sphinx.ext.linkcode""" + if domain != 'py' or not info['module']: + return + try: + home = os.path.abspath(import_module('pybitmessage').__path__[0]) + mod = import_module(info['module']).__file__ + except ImportError: + return + repo = 'https://github.com/Bitmessage/PyBitmessage/blob/v0.6/src%s' + path = mod.replace(home, '') + if path != mod: + # put the link only for top level definitions + if len(info['fullname'].split('.')) > 1: + return + if path.endswith('.pyc'): + path = path[:-1] + return repo % path + + +# -- Options for intersphinx extension --------------------------------------- + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'https://docs.python.org/2.7/': None} + +# -- Options for todo extension ---------------------------------------------- + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True diff --git a/docs/encrypted_payload.rst b/docs/encrypted_payload.rst new file mode 100644 index 0000000000..346d370d2b --- /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 0000000000..61c7fb3e7f --- /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 0000000000..25539ad416 --- /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 new file mode 100644 index 0000000000..6edb031357 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,30 @@ +.. mdinclude:: ../README.md + :end-line: 20 + +Protocol documentation +---------------------- +.. toctree:: + :maxdepth: 2 + + protocol + address + encryption + pow + +Code documentation +------------------ +.. toctree:: + :maxdepth: 3 + + autodoc/pybitmessage + + +Indices and tables +------------------ + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + +.. mdinclude:: ../README.md + :start-line: 21 diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000000..2548d34efe --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,36 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build +set SPHINXPROJ=PyBitmessage + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/docs/pow.rst b/docs/pow.rst new file mode 100644 index 0000000000..3786b07533 --- /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 0000000000..16c3f17483 --- /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 0000000000..17a13dd946 --- /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 new file mode 100644 index 0000000000..f8b4b17cbe --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,5 @@ +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 0000000000..3523a27432 --- /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 new file mode 100644 index 0000000000..b181a57bb9 --- /dev/null +++ b/kivy-requirements.txt @@ -0,0 +1,12 @@ +kivy-garden.qrcode +kivymd==1.0.2 +kivy==2.1.0 +opencv-python +pyzbar +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 +zbarcam \ No newline at end of file diff --git a/packages/AppImage/AppImageBuilder.yml b/packages/AppImage/AppImageBuilder.yml new file mode 100644 index 0000000000..2c5890d2d7 --- /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 new file mode 100644 index 0000000000..3eeaef64c3 --- /dev/null +++ b/packages/AppImage/PyBitmessage.yml @@ -0,0 +1,40 @@ +app: PyBitmessage +binpatch: true + +ingredients: + dist: bionic + sources: + - deb http://archive.ubuntu.com/ubuntu/ bionic main universe + packages: + - python-defusedxml + - python-jsonrpclib + - python-msgpack + - python-qrcode + - python-qt4 + - python-setuptools + - python-sip + - python-six + - python-xdg + - sni-qt + exclude: + - libdb5.3 + - libglib2.0-0 + - libmng2 + - libncursesw5 + - libqt4-declarative + - libqt4-designer + - libqt4-help + - libqt4-script + - libqt4-scripttools + - libqt4-sql + - libqt4-test + - libqt4-xmlpatterns + - libqtassistantclient4 + - libreadline7 + debs: + - ../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 0000000000..0e343236e7 --- /dev/null +++ b/packages/AppImage/qt.conf @@ -0,0 +1,2 @@ +[Paths] +Prefix = ../lib/x86_64-linux-gnu/qt4 diff --git a/packages/README.md b/packages/README.md index ed2df3cca8..2905ec20df 100644 --- a/packages/README.md +++ b/packages/README.md @@ -15,7 +15,7 @@ OSX: https://github.com/Bitmessage/PyBitmessage/releases -Wors on OSX 10.7.5 or higher +Works on OSX 10.7.5 or higher Arch linux: diff --git a/packages/android/buildozer.spec b/packages/android/buildozer.spec new file mode 100644 index 0000000000..edad9241e7 --- /dev/null +++ b/packages/android/buildozer.spec @@ -0,0 +1,359 @@ +[app] + +# (str) Title of your application +title = PyBitmessage Mock + +# (str) Package name +package.name = pybitmessagemock + +# (str) Package domain (needed for android/ios packaging) +package.domain = at.bitmessage + +# (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,json + +# (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.2 + +# (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,sqlite3,kivymd==1.0.2,Pillow,opencv,kivy-garden.qrcode,qrcode,typing_extensions,pypng,pyzbar,xcamera,zbarcam + +# (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 = 33 + +# (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 = [:] + +android.release_artifact = apk + +# +# 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/apparmor/pybitmessage b/packages/apparmor/pybitmessage new file mode 100644 index 0000000000..3ec3d23795 --- /dev/null +++ b/packages/apparmor/pybitmessage @@ -0,0 +1,19 @@ +# Last Modified: Wed Apr 29 21:04:08 2020 +#include + +/usr/bin/pybitmessage { + #include + #include + #include + #include + #include + + owner /home/*/.ICEauthority r, + owner /home/*/.Xauthority r, + owner /home/*/.config/PyBitmessage/ rw, + owner /home/*/.config/PyBitmessage/* rwk, + owner /home/*/.config/Trolltech.conf rwk, + owner /home/*/.config/Trolltech.conf.* rw, + owner /proc/*/mounts r, + +} diff --git a/packages/collectd/pybitmessagestatus.py b/packages/collectd/pybitmessagestatus.py index 1db9f5b13d..f82656f7c8 100644 --- a/packages/collectd/pybitmessagestatus.py +++ b/packages/collectd/pybitmessagestatus.py @@ -1,17 +1,20 @@ #!/usr/bin/env python2.7 -import collectd import json -import xmlrpclib + +import collectd +from six.moves import xmlrpc_client as xmlrpclib pybmurl = "" api = "" + def init_callback(): global api api = xmlrpclib.ServerProxy(pybmurl) collectd.info('pybitmessagestatus.py init done') + def config_callback(ObjConfiguration): global pybmurl apiUsername = "" @@ -28,17 +31,22 @@ def config_callback(ObjConfiguration): apiInterface = node.values[0] elif key.lower() == "apiport" and node.values: apiPort = node.values[0] - pybmurl = "http://" + apiUsername + ":" + apiPassword + "@" + apiInterface+ ":" + str(int(apiPort)) + "/" + pybmurl = "http://{}:{}@{}:{}/".format(apiUsername, apiPassword, apiInterface, str(int(apiPort))) collectd.info('pybitmessagestatus.py config done') + def read_callback(): try: clientStatus = json.loads(api.clientStatus()) - except: + except (ValueError, TypeError): + collectd.info("Exception loading or parsing JSON") + return + except: # noqa:E722 collectd.info("Exception loading or parsing JSON") return - for i in ["networkConnections", "numberOfPubkeysProcessed", "numberOfMessagesProcessed", "numberOfBroadcastsProcessed"]: + for i in ["networkConnections", "numberOfPubkeysProcessed", + "numberOfMessagesProcessed", "numberOfBroadcastsProcessed"]: metric = collectd.Values() metric.plugin = "pybitmessagestatus" if i[0:6] == "number": @@ -48,10 +56,11 @@ def read_callback(): metric.type_instance = i.lower() try: metric.values = [clientStatus[i]] - except: + except (TypeError, KeyError): collectd.info("Value for %s missing" % (i)) metric.dispatch() + if __name__ == "__main__": main() else: diff --git a/packages/docker/Dockerfile.bionic b/packages/docker/Dockerfile.bionic new file mode 100644 index 0000000000..ff53e4e774 --- /dev/null +++ b/packages/docker/Dockerfile.bionic @@ -0,0 +1,120 @@ +FROM ubuntu:bionic AS base + +ENV DEBIAN_FRONTEND noninteractive + +RUN apt-get update + +# Common apt packages +RUN apt-get install -yq --no-install-suggests --no-install-recommends \ + software-properties-common build-essential libcap-dev libssl-dev \ + python-all-dev python-setuptools wget xvfb + +############################################################################### + +FROM base AS appimage + +RUN apt-get install -yq --no-install-suggests --no-install-recommends \ + debhelper dh-apparmor dh-python python-stdeb fakeroot + +COPY . /home/builder/src + +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 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 + +# copy sources +COPY . /home/builder/src +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 ["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 + +# copy entrypoint +COPY packages/docker/buildbot-entrypoint.sh entrypoint.sh +RUN chmod +x entrypoint.sh + +RUN useradd -m -U buildbot +RUN echo 'buildbot ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers + +USER buildbot + +ENTRYPOINT /entrypoint.sh "$BUILDMASTER" "$WORKERNAME" "$WORKERPASS" + +############################################################################### + +FROM base AS appandroid + +COPY . /home/builder/src + +WORKDIR /home/builder/src + +RUN chmod +x buildscripts/androiddev.sh + +RUN buildscripts/androiddev.sh diff --git a/packages/docker/Dockerfile.kivy-travis b/packages/docker/Dockerfile.kivy-travis new file mode 100644 index 0000000000..4dcdf60ba9 --- /dev/null +++ b/packages/docker/Dockerfile.kivy-travis @@ -0,0 +1,64 @@ +FROM ubuntu:bionic AS pybm-kivy-travis-bionic + +ENV DEBIAN_FRONTEND noninteractive +ENV TRAVIS_SKIP_APT_UPDATE 1 + +RUN apt-get update + +RUN apt-get install -yq --no-install-suggests --no-install-recommends \ + software-properties-common + +RUN dpkg --add-architecture i386 + +RUN add-apt-repository ppa:deadsnakes/ppa + +RUN apt-get -y install sudo + +RUN apt-get -y install git + +RUN apt-get install -yq --no-install-suggests --no-install-recommends \ + # travis xenial bionic + python-setuptools libssl-dev libpq-dev python-prctl python-dev \ + python-dev python-virtualenv python-pip virtualenv \ + # 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-kivy.yml + build-essential libcap-dev tor \ + language-pack-en \ + xclip xsel \ + libzbar-dev + +# cleanup +RUN rm -rf /var/lib/apt/lists/* + +RUN useradd -m -U builder + +RUN echo 'builder ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers + +# 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 + +# copy sources +COPY . /home/builder/src +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", ".travis-kivy.yml"] diff --git a/packages/docker/buildbot-entrypoint.sh b/packages/docker/buildbot-entrypoint.sh new file mode 100644 index 0000000000..0e6ee5c37b --- /dev/null +++ b/packages/docker/buildbot-entrypoint.sh @@ -0,0 +1,9 @@ +#!/bin/bash + + +buildbot-worker create-worker /var/lib/buildbot/workers/default "$1" "$2" "$3" + +unset BUILDMASTER BUILDMASTER_PORT WORKERNAME WORKERPASS + +cd /var/lib/buildbot/workers/default +/usr/bin/dumb-init buildbot-worker start --nodaemon diff --git a/packages/docker/launcher.sh b/packages/docker/launcher.sh new file mode 100755 index 0000000000..c0e4885586 --- /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 06cf6e76b2..4816a63f2d 100644 --- a/packages/pyinstaller/bitmessagemain.spec +++ b/packages/pyinstaller/bitmessagemain.spec @@ -1,79 +1,141 @@ +# -*- mode: python -*- import ctypes import os -import time +import sys -srcPath = "C:\\src\\PyBitmessage\\src\\" -qtPath = "C:\\Qt-4.8.7\\" -openSSLPath = "C:\\OpenSSL-1.0.2j\\bin\\" -outPath = "C:\\src\\PyInstaller-3.2.1\\bitmessagemain" -today = time.strftime("%Y%m%d") -snapshot = False +from PyInstaller.utils.hooks import copy_metadata -os.rename(os.path.join(srcPath, '__init__.py'), os.path.join(srcPath, '__init__.py.backup')) -# -*- mode: python -*- -a = Analysis([srcPath + 'bitmessagemain.py'], - pathex=[outPath], - hiddenimports=[], - hookspath=None, - runtime_hooks=None) +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 +cdrivePath = site_root[0:3] +srcPath = os.path.join(spec_root[:-20], "pybitmessage") +sslName = 'OpenSSL-Win%i' % arch +openSSLPath = os.path.join(cdrivePath, sslName) +msvcrDllPath = os.path.join(cdrivePath, "windows", "system32") +outPath = os.path.join(spec_root, "bitmessagemain") +qtBase = "PyQt4" + +sys.path.insert(0, srcPath) +os.chdir(srcPath) + +snapshot = os.getenv('SNAPSHOT') + +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=[ + (os.path.join(spec_root[:-20], 'pybitmessage.egg-info') + '/*', + 'pybitmessage.egg-info') + ] + copy_metadata('msgpack-python') + copy_metadata('qrcode') + + copy_metadata('six') + copy_metadata('stem'), + pathex=[outPath], + hiddenimports=[ + 'bitmessageqt.languagebox', 'pyopencl', 'numpy', 'win32com', + 'setuptools.msvc', '_cffi_backend', + 'plugins.menu_qrcode', 'plugins.proxyconfig_stem' + ], + runtime_hooks=[os.path.join(hookspath, 'pyinstaller_rthook_plugins.py')], + excludes=excludes +) -os.rename(os.path.join(srcPath, '__init__.py.backup'), os.path.join(srcPath, '__init__.py')) def addTranslations(): - import os extraDatas = [] - for file in os.listdir(srcPath + 'translations'): - if file[-3:] != ".qm": + for file_ in os.listdir(os.path.join(srcPath, 'translations')): + if file_[-3:] != ".qm": continue - extraDatas.append((os.path.join('translations', file), os.path.join(srcPath, 'translations', file), 'DATA')) - for file in os.listdir(qtPath + 'translations'): - if file[0:3] != "qt_" or file[5:8] != ".qm": + extraDatas.append(( + os.path.join('translations', file_), + os.path.join(srcPath, 'translations', file_), 'DATA')) + for libdir in sys.path: + qtdir = os.path.join(libdir, qtBase, 'translations') + if os.path.isdir(qtdir): + break + if not os.path.isdir(qtdir): + return extraDatas + for file_ in os.listdir(qtdir): + if file_[0:3] != "qt_" or file_[5:8] != ".qm": continue - extraDatas.append((os.path.join('translations', file), os.path.join(qtPath, 'translations', file), 'DATA')) + extraDatas.append(( + os.path.join('translations', file_), + os.path.join(qtdir, file_), 'DATA')) return extraDatas -def addUIs(): - import os - extraDatas = [] - for file in os.listdir(srcPath + 'bitmessageqt'): - if file[-3:] != ".ui": - continue - extraDatas.append((os.path.join('ui', file), os.path.join(srcPath, 'bitmessageqt', file), 'DATA')) - return extraDatas + +dir_append = os.path.join(srcPath, 'bitmessageqt') + +a.datas += [ + (os.path.join('ui', file_), os.path.join(dir_append, file_), 'DATA') + 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 += addUIs() - -if ctypes.sizeof(ctypes.c_voidp) == 4: - arch=32 -else: - arch=64 - -a.binaries += [('libeay32.dll', openSSLPath + 'libeay32.dll', 'BINARY'), - (os.path.join('bitmsghash', 'bitmsghash%i.dll' % (arch)), os.path.join(srcPath, 'bitmsghash', 'bitmsghash%i.dll' % (arch)), 'BINARY'), - (os.path.join('bitmsghash', 'bitmsghash.cl'), 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('sslkeys', 'key.pem'), os.path.join(srcPath, 'sslkeys', 'key.pem'), 'BINARY') - ] - -with open(os.path.join(srcPath, 'version.py'), 'rt') as f: - softwareVersion = f.readline().split('\'')[1] - -fname = 'Bitmessage_%s_%s.exe' % ("x86" if arch == 32 else "x64", softwareVersion) -if snapshot: - fname = 'Bitmessagedev_%s_%s.exe' % ("x86" if arch == 32 else "x64", today) - +a.datas += [('default.ini', os.path.join(srcPath, 'default.ini'), 'DATA')] + +excluded_binaries = [ + 'QtOpenGL4.dll', + 'QtSvg4.dll', + 'QtXml4.dll', +] +a.binaries = TOC([x for x in a.binaries if x[0] not in excluded_binaries]) + +a.binaries += [ + # No effect: libeay32.dll will be taken from PyQt if installed + ('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'), + (os.path.join('bitmsghash', 'bitmsghash.cl'), + 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('sslkeys', 'key.pem'), + os.path.join(srcPath, 'sslkeys', 'key.pem'), 'BINARY') +] + +from version import softwareVersion + +fname = 'Bitmessage_%%s_%s.exe' % ( + '{}-{}'.format(softwareVersion, snapshot) if snapshot else softwareVersion +) % ("x86" if arch == 32 else "x64") + + pyz = PYZ(a.pure) -exe = EXE(pyz, - a.scripts, - a.binaries, - a.zipfiles, - a.datas, - a.binaries, - name=fname, - debug=False, - strip=None, - upx=False, - console=False, icon= os.path.join(srcPath, 'images', 'can-icon.ico')) +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + name=fname, + debug=DEBUG, + strip=None, + upx=False, + console=DEBUG, icon=os.path.join(srcPath, 'images', 'can-icon.ico') +) + +coll = COLLECT( + exe, + a.binaries, + a.zipfiles, + a.datas, + strip=False, + upx=False, + name='main' +) diff --git a/packages/pyinstaller/hooks/pyinstaller_rthook_plugins.py b/packages/pyinstaller/hooks/pyinstaller_rthook_plugins.py new file mode 100644 index 0000000000..e796c1f54f --- /dev/null +++ b/packages/pyinstaller/hooks/pyinstaller_rthook_plugins.py @@ -0,0 +1,17 @@ +"""Runtime PyInstaller hook to load plugins""" + +import os +import sys + +homepath = os.path.abspath(os.path.dirname(sys.argv[0])) + +os.environ['PATH'] += ';' + ';'.join([ + homepath, os.path.join(homepath, 'Tor'), + os.path.abspath(os.curdir) +]) + +try: + import pybitmessage.plugins.menu_qrcode + import pybitmessage.plugins.proxyconfig_stem # noqa:F401 +except ImportError: + pass diff --git a/packages/snap/snapcraft.yaml b/packages/snap/snapcraft.yaml new file mode 100644 index 0000000000..47c279360b --- /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/packages/unmaintained/Makefile b/packages/unmaintained/Makefile deleted file mode 100644 index fa1c4c914f..0000000000 --- a/packages/unmaintained/Makefile +++ /dev/null @@ -1,62 +0,0 @@ -APP=pybitmessage -APPDIR=`basename "\`pwd\`"` -VERSION=0.6.0 -RELEASE=1 -ARCH_TYPE=`uname -m` -PREFIX?=/usr/local -LIBDIR=lib - -all: -debug: -source: - tar -cvf ../${APP}_${VERSION}.orig.tar ../${APPDIR} --exclude-vcs - gzip -f9n ../${APP}_${VERSION}.orig.tar -install: - mkdir -p ${DESTDIR}/usr - mkdir -p ${DESTDIR}${PREFIX} - mkdir -p ${DESTDIR}${PREFIX}/bin - mkdir -m 755 -p ${DESTDIR}${PREFIX}/share - mkdir -m 755 -p ${DESTDIR}${PREFIX}/share/man - mkdir -m 755 -p ${DESTDIR}${PREFIX}/share/man/man1 - install -m 644 man/${APP}.1.gz ${DESTDIR}${PREFIX}/share/man/man1 - mkdir -m 755 -p ${DESTDIR}${PREFIX}/share/${APP} - mkdir -m 755 -p ${DESTDIR}${PREFIX}/share/applications - mkdir -m 755 -p ${DESTDIR}${PREFIX}/share/pixmaps - mkdir -m 755 -p ${DESTDIR}${PREFIX}/share/icons - mkdir -m 755 -p ${DESTDIR}${PREFIX}/share/icons/hicolor - mkdir -m 755 -p ${DESTDIR}${PREFIX}/share/icons/hicolor/scalable - mkdir -m 755 -p ${DESTDIR}${PREFIX}/share/icons/hicolor/scalable/apps - mkdir -m 755 -p ${DESTDIR}${PREFIX}/share/icons/hicolor/24x24 - mkdir -m 755 -p ${DESTDIR}${PREFIX}/share/icons/hicolor/24x24/apps - install -m 644 desktop/${APP}.desktop ${DESTDIR}${PREFIX}/share/applications/${APP}.desktop - install -m 644 desktop/icon24.png ${DESTDIR}${PREFIX}/share/icons/hicolor/24x24/apps/${APP}.png - cp -rf src/* ${DESTDIR}${PREFIX}/share/${APP} - echo '#!/bin/sh' > ${DESTDIR}${PREFIX}/bin/${APP} - echo "if [ -d ${PREFIX}/share/${APP} ]; then" >> ${DESTDIR}${PREFIX}/bin/${APP} - echo " cd ${PREFIX}/share/${APP}" >> ${DESTDIR}${PREFIX}/bin/${APP} - echo 'else' >> ${DESTDIR}${PREFIX}/bin/${APP} - echo " cd /usr/share/pybitmessage" >> ${DESTDIR}${PREFIX}/bin/${APP} - echo 'fi' >> ${DESTDIR}${PREFIX}/bin/${APP} - echo 'if [ -d /opt/openssl-compat-bitcoin/lib ]; then' >> ${DESTDIR}${PREFIX}/bin/${APP} - echo ' LD_LIBRARY_PATH="/opt/openssl-compat-bitcoin/lib/" exec python2 bitmessagemain.py' >> ${DESTDIR}${PREFIX}/bin/${APP} - echo 'else' >> ${DESTDIR}${PREFIX}/bin/${APP} - echo ' exec python2 bitmessagemain.py' >> ${DESTDIR}${PREFIX}/bin/${APP} - echo 'fi' >> ${DESTDIR}${PREFIX}/bin/${APP} - chmod +x ${DESTDIR}${PREFIX}/bin/${APP} -uninstall: - rm -f ${PREFIX}/share/man/man1/${APP}.1.gz - rm -rf ${PREFIX}/share/${APP} - rm -f ${PREFIX}/bin/${APP} - rm -f ${PREFIX}/share/applications/${APP}.desktop - rm -f ${PREFIX}/share/icons/hicolor/scalable/apps/${APP}.svg - rm -f ${PREFIX}/share/pixmaps/${APP}.svg -clean: - rm -f ${APP} \#* \.#* gnuplot* *.png debian/*.substvars debian/*.log - rm -fr deb.* debian/${APP} rpmpackage/${ARCH_TYPE} - rm -f ../${APP}*.deb ../${APP}*.changes ../${APP}*.asc ../${APP}*.dsc - rm -f rpmpackage/*.src.rpm archpackage/*.gz archpackage/*.xz - rm -f puppypackage/*.gz puppypackage/*.pet slackpackage/*.txz - -sourcedeb: - tar -cvf ../${APP}_${VERSION}.orig.tar ../${APPDIR} --exclude-vcs --exclude 'debian' - gzip -f9n ../${APP}_${VERSION}.orig.tar diff --git a/packages/unmaintained/debian.sh b/packages/unmaintained/debian.sh deleted file mode 100755 index 9caed2dcb9..0000000000 --- a/packages/unmaintained/debian.sh +++ /dev/null @@ -1,48 +0,0 @@ -#!/bin/bash - -APP=pybitmessage -PREV_VERSION=0.4.4 -VERSION=0.6.0 -RELEASE=1 -ARCH_TYPE=all -DIR=${APP}-${VERSION} -CURDIR=`pwd` -SHORTDIR=`basename ${CURDIR}` - -if [ $ARCH_TYPE == "x86_64" ]; then - ARCH_TYPE="amd64" -fi -if [ $ARCH_TYPE == "i686" ]; then - ARCH_TYPE="i386" -fi - - -# Update version numbers automatically - so you don't have to -sed -i 's/VERSION='${PREV_VERSION}'/VERSION='${VERSION}'/g' Makefile rpm.sh arch.sh puppy.sh ebuild.sh slack.sh -sed -i 's/Version: '${PREV_VERSION}'/Version: '${VERSION}'/g' rpmpackage/${APP}.spec -sed -i 's/Release: '${RELEASE}'/Release: '${RELEASE}'/g' rpmpackage/${APP}.spec -sed -i 's/pkgrel='${RELEASE}'/pkgrel='${RELEASE}'/g' archpackage/PKGBUILD -sed -i 's/pkgver='${PREV_VERSION}'/pkgver='${VERSION}'/g' archpackage/PKGBUILD -sed -i "s/-${PREV_VERSION}-/-${VERSION}-/g" puppypackage/*.specs -sed -i "s/|${PREV_VERSION}|/|${VERSION}|/g" puppypackage/*.specs -sed -i 's/VERSION='${PREV_VERSION}'/VERSION='${VERSION}'/g' puppypackage/pinstall.sh puppypackage/puninstall.sh -sed -i 's/-'${PREV_VERSION}'.so/-'${VERSION}'.so/g' debian/*.links - -make clean -make - -# Change the parent directory name to Debian format -mv ../${SHORTDIR} ../${DIR} - -# Create a source archive -make sourcedeb - -# Build the package -dpkg-buildpackage -F -us -uc - -# Sign files -gpg -ba ../${APP}_${VERSION}-1_${ARCH_TYPE}.deb -gpg -ba ../${APP}_${VERSION}.orig.tar.gz - -# Restore the parent directory name -mv ../${DIR} ../${SHORTDIR} diff --git a/packages/unmaintained/debian/changelog b/packages/unmaintained/debian/changelog deleted file mode 100644 index 9fc04ddb00..0000000000 --- a/packages/unmaintained/debian/changelog +++ /dev/null @@ -1,483 +0,0 @@ -pybitmessage (0.6.0-1) trusty; urgency=low - - * Bugfixes - * UI improvements - * performance and security improvements - * integration with email gateway (mailchuck.com) - - -- Peter Surda Mon, 2 May 2016 16:25:00 +0200 - -pybitmessage (0.4.4-1) utopic; urgency=low - - * Added ability to limit network transfer rate - * Updated to Protocol Version 3 - * Removed use of memoryview so that we can support python 2.7.3 - * Make use of l10n for localizations - - -- Bob Mottram (4096 bits) Sun, 2 November 2014 12:55:00 +0100 - -pybitmessage (0.4.3-1) saucy; urgency=low - - * Support pyelliptic's updated HMAC algorithm. We'll remove support for the old method after an upgrade period. - * Improved version check - * Refactored decodeBase58 function - * Ignore duplicate messages - * Added bytes received/sent counts and rate on the network information tab - * Fix unicode handling in 'View HTML code as formatted text' - * Refactor handling of packet headers - * Use pointMult function instead of arithmetic.privtopub since it is faster - * Fixed issue where client wasn't waiting for a verack before continuing on with the conversation - * Fixed CPU hogging by implementing tab-based refresh improvements - * Added curses interface - * Added support for IPv6 - * Added a 'trustedpeer' option to keys.dat - * Limit maximum object size to 20 MB - * Support email-like > quote characters and reply-below-quote - * Added Japanese and Dutch language files; updated Norwegian and Russian languages files - - -- Bob Mottram (4096 bits) Thu, 6 March 2014 20:23:00 +0100 - -pybitmessage (0.4.2-1) saucy; urgency=low - - * Exclude debian directory from orig.tar.gz - - * Added Norwegian, Chinese, and Arabic translations - - * sock.sendall function isn't atomic. - Let sendDataThread be the only thread which sends data. - - * Moved API code to api.py - - * Populate comboBoxSendFrom when replying - - * Added option to show recent broadcasts when subscribing - - * Fixed issue: If Windows username contained an international character, - Bitmessage wouldn't start - - * Added some code for FreeBSD compatibility - - * Moved responsibility for processing network objects - to the new ObjectProcessorThread - - * Refactored main QT module - Moved popup menus initialization to separate methods - Simplified inbox loading - Moved magic strings to the model scope constants so they won't - be created every time. - - * Updated list of defaultKnownNodes - - * Fixed issue: [Linux] When too many messages arrive too quickly, - exception occurs: "Exceeded maximum number of notifications" - - * Fixed issue: creating then deleting an Address in short time crashes - class_singleWorker.py - - * Refactored code which displays messages to improve code readability - - * load "Sent To" label from subscriptions if available - - * Removed code to add chans to our address book as it is no longer necessary - - * Added identicons - - * Modified addresses.decodeAddress so that API command decodeAddress - works properly - - * Added API commands createChan, joinChan, leaveChan, deleteAddress - - * In pyelliptic, check the return value of RAND_bytes to make sure enough - random data was generated - - * Don't store messages in UI table (and thus in memory), pull from SQL - inventory as needed - - * Fix typos in API commands addSubscription and getInboxMessagesByAddress - - * Add feature in settings menu to give up resending a message after a - specified period of time - - -- Bob Mottram (4096 bits) Thu, 6 March 2014 20:23:00 +0100 - -pybitmessage (0.4.1-1) raring; urgency=low - - * Fixed whitelist bug - - * Fixed chan bug - Added addressversion field to pubkeys table - Sending messages to a chan no longer uses anything in the pubkeys table - Sending messages to yourself is now fully supported - - * Change _verifyAddress function to support v4 addresses - - -- Bob Mottram (4096 bits) Sun, 29 September 2013 09:54:00 +0100 - -pybitmessage (0.4.0-1) raring; urgency=low - - * Raised default demanded difficulty from 1 to 2 for new addresses - - * Added v4 addresses: - pubkeys are now encrypted and tagged in the inventory - - * Use locks when accessing dictionary inventory - - * Refactored the way inv and addr messages are shared - - * Give user feedback when disk is full - - * Added chan true/false to listAddresses results - - * When replying using chan address, send to whole chan not just sender - - * Refactored of the way PyBitmessage looks for interesting new objects - in large inv messages from peers - - * Show inventory lookup rate on Network Status tab - - * Added SqlBulkExecute class - so we can update inventory with only one commit - - * Updated Russian translations - - * Move duplicated SQL code into helper - - * Allow specification of alternate settings dir - via BITMESSAGE_HOME environment variable - - * Removed use of gevent. Removed class_bgWorker.py - - * Added Sip and PyQt to includes in build_osx.py - - * Show number of each message type processed - in the API command clientStatus - - * Use fast PoW - unless we're explicitly a frozen (binary) version of the code - - * Enable user-set localization in settings - - * Fix Archlinux package creation - - * Fallback to language only localization when region doesn't match - - * Fixed brew install instructions - - * Added German translation - - * Made inbox and sent messages table panels read-only - - * Allow inbox and sent preview panels to resize - - * Count RE: as a reply header, just like Re: so we don't chain Re: RE: - - * Fix for traceback on OSX - - * Added backend ability to understand shorter addresses - - * Convert 'API Error' to raise APIError() - - * Added option in settings to allow sending to a mobile device - (app not yet done) - - * Added ability to start daemon mode when using Bitmessage as a module - - * Improved the way client detects locale - - * Added API commands: - getInboxMessageIds, getSentMessageIds, listAddressBookEntries, - trashSentMessageByAckData, addAddressBookEntry, - deleteAddressBookEntry, listAddresses2, listSubscriptions - - * Set a maximum frequency for playing sounds - - * Show Invalid Method error in same format as other API errors - - * Update status of separate broadcasts separately - even if the sent data is identical - - * Added Namecoin integration - - * Internally distinguish peers by IP and port - - * Inbox message retrieval API - functions now also returns read status - - -- Bob Mottram (4096 bits) Sat, 28 September 2013 09:54:00 +0100 - -pybitmessage (0.3.5-1) raring; urgency=low - - * Inbox message retrieval API functions now also returns read status - - * Added right-click option to mark a message as unread - - * Prompt user to connect at first startup - - * Install into /usr/local by default - - * Add a missing rm -f to the uninstall task. - - * Use system text color for enabled addresses instead of black - - * Added support for Chans - - * Start storing msgid in sent table - - * Optionally play sounds on connection/disconnection or when messages arrive - - * Adding configuration option to listen for connections when using SOCKS - - * Added packaging for multiple distros (Arch, Puppy, Slack, etc.) - - * Added Russian translation - - * Added search support in the UI - - * Added 'make uninstall' - - * To improve OSX support, use PKCS5_PBKDF2_HMAC_SHA1 - if PKCS5_PBKDF2_HMAC is unavailable - - * Added better warnings for OSX users who are using old versions of Python - - * Repaired debian packaging - - * Altered Makefile to avoid needing to chase changes - - * Added logger module - - * Added bgWorker class for background tasks - - * Added use of gevent module - - * On not-Windows: Fix insecure keyfile permissions - - * Fix 100% CPU usage issue - - -- Bob Mottram (4096 bits) Mon, 29 July 2013 22:11:00 +0100 - -pybitmessage (0.3.4-1) raring; urgency=low - - * Switched addr, msg, broadcast, and getpubkey message types - to 8 byte time. Last remaining type is pubkey. - - * Added tooltips to show the full subject of messages - - * Added Maximum Acceptable Difficulty fields in the settings - - * Send out pubkey immediately after generating deterministic - addresses rather than waiting for a request - - -- Bob Mottram (4096 bits) Sun, 30 June 2013 11:23:00 +0100 - -pybitmessage (0.3.3-1) raring; urgency=low - - * Remove inbox item from GUI when using API command trashMessage - - * Add missing trailing semicolons to pybitmessage.desktop - - * Ensure $(DESTDIR)/usr/bin exists - - * Update Makefile to correct sandbox violations when built - via Portage (Gentoo) - - * Fix message authentication bug - - -- Bob Mottram (4096 bits) Sat, 29 June 2013 11:23:00 +0100 - -pybitmessage (0.3.211-1) raring; urgency=low - - * Removed multi-core proof of work - as the multiprocessing module does not work well with - pyinstaller's --onefile option. - - -- Bob Mottram (4096 bits) Fri, 28 June 2013 11:23:00 +0100 - -pybitmessage (0.3.2-1) raring; urgency=low - - * Bugfix: Remove remaining references to the old myapp.trayIcon - - * Refactored message status-related code. API function getStatus - now returns one of these strings: notfound, msgqueued, - broadcastqueued, broadcastsent, doingpubkeypow, awaitingpubkey, - doingmsgpow, msgsent, or ackreceived - - * Moved proof of work to low-priority multi-threaded child - processes - - * Added menu option to delete all trashed messages - - * Added inv flooding attack mitigation - - * On Linux, when selecting Show Bitmessage, do not maximize - automatically - - * Store tray icons in bitmessage_icons_rc.py - - -- Bob Mottram (4096 bits) Mon, 03 June 2013 20:17:00 +0100 - -pybitmessage (0.3.1-1) raring; urgency=low - - * Added new API commands: getDeterministicAddress, - addSubscription, deleteSubscription - - * TCP Connection timeout for non-fully-established connections - now 20 seconds - - * Don't update the time we last communicated with a node unless - the connection is fully established. This will allow us to - forget about active but non-Bitmessage nodes which have made - it into our knownNodes file. - - * Prevent incoming connection flooding from crashing - singleListener thread. Client will now only accept one - connection per remote node IP - - * Bugfix: Worker thread crashed when doing a POW to send out - a v2 pubkey (bug introduced in 0.3.0) - - * Wrap all sock.shutdown functions in error handlers - - * Put all 'commit' commands within SQLLocks - - * Bugfix: If address book label is blank, Bitmessage wouldn't - show message (bug introduced in 0.3.0) - - * Messaging menu item selects the oldest unread message - - * Standardize on 'Quit' rather than 'Exit' - - * [OSX] Try to seek homebrew installation of OpenSSL - - * Prevent multiple instances of the application from running - - * Show 'Connected' or 'Connection Lost' indicators - - * Use only 9 half-open connections on Windows but 32 for - everyone else - - * Added appIndicator (a more functional tray icon) and Ubuntu - Messaging Menu integration - - * Changed Debian install directory and run script name based - on Github issue #135 - - -- Jonathan Warren (4096 bits) Sat, 25 May 2013 12:06:00 +0100 - -pybitmessage (0.3.0-1) raring; urgency=low - - * Added new API function: getStatus - - * Added error-handling around all sock.sendall() functions - in the receiveData thread so that if there is a problem - sending data, the threads will close gracefully - - * Abandoned and removed the connectionsCount data structure; - use the connectedHostsList instead because it has proved to be - more accurate than trying to maintain the connectionsCount - - * Added daemon mode. All UI code moved into a module and many - shared objects moved into shared.py - - * Truncate display of very long messages to avoid freezing the UI - - * Added encrypted broadcasts for v3 addresses or v2 addresses - after 2013-05-28 10:00 UTC - - * No longer self.sock.close() from within receiveDataThreads, - let the sendDataThreads do it - - * Swapped out the v2 announcements subscription address for a v3 - announcements subscription address - - * Vacuum the messages.dat file once a month: - will greatly reduce the file size - - * Added a settings table in message.dat - - * Implemented v3 addresses: - pubkey messages must now include two var_ints: nonce_trials_per_byte - and extra_bytes, and also be signed. When sending a message to a v3 - address, the sender must use these values in calculating its POW or - else the message will not be accepted by the receiver. - - * Display a privacy warning when selecting 'Send Broadcast from this address' - - * Added gitignore file - - * Added code in preparation for a switch from 32-bit time to 64-bit time. - Nodes will now advertise themselves as using protocol version 2. - - * Don't necessarily delete entries from the inventory after 2.5 days; - leave pubkeys there for 28 days so that we don't process the same ones - many times throughout a month. This was causing the 'pubkeys processed' - indicator on the 'Network Status' tab to not accurately reflect the - number of truly new addresses on the network. - - * Use 32 threads for outgoing connections in order to connect quickly - - * Fix typo when calling os.environ in the sys.platform=='darwin' case - - * Allow the cancelling of a message which is in the process of being - sent by trashing it then restarting Bitmessage - - * Bug fix: can't delete address from address book - - -- Bob Mottram (4096 bits) Mon, 6 May 2013 12:06:00 +0100 - -pybitmessage (0.2.8-1) unstable; urgency=low - - * Fixed Ubuntu & OS X issue: - Bitmessage wouldn't receive any objects from peers after restart. - - * Inventory flush to disk when exiting program now vastly faster. - - * Fixed address generation bug (kept Bitmessage from restarting). - - * Improve deserialization of messages - before processing (a 'best practice'). - - * Change to help Macs find OpenSSL the way Unix systems find it. - - * Do not share or accept IPs which are in the private IP ranges. - - * Added time-fuzzing - to the embedded time in pubkey and getpubkey messages. - - * Added a knownNodes lock - to prevent an exception from sometimes occurring when saving - the data-structure to disk. - - * Show unread messages in bold - and do not display new messages automatically. - - * Support selecting multiple items - in the inbox, sent box, and address book. - - * Use delete key to trash Inbox or Sent messages. - - * Display richtext(HTML) messages - from senders in address book or subscriptions (although not - pseudo-mailing-lists; use new right-click option). - - * Trim spaces - from the beginning and end of addresses when adding to - address book, subscriptions, and blacklist. - - * Improved the display of the time for foreign language users. - - -- Bob Mottram (4096 bits) Tue, 9 Apr 2013 17:44:00 +0100 - -pybitmessage (0.2.7-1) unstable; urgency=low - - * Added debian packaging - - * Script to generate debian packages - - * SVG icon for Gnome shell, etc - - * Source moved int src directory for debian standards compatibility - - * Trailing carriage return on COPYING LICENSE and README.md - - -- Bob Mottram (4096 bits) Mon, 1 Apr 2013 17:12:14 +0100 diff --git a/packages/unmaintained/debian/compat b/packages/unmaintained/debian/compat deleted file mode 100644 index ec635144f6..0000000000 --- a/packages/unmaintained/debian/compat +++ /dev/null @@ -1 +0,0 @@ -9 diff --git a/packages/unmaintained/debian/control b/packages/unmaintained/debian/control deleted file mode 100644 index e72de58a0c..0000000000 --- a/packages/unmaintained/debian/control +++ /dev/null @@ -1,21 +0,0 @@ -Source: pybitmessage -Section: mail -Priority: extra -Maintainer: Bob Mottram (4096 bits) -Build-Depends: debhelper (>= 9.0.0), libqt4-dev (>= 4.8.0), python-qt4-dev, libsqlite3-dev -Standards-Version: 3.9.4 -Homepage: https://github.com/Bitmessage/PyBitmessage -Vcs-Git: https://github.com/Bitmessage/PyBitmessage.git - -Package: pybitmessage -Architecture: all -Depends: ${shlibs:Depends}, ${misc:Depends}, ${python:Depends}, python (>= 2.7), openssl, python-qt4, sqlite3, gst123 -Suggests: libmessaging-menu-dev -Description: Send encrypted messages - Bitmessage is a P2P communications 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, and it aims to hide - "non-content" data, like the sender and receiver of messages, from passive - eavesdroppers like those running warrantless wiretapping programs. diff --git a/packages/unmaintained/debian/copyright b/packages/unmaintained/debian/copyright deleted file mode 100644 index b341b873ba..0000000000 --- a/packages/unmaintained/debian/copyright +++ /dev/null @@ -1,30 +0,0 @@ -Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ -Upstream-Name: -Source: - -Files: * -Copyright: Copyright 2016 Bob Mottram (4096 bits) -License: MIT - -Files: debian/* -Copyright: Copyright 2016 Bob Mottram (4096 bits) -License: MIT - -License: MIT - 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 the Software without restriction, including without limitation - the rights to use, copy, modify, merge, publish, distribute, sublicense, - and/or sell copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following conditions: - . - The above copyright notice and this permission notice shall be included - in all copies or substantial portions of the Software. - . - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, 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. diff --git a/packages/unmaintained/debian/docs b/packages/unmaintained/debian/docs deleted file mode 100644 index 8b13789179..0000000000 --- a/packages/unmaintained/debian/docs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/packages/unmaintained/debian/manpages b/packages/unmaintained/debian/manpages deleted file mode 100644 index 54af564899..0000000000 --- a/packages/unmaintained/debian/manpages +++ /dev/null @@ -1 +0,0 @@ -man/pybitmessage.1.gz diff --git a/packages/unmaintained/debian/pybm b/packages/unmaintained/debian/pybm deleted file mode 100644 index 95e61e54d4..0000000000 --- a/packages/unmaintained/debian/pybm +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -cd /usr/share/pybitmessage -exec python bitmessagemain.py - diff --git a/packages/unmaintained/debian/rules b/packages/unmaintained/debian/rules deleted file mode 100755 index 5b29d2436b..0000000000 --- a/packages/unmaintained/debian/rules +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/make -f - -APP=pybitmessage -PREFIX=/usr -build: build-stamp - make -build-arch: build-stamp -build-indep: build-stamp -build-stamp: - dh_testdir - touch build-stamp - -clean: - dh_testdir - dh_testroot - rm -f build-stamp - dh_clean - -install: build clean - dh_testdir - dh_testroot - dh_prep - dh_installdirs - ${MAKE} install -B DESTDIR=${CURDIR}/debian/${APP} PREFIX=/usr -binary-indep: build install - dh_testdir - dh_testroot - dh_installchangelogs - dh_installdocs - dh_installexamples - dh_installman - dh_link - dh_compress - dh_fixperms - dh_installdeb - dh_gencontrol - dh_md5sums - dh_builddeb - -binary-arch: build install - -binary: binary-indep binary-arch -.PHONY: build clean binary-indep binary-arch binary install diff --git a/packages/unmaintained/debian/source/format b/packages/unmaintained/debian/source/format deleted file mode 100644 index 163aaf8d82..0000000000 --- a/packages/unmaintained/debian/source/format +++ /dev/null @@ -1 +0,0 @@ -3.0 (quilt) diff --git a/packages/unmaintained/debian/source/include-binaries b/packages/unmaintained/debian/source/include-binaries deleted file mode 100644 index f676fce853..0000000000 --- a/packages/unmaintained/debian/source/include-binaries +++ /dev/null @@ -1,18 +0,0 @@ -src/images/sent.png -src/images/can-icon-16px.png -src/images/addressbook.png -src/images/networkstatus.png -src/images/redicon.png -src/images/subscriptions.png -src/images/blacklist.png -src/images/can-icon-24px.png -src/images/can-icon-24px-red.png -src/images/can-icon-24px-yellow.png -src/images/can-icon-24px-green.png -src/images/identities.png -src/images/yellowicon.png -src/images/inbox.png -src/images/greenicon.png -src/images/can-icon.ico -src/images/send.png -desktop/can-icon.svg diff --git a/pybitmessage b/pybitmessage new file mode 120000 index 0000000000..e8310385c5 --- /dev/null +++ b/pybitmessage @@ -0,0 +1 @@ +src \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 09b3a19c16..60b3173ef6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,8 @@ -python_prctl +coverage +psutil +pycryptodome +PyQt5;python_version>="3.7" and platform_machine=="x86_64" +mock;python_version<="2.7" +python_prctl;platform_system=="Linux" +six +xvfbwrapper<0.2.10;platform_system=="Linux" diff --git a/run-kivy-tests-in-docker.sh b/run-kivy-tests-in-docker.sh new file mode 100755 index 0000000000..f34bbd1976 --- /dev/null +++ b/run-kivy-tests-in-docker.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +docker build -t pybm-kivy-travis-bionic -f packages/docker/Dockerfile.kivy-travis . +docker run pybm-kivy-travis-bionic diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000000..c735e0a876 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,27 @@ +# Since there is overlap in the violations that the different tools check for, it makes sense to quiesce some warnings +# in some tools if those warnings in other tools are preferred. This avoids the need to add duplicate lint warnings. + +# max-line-length should be removed ASAP! + +[pycodestyle] +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 +# W503: deprecated: https://bugs.python.org/issue26763 - https://www.python.org/dev/peps/pep-0008/#should-a-line-break-before-or-after-a-binary-operator + +# pylint honours the [MESSAGES CONTROL] section +# as well as [MASTER] section +[MESSAGES CONTROL] +disable=invalid-name,bare-except,broad-except +# invalid-name: needs fixing during a large, project-wide refactor +# bare-except,broad-except: Need fixing once thorough testing is easier +max-args = 8 +max-attributes = 8 + +[MASTER] +init-hook = import sys;sys.path.append('src') diff --git a/setup.py b/setup.py index 12670ed692..e1dc1ba656 100644 --- a/setup.py +++ b/setup.py @@ -1,14 +1,34 @@ #!/usr/bin/env python2.7 import os -import sys +import platform import shutil -from setuptools import setup, Extension +import sys +from importlib import import_module + +from setuptools import Extension, setup from setuptools.command.install import install from src.version import softwareVersion +EXTRAS_REQUIRE = { + 'docs': ['sphinx'], + 'gir': ['pygobject'], + 'json': ['jsonrpclib'], + 'notify2': ['notify2'], + 'opencl': ['pyopencl', 'numpy'], + 'prctl': ['python_prctl'], # Named threads + 'qrcode': ['qrcode'], + 'sound;platform_system=="Windows"': ['winsound'], + 'tor': ['stem'], + 'xdg': ['pyxdg'], + 'xml': ['defusedxml'] +} + + class InstallCmd(install): + """Custom setuptools install command preparing icons""" + def run(self): # prepare icons directories try: @@ -32,37 +52,80 @@ def run(self): with open(os.path.join(here, 'README.md')) as f: README = f.read() + with open(os.path.join(here, 'requirements.txt'), 'r') as f: + requirements = list(f.readlines()) + bitmsghash = Extension( 'pybitmessage.bitmsghash.bitmsghash', sources=['src/bitmsghash/bitmsghash.cpp'], libraries=['pthread', 'crypto'], ) - installRequires = [] + installRequires = ['six'] packages = [ 'pybitmessage', 'pybitmessage.bitmessageqt', 'pybitmessage.bitmessagecurses', + 'pybitmessage.fallback', 'pybitmessage.messagetypes', 'pybitmessage.network', + 'pybitmessage.plugins', 'pybitmessage.pyelliptic', - 'pybitmessage.socks', - 'pybitmessage.storage', - 'pybitmessage.plugins' + '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.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 try: import msgpack - installRequires.append("msgpack-python") + installRequires.append( + "msgpack-python" if msgpack.version[:2] < (0, 6) else "msgpack") except ImportError: try: - import umsgpack + import_module('umsgpack') installRequires.append("umsgpack") except ImportError: - packages += ['pybitmessage.fallback', 'pybitmessage.fallback.umsgpack'] + packages += ['pybitmessage.fallback.umsgpack'] + + data_files = [ + ('share/applications/', + ['desktop/pybitmessage.desktop']), + ('share/icons/hicolor/scalable/apps/', + ['desktop/icons/scalable/pybitmessage.svg']), + ('share/icons/hicolor/24x24/apps/', + ['desktop/icons/24x24/pybitmessage.png']) + ] + + try: + if platform.dist()[0] in ('Debian', 'Ubuntu'): + data_files += [ + ("etc/apparmor.d/", + ['packages/apparmor/pybitmessage']) + ] + except AttributeError: + pass # FIXME: use distro for more recent python dist = setup( name='pybitmessage', @@ -72,19 +135,12 @@ def run(self): long_description=README, license='MIT', # TODO: add author info - #author='', - #author_email='', url='https://bitmessage.org', # TODO: add keywords - #keywords='', install_requires=installRequires, - extras_require={ - 'gir': ['pygobject'], - 'qrcode': ['qrcode'], - 'pyopencl': ['pyopencl'], - 'notify2': ['notify2'], - 'sound;platform_system=="Windows"': ['winsound'] - }, + tests_require=requirements, + test_suite='tests.unittest_discover', + extras_require=EXTRAS_REQUIRE, classifiers=[ "License :: OSI Approved :: MIT License" "Operating System :: OS Independent", @@ -95,19 +151,8 @@ def run(self): ], package_dir={'pybitmessage': 'src'}, packages=packages, - package_data={'': [ - 'bitmessageqt/*.ui', 'bitmsghash/*.cl', 'sslkeys/*.pem', - 'translations/*.ts', 'translations/*.qm', - 'images/*.png', 'images/*.ico', 'images/*.icns' - ]}, - data_files=[ - ('share/applications/', - ['desktop/pybitmessage.desktop']), - ('share/icons/hicolor/scalable/apps/', - ['desktop/icons/scalable/pybitmessage.svg']), - ('share/icons/hicolor/24x24/apps/', - ['desktop/icons/24x24/pybitmessage.png']) - ], + package_data=package_data, + data_files=data_files, ext_modules=[bitmsghash], zip_safe=False, entry_points={ @@ -129,10 +174,20 @@ def run(self): 'libmessaging =' 'pybitmessage.plugins.indicator_libmessaging [gir]' ], - # 'console_scripts': [ - # 'pybitmessage = pybitmessage.bitmessagemain:main' - # ] + 'bitmessage.desktop': [ + 'freedesktop = pybitmessage.plugins.desktop_xdg [xdg]' + ], + 'bitmessage.proxyconfig': [ + 'stem = pybitmessage.plugins.proxyconfig_stem [tor]' + ], + 'console_scripts': [ + 'pybitmessage = pybitmessage.bitmessagemain:main' + ] if sys.platform[:3] == 'win' else [] }, scripts=['src/pybitmessage'], - cmdclass={'install': InstallCmd} + cmdclass={'install': InstallCmd}, + command_options={ + 'build_sphinx': { + 'source_dir': ('setup.py', 'docs')} + } ) diff --git a/src/addresses.py b/src/addresses.py index ed07436f04..885c1f649f 100644 --- a/src/addresses.py +++ b/src/addresses.py @@ -1,67 +1,77 @@ -import hashlib -from struct import pack, unpack -from pyelliptic import arithmetic +""" +Operations with addresses +""" +# pylint: disable=inconsistent-return-statements + +import logging from binascii import hexlify, unhexlify +from struct import pack, unpack -from debug import logger +try: + from highlevelcrypto import double_sha512 +except ImportError: + from .highlevelcrypto import double_sha512 -# There is another copy of this function in Bitmessagemain.py -def convertIntToString(n): - a = __builtins__.hex(n) - if a[-1:] == 'L': - a = a[:-1] - if (len(a) % 2) == 0: - return unhexlify(a[2:]) - else: - return unhexlify('0'+a[2:]) +logger = logging.getLogger('default') ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" -def encodeBase58(num, alphabet=ALPHABET): +def encodeBase58(num): """Encode a number in Base X - `num`: The number to encode - `alphabet`: The alphabet to use for encoding + Args: + num: The number to encode + alphabet: The alphabet to use for encoding """ - if (num == 0): - return alphabet[0] + if num < 0: + return None + if num == 0: + return ALPHABET[0] arr = [] - base = len(alphabet) + base = len(ALPHABET) while num: - rem = num % base - # print 'num is:', num - num = num // base - arr.append(alphabet[rem]) + num, rem = divmod(num, base) + arr.append(ALPHABET[rem]) arr.reverse() return ''.join(arr) -def decodeBase58(string, alphabet=ALPHABET): +def decodeBase58(string): """Decode a Base X encoded string into the number - Arguments: - - `string`: The encoded string - - `alphabet`: The alphabet to use for encoding + Args: + string: The encoded string + alphabet: The alphabet to use for encoding """ - base = len(alphabet) + base = len(ALPHABET) num = 0 try: for char in string: num *= base - num += alphabet.index(char) - except: # ValueError + num += ALPHABET.index(char) + except ValueError: # character not found (like a space character or a 0) return 0 return num +class varintEncodeError(Exception): + """Exception class for encoding varint""" + pass + + +class varintDecodeError(Exception): + """Exception class for decoding varint data""" + pass + + def encodeVarint(integer): + """Convert integer into varint bytes""" if integer < 0: - logger.error('varint cannot be < 0') - raise SystemExit + raise varintEncodeError('varint cannot be < 0') if integer < 253: return pack('>B', integer) if integer >= 253 and integer < 65536: @@ -71,12 +81,7 @@ def encodeVarint(integer): if integer >= 4294967296 and integer < 18446744073709551616: return pack('>B', 255) + pack('>Q', integer) if integer >= 18446744073709551616: - logger.error('varint cannot be >= 18446744073709551616') - raise SystemExit - - -class varintDecodeError(Exception): - pass + raise varintEncodeError('varint cannot be >= 18446744073709551616') def decodeVarint(data): @@ -87,7 +92,7 @@ def decodeVarint(data): Returns a tuple: (theEncodedValue, theSizeOfTheVarintInBytes) """ - if len(data) == 0: + if not data: return (0, 0) firstByte, = unpack('>B', data[0:1]) if firstByte < 253: @@ -134,49 +139,45 @@ def decodeVarint(data): return (encodedValue, 9) -def calculateInventoryHash(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: if len(ripe) != 20: raise Exception( 'Programming error in encodeAddress: The length of' ' a given ripe hash was not 20.' ) - if ripe[:2] == '\x00\x00': + + if ripe[:2] == b'\x00\x00': ripe = ripe[2:] - elif ripe[:1] == '\x00': + elif ripe[:1] == b'\x00': ripe = ripe[1:] elif version == 4: if len(ripe) != 20: raise Exception( 'Programming error in encodeAddress: The length of' ' a given ripe hash was not 20.') - ripe = ripe.lstrip('\x00') + ripe = ripe.lstrip(b'\x00') 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) asInt = int(hexlify(storedBinaryData) + hexlify(checksum), 16) + # should it be str? If yes, it should be everywhere in the code return 'BM-' + encodeBase58(asInt) def decodeAddress(address): - # returns (status, address version number, stream number, - # data (almost certainly a ripe hash)) + """ + returns (status, address version number, stream number, + data (almost certainly a ripe hash)) + """ + # pylint: disable=too-many-return-statements,too-many-statements + # pylint: disable=too-many-branches address = str(address).strip() @@ -188,30 +189,18 @@ def decodeAddress(address): status = 'invalidcharacters' return status, 0, 0, '' # after converting to hex, the string will be prepended - # with a 0x and appended with a L - hexdata = hex(integer)[2:-1] + # with a 0x and appended with a L in python2 + hexdata = hex(integer)[2:].rstrip('L') if len(hexdata) % 2 != 0: hexdata = '0' + hexdata - # print 'hexdata', hexdata - data = unhexlify(hexdata) checksum = data[-4:] - sha = hashlib.new('sha512') - sha.update(data[:-4]) - currentHash = sha.digest() - # print 'sha after first hashing: ', sha.hexdigest() - sha = hashlib.new('sha512') - sha.update(currentHash) - # print 'sha after second hashing: ', sha.hexdigest() - - if checksum != sha.digest()[0:4]: + if checksum != double_sha512(data[:-4])[0:4]: status = 'checksumfailed' return status, 0, 0, '' - # else: - # print 'checksum PASSED' try: addressVersionNumber, bytesUsedByVersionNumber = decodeVarint(data[:9]) @@ -219,8 +208,6 @@ def decodeAddress(address): logger.error(str(e)) status = 'varintmalformed' return status, 0, 0, '' - # print 'addressVersionNumber', addressVersionNumber - # print 'bytesUsedByVersionNumber', bytesUsedByVersionNumber if addressVersionNumber > 4: logger.error('cannot decode address version numbers this high') @@ -233,37 +220,36 @@ def decodeAddress(address): try: streamNumber, bytesUsedByStreamNumber = \ - decodeVarint(data[bytesUsedByVersionNumber:]) + decodeVarint(data[bytesUsedByVersionNumber:]) except varintDecodeError as e: logger.error(str(e)) status = 'varintmalformed' return status, 0, 0, '' - # print streamNumber + status = 'success' if addressVersionNumber == 1: return status, addressVersionNumber, streamNumber, data[-24:-4] elif addressVersionNumber == 2 or addressVersionNumber == 3: embeddedRipeData = \ - data[bytesUsedByVersionNumber + bytesUsedByStreamNumber:-4] + data[bytesUsedByVersionNumber + bytesUsedByStreamNumber:-4] if len(embeddedRipeData) == 19: return status, addressVersionNumber, streamNumber, \ - '\x00'+embeddedRipeData + b'\x00' + embeddedRipeData elif len(embeddedRipeData) == 20: return status, addressVersionNumber, streamNumber, \ - embeddedRipeData + embeddedRipeData elif len(embeddedRipeData) == 18: return status, addressVersionNumber, streamNumber, \ - '\x00\x00' + embeddedRipeData + b'\x00\x00' + embeddedRipeData elif len(embeddedRipeData) < 18: return 'ripetooshort', 0, 0, '' elif len(embeddedRipeData) > 20: return 'ripetoolong', 0, 0, '' - else: - return 'otherproblem', 0, 0, '' + return 'otherproblem', 0, 0, '' elif addressVersionNumber == 4: embeddedRipeData = \ - data[bytesUsedByVersionNumber + bytesUsedByStreamNumber:-4] - if embeddedRipeData[0:1] == '\x00': + data[bytesUsedByVersionNumber + bytesUsedByStreamNumber:-4] + if embeddedRipeData[0:1] == b'\x00': # In order to enforce address non-malleability, encoded # RIPE data must have NULL bytes removed from the front return 'encodingproblem', 0, 0, '' @@ -271,75 +257,12 @@ def decodeAddress(address): return 'ripetoolong', 0, 0, '' elif len(embeddedRipeData) < 4: return 'ripetooshort', 0, 0, '' - else: - x00string = '\x00' * (20 - len(embeddedRipeData)) - return status, addressVersionNumber, streamNumber, \ - x00string + embeddedRipeData + x00string = b'\x00' * (20 - len(embeddedRipeData)) + return status, addressVersionNumber, streamNumber, \ + x00string + embeddedRipeData def addBMIfNotPresent(address): + """Prepend BM- to an address if it doesn't already have it""" address = str(address).strip() return address if address[:3] == 'BM-' else 'BM-' + address - - -if __name__ == "__main__": - print( - '\nLet us make an address from scratch. Suppose we generate two' - ' random 32 byte values and call the first one the signing key' - ' and the second one the encryption key:' - ) - privateSigningKey = \ - '93d0b61371a54b53df143b954035d612f8efa8a3ed1cf842c2186bfd8f876665' - privateEncryptionKey = \ - '4b0b73a54e19b059dc274ab69df095fe699f43b17397bca26fdf40f4d7400a3a' - print( - '\nprivateSigningKey = %s\nprivateEncryptionKey = %s' % - (privateSigningKey, privateEncryptionKey) - ) - print( - '\nNow let us convert them to public keys by doing' - ' an elliptic curve point multiplication.' - ) - publicSigningKey = arithmetic.privtopub(privateSigningKey) - publicEncryptionKey = arithmetic.privtopub(privateEncryptionKey) - print( - '\npublicSigningKey = %s\npublicEncryptionKey = %s' % - (publicSigningKey, publicEncryptionKey) - ) - - print( - '\nNotice that they both begin with the \\x04 which specifies' - ' the encoding type. This prefix is not send over the wire.' - ' You must strip if off before you send your public key across' - ' the wire, and you must add it back when you receive a public key.' - ) - - publicSigningKeyBinary = \ - arithmetic.changebase(publicSigningKey, 16, 256, minlen=64) - publicEncryptionKeyBinary = \ - arithmetic.changebase(publicEncryptionKey, 16, 256, minlen=64) - - ripe = hashlib.new('ripemd160') - sha = hashlib.new('sha512') - sha.update(publicSigningKeyBinary + publicEncryptionKeyBinary) - - ripe.update(sha.digest()) - addressVersionNumber = 2 - streamNumber = 1 - print( - '\nRipe digest that we will encode in the address: %s' % - hexlify(ripe.digest()) - ) - returnedAddress = \ - encodeAddress(addressVersionNumber, streamNumber, ripe.digest()) - print('Encoded address: %s' % returnedAddress) - status, addressVersionNumber, streamNumber, data = \ - decodeAddress(returnedAddress) - print( - '\nAfter decoding address:\n\tStatus: %s' - '\n\taddressVersionNumber %s' - '\n\tstreamNumber %s' - '\n\tlength of data (the ripe hash): %s' - '\n\tripe data: %s' % - (status, addressVersionNumber, streamNumber, len(data), hexlify(data)) - ) diff --git a/src/api.py b/src/api.py index d43bdf0b45..ded65b3fc0 100644 --- a/src/api.py +++ b/src/api.py @@ -1,81 +1,375 @@ # Copyright (c) 2012-2016 Jonathan Warren -# Copyright (c) 2012-2016 The Bitmessage developers +# Copyright (c) 2012-2023 The Bitmessage developers -comment = """ -This is not what you run to run the Bitmessage API. Instead, enable the API -( https://bitmessage.org/wiki/API ) and optionally enable daemon mode -( https://bitmessage.org/wiki/Daemon ) then run bitmessagemain.py. """ - -if __name__ == "__main__": - print comment - import sys - sys.exit(0) +This is not what you run to start the Bitmessage API. +Instead, `enable the API `_ +and optionally `enable daemon mode `_ +then run the PyBitmessage. + +The PyBitmessage API is provided either as +`XML-RPC `_ or +`JSON-RPC `_ like in bitcoin. +It's selected according to 'apivariant' setting in config file. + +Special value ``apivariant=legacy`` is to mimic the old pre 0.6.3 +behaviour when any results are returned as strings of json. + +.. list-table:: All config settings related to API: + :header-rows: 0 + + * - apienabled = true + - if 'false' the `singleAPI` wont start + * - apiinterface = 127.0.0.1 + - this is the recommended default + * - apiport = 8442 + - the API listens apiinterface:apiport if apiport is not used, + random in range (32767, 65535) otherwice + * - apivariant = xml + - current default for backward compatibility, 'json' is recommended + * - apiusername = username + - set the username + * - apipassword = password + - and the password + * - apinotifypath = + - not really the API setting, this sets a path for the executable to be ran + when certain internal event happens + +To use the API concider such simple example: + +.. code-block:: python + + from jsonrpclib import jsonrpc + + from pybitmessage import helper_startup + from pybitmessage.bmconfigparser import config + + helper_startup.loadConfig() # find and load local config file + api_uri = "http://%s:%s@127.0.0.1:%s/" % ( + config.safeGet('bitmessagesettings', 'apiusername'), + config.safeGet('bitmessagesettings', 'apipassword'), + config.safeGet('bitmessagesettings', 'apiport') + ) + api = jsonrpc.ServerProxy(api_uri) + print(api.clientStatus()) + + +For further examples please reference `.tests.test_api`. +""" import base64 -from SimpleXMLRPCServer import SimpleXMLRPCRequestHandler, SimpleXMLRPCServer +import errno +import hashlib import json - +import random +import socket +import subprocess # nosec B404 +import time from binascii import hexlify, unhexlify +from struct import pack, unpack + +import six +from six.moves import configparser, http_client, xmlrpc_server +from six.moves.reprlib import repr -import shared -import time -from addresses import (decodeAddress, addBMIfNotPresent, decodeVarint, - calculateInventoryHash, varintDecodeError) -from bmconfigparser import BMConfigParser -import defaults import helper_inbox import helper_sent -import hashlib - -import state +import proofofwork +import protocol import queues +import shared import shutdown -from struct import pack -import network.stats - -# Classes -from helper_sql import sqlQuery, sqlExecute, SqlBulkExecute, sqlStoredProcedure -from helper_ackPayload import genAckPayload +import state +from addresses import (addBMIfNotPresent, decodeAddress, decodeVarint, + varintDecodeError) +from bmconfigparser import config from debug import logger -from inventory import Inventory +from defaults import (networkDefaultPayloadLengthExtraBytes, + networkDefaultProofOfWorkNonceTrialsPerByte) +from helper_sql import (SqlBulkExecute, sql_ready, sqlExecute, sqlQuery, + sqlStoredProcedure) +from highlevelcrypto import calculateInventoryHash + +try: + from network import connectionpool +except ImportError: + connectionpool = None + +from network import StoppableThread, invQueue, stats from version import softwareVersion -# Helper Functions -import proofofwork +try: # TODO: write tests for XML vulnerabilities + from defusedxml.xmlrpc import monkey_patch +except ImportError: + logger.warning( + 'defusedxml not available, only use API on a secure, closed network.') +else: + monkey_patch() -str_chan = '[chan]' +str_chan = '[chan]' +str_broadcast_subscribers = '[Broadcast subscribers]' + + +class ErrorCodes(type): + """Metaclass for :class:`APIError` documenting error codes.""" + _CODES = { + 0: 'Invalid command parameters number', + 1: 'The specified passphrase is blank.', + 2: 'The address version number currently must be 3, 4, or 0' + ' (which means auto-select).', + 3: 'The stream number must be 1 (or 0 which means' + ' auto-select). Others aren\'t supported.', + 4: 'Why would you ask me to generate 0 addresses for you?', + 5: 'You have (accidentally?) specified too many addresses to' + ' make. Maximum 999. This check only exists to prevent' + ' mischief; if you really want to create more addresses than' + ' this, contact the Bitmessage developers and we can modify' + ' the check or you can do it yourself by searching the source' + ' code for this message.', + 6: 'The encoding type must be 2 or 3.', + 7: 'Could not decode address', + 8: 'Checksum failed for address', + 9: 'Invalid characters in address', + 10: 'Address version number too high (or zero)', + 11: 'The address version number currently must be 2, 3 or 4.' + ' Others aren\'t supported. Check the address.', + 12: 'The stream number must be 1. Others aren\'t supported.' + ' Check the address.', + 13: 'Could not find this address in your keys.dat file.', + 14: 'Your fromAddress is disabled. Cannot send.', + 15: 'Invalid ackData object size.', + 16: 'You are already subscribed to that address.', + 17: 'Label is not valid UTF-8 data.', + 18: 'Chan name does not match address.', + 19: 'The length of hash should be 32 bytes (encoded in hex' + ' thus 64 characters).', + 20: 'Invalid method:', + 21: 'Unexpected API Failure', + 22: 'Decode error', + 23: 'Bool expected in eighteenByteRipe', + 24: 'Chan address is already present.', + 25: 'Specified address is not a chan address.' + ' Use deleteAddress API call instead.', + 26: 'Malformed varint in address: ', + 27: 'Message is too long.', + 28: 'Invalid parameter' + } + + def __new__(mcs, name, bases, namespace): + result = super(ErrorCodes, mcs).__new__(mcs, name, bases, namespace) + for code in six.iteritems(mcs._CODES): + # beware: the formatting is adjusted for list-table + result.__doc__ += """ * - %04i + - %s + """ % code + return result + + +class APIError(xmlrpc_server.Fault): + """ + APIError exception class + + .. list-table:: Possible error values + :header-rows: 1 + :widths: auto + + * - Error Number + - Message + """ + __metaclass__ = ErrorCodes -class APIError(Exception): - def __init__(self, error_number, error_message): - super(APIError, self).__init__() - self.error_number = error_number - self.error_message = error_message def __str__(self): - return "API Error %04i: %s" % (self.error_number, self.error_message) + return "API Error %04i: %s" % (self.faultCode, self.faultString) -class StoppableXMLRPCServer(SimpleXMLRPCServer): - allow_reuse_address = True +# This thread, of which there is only one, runs the API. +class singleAPI(StoppableThread): + """API thread""" - def serve_forever(self): - while state.shutdown == 0: - self.handle_request() + name = "singleAPI" + def stopThread(self): + super(singleAPI, self).stopThread() + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + s.connect(( + config.get('bitmessagesettings', 'apiinterface'), + config.getint('bitmessagesettings', 'apiport') + )) + s.shutdown(socket.SHUT_RDWR) + s.close() + except BaseException: + pass + + def run(self): + """ + The instance of `SimpleXMLRPCServer.SimpleXMLRPCServer` or + :class:`jsonrpclib.SimpleJSONRPCServer` is created and started here + with `BMRPCDispatcher` dispatcher. + """ + port = config.getint('bitmessagesettings', 'apiport') + try: + getattr(errno, 'WSAEADDRINUSE') + except AttributeError: + errno.WSAEADDRINUSE = errno.EADDRINUSE + + RPCServerBase = xmlrpc_server.SimpleXMLRPCServer + ct = 'text/xml' + if config.safeGet( + 'bitmessagesettings', 'apivariant') == 'json': + try: + from jsonrpclib.SimpleJSONRPCServer import \ + SimpleJSONRPCServer as RPCServerBase + except ImportError: + logger.warning( + 'jsonrpclib not available, failing back to XML-RPC') + else: + ct = 'application/json-rpc' + + # Nested class. FIXME not found a better solution. + class StoppableRPCServer(RPCServerBase): + """A SimpleXMLRPCServer that honours state.shutdown""" + allow_reuse_address = True + content_type = ct + + def serve_forever(self, poll_interval=None): + """Start the RPCServer""" + sql_ready.wait() + while state.shutdown == 0: + self.handle_request() + + for attempt in range(50): + try: + if attempt > 0: + logger.warning( + 'Failed to start API listener on port %s', port) + port = random.randint(32767, 65535) # nosec B311 + se = StoppableRPCServer( + (config.get( + 'bitmessagesettings', 'apiinterface'), + port), + BMXMLRPCRequestHandler, True, encoding='UTF-8') + except socket.error as e: + if e.errno in (errno.EADDRINUSE, errno.WSAEADDRINUSE): + continue + else: + if attempt > 0: + logger.warning('Setting apiport to %s', port) + config.set( + 'bitmessagesettings', 'apiport', str(port)) + config.save() + break + + se.register_instance(BMRPCDispatcher()) + se.register_introspection_functions() + + apiNotifyPath = config.safeGet( + 'bitmessagesettings', 'apinotifypath') + + if apiNotifyPath: + logger.info('Trying to call %s', apiNotifyPath) + try: + subprocess.call([apiNotifyPath, "startingUp"]) # nosec B603 + except OSError: + logger.warning( + 'Failed to call %s, removing apinotifypath setting', + apiNotifyPath) + config.remove_option( + 'bitmessagesettings', 'apinotifypath') + + se.serve_forever() + + +class CommandHandler(type): + """ + The metaclass for `BMRPCDispatcher` which fills _handlers dict by + methods decorated with @command + """ + def __new__(mcs, name, bases, namespace): + # pylint: disable=protected-access + result = super(CommandHandler, mcs).__new__( + mcs, name, bases, namespace) + result.config = config + result._handlers = {} + apivariant = result.config.safeGet('bitmessagesettings', 'apivariant') + for func in namespace.values(): + try: + for alias in getattr(func, '_cmd'): + try: + prefix, alias = alias.split(':') + if apivariant != prefix: + continue + except ValueError: + pass + result._handlers[alias] = func + except AttributeError: + pass + return result + + +class testmode(object): # pylint: disable=too-few-public-methods + """Decorator to check testmode & route to command decorator""" + + def __init__(self, *aliases): + self.aliases = aliases + + def __call__(self, func): + """Testmode call method""" + + if not state.testmode: + return None + return command(self.aliases[0]).__call__(func) + + +class command(object): # pylint: disable=too-few-public-methods + """Decorator for API command method""" + def __init__(self, *aliases): + self.aliases = aliases + + def __call__(self, func): + + if config.safeGet( + 'bitmessagesettings', 'apivariant') == 'legacy': + def wrapper(*args): + """ + A wrapper for legacy apivariant which dumps the result + into string of json + """ + result = func(*args) + return result if isinstance(result, (int, str)) \ + else json.dumps(result, indent=4) + wrapper.__doc__ = func.__doc__ + else: + wrapper = func + # pylint: disable=protected-access + wrapper._cmd = self.aliases + wrapper.__doc__ = """Commands: *%s* + + """ % ', '.join(self.aliases) + wrapper.__doc__.lstrip() + return wrapper -# This is one of several classes that constitute the API -# This class was written by Vaibhav Bhatia. Modified by Jonathan Warren (Atheros). -# http://code.activestate.com/recipes/501148-xmlrpc-serverclient-which-does-cookie-handling-and/ -class MySimpleXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): +# This is one of several classes that constitute the API +# This class was written by Vaibhav Bhatia. +# Modified by Jonathan Warren (Atheros). +# Further modified by the Bitmessage developers +# http://code.activestate.com/recipes/501148 +class BMXMLRPCRequestHandler(xmlrpc_server.SimpleXMLRPCRequestHandler): + """The main API handler""" + + # pylint: disable=protected-access def do_POST(self): - # Handles the HTTP POST request. - # Attempts to interpret all HTTP POST requests as XML-RPC calls, - # which are forwarded to the server's _dispatch method for handling. + """ + Handles the HTTP POST request. + + Attempts to interpret all HTTP POST requests as XML-RPC calls, + which are forwarded to the server's _dispatch method for handling. - # Note: this method is the same as in SimpleXMLRPCRequestHandler, - # just hacked to handle cookies + .. note:: this method is the same as in + `SimpleXMLRPCServer.SimpleXMLRPCRequestHandler`, + just hacked to handle cookies + """ # Check that the path is legal if not self.is_rpc_path_valid(): @@ -92,26 +386,43 @@ def do_POST(self): 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) - - # In previous versions of SimpleXMLRPCServer, _dispatch - # could be overridden in this class, instead of in - # SimpleXMLRPCDispatcher. To maintain backwards compatibility, - # check to see if a subclass implements _dispatch and dispatch - # using that method if present. - response = self.server._marshaled_dispatch( - data, getattr(self, '_dispatch', None) - ) - except: # This should only happen if the module is buggy + 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(http_client.UNAUTHORIZED) + self.end_headers() + return + # "RPC Username or password incorrect or HTTP header" + # " lacks authentication at all." + else: + # In previous versions of SimpleXMLRPCServer, _dispatch + # could be overridden in this class, instead of in + # SimpleXMLRPCDispatcher. To maintain backwards compatibility, + # check to see if a subclass implements _dispatch and dispatch + # using that method if present. + + response = self.server._marshaled_dispatch( + data, getattr(self, '_dispatch', None) + ) + except Exception: # This should only happen if the module is buggy # internal error, report as HTTP server error - self.send_response(500) + self.send_response(http_client.INTERNAL_SERVER_ERROR) self.end_headers() else: # got a valid XML RPC response - self.send_response(200) - self.send_header("Content-type", "text/xml") + self.send_response(http_client.OK) + self.send_header("Content-type", self.server.content_type) self.send_header("Content-length", str(len(response))) # HACK :start -> sends cookies here @@ -127,539 +438,775 @@ def do_POST(self): self.wfile.flush() self.connection.shutdown(1) + # actually handle shutdown command after sending response + if state.shutdown is False: + shutdown.doCleanShutdown() + def APIAuthenticateClient(self): + """ + Predicate to check for valid API credentials in the request header + """ + if 'Authorization' in self.headers: # handle Basic authentication - (enctype, encstr) = self.headers.get('Authorization').split() - (emailid, password) = encstr.decode('base64').split(':') - if emailid == BMConfigParser().get('bitmessagesettings', 'apiusername') and password == BMConfigParser().get('bitmessagesettings', 'apipassword'): - return True - else: - return False + encstr = self.headers.get('Authorization').split()[1] + emailid, password = base64.b64decode( + encstr).decode('utf-8').split(':') + return ( + emailid == config.get( + 'bitmessagesettings', 'apiusername' + ) and password == config.get( + 'bitmessagesettings', 'apipassword')) else: - logger.warn('Authentication failed because header lacks Authentication field') + logger.warning( + 'Authentication failed because header lacks' + ' Authentication field') time.sleep(2) - return False return False - def _decode(self, text, decode_type): + +# 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""" + + @staticmethod + def _decode(text, decode_type): try: if decode_type == 'hex': return unhexlify(text) elif decode_type == 'base64': return base64.b64decode(text) except Exception as e: - raise APIError(22, "Decode error - " + str(e) + ". Had trouble while decoding string: " + repr(text)) + raise APIError( + 22, 'Decode error - %s. Had trouble while decoding string: %r' + % (e, text) + ) def _verifyAddress(self, address): - status, addressVersionNumber, streamNumber, ripe = decodeAddress(address) + status, addressVersionNumber, streamNumber, ripe = \ + decodeAddress(address) if status != 'success': - logger.warn('API Error 0007: Could not decode address %s. Status: %s.', address, status) - if status == 'checksumfailed': raise APIError(8, 'Checksum failed for address: ' + address) if status == 'invalidcharacters': raise APIError(9, 'Invalid characters in address: ' + address) if status == 'versiontoohigh': - raise APIError(10, 'Address version number too high (or zero) in address: ' + address) + raise APIError( + 10, 'Address version number too high (or zero) in address: ' + + address) if status == 'varintmalformed': raise APIError(26, 'Malformed varint in address: ' + address) - raise APIError(7, 'Could not decode address: ' + address + ' : ' + status) + raise APIError( + 7, 'Could not decode address: %s : %s' % (address, status)) if addressVersionNumber < 2 or addressVersionNumber > 4: - raise APIError(11, 'The address version number currently must be 2, 3 or 4. Others aren\'t supported. Check the address.') + raise APIError( + 11, 'The address version number currently must be 2, 3 or 4.' + ' Others aren\'t supported. Check the address.' + ) if streamNumber != 1: - raise APIError(12, 'The stream number must be 1. Others aren\'t supported. Check the address.') - - return (status, addressVersionNumber, streamNumber, ripe) - + raise APIError( + 12, 'The stream number must be 1. Others aren\'t supported.' + ' Check the address.' + ) - #Request Handlers + return { + 'status': status, + 'addressVersion': addressVersionNumber, + 'streamNumber': streamNumber, + 'ripe': base64.b64encode(ripe) + } if self._method == 'decodeAddress' else ( + status, addressVersionNumber, streamNumber, ripe) + + @staticmethod + def _dump_inbox_message( + msgid, toAddress, fromAddress, subject, received, + message, encodingtype, read): + subject = shared.fixPotentiallyInvalidUTF8Data(subject) + message = shared.fixPotentiallyInvalidUTF8Data(message) + return { + 'msgid': hexlify(msgid), + 'toAddress': toAddress, + 'fromAddress': fromAddress, + 'subject': base64.b64encode(subject), + 'message': base64.b64encode(message), + 'encodingType': encodingtype, + 'receivedTime': received, + 'read': read + } + + @staticmethod + def _dump_sent_message( # pylint: disable=too-many-arguments + msgid, toAddress, fromAddress, subject, lastactiontime, + message, encodingtype, status, ackdata): + subject = shared.fixPotentiallyInvalidUTF8Data(subject) + message = shared.fixPotentiallyInvalidUTF8Data(message) + return { + 'msgid': hexlify(msgid), + 'toAddress': toAddress, + 'fromAddress': fromAddress, + 'subject': base64.b64encode(subject), + 'message': base64.b64encode(message), + 'encodingType': encodingtype, + 'lastActionTime': lastactiontime, + 'status': status, + 'ackData': hexlify(ackdata) + } + + @staticmethod + def _blackwhitelist_entries(kind='black'): + queryreturn = sqlQuery( + "SELECT label, address FROM %slist WHERE enabled = 1" % kind + ) + data = [ + {'label': base64.b64encode( + shared.fixPotentiallyInvalidUTF8Data(label)), + 'address': address} for label, address in queryreturn + ] + return {'addresses': data} + + def _blackwhitelist_add(self, address, label, kind='black'): + label = self._decode(label, "base64") + address = addBMIfNotPresent(address) + self._verifyAddress(address) + queryreturn = sqlQuery( + "SELECT address FROM %slist WHERE address=?" % kind, address) + if queryreturn != []: + sqlExecute( + "UPDATE %slist SET label=?, enabled=1 WHERE address=?" % kind, + address) + else: + sqlExecute( + "INSERT INTO %slist VALUES (?,?,1)" % kind, label, address) + queues.UISignalQueue.put(('rerenderBlackWhiteList', '')) - def HandleListAddresses(self, method): - data = '{"addresses":[' - for addressInKeysFile in BMConfigParser().addresses(): - status, addressVersionNumber, streamNumber, hash01 = decodeAddress( - addressInKeysFile) - if len(data) > 20: - data += ',' - if BMConfigParser().has_option(addressInKeysFile, 'chan'): - chan = BMConfigParser().getboolean(addressInKeysFile, 'chan') - else: - chan = False - label = BMConfigParser().get(addressInKeysFile, 'label') - if method == 'listAddresses2': + def _blackwhitelist_del(self, address, kind='black'): + address = addBMIfNotPresent(address) + self._verifyAddress(address) + sqlExecute("DELETE FROM %slist WHERE address=?" % kind, address) + queues.UISignalQueue.put(('rerenderBlackWhiteList', '')) + + # Request Handlers + + @command('decodeAddress') + def HandleDecodeAddress(self, address): + """ + Decode given address and return dict with + status, addressVersion, streamNumber and ripe keys + """ + return self._verifyAddress(address) + + @command('listAddresses', 'listAddresses2') + def HandleListAddresses(self): + """ + Returns dict with a list of all used addresses with their properties + in the *addresses* key. + """ + data = [] + for address in self.config.addresses(): + streamNumber = decodeAddress(address)[2] + label = self.config.get(address, 'label') + if self._method == 'listAddresses2': label = base64.b64encode(label) - data += json.dumps({'label': label, 'address': addressInKeysFile, 'stream': - streamNumber, 'enabled': BMConfigParser().getboolean(addressInKeysFile, 'enabled'), 'chan': chan}, indent=4, separators=(',', ': ')) - data += ']}' - return data - - def HandleListAddressBookEntries(self, params): - if len(params) == 1: - label, = params - label = self._decode(label, "base64") - queryreturn = sqlQuery('''SELECT label, address from addressbook WHERE label = ?''', label) - elif len(params) > 1: - raise APIError(0, "Too many paremeters, max 1") - else: - queryreturn = sqlQuery('''SELECT label, address from addressbook''') - data = '{"addresses":[' - for row in queryreturn: - label, address = row + data.append({ + 'label': label, + 'address': address, + 'stream': streamNumber, + 'enabled': self.config.safeGetBoolean(address, 'enabled'), + 'chan': self.config.safeGetBoolean(address, 'chan') + }) + return {'addresses': data} + + # the listAddressbook alias should be removed eventually. + @command('listAddressBookEntries', 'legacy:listAddressbook') + def HandleListAddressBookEntries(self, label=None): + """ + Returns dict with a list of all address book entries (address and label) + in the *addresses* key. + """ + queryreturn = sqlQuery( + "SELECT label, address from addressbook WHERE label = ?", + label + ) if label else sqlQuery("SELECT label, address from addressbook") + data = [] + for label, address in queryreturn: label = shared.fixPotentiallyInvalidUTF8Data(label) - if len(data) > 20: - data += ',' - data += json.dumps({'label':base64.b64encode(label), 'address': address}, indent=4, separators=(',', ': ')) - data += ']}' - return data - - def HandleAddAddressBookEntry(self, params): - if len(params) != 2: - raise APIError(0, "I need label and address") - address, label = params + data.append({ + 'label': base64.b64encode(label), + 'address': address + }) + return {'addresses': data} + + # the addAddressbook alias should be deleted eventually. + @command('addAddressBookEntry', 'legacy:addAddressbook') + def HandleAddAddressBookEntry(self, address, label): + """Add an entry to address book. label must be base64 encoded.""" label = self._decode(label, "base64") address = addBMIfNotPresent(address) self._verifyAddress(address) - queryreturn = sqlQuery("SELECT address FROM addressbook WHERE address=?", address) + # TODO: add unique together constraint in the table + queryreturn = sqlQuery( + "SELECT address FROM addressbook WHERE address=?", address) if queryreturn != []: - raise APIError(16, 'You already have this address in your address book.') + raise APIError( + 16, 'You already have this address in your address book.') sqlExecute("INSERT INTO addressbook VALUES(?,?)", label, address) - queues.UISignalQueue.put(('rerenderMessagelistFromLabels','')) - queues.UISignalQueue.put(('rerenderMessagelistToLabels','')) - queues.UISignalQueue.put(('rerenderAddressBook','')) + queues.UISignalQueue.put(('rerenderMessagelistFromLabels', '')) + queues.UISignalQueue.put(('rerenderMessagelistToLabels', '')) + queues.UISignalQueue.put(('rerenderAddressBook', '')) return "Added address %s to address book" % address - def HandleDeleteAddressBookEntry(self, params): - if len(params) != 1: - raise APIError(0, "I need an address") - address, = params + # the deleteAddressbook alias should be deleted eventually. + @command('deleteAddressBookEntry', 'legacy:deleteAddressbook') + def HandleDeleteAddressBookEntry(self, address): + """Delete an entry from address book.""" address = addBMIfNotPresent(address) self._verifyAddress(address) sqlExecute('DELETE FROM addressbook WHERE address=?', address) - queues.UISignalQueue.put(('rerenderMessagelistFromLabels','')) - queues.UISignalQueue.put(('rerenderMessagelistToLabels','')) - queues.UISignalQueue.put(('rerenderAddressBook','')) + queues.UISignalQueue.put(('rerenderMessagelistFromLabels', '')) + queues.UISignalQueue.put(('rerenderMessagelistToLabels', '')) + queues.UISignalQueue.put(('rerenderAddressBook', '')) return "Deleted address book entry for %s if it existed" % address - def HandleCreateRandomAddress(self, params): - if len(params) == 0: - raise APIError(0, 'I need parameters!') - elif len(params) == 1: - label, = params - eighteenByteRipe = False - nonceTrialsPerByte = BMConfigParser().get( - 'bitmessagesettings', 'defaultnoncetrialsperbyte') - payloadLengthExtraBytes = BMConfigParser().get( - 'bitmessagesettings', 'defaultpayloadlengthextrabytes') - elif len(params) == 2: - label, eighteenByteRipe = params - nonceTrialsPerByte = BMConfigParser().get( - 'bitmessagesettings', 'defaultnoncetrialsperbyte') - payloadLengthExtraBytes = BMConfigParser().get( - 'bitmessagesettings', 'defaultpayloadlengthextrabytes') - elif len(params) == 3: - label, eighteenByteRipe, totalDifficulty = params - nonceTrialsPerByte = int( - defaults.networkDefaultProofOfWorkNonceTrialsPerByte * totalDifficulty) - payloadLengthExtraBytes = BMConfigParser().get( - 'bitmessagesettings', 'defaultpayloadlengthextrabytes') - elif len(params) == 4: - label, eighteenByteRipe, totalDifficulty, smallMessageDifficulty = params - nonceTrialsPerByte = int( - defaults.networkDefaultProofOfWorkNonceTrialsPerByte * totalDifficulty) - payloadLengthExtraBytes = int( - defaults.networkDefaultPayloadLengthExtraBytes * smallMessageDifficulty) - else: - raise APIError(0, 'Too many parameters!') + @command('getBlackWhitelistKind') + def HandleGetBlackWhitelistKind(self): + """Get the list kind set in config - black or white.""" + return self.config.get('bitmessagesettings', 'blackwhitelist') + + @command('setBlackWhitelistKind') + def HandleSetBlackWhitelistKind(self, kind): + """Set the list kind used - black or white.""" + blackwhitelist_kinds = ('black', 'white') + if kind not in blackwhitelist_kinds: + raise APIError( + 28, 'Invalid kind, should be one of %s' + % (blackwhitelist_kinds,)) + return self.config.set('bitmessagesettings', 'blackwhitelist', kind) + + @command('listBlacklistEntries') + def HandleListBlacklistEntries(self): + """ + Returns dict with a list of all blacklist entries (address and label) + in the *addresses* key. + """ + return self._blackwhitelist_entries('black') + + @command('listWhitelistEntries') + def HandleListWhitelistEntries(self): + """ + Returns dict with a list of all whitelist entries (address and label) + in the *addresses* key. + """ + return self._blackwhitelist_entries('white') + + @command('addBlacklistEntry') + def HandleAddBlacklistEntry(self, address, label): + """Add an entry to blacklist. label must be base64 encoded.""" + self._blackwhitelist_add(address, label, 'black') + return "Added address %s to blacklist" % address + + @command('addWhitelistEntry') + def HandleAddWhitelistEntry(self, address, label): + """Add an entry to whitelist. label must be base64 encoded.""" + self._blackwhitelist_add(address, label, 'white') + return "Added address %s to whitelist" % address + + @command('deleteBlacklistEntry') + def HandleDeleteBlacklistEntry(self, address): + """Delete an entry from blacklist.""" + self._blackwhitelist_del(address, 'black') + return "Deleted blacklist entry for %s if it existed" % address + + @command('deleteWhitelistEntry') + def HandleDeleteWhitelistEntry(self, address): + """Delete an entry from whitelist.""" + self._blackwhitelist_del(address, 'white') + return "Deleted whitelist entry for %s if it existed" % address + + @command('createRandomAddress') + def HandleCreateRandomAddress( + self, label, eighteenByteRipe=False, totalDifficulty=0, + smallMessageDifficulty=0 + ): + """ + Create one address using the random number generator. + + :param str label: base64 encoded label for the address + :param bool eighteenByteRipe: is telling Bitmessage whether to + generate an address with an 18 byte RIPE hash + (as opposed to a 19 byte hash). + """ + + nonceTrialsPerByte = self.config.get( + 'bitmessagesettings', 'defaultnoncetrialsperbyte' + ) if not totalDifficulty else int( + networkDefaultProofOfWorkNonceTrialsPerByte * totalDifficulty) + payloadLengthExtraBytes = self.config.get( + 'bitmessagesettings', 'defaultpayloadlengthextrabytes' + ) if not smallMessageDifficulty else int( + networkDefaultPayloadLengthExtraBytes * smallMessageDifficulty) + + if not isinstance(eighteenByteRipe, bool): + raise APIError( + 23, 'Bool expected in eighteenByteRipe, saw %s instead' + % type(eighteenByteRipe)) label = self._decode(label, "base64") try: - unicode(label, 'utf-8') - except: + label.decode('utf-8') + except UnicodeDecodeError: raise APIError(17, 'Label is not valid UTF-8 data.') queues.apiAddressGeneratorReturnQueue.queue.clear() + # FIXME hard coded stream no streamNumberForAddress = 1 queues.addressGeneratorQueue.put(( - 'createRandomAddress', 4, streamNumberForAddress, label, 1, "", eighteenByteRipe, nonceTrialsPerByte, payloadLengthExtraBytes)) + 'createRandomAddress', 4, streamNumberForAddress, label, 1, "", + eighteenByteRipe, nonceTrialsPerByte, payloadLengthExtraBytes + )) return queues.apiAddressGeneratorReturnQueue.get() - def HandleCreateDeterministicAddresses(self, params): - if len(params) == 0: - raise APIError(0, 'I need parameters!') - elif len(params) == 1: - passphrase, = params - numberOfAddresses = 1 - addressVersionNumber = 0 - streamNumber = 0 - eighteenByteRipe = False - nonceTrialsPerByte = BMConfigParser().get( - 'bitmessagesettings', 'defaultnoncetrialsperbyte') - payloadLengthExtraBytes = BMConfigParser().get( - 'bitmessagesettings', 'defaultpayloadlengthextrabytes') - elif len(params) == 2: - passphrase, numberOfAddresses = params - addressVersionNumber = 0 - streamNumber = 0 - eighteenByteRipe = False - nonceTrialsPerByte = BMConfigParser().get( - 'bitmessagesettings', 'defaultnoncetrialsperbyte') - payloadLengthExtraBytes = BMConfigParser().get( - 'bitmessagesettings', 'defaultpayloadlengthextrabytes') - elif len(params) == 3: - passphrase, numberOfAddresses, addressVersionNumber = params - streamNumber = 0 - eighteenByteRipe = False - nonceTrialsPerByte = BMConfigParser().get( - 'bitmessagesettings', 'defaultnoncetrialsperbyte') - payloadLengthExtraBytes = BMConfigParser().get( - 'bitmessagesettings', 'defaultpayloadlengthextrabytes') - elif len(params) == 4: - passphrase, numberOfAddresses, addressVersionNumber, streamNumber = params - eighteenByteRipe = False - nonceTrialsPerByte = BMConfigParser().get( - 'bitmessagesettings', 'defaultnoncetrialsperbyte') - payloadLengthExtraBytes = BMConfigParser().get( - 'bitmessagesettings', 'defaultpayloadlengthextrabytes') - elif len(params) == 5: - passphrase, numberOfAddresses, addressVersionNumber, streamNumber, eighteenByteRipe = params - nonceTrialsPerByte = BMConfigParser().get( - 'bitmessagesettings', 'defaultnoncetrialsperbyte') - payloadLengthExtraBytes = BMConfigParser().get( - 'bitmessagesettings', 'defaultpayloadlengthextrabytes') - elif len(params) == 6: - passphrase, numberOfAddresses, addressVersionNumber, streamNumber, eighteenByteRipe, totalDifficulty = params - nonceTrialsPerByte = int( - defaults.networkDefaultProofOfWorkNonceTrialsPerByte * totalDifficulty) - payloadLengthExtraBytes = BMConfigParser().get( - 'bitmessagesettings', 'defaultpayloadlengthextrabytes') - elif len(params) == 7: - passphrase, numberOfAddresses, addressVersionNumber, streamNumber, eighteenByteRipe, totalDifficulty, smallMessageDifficulty = params - nonceTrialsPerByte = int( - defaults.networkDefaultProofOfWorkNonceTrialsPerByte * totalDifficulty) - payloadLengthExtraBytes = int( - defaults.networkDefaultPayloadLengthExtraBytes * smallMessageDifficulty) - else: - raise APIError(0, 'Too many parameters!') - if len(passphrase) == 0: + @command('createDeterministicAddresses') + def HandleCreateDeterministicAddresses( + self, passphrase, numberOfAddresses=1, addressVersionNumber=0, + streamNumber=0, eighteenByteRipe=False, totalDifficulty=0, + smallMessageDifficulty=0 + ): + """ + Create many addresses deterministically using the passphrase. + + :param str passphrase: base64 encoded passphrase + :param int numberOfAddresses: number of addresses to create, + up to 999 + + *addressVersionNumber* and *streamNumber* may be set to 0 + which will tell Bitmessage to use the most up-to-date + address version and the most available stream. + """ + + nonceTrialsPerByte = self.config.get( + 'bitmessagesettings', 'defaultnoncetrialsperbyte' + ) if not totalDifficulty else int( + networkDefaultProofOfWorkNonceTrialsPerByte * totalDifficulty) + payloadLengthExtraBytes = self.config.get( + 'bitmessagesettings', 'defaultpayloadlengthextrabytes' + ) if not smallMessageDifficulty else int( + networkDefaultPayloadLengthExtraBytes * smallMessageDifficulty) + + if not passphrase: raise APIError(1, 'The specified passphrase is blank.') if not isinstance(eighteenByteRipe, bool): - raise APIError(23, 'Bool expected in eighteenByteRipe, saw %s instead' % type(eighteenByteRipe)) + raise APIError( + 23, 'Bool expected in eighteenByteRipe, saw %s instead' + % type(eighteenByteRipe)) passphrase = self._decode(passphrase, "base64") - if addressVersionNumber == 0: # 0 means "just use the proper addressVersionNumber" + # 0 means "just use the proper addressVersionNumber" + if addressVersionNumber == 0: addressVersionNumber = 4 - if addressVersionNumber != 3 and addressVersionNumber != 4: - raise APIError(2,'The address version number currently must be 3, 4, or 0 (which means auto-select). ' + addressVersionNumber + ' isn\'t supported.') + if addressVersionNumber not in (3, 4): + raise APIError( + 2, 'The address version number currently must be 3, 4, or 0' + ' (which means auto-select). %i isn\'t supported.' + % addressVersionNumber) if streamNumber == 0: # 0 means "just use the most available stream" - streamNumber = 1 + streamNumber = 1 # FIXME hard coded stream no if streamNumber != 1: - raise APIError(3,'The stream number must be 1 (or 0 which means auto-select). Others aren\'t supported.') + raise APIError( + 3, 'The stream number must be 1 (or 0 which means' + ' auto-select). Others aren\'t supported.') if numberOfAddresses == 0: - raise APIError(4, 'Why would you ask me to generate 0 addresses for you?') + raise APIError( + 4, 'Why would you ask me to generate 0 addresses for you?') if numberOfAddresses > 999: - raise APIError(5, 'You have (accidentally?) specified too many addresses to make. Maximum 999. This check only exists to prevent mischief; if you really want to create more addresses than this, contact the Bitmessage developers and we can modify the check or you can do it yourself by searching the source code for this message.') + raise APIError( + 5, 'You have (accidentally?) specified too many addresses to' + ' make. Maximum 999. This check only exists to prevent' + ' mischief; if you really want to create more addresses than' + ' this, contact the Bitmessage developers and we can modify' + ' the check or you can do it yourself by searching the source' + ' code for this message.') queues.apiAddressGeneratorReturnQueue.queue.clear() - logger.debug('Requesting that the addressGenerator create %s addresses.', numberOfAddresses) - queues.addressGeneratorQueue.put( - ('createDeterministicAddresses', addressVersionNumber, streamNumber, - 'unused API address', numberOfAddresses, passphrase, eighteenByteRipe, nonceTrialsPerByte, payloadLengthExtraBytes)) - data = '{"addresses":[' - queueReturn = queues.apiAddressGeneratorReturnQueue.get() - for item in queueReturn: - if len(data) > 20: - data += ',' - data += "\"" + item + "\"" - data += ']}' - return data - - def HandleGetDeterministicAddress(self, params): - if len(params) != 3: - raise APIError(0, 'I need exactly 3 parameters.') - passphrase, addressVersionNumber, streamNumber = params + logger.debug( + 'Requesting that the addressGenerator create %s addresses.', + numberOfAddresses) + queues.addressGeneratorQueue.put(( + 'createDeterministicAddresses', addressVersionNumber, streamNumber, + 'unused API address', numberOfAddresses, passphrase, + eighteenByteRipe, nonceTrialsPerByte, payloadLengthExtraBytes + )) + + return {'addresses': queues.apiAddressGeneratorReturnQueue.get()} + + @command('getDeterministicAddress') + def HandleGetDeterministicAddress( + self, passphrase, addressVersionNumber, streamNumber): + """ + Similar to *createDeterministicAddresses* except that the one + address that is returned will not be added to the Bitmessage + user interface or the keys.dat file. + """ + numberOfAddresses = 1 eighteenByteRipe = False - if len(passphrase) == 0: + if not passphrase: raise APIError(1, 'The specified passphrase is blank.') passphrase = self._decode(passphrase, "base64") - if addressVersionNumber != 3 and addressVersionNumber != 4: - raise APIError(2, 'The address version number currently must be 3 or 4. ' + addressVersionNumber + ' isn\'t supported.') + if addressVersionNumber not in (3, 4): + raise APIError( + 2, 'The address version number currently must be 3 or 4. %i' + ' isn\'t supported.' % addressVersionNumber) if streamNumber != 1: - raise APIError(3, ' The stream number must be 1. Others aren\'t supported.') + raise APIError( + 3, ' The stream number must be 1. Others aren\'t supported.') queues.apiAddressGeneratorReturnQueue.queue.clear() - logger.debug('Requesting that the addressGenerator create %s addresses.', numberOfAddresses) - queues.addressGeneratorQueue.put( - ('getDeterministicAddress', addressVersionNumber, - streamNumber, 'unused API address', numberOfAddresses, passphrase, eighteenByteRipe)) + logger.debug( + 'Requesting that the addressGenerator create %s addresses.', + numberOfAddresses) + queues.addressGeneratorQueue.put(( + 'getDeterministicAddress', addressVersionNumber, streamNumber, + 'unused API address', numberOfAddresses, passphrase, + eighteenByteRipe + )) return queues.apiAddressGeneratorReturnQueue.get() - def HandleCreateChan(self, params): - if len(params) == 0: - raise APIError(0, 'I need parameters.') - elif len(params) == 1: - passphrase, = params + @command('createChan') + def HandleCreateChan(self, passphrase): + """ + Creates a new chan. passphrase must be base64 encoded. + Returns the corresponding Bitmessage address. + """ + passphrase = self._decode(passphrase, "base64") - if len(passphrase) == 0: + if not passphrase: raise APIError(1, 'The specified passphrase is blank.') # It would be nice to make the label the passphrase but it is # possible that the passphrase contains non-utf-8 characters. try: - unicode(passphrase, 'utf-8') + passphrase.decode('utf-8') label = str_chan + ' ' + passphrase - except: + except UnicodeDecodeError: label = str_chan + ' ' + repr(passphrase) addressVersionNumber = 4 streamNumber = 1 queues.apiAddressGeneratorReturnQueue.queue.clear() - logger.debug('Requesting that the addressGenerator create chan %s.', passphrase) - queues.addressGeneratorQueue.put(('createChan', addressVersionNumber, streamNumber, label, passphrase, True)) + logger.debug( + 'Requesting that the addressGenerator create chan %s.', passphrase) + queues.addressGeneratorQueue.put(( + 'createChan', addressVersionNumber, streamNumber, label, + passphrase, True + )) queueReturn = queues.apiAddressGeneratorReturnQueue.get() - if len(queueReturn) == 0: + try: + return queueReturn[0] + except IndexError: raise APIError(24, 'Chan address is already present.') - address = queueReturn[0] - return address - - def HandleJoinChan(self, params): - if len(params) < 2: - raise APIError(0, 'I need two parameters.') - elif len(params) == 2: - passphrase, suppliedAddress= params + + @command('joinChan') + def HandleJoinChan(self, passphrase, suppliedAddress): + """ + Join a chan. passphrase must be base64 encoded. Returns 'success'. + """ + passphrase = self._decode(passphrase, "base64") - if len(passphrase) == 0: + if not passphrase: raise APIError(1, 'The specified passphrase is blank.') # It would be nice to make the label the passphrase but it is # possible that the passphrase contains non-utf-8 characters. try: - unicode(passphrase, 'utf-8') + passphrase.decode('utf-8') label = str_chan + ' ' + passphrase - except: + except UnicodeDecodeError: label = str_chan + ' ' + repr(passphrase) - status, addressVersionNumber, streamNumber, toRipe = self._verifyAddress(suppliedAddress) + self._verifyAddress(suppliedAddress) suppliedAddress = addBMIfNotPresent(suppliedAddress) queues.apiAddressGeneratorReturnQueue.queue.clear() - queues.addressGeneratorQueue.put(('joinChan', suppliedAddress, label, passphrase, True)) - addressGeneratorReturnValue = queues.apiAddressGeneratorReturnQueue.get() - - if addressGeneratorReturnValue[0] == 'chan name does not match address': - raise APIError(18, 'Chan name does not match address.') - if len(addressGeneratorReturnValue) == 0: + queues.addressGeneratorQueue.put(( + 'joinChan', suppliedAddress, label, passphrase, True + )) + queueReturn = queues.apiAddressGeneratorReturnQueue.get() + try: + if queueReturn[0] == 'chan name does not match address': + raise APIError(18, 'Chan name does not match address.') + except IndexError: raise APIError(24, 'Chan address is already present.') - #TODO: this variable is not used to anything - createdAddress = addressGeneratorReturnValue[0] # in case we ever want it for anything. + return "success" - def HandleLeaveChan(self, params): - if len(params) == 0: - raise APIError(0, 'I need parameters.') - elif len(params) == 1: - address, = params - status, addressVersionNumber, streamNumber, toRipe = self._verifyAddress(address) + @command('leaveChan') + def HandleLeaveChan(self, address): + """ + Leave a chan. Returns 'success'. + + .. note:: at this time, the address is still shown in the UI + until a restart. + """ + self._verifyAddress(address) address = addBMIfNotPresent(address) - if not BMConfigParser().has_section(address): - raise APIError(13, 'Could not find this address in your keys.dat file.') - if not BMConfigParser().safeGetBoolean(address, 'chan'): - raise APIError(25, 'Specified address is not a chan address. Use deleteAddress API call instead.') - BMConfigParser().remove_section(address) - with open(state.appdata + 'keys.dat', 'wb') as configfile: - BMConfigParser().write(configfile) - return 'success' - - def HandleDeleteAddress(self, params): - if len(params) == 0: - raise APIError(0, 'I need parameters.') - elif len(params) == 1: - address, = params - status, addressVersionNumber, streamNumber, toRipe = self._verifyAddress(address) + if not self.config.safeGetBoolean(address, 'chan'): + raise APIError( + 25, 'Specified address is not a chan address.' + ' Use deleteAddress API call instead.') + try: + self.config.remove_section(address) + except configparser.NoSectionError: + raise APIError( + 13, 'Could not find this address in your keys.dat file.') + self.config.save() + queues.UISignalQueue.put(('rerenderMessagelistFromLabels', '')) + queues.UISignalQueue.put(('rerenderMessagelistToLabels', '')) + return "success" + + @command('deleteAddress') + def HandleDeleteAddress(self, address): + """ + Permanently delete the address from keys.dat file. Returns 'success'. + """ + self._verifyAddress(address) address = addBMIfNotPresent(address) - if not BMConfigParser().has_section(address): - raise APIError(13, 'Could not find this address in your keys.dat file.') - BMConfigParser().remove_section(address) - with open(state.appdata + 'keys.dat', 'wb') as configfile: - BMConfigParser().write(configfile) - queues.UISignalQueue.put(('rerenderMessagelistFromLabels','')) - queues.UISignalQueue.put(('rerenderMessagelistToLabels','')) + try: + self.config.remove_section(address) + except configparser.NoSectionError: + raise APIError( + 13, 'Could not find this address in your keys.dat file.') + self.config.save() + queues.UISignalQueue.put(('writeNewAddressToTable', ('', '', ''))) shared.reloadMyAddressHashes() - return 'success' + 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): + """ + Returns a dict with all inbox messages in the *inboxMessages* key. + The message is a dict with such keys: + *msgid*, *toAddress*, *fromAddress*, *subject*, *message*, + *encodingType*, *receivedTime*, *read*. + *msgid* is hex encoded string. + *subject* and *message* are base64 encoded. + """ - def HandleGetAllInboxMessages(self, params): queryreturn = sqlQuery( - '''SELECT msgid, toaddress, fromaddress, subject, received, message, encodingtype, read FROM inbox where folder='inbox' ORDER BY received''') - data = '{"inboxMessages":[' - for row in queryreturn: - msgid, toAddress, fromAddress, subject, received, message, encodingtype, read = row - subject = shared.fixPotentiallyInvalidUTF8Data(subject) - message = shared.fixPotentiallyInvalidUTF8Data(message) - if len(data) > 25: - data += ',' - data += json.dumps({'msgid': hexlify(msgid), 'toAddress': toAddress, - 'fromAddress': fromAddress, 'subject': base64.b64encode(subject), - 'message': base64.b64encode(message), 'encodingType': encodingtype, - 'receivedTime': received, 'read': read}, indent=4, separators=(',', ': ')) - data += ']}' - return data - - def HandleGetAllInboxMessageIds(self, params): + "SELECT msgid, toaddress, fromaddress, subject, received, message," + " encodingtype, read FROM inbox WHERE folder='inbox'" + " ORDER BY received" + ) + return {"inboxMessages": [ + self._dump_inbox_message(*data) for data in queryreturn + ]} + + @command('getAllInboxMessageIds', 'getAllInboxMessageIDs') + def HandleGetAllInboxMessageIds(self): + """ + The same as *getAllInboxMessages* but returns only *msgid*s, + result key - *inboxMessageIds*. + """ + queryreturn = sqlQuery( - '''SELECT msgid FROM inbox where folder='inbox' ORDER BY received''') - data = '{"inboxMessageIds":[' - for row in queryreturn: - msgid = row[0] - if len(data) > 25: - data += ',' - data += json.dumps({'msgid': hexlify(msgid)}, indent=4, separators=(',', ': ')) - data += ']}' - return data - - def HandleGetInboxMessageById(self, params): - if len(params) == 0: - raise APIError(0, 'I need parameters!') - elif len(params) == 1: - msgid = self._decode(params[0], "hex") - elif len(params) >= 2: - msgid = self._decode(params[0], "hex") - readStatus = params[1] + "SELECT msgid FROM inbox where folder='inbox' ORDER BY received") + + return {"inboxMessageIds": [ + {'msgid': hexlify(msgid)} for msgid, in queryreturn + ]} + + @command('getInboxMessageById', 'getInboxMessageByID') + def HandleGetInboxMessageById(self, hid, readStatus=None): + """ + Returns a dict with list containing single message in the result + key *inboxMessage*. May also return None if message was not found. + + :param str hid: hex encoded msgid + :param bool readStatus: sets the message's read status if present + """ + + msgid = self._decode(hid, "hex") + if readStatus is not None: if not isinstance(readStatus, bool): - raise APIError(23, 'Bool expected in readStatus, saw %s instead.' % type(readStatus)) - queryreturn = sqlQuery('''SELECT read FROM inbox WHERE msgid=?''', msgid) + raise APIError( + 23, 'Bool expected in readStatus, saw %s instead.' + % type(readStatus)) + queryreturn = sqlQuery( + "SELECT read FROM inbox WHERE msgid=?", msgid) # UPDATE is slow, only update if status is different - if queryreturn != [] and (queryreturn[0][0] == 1) != readStatus: - sqlExecute('''UPDATE inbox set read = ? WHERE msgid=?''', readStatus, msgid) - queues.UISignalQueue.put(('changedInboxUnread', None)) - queryreturn = sqlQuery('''SELECT msgid, toaddress, fromaddress, subject, received, message, encodingtype, read FROM inbox WHERE msgid=?''', msgid) - data = '{"inboxMessage":[' - for row in queryreturn: - msgid, toAddress, fromAddress, subject, received, message, encodingtype, read = row - subject = shared.fixPotentiallyInvalidUTF8Data(subject) - message = shared.fixPotentiallyInvalidUTF8Data(message) - data += json.dumps({'msgid':hexlify(msgid), 'toAddress':toAddress, 'fromAddress':fromAddress, 'subject':base64.b64encode(subject), 'message':base64.b64encode(message), 'encodingType':encodingtype, 'receivedTime':received, 'read': read}, indent=4, separators=(',', ': ')) - data += ']}' - return data - - def HandleGetAllSentMessages(self, params): - queryreturn = sqlQuery('''SELECT msgid, toaddress, fromaddress, subject, lastactiontime, message, encodingtype, status, ackdata FROM sent where folder='sent' ORDER BY lastactiontime''') - data = '{"sentMessages":[' - for row in queryreturn: - msgid, toAddress, fromAddress, subject, lastactiontime, message, encodingtype, status, ackdata = row - subject = shared.fixPotentiallyInvalidUTF8Data(subject) - message = shared.fixPotentiallyInvalidUTF8Data(message) - if len(data) > 25: - data += ',' - data += json.dumps({'msgid':hexlify(msgid), 'toAddress':toAddress, 'fromAddress':fromAddress, 'subject':base64.b64encode(subject), 'message':base64.b64encode(message), 'encodingType':encodingtype, 'lastActionTime':lastactiontime, 'status':status, 'ackData':hexlify(ackdata)}, indent=4, separators=(',', ': ')) - data += ']}' - return data - - def HandleGetAllSentMessageIds(self, params): - queryreturn = sqlQuery('''SELECT msgid FROM sent where folder='sent' ORDER BY lastactiontime''') - data = '{"sentMessageIds":[' - for row in queryreturn: - msgid = row[0] - if len(data) > 25: - data += ',' - data += json.dumps({'msgid':hexlify(msgid)}, indent=4, separators=(',', ': ')) - data += ']}' - return data - - def HandleInboxMessagesByReceiver(self, params): - if len(params) == 0: - raise APIError(0, 'I need parameters!') - toAddress = params[0] - queryreturn = sqlQuery('''SELECT msgid, toaddress, fromaddress, subject, received, message, encodingtype FROM inbox WHERE folder='inbox' AND toAddress=?''', toAddress) - data = '{"inboxMessages":[' - for row in queryreturn: - msgid, toAddress, fromAddress, subject, received, message, encodingtype = row - subject = shared.fixPotentiallyInvalidUTF8Data(subject) - message = shared.fixPotentiallyInvalidUTF8Data(message) - if len(data) > 25: - data += ',' - data += json.dumps({'msgid':hexlify(msgid), 'toAddress':toAddress, 'fromAddress':fromAddress, 'subject':base64.b64encode(subject), 'message':base64.b64encode(message), 'encodingType':encodingtype, 'receivedTime':received}, indent=4, separators=(',', ': ')) - data += ']}' - return data - - def HandleGetSentMessageById(self, params): - if len(params) == 0: - raise APIError(0, 'I need parameters!') - msgid = self._decode(params[0], "hex") - queryreturn = sqlQuery('''SELECT msgid, toaddress, fromaddress, subject, lastactiontime, message, encodingtype, status, ackdata FROM sent WHERE msgid=?''', msgid) - data = '{"sentMessage":[' - for row in queryreturn: - msgid, toAddress, fromAddress, subject, lastactiontime, message, encodingtype, status, ackdata = row - subject = shared.fixPotentiallyInvalidUTF8Data(subject) - message = shared.fixPotentiallyInvalidUTF8Data(message) - data += json.dumps({'msgid':hexlify(msgid), 'toAddress':toAddress, 'fromAddress':fromAddress, 'subject':base64.b64encode(subject), 'message':base64.b64encode(message), 'encodingType':encodingtype, 'lastActionTime':lastactiontime, 'status':status, 'ackData':hexlify(ackdata)}, indent=4, separators=(',', ': ')) - data += ']}' - return data - - def HandleGetSentMessagesByAddress(self, params): - if len(params) == 0: - raise APIError(0, 'I need parameters!') - fromAddress = params[0] - queryreturn = sqlQuery('''SELECT msgid, toaddress, fromaddress, subject, lastactiontime, message, encodingtype, status, ackdata FROM sent WHERE folder='sent' AND fromAddress=? ORDER BY lastactiontime''', - fromAddress) - data = '{"sentMessages":[' - for row in queryreturn: - msgid, toAddress, fromAddress, subject, lastactiontime, message, encodingtype, status, ackdata = row - subject = shared.fixPotentiallyInvalidUTF8Data(subject) - message = shared.fixPotentiallyInvalidUTF8Data(message) - if len(data) > 25: - data += ',' - data += json.dumps({'msgid':hexlify(msgid), 'toAddress':toAddress, 'fromAddress':fromAddress, 'subject':base64.b64encode(subject), 'message':base64.b64encode(message), 'encodingType':encodingtype, 'lastActionTime':lastactiontime, 'status':status, 'ackData':hexlify(ackdata)}, indent=4, separators=(',', ': ')) - data += ']}' - return data - - def HandleGetSentMessagesByAckData(self, params): - if len(params) == 0: - raise APIError(0, 'I need parameters!') - ackData = self._decode(params[0], "hex") - queryreturn = sqlQuery('''SELECT msgid, toaddress, fromaddress, subject, lastactiontime, message, encodingtype, status, ackdata FROM sent WHERE ackdata=?''', - ackData) - data = '{"sentMessage":[' - for row in queryreturn: - msgid, toAddress, fromAddress, subject, lastactiontime, message, encodingtype, status, ackdata = row - subject = shared.fixPotentiallyInvalidUTF8Data(subject) - message = shared.fixPotentiallyInvalidUTF8Data(message) - data += json.dumps({'msgid':hexlify(msgid), 'toAddress':toAddress, 'fromAddress':fromAddress, 'subject':base64.b64encode(subject), 'message':base64.b64encode(message), 'encodingType':encodingtype, 'lastActionTime':lastactiontime, 'status':status, 'ackData':hexlify(ackdata)}, indent=4, separators=(',', ': ')) - data += ']}' - return data - - def HandleTrashMessage(self, params): - if len(params) == 0: - raise APIError(0, 'I need parameters!') - msgid = self._decode(params[0], "hex") + try: + if (queryreturn[0][0] == 1) != readStatus: + sqlExecute( + "UPDATE inbox set read = ? WHERE msgid=?", + readStatus, msgid) + queues.UISignalQueue.put(('changedInboxUnread', None)) + except IndexError: + pass + queryreturn = sqlQuery( + "SELECT msgid, toaddress, fromaddress, subject, received, message," + " encodingtype, read FROM inbox WHERE msgid=?", msgid + ) + try: + return {"inboxMessage": [ + self._dump_inbox_message(*queryreturn[0])]} + except IndexError: + pass # FIXME inconsistent + + @command('getAllSentMessages') + def HandleGetAllSentMessages(self): + """ + The same as *getAllInboxMessages* but for sent, + result key - *sentMessages*. Message dict keys are: + *msgid*, *toAddress*, *fromAddress*, *subject*, *message*, + *encodingType*, *lastActionTime*, *status*, *ackData*. + *ackData* is also a hex encoded string. + """ + + queryreturn = sqlQuery( + "SELECT msgid, toaddress, fromaddress, subject, lastactiontime," + " message, encodingtype, status, ackdata FROM sent" + " WHERE folder='sent' ORDER BY lastactiontime" + ) + return {"sentMessages": [ + self._dump_sent_message(*data) for data in queryreturn + ]} + + @command('getAllSentMessageIds', 'getAllSentMessageIDs') + def HandleGetAllSentMessageIds(self): + """ + The same as *getAllInboxMessageIds* but for sent, + result key - *sentMessageIds*. + """ + + queryreturn = sqlQuery( + "SELECT msgid FROM sent WHERE folder='sent'" + " ORDER BY lastactiontime" + ) + return {"sentMessageIds": [ + {'msgid': hexlify(msgid)} for msgid, in queryreturn + ]} + + # after some time getInboxMessagesByAddress should be removed + @command('getInboxMessagesByReceiver', 'legacy:getInboxMessagesByAddress') + def HandleInboxMessagesByReceiver(self, toAddress): + """ + The same as *getAllInboxMessages* but returns only messages + for toAddress. + """ + + queryreturn = sqlQuery( + "SELECT msgid, toaddress, fromaddress, subject, received," + " message, encodingtype, read FROM inbox WHERE folder='inbox'" + " AND toAddress=?", toAddress) + return {"inboxMessages": [ + self._dump_inbox_message(*data) for data in queryreturn + ]} + + @command('getSentMessageById', 'getSentMessageByID') + def HandleGetSentMessageById(self, hid): + """ + Similiar to *getInboxMessageById* but doesn't change message's + read status (sent messages have no such field). + Result key is *sentMessage* + """ + + msgid = self._decode(hid, "hex") + queryreturn = sqlQuery( + "SELECT msgid, toaddress, fromaddress, subject, lastactiontime," + " message, encodingtype, status, ackdata FROM sent WHERE msgid=?", + msgid + ) + try: + return {"sentMessage": [ + self._dump_sent_message(*queryreturn[0]) + ]} + except IndexError: + pass # FIXME inconsistent + + @command('getSentMessagesByAddress', 'getSentMessagesBySender') + def HandleGetSentMessagesByAddress(self, fromAddress): + """ + The same as *getAllSentMessages* but returns only messages + from fromAddress. + """ + + queryreturn = sqlQuery( + "SELECT msgid, toaddress, fromaddress, subject, lastactiontime," + " message, encodingtype, status, ackdata FROM sent" + " WHERE folder='sent' AND fromAddress=? ORDER BY lastactiontime", + fromAddress + ) + return {"sentMessages": [ + self._dump_sent_message(*data) for data in queryreturn + ]} + + @command('getSentMessageByAckData') + def HandleGetSentMessagesByAckData(self, ackData): + """ + Similiar to *getSentMessageById* but searches by ackdata + (also hex encoded). + """ + + ackData = self._decode(ackData, "hex") + queryreturn = sqlQuery( + "SELECT msgid, toaddress, fromaddress, subject, lastactiontime," + " message, encodingtype, status, ackdata FROM sent" + " WHERE ackdata=?", ackData + ) + try: + return {"sentMessage": [ + self._dump_sent_message(*queryreturn[0]) + ]} + except IndexError: + pass # FIXME inconsistent + + @command('trashMessage') + def HandleTrashMessage(self, msgid): + """ + Trash message by msgid (encoded in hex). Returns a simple message + saying that the message was trashed assuming it ever even existed. + Prior existence is not checked. + """ + msgid = self._decode(msgid, "hex") # Trash if in inbox table helper_inbox.trash(msgid) # Trash if in sent table - sqlExecute('''UPDATE sent SET folder='trash' WHERE msgid=?''', msgid) + sqlExecute("UPDATE sent SET folder='trash' WHERE msgid=?", msgid) return 'Trashed message (assuming message existed).' - def HandleTrashInboxMessage(self, params): - if len(params) == 0: - raise APIError(0, 'I need parameters!') - msgid = self._decode(params[0], "hex") + @command('trashInboxMessage') + def HandleTrashInboxMessage(self, msgid): + """Trash inbox message by msgid (encoded in hex).""" + msgid = self._decode(msgid, "hex") helper_inbox.trash(msgid) return 'Trashed inbox message (assuming message existed).' - def HandleTrashSentMessage(self, params): - if len(params) == 0: - raise APIError(0, 'I need parameters!') - msgid = self._decode(params[0], "hex") + @command('trashSentMessage') + def HandleTrashSentMessage(self, msgid): + """Trash sent message by msgid (encoded in hex).""" + msgid = self._decode(msgid, "hex") sqlExecute('''UPDATE sent SET folder='trash' WHERE msgid=?''', msgid) return 'Trashed sent message (assuming message existed).' - def HandleSendMessage(self, params): - if len(params) == 0: - raise APIError(0, 'I need parameters!') - elif len(params) == 4: - toAddress, fromAddress, subject, message = params - encodingType = 2 - TTL = 4 * 24 * 60 * 60 - elif len(params) == 5: - toAddress, fromAddress, subject, message, encodingType = params - TTL = 4 * 24 * 60 * 60 - elif len(params) == 6: - toAddress, fromAddress, subject, message, encodingType, TTL = params - if encodingType not in [2, 3]: + @command('sendMessage') + def HandleSendMessage( + self, toAddress, fromAddress, subject, message, + encodingType=2, TTL=4 * 24 * 60 * 60 + ): + """ + Send the message and return ackdata (hex encoded string). + subject and message must be encoded in base64 which may optionally + include line breaks. TTL is specified in seconds; values outside + the bounds of 3600 to 2419200 will be moved to be within those + bounds. TTL defaults to 4 days. + """ + # pylint: disable=too-many-locals + if encodingType not in (2, 3): raise APIError(6, 'The encoding type must be 2 or 3.') subject = self._decode(subject, "base64") message = self._decode(message, "base64") @@ -671,63 +1218,42 @@ def HandleSendMessage(self, params): TTL = 28 * 24 * 60 * 60 toAddress = addBMIfNotPresent(toAddress) fromAddress = addBMIfNotPresent(fromAddress) - status, addressVersionNumber, streamNumber, toRipe = self._verifyAddress(toAddress) self._verifyAddress(fromAddress) try: - fromAddressEnabled = BMConfigParser().getboolean( - fromAddress, 'enabled') - except: - raise APIError(13, 'Could not find your fromAddress in the keys.dat file.') + 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.') - stealthLevel = BMConfigParser().safeGetInt('bitmessagesettings', 'ackstealthlevel') - ackdata = genAckPayload(streamNumber, stealthLevel) - - t = ('', - toAddress, - toRipe, - fromAddress, - subject, - message, - ackdata, - int(time.time()), # sentTime (this won't change) - int(time.time()), # lastActionTime - 0, - 'msgqueued', - 0, - 'sent', - 2, - TTL) - helper_sent.insert(t) + ackdata = helper_sent.insert( + toAddress=toAddress, fromAddress=fromAddress, + subject=subject, message=message, encoding=encodingType, ttl=TTL) toLabel = '' - queryreturn = sqlQuery('''select label from addressbook where address=?''', toAddress) - if queryreturn != []: - for row in queryreturn: - toLabel, = row - # apiSignalQueue.put(('displayNewSentMessage',(toAddress,toLabel,fromAddress,subject,message,ackdata))) + queryreturn = sqlQuery( + "SELECT label FROM addressbook WHERE address=?", toAddress) + try: + toLabel = queryreturn[0][0] + except IndexError: + pass + queues.UISignalQueue.put(('displayNewSentMessage', ( toAddress, toLabel, fromAddress, subject, message, ackdata))) - queues.workerQueue.put(('sendmessage', toAddress)) return hexlify(ackdata) - def HandleSendBroadcast(self, params): - if len(params) == 0: - raise APIError(0, 'I need parameters!') - if len(params) == 3: - fromAddress, subject, message = params - encodingType = 2 - TTL = 4 * 24 * 60 * 60 - elif len(params) == 4: - fromAddress, subject, message, encodingType = params - TTL = 4 * 24 * 60 * 60 - elif len(params) == 5: - fromAddress, subject, message, encodingType, TTL = params - if encodingType not in [2, 3]: + @command('sendBroadcast') + def HandleSendBroadcast( + self, fromAddress, subject, message, encodingType=2, + TTL=4 * 24 * 60 * 60): + """Send the broadcast message. Similiar to *sendMessage*.""" + + if encodingType not in (2, 3): raise APIError(6, 'The encoding type must be 2 or 3.') + subject = self._decode(subject, "base64") message = self._decode(message, "base64") if len(subject + message) > (2 ** 18 - 500): @@ -739,332 +1265,404 @@ def HandleSendBroadcast(self, params): fromAddress = addBMIfNotPresent(fromAddress) self._verifyAddress(fromAddress) try: - fromAddressEnabled = BMConfigParser().getboolean( - fromAddress, 'enabled') - except: - raise APIError(13, 'could not find your fromAddress in the keys.dat file.') - streamNumber = decodeAddress(fromAddress)[2] - ackdata = genAckPayload(streamNumber, 0) - toAddress = '[Broadcast subscribers]' - ripe = '' - - t = ('', - toAddress, - ripe, - fromAddress, - subject, - message, - ackdata, - int(time.time()), # sentTime (this doesn't change) - int(time.time()), # lastActionTime - 0, - 'broadcastqueued', - 0, - 'sent', - 2, - TTL) - helper_sent.insert(t) - - toLabel = '[Broadcast subscribers]' + 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( + fromAddress=fromAddress, subject=subject, + message=message, status='broadcastqueued', + encoding=encodingType) + + toLabel = str_broadcast_subscribers queues.UISignalQueue.put(('displayNewSentMessage', ( toAddress, toLabel, fromAddress, subject, message, ackdata))) queues.workerQueue.put(('sendbroadcast', '')) return hexlify(ackdata) - def HandleGetStatus(self, params): - if len(params) != 1: - raise APIError(0, 'I need one parameter!') - ackdata, = params - if len(ackdata) != 64: - raise APIError(15, 'The length of ackData should be 32 bytes (encoded in hex thus 64 characters).') + @command('getStatus') + def HandleGetStatus(self, ackdata): + """ + Get the status of sent message by its ackdata (hex encoded). + Returns one of these strings: notfound, msgqueued, + broadcastqueued, broadcastsent, doingpubkeypow, awaitingpubkey, + doingmsgpow, forcepow, msgsent, msgsentnoackexpected or ackreceived. + """ + + if len(ackdata) < 76: + # The length of ackData should be at least 38 bytes (76 hex digits) + raise APIError(15, 'Invalid ackData object size.') ackdata = self._decode(ackdata, "hex") queryreturn = sqlQuery( - '''SELECT status FROM sent where ackdata=?''', - ackdata) - if queryreturn == []: + "SELECT status FROM sent where ackdata=?", ackdata) + try: + return queryreturn[0][0] + except IndexError: return 'notfound' - for row in queryreturn: - status, = row - return status - - def HandleAddSubscription(self, params): - if len(params) == 0: - raise APIError(0, 'I need parameters!') - if len(params) == 1: - address, = params - label = '' - if len(params) == 2: - address, label = params + + @command('addSubscription') + def HandleAddSubscription(self, address, label=''): + """Subscribe to the address. label must be base64 encoded.""" + + if label: label = self._decode(label, "base64") try: - unicode(label, 'utf-8') - except: + label.decode('utf-8') + except UnicodeDecodeError: raise APIError(17, 'Label is not valid UTF-8 data.') - if len(params) > 2: - raise APIError(0, 'I need either 1 or 2 parameters!') - address = addBMIfNotPresent(address) self._verifyAddress(address) + address = addBMIfNotPresent(address) # First we must check to see if the address is already in the # subscriptions list. - queryreturn = sqlQuery('''select * from subscriptions where address=?''', address) - if queryreturn != []: + queryreturn = sqlQuery( + "SELECT * FROM subscriptions WHERE address=?", address) + if queryreturn: raise APIError(16, 'You are already subscribed to that address.') - sqlExecute('''INSERT INTO subscriptions VALUES (?,?,?)''',label, address, True) + sqlExecute( + "INSERT INTO subscriptions VALUES (?,?,?)", label, address, True) shared.reloadBroadcastSendersForWhichImWatching() queues.UISignalQueue.put(('rerenderMessagelistFromLabels', '')) queues.UISignalQueue.put(('rerenderSubscriptions', '')) return 'Added subscription.' - def HandleDeleteSubscription(self, params): - if len(params) != 1: - raise APIError(0, 'I need 1 parameter!') - address, = params + @command('deleteSubscription') + def HandleDeleteSubscription(self, address): + """ + Unsubscribe from the address. The program does not check whether + you were subscribed in the first place. + """ + address = addBMIfNotPresent(address) - sqlExecute('''DELETE FROM subscriptions WHERE address=?''', address) + sqlExecute("DELETE FROM subscriptions WHERE address=?", address) shared.reloadBroadcastSendersForWhichImWatching() queues.UISignalQueue.put(('rerenderMessagelistFromLabels', '')) queues.UISignalQueue.put(('rerenderSubscriptions', '')) return 'Deleted subscription if it existed.' - def ListSubscriptions(self, params): - queryreturn = sqlQuery('''SELECT label, address, enabled FROM subscriptions''') - data = {'subscriptions': []} - for row in queryreturn: - label, address, enabled = row + @command('listSubscriptions') + def ListSubscriptions(self): + """ + Returns dict with a list of all subscriptions + in the *subscriptions* key. + """ + + queryreturn = sqlQuery( + "SELECT label, address, enabled FROM subscriptions") + data = [] + for label, address, enabled in queryreturn: label = shared.fixPotentiallyInvalidUTF8Data(label) - data['subscriptions'].append({'label':base64.b64encode(label), 'address': address, 'enabled': enabled == 1}) - return json.dumps(data, indent=4, separators=(',',': ')) - - def HandleDisseminatePreEncryptedMsg(self, params): - # 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. - if len(params) != 3: - raise APIError(0, 'I need 3 parameter!') - encryptedPayload, requiredAverageProofOfWorkNonceTrialsPerByte, requiredPayloadLengthExtraBytes = params + data.append({ + 'label': base64.b64encode(label), + 'address': address, + 'enabled': enabled == 1 + }) + return {'subscriptions': data} + + @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 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) - with shared.printLock: - print '(For msg message via API) Doing proof of work. Total required difficulty:', float(requiredAverageProofOfWorkNonceTrialsPerByte) / defaults.networkDefaultProofOfWorkNonceTrialsPerByte, 'Required small message difficulty:', float(requiredPayloadLengthExtraBytes) / defaults.networkDefaultPayloadLengthExtraBytes - powStartTime = time.time() - initialHash = hashlib.sha512(encryptedPayload).digest() - trialValue, nonce = proofofwork.run(target, initialHash) - with shared.printLock: - print '(For msg message via API) Found proof of work', trialValue, 'Nonce:', nonce - try: - print 'POW took', int(time.time() - powStartTime), 'seconds.', nonce / (time.time() - powStartTime), 'nonce trials per second.' - except: - pass - 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() + trialValue, nonce = proofofwork.calculate( + encryptedPayload, TTL, + nonceTrialsPerByte, payloadLengthExtraBytes + ) + 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] = ( - objectType, toStreamNumber, encryptedPayload, int(time.time()) + TTL,'') - with shared.printLock: - print 'Broadcasting inv for msg(API disseminatePreEncryptedMsg command):', hexlify(inventoryHash) - queues.invQueue.put((toStreamNumber, inventoryHash)) - - def HandleTrashSentMessageByAckDAta(self, params): + state.Inventory[inventoryHash] = ( + objectType, toStreamNumber, encryptedPayload, expiresTime, b'') + logger.info( + 'Broadcasting inv for msg(API disseminatePreEncryptedMsg' + ' command): %s', hexlify(inventoryHash)) + invQueue.put((toStreamNumber, inventoryHash)) + return hexlify(inventoryHash).decode() + + @command('trashSentMessageByAckData') + def HandleTrashSentMessageByAckDAta(self, ackdata): + """Trash a sent message by ackdata (hex encoded)""" # This API method should only be used when msgid is not available - if len(params) == 0: - raise APIError(0, 'I need parameters!') - ackdata = self._decode(params[0], "hex") - sqlExecute('''UPDATE sent SET folder='trash' WHERE ackdata=?''', ackdata) + ackdata = self._decode(ackdata, "hex") + sqlExecute("UPDATE sent SET folder='trash' WHERE ackdata=?", ackdata) return 'Trashed sent message (assuming message existed).' - def HandleDissimatePubKey(self, params): - # The device issuing this command to PyBitmessage supplies a pubkey object to be - # disseminated to the rest of the Bitmessage network. PyBitmessage accepts this - # pubkey object and sends it out to the rest of the Bitmessage network as if it - # had generated the pubkey object itself. Please do not yet add this to the api - # doc. - if len(params) != 1: - raise APIError(0, 'I need 1 parameter!') - payload, = params + @command('disseminatePubkey') + def HandleDissimatePubKey(self, payload): + """Handle a request to disseminate a public key""" + + # The device issuing this command to PyBitmessage supplies a pubkey + # object to be disseminated to the rest of the Bitmessage network. + # PyBitmessage accepts this pubkey object and sends it out to the rest + # of the Bitmessage network as if it had generated the pubkey object + # itself. Please do not yet add this to the api doc. payload = self._decode(payload, "hex") # Let us do the POW - target = 2 ** 64 / ((len(payload) + defaults.networkDefaultPayloadLengthExtraBytes + - 8) * defaults.networkDefaultProofOfWorkNonceTrialsPerByte) - print '(For pubkey message via API) Doing proof of work...' + target = 2 ** 64 / (( + 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) - print '(For pubkey message via API) Found proof of work', trialValue, 'Nonce:', nonce + logger.info( + '(For pubkey message via API) Found proof of work %s Nonce: %s', + trialValue, nonce + ) payload = pack('>Q', nonce) + payload - pubkeyReadPosition = 8 # bypass the nonce - if payload[pubkeyReadPosition:pubkeyReadPosition+4] == '\x00\x00\x00\x00': # if this pubkey uses 8 byte time + pubkeyReadPosition = 8 # bypass the nonce + if payload[pubkeyReadPosition:pubkeyReadPosition + 4] == \ + '\x00\x00\x00\x00': # if this pubkey uses 8 byte time pubkeyReadPosition += 8 else: pubkeyReadPosition += 4 - addressVersion, addressVersionLength = decodeVarint(payload[pubkeyReadPosition:pubkeyReadPosition+10]) + addressVersionLength = decodeVarint( + payload[pubkeyReadPosition:pubkeyReadPosition + 10])[1] pubkeyReadPosition += addressVersionLength - pubkeyStreamNumber = decodeVarint(payload[pubkeyReadPosition:pubkeyReadPosition+10])[0] + pubkeyStreamNumber = decodeVarint( + payload[pubkeyReadPosition:pubkeyReadPosition + 10])[0] inventoryHash = calculateInventoryHash(payload) - objectType = 1 - #todo: support v4 pubkeys + objectType = 1 # .. todo::: support v4 pubkeys TTL = 28 * 24 * 60 * 60 - Inventory()[inventoryHash] = ( - objectType, pubkeyStreamNumber, payload, int(time.time()) + TTL,'') - with shared.printLock: - print 'broadcasting inv within API command disseminatePubkey with hash:', hexlify(inventoryHash) - queues.invQueue.put((pubkeyStreamNumber, inventoryHash)) + state.Inventory[inventoryHash] = ( + objectType, pubkeyStreamNumber, payload, int(time.time()) + TTL, '' + ) + logger.info( + 'broadcasting inv within API command disseminatePubkey with' + ' hash: %s', hexlify(inventoryHash)) + invQueue.put((pubkeyStreamNumber, inventoryHash)) + + @command( + 'getMessageDataByDestinationHash', 'getMessageDataByDestinationTag') + def HandleGetMessageDataByDestinationHash(self, requestedHash): + """Handle a request to get message data by destination hash""" - def HandleGetMessageDataByDestinationHash(self, params): # Method will eventually be used by a particular Android app to # select relevant messages. Do not yet add this to the api # doc. - if len(params) != 1: - raise APIError(0, 'I need 1 parameter!') - requestedHash, = params if len(requestedHash) != 32: - raise APIError(19, 'The length of hash should be 32 bytes (encoded in hex thus 64 characters).') + raise APIError( + 19, 'The length of hash should be 32 bytes (encoded in hex' + ' thus 64 characters).') requestedHash = self._decode(requestedHash, "hex") # This is not a particularly commonly used API function. Before we # use it we'll need to fill out a field in our inventory database # which is blank by default (first20bytesofencryptedmessage). queryreturn = sqlQuery( - '''SELECT hash, payload FROM inventory WHERE tag = '' and objecttype = 2 ; ''') + "SELECT hash, payload FROM inventory WHERE tag = ''" + " and objecttype = 2") with SqlBulkExecute() as sql: - for row in queryreturn: - hash01, payload = row - readPosition = 16 # Nonce length + time length - readPosition += decodeVarint(payload[readPosition:readPosition+10])[1] # Stream Number length - t = (payload[readPosition:readPosition+32],hash01) - sql.execute('''UPDATE inventory SET tag=? WHERE hash=?; ''', *t) - - queryreturn = sqlQuery('''SELECT payload FROM inventory WHERE tag = ?''', - requestedHash) - data = '{"receivedMessageDatas":[' - for row in queryreturn: - payload, = row - if len(data) > 25: - data += ',' - data += json.dumps({'data':hexlify(payload)}, indent=4, separators=(',', ': ')) - data += ']}' - return data - - def HandleClientStatus(self, params): - if len(network.stats.connectedHostsList()) == 0: + for hash01, payload in queryreturn: + readPosition = 16 # Nonce length + time length + # Stream Number length + readPosition += decodeVarint( + payload[readPosition:readPosition + 10])[1] + t = (payload[readPosition:readPosition + 32], hash01) + sql.execute("UPDATE inventory SET tag=? WHERE hash=?", *t) + + queryreturn = sqlQuery( + "SELECT payload FROM inventory WHERE tag = ?", requestedHash) + return {"receivedMessageDatas": [ + {'data': hexlify(payload)} for payload, in queryreturn + ]} + + @command('clientStatus') + def HandleClientStatus(self): + """ + Returns the bitmessage status as dict with keys *networkConnections*, + *numberOfMessagesProcessed*, *numberOfBroadcastsProcessed*, + *numberOfPubkeysProcessed*, *pendingDownload*, *networkStatus*, + *softwareName*, *softwareVersion*. *networkStatus* will be one of + these strings: "notConnected", + "connectedButHaveNotReceivedIncomingConnections", + or "connectedAndReceivingIncomingConnections". + """ + + connections_num = len(stats.connectedHostsList()) + + if connections_num == 0: networkStatus = 'notConnected' - elif len(network.stats.connectedHostsList()) > 0 and not shared.clientHasReceivedIncomingConnections: - networkStatus = 'connectedButHaveNotReceivedIncomingConnections' - else: + elif state.clientHasReceivedIncomingConnections: networkStatus = 'connectedAndReceivingIncomingConnections' - return json.dumps({'networkConnections':len(network.stats.connectedHostsList()),'numberOfMessagesProcessed':shared.numberOfMessagesProcessed, 'numberOfBroadcastsProcessed':shared.numberOfBroadcastsProcessed, 'numberOfPubkeysProcessed':shared.numberOfPubkeysProcessed, 'networkStatus':networkStatus, 'softwareName':'PyBitmessage','softwareVersion':softwareVersion}, indent=4, separators=(',', ': ')) - - def HandleDecodeAddress(self, params): - # Return a meaningful decoding of an address. - if len(params) != 1: - raise APIError(0, 'I need 1 parameter!') - address, = params - status, addressVersion, streamNumber, ripe = decodeAddress(address) - return json.dumps({'status':status, 'addressVersion':addressVersion, - 'streamNumber':streamNumber, 'ripe':base64.b64encode(ripe)}, indent=4, - separators=(',', ': ')) - - def HandleHelloWorld(self, params): - (a, b) = params + else: + networkStatus = 'connectedButHaveNotReceivedIncomingConnections' + return { + 'networkConnections': connections_num, + 'numberOfMessagesProcessed': state.numberOfMessagesProcessed, + 'numberOfBroadcastsProcessed': state.numberOfBroadcastsProcessed, + 'numberOfPubkeysProcessed': state.numberOfPubkeysProcessed, + '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""" return a + '-' + b - def HandleAdd(self, params): - (a, b) = params + @command('add') + def HandleAdd(self, a, b): + """Test two numeric params""" return a + b - def HandleStatusBar(self, params): - message, = params + @command('statusBar') + def HandleStatusBar(self, message): + """Update GUI statusbar message""" queues.UISignalQueue.put(('updateStatusBar', message)) + return "success" - def HandleDeleteAndVacuum(self, params): - if not params: - sqlStoredProcedure('deleteandvacuume') - return 'done' - - def HandleShutdown(self, params): - if not params: - shutdown.doCleanShutdown() - return 'done' - - handlers = {} - handlers['helloWorld'] = HandleHelloWorld - handlers['add'] = HandleAdd - handlers['statusBar'] = HandleStatusBar - handlers['listAddresses'] = HandleListAddresses - handlers['listAddressBookEntries'] = HandleListAddressBookEntries; - handlers['listAddressbook'] = HandleListAddressBookEntries # the listAddressbook alias should be removed eventually. - handlers['addAddressBookEntry'] = HandleAddAddressBookEntry - handlers['addAddressbook'] = HandleAddAddressBookEntry # the addAddressbook alias should be deleted eventually. - handlers['deleteAddressBookEntry'] = HandleDeleteAddressBookEntry - handlers['deleteAddressbook'] = HandleDeleteAddressBookEntry # The deleteAddressbook alias should be deleted eventually. - handlers['createRandomAddress'] = HandleCreateRandomAddress - handlers['createDeterministicAddresses'] = HandleCreateDeterministicAddresses - handlers['getDeterministicAddress'] = HandleGetDeterministicAddress - handlers['createChan'] = HandleCreateChan - handlers['joinChan'] = HandleJoinChan - handlers['leaveChan'] = HandleLeaveChan - handlers['deleteAddress'] = HandleDeleteAddress - handlers['getAllInboxMessages'] = HandleGetAllInboxMessages - handlers['getAllInboxMessageIds'] = HandleGetAllInboxMessageIds - handlers['getAllInboxMessageIDs'] = HandleGetAllInboxMessageIds - handlers['getInboxMessageById'] = HandleGetInboxMessageById - handlers['getInboxMessageByID'] = HandleGetInboxMessageById - handlers['getAllSentMessages'] = HandleGetAllSentMessages - handlers['getAllSentMessageIds'] = HandleGetAllSentMessageIds - handlers['getAllSentMessageIDs'] = HandleGetAllSentMessageIds - handlers['getInboxMessagesByReceiver'] = HandleInboxMessagesByReceiver - handlers['getInboxMessagesByAddress'] = HandleInboxMessagesByReceiver #after some time getInboxMessagesByAddress should be removed - handlers['getSentMessageById'] = HandleGetSentMessageById - handlers['getSentMessageByID'] = HandleGetSentMessageById - handlers['getSentMessagesByAddress'] = HandleGetSentMessagesByAddress - handlers['getSentMessagesBySender'] = HandleGetSentMessagesByAddress - handlers['getSentMessageByAckData'] = HandleGetSentMessagesByAckData - handlers['trashMessage'] = HandleTrashMessage - handlers['trashInboxMessage'] = HandleTrashInboxMessage - handlers['trashSentMessage'] = HandleTrashSentMessage - handlers['trashSentMessageByAckData'] = HandleTrashSentMessageByAckDAta - handlers['sendMessage'] = HandleSendMessage - handlers['sendBroadcast'] = HandleSendBroadcast - handlers['getStatus'] = HandleGetStatus - handlers['addSubscription'] = HandleAddSubscription - handlers['deleteSubscription'] = HandleDeleteSubscription - handlers['listSubscriptions'] = ListSubscriptions - handlers['disseminatePreEncryptedMsg'] = HandleDisseminatePreEncryptedMsg - handlers['disseminatePubkey'] = HandleDissimatePubKey - handlers['getMessageDataByDestinationHash'] = HandleGetMessageDataByDestinationHash - handlers['getMessageDataByDestinationTag'] = HandleGetMessageDataByDestinationHash - handlers['clientStatus'] = HandleClientStatus - handlers['decodeAddress'] = HandleDecodeAddress - handlers['deleteAndVacuum'] = HandleDeleteAndVacuum - handlers['shutdown'] = HandleShutdown + @testmode('undeleteMessage') + def HandleUndeleteMessage(self, msgid): + """Undelete message""" + msgid = self._decode(msgid, "hex") + helper_inbox.undeleteMessage(msgid) + return "Undeleted message" + + @command('deleteAndVacuum') + def HandleDeleteAndVacuum(self): + """Cleanup trashes and vacuum messages database""" + sqlStoredProcedure('deleteandvacuume') + return 'done' + + @command('shutdown') + def HandleShutdown(self): + """Shutdown the bitmessage. Returns 'done'.""" + # backward compatible trick because False == 0 is True + state.shutdown = False + return 'done' def _handle_request(self, method, params): - if (self.handlers.has_key(method)): - return self.handlers[method](self, params) - else: + try: + # pylint: disable=attribute-defined-outside-init + self._method = method + func = self._handlers[method] + return func(self, *params) + except KeyError: raise APIError(20, 'Invalid method: %s' % method) + except TypeError as e: + msg = 'Unexpected API Failure - %s' % e + if 'argument' not in str(e): + raise APIError(21, msg) + argcount = len(params) + maxcount = func.func_code.co_argcount + if argcount > maxcount: + msg = ( + 'Command %s takes at most %s parameters (%s given)' + % (method, maxcount, argcount)) + else: + mincount = maxcount - len(func.func_defaults or []) + if argcount < mincount: + msg = ( + 'Command %s takes at least %s parameters (%s given)' + % (method, mincount, argcount)) + raise APIError(0, msg) + finally: + state.last_api_response = time.time() def _dispatch(self, method, params): - self.cookies = [] - - validuser = self.APIAuthenticateClient() - if not validuser: - time.sleep(2) - return "RPC Username or password incorrect or HTTP header lacks authentication at all." + _fault = None try: return self._handle_request(method, params) except APIError as e: - return str(e) + _fault = e except varintDecodeError as e: logger.error(e) - return "API Error 0026: Data contains a malformed varint. Some details: %s" % e + _fault = APIError( + 26, 'Data contains a malformed varint. Some details: %s' % e) except Exception as e: logger.exception(e) - return "API Error 0021: Unexpected API Failure - %s" % str(e) + _fault = APIError(21, 'Unexpected API Failure - %s' % e) + + if _fault: + if self.config.safeGet( + 'bitmessagesettings', 'apivariant') == 'legacy': + return str(_fault) + else: + raise _fault # pylint: disable=raising-bad-type + + def _listMethods(self): + """List all API commands""" + return self._handlers.keys() + + def _methodHelp(self, method): + return self._handlers[method].__doc__ diff --git a/src/api_client.py b/src/api_client.py deleted file mode 100644 index 6dc0c7b0d2..0000000000 --- a/src/api_client.py +++ /dev/null @@ -1,70 +0,0 @@ -# This is an example of how to connect to and use the Bitmessage API. -# See https://bitmessage.org/wiki/API_Reference - -import xmlrpclib -import json -import time - -api = xmlrpclib.ServerProxy("http://bradley:password@localhost:8442/") - -print 'Let\'s test the API first.' -inputstr1 = "hello" -inputstr2 = "world" -print api.helloWorld(inputstr1, inputstr2) -print api.add(2,3) - -print 'Let\'s set the status bar message.' -print api.statusBar("new status bar message") - -print 'Let\'s list our addresses:' -print api.listAddresses() - -print 'Let\'s list our address again, but this time let\'s parse the json data into a Python data structure:' -jsonAddresses = json.loads(api.listAddresses()) -print jsonAddresses -print 'Now that we have our address data in a nice Python data structure, let\'s look at the first address (index 0) and print its label:' -print jsonAddresses['addresses'][0]['label'] - -print 'Uncomment the next two lines to create a new random address with a slightly higher difficulty setting than normal.' -#addressLabel = 'new address label'.encode('base64') -#print api.createRandomAddress(addressLabel,False,1.05,1.1111) - -print 'Uncomment these next four lines to create new deterministic addresses.' -#passphrase = 'asdfasdfqwser'.encode('base64') -#jsonDeterministicAddresses = api.createDeterministicAddresses(passphrase, 2, 4, 1, False) -#print jsonDeterministicAddresses -#print json.loads(jsonDeterministicAddresses) - -#print 'Uncomment this next line to print the first deterministic address that would be generated with the given passphrase. This will Not add it to the Bitmessage interface or the keys.dat file.' -#print api.getDeterministicAddress('asdfasdfqwser'.encode('base64'),4,1) - -#print 'Uncomment this line to subscribe to an address. (You must use your own address, this one is invalid).' -#print api.addSubscription('2D94G5d8yp237GGqAheoecBYpdehdT3dha','test sub'.encode('base64')) - -#print 'Uncomment this line to unsubscribe from an address.' -#print api.deleteSubscription('2D94G5d8yp237GGqAheoecBYpdehdT3dha') - -print 'Let\'s now print all of our inbox messages:' -print api.getAllInboxMessages() -inboxMessages = json.loads(api.getAllInboxMessages()) -print inboxMessages - -print 'Uncomment this next line to decode the actual message data in the first message:' -#print inboxMessages['inboxMessages'][0]['message'].decode('base64') - -print 'Uncomment this next line in the code to delete a message' -#print api.trashMessage('584e5826947242a82cb883c8b39ac4a14959f14c228c0fbe6399f73e2cba5b59') - -print 'Uncomment these lines to send a message. The example addresses are invalid; you will have to put your own in.' -#subject = 'subject!'.encode('base64') -#message = 'Hello, this is the message'.encode('base64') -#ackData = api.sendMessage('BM-Gtsm7PUabZecs3qTeXbNPmqx3xtHCSXF', 'BM-2DCutnUZG16WiW3mdAm66jJUSCUv88xLgS', subject,message) -#print 'The ackData is:', ackData -#while True: -# time.sleep(2) -# print 'Current status:', api.getStatus(ackData) - -print 'Uncomment these lines to send a broadcast. The example address is invalid; you will have to put your own in.' -#subject = 'subject within broadcast'.encode('base64') -#message = 'Hello, this is the message within a broadcast.'.encode('base64') -#print api.sendBroadcast('BM-onf6V1RELPgeNN6xw9yhpAiNiRexSRD4e', subject,message) diff --git a/src/backend/address_generator.py b/src/backend/address_generator.py new file mode 100644 index 0000000000..312c313bc2 --- /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 05f4506ee9..d46c7debfd 100644 --- a/src/bitmessagecli.py +++ b/src/bitmessagecli.py @@ -1,610 +1,636 @@ -#!/usr/bin/python2.7 +#!/usr/bin/python2.7 # -*- coding: utf-8 -*- -# Created by Adam Melton (.dok) referenceing https://bitmessage.org/wiki/API_Reference for API documentation -# Distributed under the MIT/X11 software license. See http://www.opensource.org/licenses/mit-license.php. +# pylint: disable=too-many-lines,global-statement,too-many-branches,too-many-statements,inconsistent-return-statements +# pylint: disable=too-many-nested-blocks,too-many-locals,protected-access,too-many-arguments,too-many-function-args +# pylint: disable=no-member +""" +Created by Adam Melton (.dok) referenceing https://bitmessage.org/wiki/API_Reference for API documentation +Distributed under the MIT/X11 software license. See http://www.opensource.org/licenses/mit-license.php. -# This is an example of a daemon client for PyBitmessage 0.6.2, by .dok (Version 0.3.1) , modified +This is an example of a daemon client for PyBitmessage 0.6.2, by .dok (Version 0.3.1) , modified +TODO: fix the following (currently ignored) violations: + +""" -import xmlrpclib import datetime -#import hashlib -#import getopt import imghdr -import ntpath import json +import ntpath +import os import socket -import time import sys -import os +import time + +from six.moves import input as raw_input +from six.moves import xmlrpc_client as xmlrpclib + +from bmconfigparser import config -from bmconfigparser import BMConfigParser api = '' keysName = 'keys.dat' keysPath = 'keys.dat' -usrPrompt = 0 #0 = First Start, 1 = prompt, 2 = no prompt if the program is starting up +usrPrompt = 0 # 0 = First Start, 1 = prompt, 2 = no prompt if the program is starting up knownAddresses = dict() -def userInput(message): #Checks input for exit or quit. Also formats for input, etc + +def userInput(message): + """Checks input for exit or quit. Also formats for input, etc""" + global usrPrompt - print '\n' + message + + print('\n' + message) uInput = raw_input('> ') - if (uInput.lower() == 'exit'): #Returns the user to the main menu + if uInput.lower() == 'exit': # Returns the user to the main menu usrPrompt = 1 main() - - elif (uInput.lower() == 'quit'): #Quits the program - print '\n Bye\n' - sys.exit() - os._exit() # _ + + elif uInput.lower() == 'quit': # Quits the program + print('\n Bye\n') + sys.exit(0) + else: return uInput - -def restartBmNotify(): #Prompts the user to restart Bitmessage. - print '\n *******************************************************************' - print ' WARNING: If Bitmessage is running locally, you must restart it now.' - print ' *******************************************************************\n' - -#Begin keys.dat interactions -def lookupAppdataFolder(): #gets the appropriate folders for the .dat files depending on the OS. Taken from bitmessagemain.py + + +def restartBmNotify(): + """Prompt the user to restart Bitmessage""" + print('\n *******************************************************************') + print(' WARNING: If Bitmessage is running locally, you must restart it now.') + print(' *******************************************************************\n') + + +# Begin keys.dat interactions + + +def lookupAppdataFolder(): + """gets the appropriate folders for the .dat files depending on the OS. Taken from bitmessagemain.py""" + APPNAME = "PyBitmessage" - from os import path, environ if sys.platform == 'darwin': - if "HOME" in environ: - dataFolder = path.join(os.environ["HOME"], "Library/Application support/", APPNAME) + '/' + if "HOME" in os.environ: + dataFolder = os.path.join(os.environ["HOME"], "Library/Application support/", APPNAME) + '/' else: - print ' Could not find home folder, please report this message and your OS X version to the Daemon Github.' - os._exit() + print( + ' Could not find home folder, please report ' + 'this message and your OS X version to the Daemon Github.') + sys.exit(1) elif 'win32' in sys.platform or 'win64' in sys.platform: - dataFolder = path.join(environ['APPDATA'], APPNAME) + '\\' + dataFolder = os.path.join(os.environ['APPDATA'], APPNAME) + '\\' else: - dataFolder = path.expanduser(path.join("~", ".config/" + APPNAME + "/")) + dataFolder = os.path.expanduser(os.path.join("~", ".config/" + APPNAME + "/")) return dataFolder + def configInit(): - BMConfigParser().add_section('bitmessagesettings') - BMConfigParser().set('bitmessagesettings', 'port', '8444') #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','apienabled','true') #Sets apienabled to true in keys.dat - + """Initialised the configuration""" + + 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. + 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') - print '\n ' + str(keysName) + ' Initalized in the same directory as daemon.py' - print ' You will now need to configure the ' + str(keysName) + ' file.\n' def apiInit(apiEnabled): + """Initialise the API""" + global usrPrompt - BMConfigParser().read(keysPath) - + config.read(keysPath) - - if (apiEnabled == False): #API information there but the api is disabled. + 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 + if uInput == "y": + config.set('bitmessagesettings', 'apienabled', 'true') # Sets apienabled to true in keys.dat with open(keysPath, 'wb') as configfile: - BMConfigParser().write(configfile) - - print 'Done' + config.write(configfile) + + print('Done') restartBmNotify() return True - + elif uInput == "n": - print ' \n************************************************************' - print ' Daemon will not work when the API is disabled. ' - print ' Please refer to the Bitmessage Wiki on how to setup the API.' - print ' ************************************************************\n' + print(' \n************************************************************') + print(' Daemon will not work when the API is disabled. ') + print(' Please refer to the Bitmessage Wiki on how to setup the API.') + print(' ************************************************************\n') usrPrompt = 1 main() - + else: - print '\n Invalid Entry\n' + print('\n Invalid Entry\n') usrPrompt = 1 main() - elif (apiEnabled == True): #API correctly setup - #Everything is as it should be + + elif apiEnabled: # API correctly setup + # Everything is as it should be return True - - else: #API information was not present. - print '\n ' + str(keysPath) + ' not properly configured!\n' + + else: # API information was not present. + print('\n ' + str(keysPath) + ' not properly configured!\n') uInput = userInput("Would you like to do this now, (Y)es or (N)o?").lower() - if uInput == "y": #User said yes, initalize the api by writing these values to the keys.dat file - print ' ' - + if uInput == "y": # User said yes, initalize the api by writing these values to the keys.dat file + print(' ') + apiUsr = userInput("API Username") apiPwd = userInput("API Password") - #apiInterface = userInput("API Interface. (127.0.0.1)") apiPort = userInput("API Port") apiEnabled = userInput("API Enabled? (True) or (False)").lower() daemon = userInput("Daemon mode Enabled? (True) or (False)").lower() if (daemon != 'true' and daemon != 'false'): - print '\n Invalid Entry for Daemon.\n' + print('\n Invalid Entry for Daemon.\n') uInput = 1 main() - - print ' -----------------------------------\n' - - BMConfigParser().set('bitmessagesettings', 'port', '8444') #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','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) + + print(' -----------------------------------\n') + + # 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. + 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) - - print '\n Finished configuring the keys.dat file with API information.\n' + config.write(configfile) + + print('\n Finished configuring the keys.dat file with API information.\n') restartBmNotify() return True - + elif uInput == "n": - print '\n ***********************************************************' - print ' Please refer to the Bitmessage Wiki on how to setup the API.' - print ' ***********************************************************\n' + print('\n ***********************************************************') + print(' Please refer to the Bitmessage Wiki on how to setup the API.') + print(' ***********************************************************\n') usrPrompt = 1 main() else: - print ' \nInvalid entry\n' + print(' \nInvalid entry\n') usrPrompt = 1 main() def apiData(): + """TBC""" + global keysName 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: - #Could not load the keys.dat file in the program directory. Perhaps it is in the appdata directory. + 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') - except: - #keys.dat was not there either, something is wrong. - print '\n ******************************************************************' - print ' There was a problem trying to access the Bitmessage keys.dat file' - print ' or keys.dat is not set up correctly' - print ' Make sure that daemon is in the same directory as Bitmessage. ' - print ' ******************************************************************\n' + config.get('bitmessagesettings', 'port') + except: # noqa:E722 + # keys.dat was not there either, something is wrong. + print('\n ******************************************************************') + print(' There was a problem trying to access the Bitmessage keys.dat file') + print(' or keys.dat is not set up correctly') + print(' Make sure that daemon is in the same directory as Bitmessage. ') + print(' ******************************************************************\n') uInput = userInput("Would you like to create a keys.dat in the local directory, (Y)es or (N)o?").lower() - - if (uInput == "y" or uInput == "yes"): + + if uInput in ("y", "yes"): configInit() keysPath = keysName usrPrompt = 0 main() - elif (uInput == "n" or uInput == "no"): - print '\n Trying Again.\n' + elif uInput in ("n", "no"): + print('\n Trying Again.\n') usrPrompt = 0 main() else: - print '\n Invalid Input.\n' + print('\n Invalid Input.\n') usrPrompt = 1 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') - except: - apiInit("") #Initalize the keys.dat file with API information + try: # checks to make sure that everyting is configured correctly. Excluding apiEnabled, it is checked after + 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(config.safeGetBoolean('bitmessagesettings','apienabled')) + # #if false it will prompt the user, if true it will return true + + 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') + + # Build the api credentials + return "http://" + apiUsername + ":" + apiPassword + "@" + apiInterface + ":" + str(apiPort) + "/" - #keys.dat file was found or appropriately configured, allow information retrieval - #apiEnabled = apiInit(BMConfigParser().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') - - print '\n API data successfully imported.\n' - - return "http://" + apiUsername + ":" + apiPassword + "@" + apiInterface+ ":" + str(apiPort) + "/" #Build the api credentials - -#End keys.dat interactions +# End keys.dat interactions -def apiTest(): #Tests the API connection to bitmessage. Returns true if it is connected. +def apiTest(): + """Tests the API connection to bitmessage. Returns true if it is connected.""" try: - result = api.add(2,3) - except: + result = api.add(2, 3) + except: # noqa:E722 return False - if (result == 5): - return True - else: - return False + return result == 5 + + +def bmSettings(): + """Allows the viewing and modification of keys.dat settings.""" -def bmSettings(): #Allows the viewing and modification of keys.dat settings. global keysPath global usrPrompt + keysPath = 'keys.dat' - - BMConfigParser().read(keysPath)#Read the keys.dat + + config.read(keysPath) # Read the keys.dat try: - port = BMConfigParser().get('bitmessagesettings', 'port') - except: - print '\n File not found.\n' + 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') - - 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') - - - print '\n -----------------------------------' - print ' | Current Bitmessage Settings |' - print ' -----------------------------------' - print ' port = ' + port - print ' startonlogon = ' + str(startonlogon) - print ' minimizetotray = ' + str(minimizetotray) - print ' showtraynotifications = ' + str(showtraynotifications) - print ' startintray = ' + str(startintray) - print ' defaultnoncetrialsperbyte = ' + defaultnoncetrialsperbyte - print ' defaultpayloadlengthextrabytes = ' + defaultpayloadlengthextrabytes - print ' daemon = ' + str(daemon) - print '\n ------------------------------------' - print ' | Current Connection Settings |' - print ' -----------------------------------' - print ' socksproxytype = ' + socksproxytype - print ' sockshostname = ' + sockshostname - print ' socksport = ' + socksport - print ' socksauthentication = ' + str(socksauthentication) - print ' socksusername = ' + socksusername - print ' sockspassword = ' + sockspassword - print ' ' + + 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 = 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 |') + print(' -----------------------------------') + print(' port = ' + port) + print(' startonlogon = ' + str(startonlogon)) + print(' minimizetotray = ' + str(minimizetotray)) + print(' showtraynotifications = ' + str(showtraynotifications)) + print(' startintray = ' + str(startintray)) + print(' defaultnoncetrialsperbyte = ' + defaultnoncetrialsperbyte) + print(' defaultpayloadlengthextrabytes = ' + defaultpayloadlengthextrabytes) + print(' daemon = ' + str(daemon)) + print('\n ------------------------------------') + print(' | Current Connection Settings |') + print(' -----------------------------------') + print(' socksproxytype = ' + socksproxytype) + print(' sockshostname = ' + sockshostname) + print(' socksport = ' + socksport) + print(' socksauthentication = ' + str(socksauthentication)) + print(' socksusername = ' + socksusername) + print(' sockspassword = ' + sockspassword) + print(' ') uInput = userInput("Would you like to modify any of these settings, (Y)es or (N)o?").lower() - + if uInput == "y": - while True: #loops if they mistype the setting name, they can exit the loop with 'exit' + while True: # loops if they mistype the setting name, they can exit the loop with 'exit' invalidInput = False uInput = userInput("What setting would you like to modify?").lower() - print ' ' + print(' ') if uInput == "port": - print ' Current port number: ' + 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) + 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) + 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) + 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) + 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 + 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 + 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) + 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'." + 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 + 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 + 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) + 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 + 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 + 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" + print("\n Invalid input. Please try again.\n") invalidInput = True - - if invalidInput != True: #don't prompt if they made a mistake. + + if invalidInput is not True: # don't prompt if they made a mistake. uInput = userInput("Would you like to change another setting, (Y)es or (N)o?").lower() if uInput != "y": - print '\n Changes Made.\n' + print('\n Changes Made.\n') with open(keysPath, 'wb') as configfile: - BMConfigParser().write(configfile) + config.write(configfile) restartBmNotify() break - - + elif uInput == "n": usrPrompt = 1 main() else: - print "Invalid input." + print("Invalid input.") usrPrompt = 1 main() + def validAddress(address): + """Predicate to test address validity""" address_information = json.loads(api.decodeAddress(address)) - - if 'success' in str(address_information['status']).lower(): - return True - else: - return False -def getAddress(passphrase,vNumber,sNumber): - passphrase = passphrase.encode('base64')#passphrase must be encoded + return 'success' in str(address_information['status']).lower() + + +def getAddress(passphrase, vNumber, sNumber): + """Get a deterministic address""" + passphrase = passphrase.encode('base64') # passphrase must be encoded + + return api.getDeterministicAddress(passphrase, vNumber, sNumber) - return api.getDeterministicAddress(passphrase,vNumber,sNumber) def subscribe(): + """Subscribe to an address""" global usrPrompt while True: address = userInput("What address would you like to subscribe to?") - if (address == "c"): - usrPrompt = 1 - print ' ' - main() - elif (validAddress(address)== False): - print '\n Invalid. "c" to cancel. Please try again.\n' + if address == "c": + usrPrompt = 1 + print(' ') + main() + elif validAddress(address) is False: + print('\n Invalid. "c" to cancel. Please try again.\n') else: break - + label = userInput("Enter a label for this address.") label = label.encode('base64') - - api.addSubscription(address,label) - print ('\n You are now subscribed to: ' + address + '\n') + + api.addSubscription(address, label) + print('\n You are now subscribed to: ' + address + '\n') + def unsubscribe(): + """Unsusbcribe from an address""" global usrPrompt - + while True: address = userInput("What address would you like to unsubscribe from?") - if (address == "c"): - usrPrompt = 1 - print ' ' - main() - elif (validAddress(address)== False): - print '\n Invalid. "c" to cancel. Please try again.\n' + if address == "c": + usrPrompt = 1 + print(' ') + main() + elif validAddress(address) is False: + print('\n Invalid. "c" to cancel. Please try again.\n') else: break - - - userInput("Are you sure, (Y)es or (N)o?").lower() # #uInput = - + + userInput("Are you sure, (Y)es or (N)o?").lower() # uInput = + api.deleteSubscription(address) - print ('\n You are now unsubscribed from: ' + address + '\n') + print('\n You are now unsubscribed from: ' + address + '\n') + def listSubscriptions(): + """List subscriptions""" + global usrPrompt - #jsonAddresses = json.loads(api.listSubscriptions()) - #numAddresses = len(jsonAddresses['addresses']) #Number of addresses - print '\nLabel, Address, Enabled\n' + print('\nLabel, Address, Enabled\n') try: - print api.listSubscriptions() - except: - print '\n Connection Error\n' + print(api.listSubscriptions()) + except: # noqa:E722 + print('\n Connection Error\n') usrPrompt = 0 main() - - '''for addNum in range (0, numAddresses): #processes all of the addresses and lists them out - label = jsonAddresses['addresses'][addNum]['label'] - address = jsonAddresses['addresses'][addNum]['address'] - enabled = jsonAddresses['addresses'][addNum]['enabled'] + print(' ') - print label, address, enabled - ''' - print ' ' def createChan(): + """Create a channel""" + global usrPrompt password = userInput("Enter channel name") password = password.encode('base64') try: - print api.createChan(password) - except: - print '\n Connection Error\n' + print(api.createChan(password)) + except: # noqa:E722 + print('\n Connection Error\n') usrPrompt = 0 main() def joinChan(): + """Join a channel""" + global usrPrompt while True: address = userInput("Enter channel address") - - if (address == "c"): - usrPrompt = 1 - print ' ' - main() - elif (validAddress(address)== False): - print '\n Invalid. "c" to cancel. Please try again.\n' + + if address == "c": + usrPrompt = 1 + print(' ') + main() + elif validAddress(address) is False: + print('\n Invalid. "c" to cancel. Please try again.\n') else: break - + password = userInput("Enter channel name") password = password.encode('base64') try: - print api.joinChan(password,address) - except: - print '\n Connection Error\n' + print(api.joinChan(password, address)) + except: # noqa:E722 + print('\n Connection Error\n') usrPrompt = 0 main() + def leaveChan(): + """Leave a channel""" + global usrPrompt while True: address = userInput("Enter channel address") - - if (address == "c"): - usrPrompt = 1 - print ' ' - main() - elif (validAddress(address)== False): - print '\n Invalid. "c" to cancel. Please try again.\n' + + if address == "c": + usrPrompt = 1 + print(' ') + main() + elif validAddress(address) is False: + print('\n Invalid. "c" to cancel. Please try again.\n') else: break - + try: - print api.leaveChan(address) - except: - print '\n Connection Error\n' + print(api.leaveChan(address)) + except: # noqa:E722 + print('\n Connection Error\n') usrPrompt = 0 main() -def listAdd(): #Lists all of the addresses and their info +def listAdd(): + """List all of the addresses and their info""" global usrPrompt try: jsonAddresses = json.loads(api.listAddresses()) - numAddresses = len(jsonAddresses['addresses']) #Number of addresses - except: - print '\n Connection Error\n' + numAddresses = len(jsonAddresses['addresses']) # Number of addresses + except: # noqa:E722 + print('\n Connection Error\n') usrPrompt = 0 main() - #print '\nAddress Number,Label,Address,Stream,Enabled\n' - print '\n --------------------------------------------------------------------------' - print ' | # | Label | Address |S#|Enabled|' - print ' |---|-------------------|-------------------------------------|--|-------|' - for addNum in range (0, numAddresses): #processes all of the addresses and lists them out - label = (jsonAddresses['addresses'][addNum]['label' ]).encode('utf') # may still misdiplay in some consoles + # print('\nAddress Number,Label,Address,Stream,Enabled\n') + print('\n --------------------------------------------------------------------------') + print(' | # | Label | Address |S#|Enabled|') + print(' |---|-------------------|-------------------------------------|--|-------|') + for addNum in range(0, numAddresses): # processes all of the addresses and lists them out + label = (jsonAddresses['addresses'][addNum]['label']).encode( + 'utf') # may still misdiplay in some consoles address = str(jsonAddresses['addresses'][addNum]['address']) - stream = str(jsonAddresses['addresses'][addNum]['stream']) + stream = str(jsonAddresses['addresses'][addNum]['stream']) enabled = str(jsonAddresses['addresses'][addNum]['enabled']) - if (len(label) > 19): + if len(label) > 19: label = label[:16] + '...' - - print ' |' + str(addNum).ljust(3) + '|' + label.ljust(19) + '|' + address.ljust(37) + '|' + stream.ljust(1), '|' + enabled.ljust(7) + '|' - print ' --------------------------------------------------------------------------\n' + print(''.join([ + ' |', + str(addNum).ljust(3), + '|', + label.ljust(19), + '|', + address.ljust(37), + '|', + stream.ljust(1), + '|', + enabled.ljust(7), + '|', + ])) + + print(''.join([ + ' ', + 74 * '-', + '\n', + ])) + + +def genAdd(lbl, deterministic, passphrase, numOfAdd, addVNum, streamNum, ripe): + """Generate address""" -def genAdd(lbl,deterministic, passphrase, numOfAdd, addVNum, streamNum, ripe): #Generate address global usrPrompt - if deterministic == False: #Generates a new address with the user defined label. non-deterministic + + if deterministic is False: # Generates a new address with the user defined label. non-deterministic addressLabel = lbl.encode('base64') try: generatedAddress = api.createRandomAddress(addressLabel) - except: - print '\n Connection Error\n' + except: # noqa:E722 + print('\n Connection Error\n') usrPrompt = 0 main() - + return generatedAddress - - elif deterministic == True: #Generates a new deterministic address with the user inputs. + + elif deterministic: # Generates a new deterministic address with the user inputs. passphrase = passphrase.encode('base64') try: generatedAddress = api.createDeterministicAddresses(passphrase, numOfAdd, addVNum, streamNum, ripe) - except: - print '\n Connection Error\n' + except: # noqa:E722 + print('\n Connection Error\n') usrPrompt = 0 main() return generatedAddress - else: - return 'Entry Error' -def delMilAddr(): #Generate address - global usrPrompt - try: - response = api.listAddresses2() - # if api is too old just return then fail - if "API Error 0020" in response: return - addresses = json.loads(response) - for entry in addresses['addresses']: - if entry['label'].decode('base64')[:6] == "random": - api.deleteAddress(entry['address']) - except: - print '\n Connection Error\n' - usrPrompt = 0 - main() + return 'Entry Error' -def genMilAddr(): #Generate address - global usrPrompt - maxn = 0 - try: - response = api.listAddresses2() - if "API Error 0020" in response: return - addresses = json.loads(response) - for entry in addresses['addresses']: - if entry['label'].decode('base64')[:6] == "random": - newn = int(entry['label'].decode('base64')[6:]) - if maxn < newn: - maxn = newn - except: - print "\n Some error\n" - print "\n Starting at " + str(maxn) + "\n" - for i in range(maxn, 10000): - lbl = "random" + str(i) - addressLabel = lbl.encode('base64') - try: - api.createRandomAddress(addressLabel) # generatedAddress = - except: - print '\n Connection Error\n' - usrPrompt = 0 - main() -def saveFile(fileName, fileData): #Allows attachments and messages/broadcats to be saved +def saveFile(fileName, fileData): + """Allows attachments and messages/broadcats to be saved""" - #This section finds all invalid characters and replaces them with ~ + # This section finds all invalid characters and replaces them with ~ fileName = fileName.replace(" ", "") fileName = fileName.replace("/", "~") - #fileName = fileName.replace("\\", "~") How do I get this to work...? + # fileName = fileName.replace("\\", "~") How do I get this to work...? fileName = fileName.replace(":", "~") fileName = fileName.replace("*", "~") fileName = fileName.replace("?", "~") @@ -613,217 +639,219 @@ def saveFile(fileName, fileData): #Allows attachments and messages/broadcats to fileName = fileName.replace(">", "~") fileName = fileName.replace("|", "~") - directory = 'attachments' + directory = os.path.abspath('attachments') if not os.path.exists(directory): os.makedirs(directory) - - filePath = directory +'/'+ fileName - '''try: #Checks if file already exists - with open(filePath): - print 'File Already Exists' - return - except IOError: pass''' + filePath = os.path.join(directory, fileName) + with open(filePath, 'wb+') as path_to_file: + path_to_file.write(fileData.decode("base64")) + print('\n Successfully saved ' + filePath + '\n') - f = open(filePath, 'wb+') #Begin saving to file - f.write(fileData.decode("base64")) - f.close - print '\n Successfully saved '+ filePath + '\n' +def attachment(): + """Allows users to attach a file to their message or broadcast""" -def attachment(): #Allows users to attach a file to their message or broadcast theAttachmentS = '' - + while True: isImage = False theAttachment = '' - - while True:#loops until valid path is entered - filePath = userInput('\nPlease enter the path to the attachment or just the attachment name if in this folder.') + + while True: # loops until valid path is entered + filePath = userInput( + '\nPlease enter the path to the attachment or just the attachment name if in this folder.') try: - with open(filePath): break + with open(filePath): + break except IOError: - print '\n %s was not found on your filesystem or can not be opened.\n' % filePath - pass + print('\n %s was not found on your filesystem or can not be opened.\n' % filePath) - #print filesize, and encoding estimate with confirmation if file is over X size (1mb?) + # print(filesize, and encoding estimate with confirmation if file is over X size(1mb?)) invSize = os.path.getsize(filePath) - invSize = (invSize / 1024) #Converts to kilobytes - round(invSize,2) #Rounds to two decimal places - - if (invSize > 500.0):#If over 500KB - print '\n WARNING:The file that you are trying to attach is ', invSize, 'KB and will take considerable time to send.\n' + invSize = (invSize / 1024) # Converts to kilobytes + round(invSize, 2) # Rounds to two decimal places + + if invSize > 500.0: # If over 500KB + print(''.join([ + '\n WARNING:The file that you are trying to attach is ', + invSize, + 'KB and will take considerable time to send.\n' + ])) uInput = userInput('Are you sure you still want to attach it, (Y)es or (N)o?').lower() if uInput != "y": - print '\n Attachment discarded.\n' + print('\n Attachment discarded.\n') return '' - elif (invSize > 184320.0): #If larger than 180MB, discard. - print '\n Attachment too big, maximum allowed size:180MB\n' + elif invSize > 184320.0: # If larger than 180MB, discard. + print('\n Attachment too big, maximum allowed size:180MB\n') main() - - pathLen = len(str(ntpath.basename(filePath))) #Gets the length of the filepath excluding the filename - fileName = filePath[(len(str(filePath)) - pathLen):] #reads the filename - - filetype = imghdr.what(filePath) #Tests if it is an image file + + pathLen = len(str(ntpath.basename(filePath))) # Gets the length of the filepath excluding the filename + fileName = filePath[(len(str(filePath)) - pathLen):] # reads the filename + + filetype = imghdr.what(filePath) # Tests if it is an image file if filetype is not None: - print '\n ---------------------------------------------------' - print ' Attachment detected as an Image.' - print ' tags will automatically be included,' - print ' allowing the recipient to view the image' - print ' using the "View HTML code..." option in Bitmessage.' - print ' ---------------------------------------------------\n' + print('\n ---------------------------------------------------') + print(' Attachment detected as an Image.') + print(' tags will automatically be included,') + print(' allowing the recipient to view the image') + print(' using the "View HTML code..." option in Bitmessage.') + print(' ---------------------------------------------------\n') isImage = True time.sleep(2) - - print '\n Encoding Attachment, Please Wait ...\n' #Alert the user that the encoding process may take some time. - - with open(filePath, 'rb') as f: #Begin the actual encoding - data = f.read(188743680) #Reads files up to 180MB, the maximum size for Bitmessage. + + # Alert the user that the encoding process may take some time. + print('\n Encoding Attachment, Please Wait ...\n') + + with open(filePath, 'rb') as f: # Begin the actual encoding + data = f.read(188743680) # Reads files up to 180MB, the maximum size for Bitmessage. data = data.encode("base64") - if (isImage == True): #If it is an image, include image tags in the message + if isImage: # If it is an image, include image tags in the message theAttachment = """ - -Filename:%s -Filesize:%sKB -Encoding:base64 - + +Filename:%s +Filesize:%sKB +Encoding:base64 +
%s
-
""" % (fileName,invSize,fileName,filetype,data) - else: #Else it is not an image so do not include the embedded image code. +""" % (fileName, invSize, fileName, filetype, data) + else: # Else it is not an image so do not include the embedded image code. theAttachment = """ - -Filename:%s -Filesize:%sKB -Encoding:base64 - -""" % (fileName,invSize,fileName,fileName,data) + +Filename:%s +Filesize:%sKB +Encoding:base64 + +""" % (fileName, invSize, fileName, fileName, data) uInput = userInput('Would you like to add another attachment, (Y)es or (N)o?').lower() - if (uInput == 'y' or uInput == 'yes'):#Allows multiple attachments to be added to one message - theAttachmentS = str(theAttachmentS) + str(theAttachment)+ '\n\n' - elif (uInput == 'n' or uInput == 'no'): + if uInput in ('y', 'yes'): # Allows multiple attachments to be added to one message + theAttachmentS = str(theAttachmentS) + str(theAttachment) + '\n\n' + elif uInput in ('n', 'no'): break - + theAttachmentS = theAttachmentS + theAttachment return theAttachmentS -def sendMsg(toAddress, fromAddress, subject, message): #With no arguments sent, sendMsg fills in the blanks. subject and message must be encoded before they are passed. + +def sendMsg(toAddress, fromAddress, subject, message): + """ + With no arguments sent, sendMsg fills in the blanks. + subject and message must be encoded before they are passed. + """ + global usrPrompt - if (validAddress(toAddress)== False): + if validAddress(toAddress) is False: while True: toAddress = userInput("What is the To Address?") - if (toAddress == "c"): + if toAddress == "c": usrPrompt = 1 - print ' ' + print(' ') main() - elif (validAddress(toAddress)== False): - print '\n Invalid Address. "c" to cancel. Please try again.\n' + elif validAddress(toAddress) is False: + print('\n Invalid Address. "c" to cancel. Please try again.\n') else: break - - - if (validAddress(fromAddress)== False): - try: + + if validAddress(fromAddress) is False: + try: jsonAddresses = json.loads(api.listAddresses()) - numAddresses = len(jsonAddresses['addresses']) #Number of addresses - except: - print '\n Connection Error\n' + numAddresses = len(jsonAddresses['addresses']) # Number of addresses + except: # noqa:E722 + print('\n Connection Error\n') usrPrompt = 0 main() - - if (numAddresses > 1): #Ask what address to send from if multiple addresses + + if numAddresses > 1: # Ask what address to send from if multiple addresses found = False while True: - print ' ' + print(' ') fromAddress = userInput("Enter an Address or Address Label to send from.") if fromAddress == "exit": usrPrompt = 1 main() - for addNum in range (0, numAddresses): #processes all of the addresses + for addNum in range(0, numAddresses): # processes all of the addresses label = jsonAddresses['addresses'][addNum]['label'] address = jsonAddresses['addresses'][addNum]['address'] - #stream = jsonAddresses['addresses'][addNum]['stream'] - #enabled = jsonAddresses['addresses'][addNum]['enabled'] - if (fromAddress == label): #address entered was a label and is found + if fromAddress == label: # address entered was a label and is found fromAddress = address found = True break - - if (found == False): - if(validAddress(fromAddress)== False): - print '\n Invalid Address. Please try again.\n' - + + if found is False: + if validAddress(fromAddress) is False: + print('\n Invalid Address. Please try again.\n') + else: - for addNum in range (0, numAddresses): #processes all of the addresses - #label = jsonAddresses['addresses'][addNum]['label'] + for addNum in range(0, numAddresses): # processes all of the addresses address = jsonAddresses['addresses'][addNum]['address'] - #stream = jsonAddresses['addresses'][addNum]['stream'] - #enabled = jsonAddresses['addresses'][addNum]['enabled'] - if (fromAddress == address): #address entered was a found in our addressbook. + if fromAddress == address: # address entered was a found in our addressbook. found = True break - - if (found == False): - print '\n The address entered is not one of yours. Please try again.\n' - - if (found == True): - break #Address was found - - else: #Only one address in address book - print '\n Using the only address in the addressbook to send from.\n' + + if found is False: + print('\n The address entered is not one of yours. Please try again.\n') + + if found: + break # Address was found + + else: # Only one address in address book + print('\n Using the only address in the addressbook to send from.\n') fromAddress = jsonAddresses['addresses'][0]['address'] - if (subject == ''): + if not subject: subject = userInput("Enter your Subject.") subject = subject.encode('base64') - if (message == ''): + if not message: message = userInput("Enter your Message.") uInput = userInput('Would you like to add an attachment, (Y)es or (N)o?').lower() if uInput == "y": message = message + '\n\n' + attachment() - + message = message.encode('base64') - + try: ackData = api.sendMessage(toAddress, fromAddress, subject, message) - print '\n Message Status:', api.getStatus(ackData), '\n' - except: - print '\n Connection Error\n' + print('\n Message Status:', api.getStatus(ackData), '\n') + except: # noqa:E722 + print('\n Connection Error\n') usrPrompt = 0 main() -def sendBrd(fromAddress, subject, message): #sends a broadcast +def sendBrd(fromAddress, subject, message): + """Send a broadcast""" + global usrPrompt - if (fromAddress == ''): + if not fromAddress: try: jsonAddresses = json.loads(api.listAddresses()) - numAddresses = len(jsonAddresses['addresses']) #Number of addresses - except: - print '\n Connection Error\n' + numAddresses = len(jsonAddresses['addresses']) # Number of addresses + except: # noqa:E722 + print('\n Connection Error\n') usrPrompt = 0 main() - - if (numAddresses > 1): #Ask what address to send from if multiple addresses + + if numAddresses > 1: # Ask what address to send from if multiple addresses found = False while True: fromAddress = userInput("\nEnter an Address or Address Label to send from.") @@ -832,316 +860,362 @@ def sendBrd(fromAddress, subject, message): #sends a broadcast usrPrompt = 1 main() - for addNum in range (0, numAddresses): #processes all of the addresses + for addNum in range(0, numAddresses): # processes all of the addresses label = jsonAddresses['addresses'][addNum]['label'] address = jsonAddresses['addresses'][addNum]['address'] - #stream = jsonAddresses['addresses'][addNum]['stream'] - #enabled = jsonAddresses['addresses'][addNum]['enabled'] - if (fromAddress == label): #address entered was a label and is found + if fromAddress == label: # address entered was a label and is found fromAddress = address found = True break - - if (found == False): - if(validAddress(fromAddress)== False): - print '\n Invalid Address. Please try again.\n' - + + if found is False: + if validAddress(fromAddress) is False: + print('\n Invalid Address. Please try again.\n') + else: - for addNum in range (0, numAddresses): #processes all of the addresses - #label = jsonAddresses['addresses'][addNum]['label'] + for addNum in range(0, numAddresses): # processes all of the addresses address = jsonAddresses['addresses'][addNum]['address'] - #stream = jsonAddresses['addresses'][addNum]['stream'] - #enabled = jsonAddresses['addresses'][addNum]['enabled'] - if (fromAddress == address): #address entered was a found in our addressbook. + if fromAddress == address: # address entered was a found in our addressbook. found = True break - - if (found == False): - print '\n The address entered is not one of yours. Please try again.\n' - - if (found == True): - break #Address was found - - else: #Only one address in address book - print '\n Using the only address in the addressbook to send from.\n' + + if found is False: + print('\n The address entered is not one of yours. Please try again.\n') + + if found: + break # Address was found + + else: # Only one address in address book + print('\n Using the only address in the addressbook to send from.\n') fromAddress = jsonAddresses['addresses'][0]['address'] - if (subject == ''): - subject = userInput("Enter your Subject.") - subject = subject.encode('base64') - if (message == ''): - message = userInput("Enter your Message.") + if not subject: + subject = userInput("Enter your Subject.") + subject = subject.encode('base64') + if not message: + message = userInput("Enter your Message.") + + uInput = userInput('Would you like to add an attachment, (Y)es or (N)o?').lower() + if uInput == "y": + message = message + '\n\n' + attachment() - uInput = userInput('Would you like to add an attachment, (Y)es or (N)o?').lower() - if uInput == "y": - message = message + '\n\n' + attachment() - - message = message.encode('base64') + message = message.encode('base64') try: ackData = api.sendBroadcast(fromAddress, subject, message) - print '\n Message Status:', api.getStatus(ackData), '\n' - except: - print '\n Connection Error\n' + print('\n Message Status:', api.getStatus(ackData), '\n') + except: # noqa:E722 + print('\n Connection Error\n') usrPrompt = 0 main() -def inbox(unreadOnly = False): #Lists the messages by: Message Number, To Address Label, From Address Label, Subject, Received Time) + +def inbox(unreadOnly=False): + """Lists the messages by: Message Number, To Address Label, From Address Label, Subject, Received Time)""" + global usrPrompt try: inboxMessages = json.loads(api.getAllInboxMessages()) numMessages = len(inboxMessages['inboxMessages']) - except: - print '\n Connection Error\n' + except: # noqa:E722 + print('\n Connection Error\n') usrPrompt = 0 main() messagesPrinted = 0 messagesUnread = 0 - for msgNum in range (0, numMessages): #processes all of the messages in the inbox + for msgNum in range(0, numMessages): # processes all of the messages in the inbox message = inboxMessages['inboxMessages'][msgNum] # if we are displaying all messages or if this message is unread then display it if not unreadOnly or not message['read']: - print ' -----------------------------------\n' - print ' Message Number:',msgNum #Message Number - print ' To:', getLabelForAddress(message['toAddress']) #Get the to address - print ' From:', getLabelForAddress(message['fromAddress']) #Get the from address - print ' Subject:', message['subject'].decode('base64') #Get the subject - print ' Received:', datetime.datetime.fromtimestamp(float(message['receivedTime'])).strftime('%Y-%m-%d %H:%M:%S') + print(' -----------------------------------\n') + print(' Message Number:', msgNum) # Message Number) + print(' To:', getLabelForAddress(message['toAddress'])) # Get the to address) + print(' From:', getLabelForAddress(message['fromAddress'])) # Get the from address) + print(' Subject:', message['subject'].decode('base64')) # Get the subject) + print(''.join([ + ' Received:', + datetime.datetime.fromtimestamp( + float(message['receivedTime'])).strftime('%Y-%m-%d %H:%M:%S'), + ])) messagesPrinted += 1 - if not message['read']: messagesUnread += 1 + if not message['read']: + messagesUnread += 1 + + if messagesPrinted % 20 == 0 and messagesPrinted != 0: + userInput('(Press Enter to continue or type (Exit) to return to the main menu.)').lower() # uInput = + + print('\n -----------------------------------') + print(' There are %d unread messages of %d messages in the inbox.' % (messagesUnread, numMessages)) + print(' -----------------------------------\n') - if (messagesPrinted%20 == 0 and messagesPrinted != 0): - userInput('(Press Enter to continue or type (Exit) to return to the main menu.)').lower() # uInput = - - print '\n -----------------------------------' - print ' There are %d unread messages of %d messages in the inbox.' % (messagesUnread, numMessages) - print ' -----------------------------------\n' def outbox(): + """TBC""" + global usrPrompt try: outboxMessages = json.loads(api.getAllSentMessages()) numMessages = len(outboxMessages['sentMessages']) - except: - print '\n Connection Error\n' + except: # noqa:E722 + print('\n Connection Error\n') usrPrompt = 0 main() - for msgNum in range (0, numMessages): #processes all of the messages in the outbox - print '\n -----------------------------------\n' - print ' Message Number:',msgNum #Message Number - #print ' Message ID:', outboxMessages['sentMessages'][msgNum]['msgid'] - print ' To:', getLabelForAddress(outboxMessages['sentMessages'][msgNum]['toAddress']) #Get the to address - print ' From:', getLabelForAddress(outboxMessages['sentMessages'][msgNum]['fromAddress']) #Get the from address - print ' Subject:', outboxMessages['sentMessages'][msgNum]['subject'].decode('base64') #Get the subject - print ' Status:', outboxMessages['sentMessages'][msgNum]['status'] #Get the subject - - print ' Last Action Time:', datetime.datetime.fromtimestamp(float(outboxMessages['sentMessages'][msgNum]['lastActionTime'])).strftime('%Y-%m-%d %H:%M:%S') - - if (msgNum%20 == 0 and msgNum != 0): - userInput('(Press Enter to continue or type (Exit) to return to the main menu.)').lower() # uInput = - - print '\n -----------------------------------' - print ' There are ',numMessages,' messages in the outbox.' - print ' -----------------------------------\n' - -def readSentMsg(msgNum): #Opens a sent message for reading + for msgNum in range(0, numMessages): # processes all of the messages in the outbox + print('\n -----------------------------------\n') + print(' Message Number:', msgNum) # Message Number) + # print(' Message ID:', outboxMessages['sentMessages'][msgNum]['msgid']) + print(' To:', getLabelForAddress( + outboxMessages['sentMessages'][msgNum]['toAddress'] + )) # Get the to address) + # Get the from address + print(' From:', getLabelForAddress(outboxMessages['sentMessages'][msgNum]['fromAddress'])) + print(' Subject:', outboxMessages['sentMessages'][msgNum]['subject'].decode('base64')) # Get the subject) + print(' Status:', outboxMessages['sentMessages'][msgNum]['status']) # Get the subject) + + # print(''.join([ + # ' Last Action Time:', + # datetime.datetime.fromtimestamp( + # float(outboxMessages['sentMessages'][msgNum]['lastActionTime'])).strftime('%Y-%m-%d %H:%M:%S'), + # ])) + print(''.join([ + ' Last Action Time:', + datetime.datetime.fromtimestamp( + float(outboxMessages['sentMessages'][msgNum]['lastActionTime'])).strftime('%Y-%m-%d %H:%M:%S'), + ])) + + if msgNum % 20 == 0 and msgNum != 0: + userInput('(Press Enter to continue or type (Exit) to return to the main menu.)').lower() # uInput = + + print('\n -----------------------------------') + print(' There are ', numMessages, ' messages in the outbox.') + print(' -----------------------------------\n') + + +def readSentMsg(msgNum): + """Opens a sent message for reading""" + global usrPrompt try: outboxMessages = json.loads(api.getAllSentMessages()) numMessages = len(outboxMessages['sentMessages']) - except: - print '\n Connection Error\n' + except: # noqa:E722 + print('\n Connection Error\n') usrPrompt = 0 main() - - print ' ' - if (msgNum >= numMessages): - print '\n Invalid Message Number.\n' + print(' ') + + if msgNum >= numMessages: + print('\n Invalid Message Number.\n') main() - #Begin attachment detection + # Begin attachment detection message = outboxMessages['sentMessages'][msgNum]['message'].decode('base64') - while True: #Allows multiple messages to be downloaded/saved - if (';base64,' in message): #Found this text in the message, there is probably an attachment. - attPos= message.index(";base64,") #Finds the attachment position - attEndPos = message.index("' />") #Finds the end of the attachment - #attLen = attEndPos - attPos #Finds the length of the message - + while True: # Allows multiple messages to be downloaded/saved + if ';base64,' in message: # Found this text in the message, there is probably an attachment. + attPos = message.index(";base64,") # Finds the attachment position + attEndPos = message.index("' />") # Finds the end of the attachment + # attLen = attEndPos - attPos #Finds the length of the message - if ('alt = "' in message): #We can get the filename too - fnPos = message.index('alt = "') #Finds position of the filename - fnEndPos = message.index('" src=') #Finds the end position - #fnLen = fnEndPos - fnPos #Finds the length of the filename + if 'alt = "' in message: # We can get the filename too + fnPos = message.index('alt = "') # Finds position of the filename + fnEndPos = message.index('" src=') # Finds the end position + # fnLen = fnEndPos - fnPos #Finds the length of the filename - fileName = message[fnPos+7:fnEndPos] + fileName = message[fnPos + 7:fnEndPos] else: fnPos = attPos fileName = 'Attachment' - uInput = userInput('\n Attachment Detected. Would you like to save the attachment, (Y)es or (N)o?').lower() - if (uInput == "y" or uInput == 'yes'): - - attachment = message[attPos+9:attEndPos] - saveFile(fileName,attachment) + uInput = userInput( + '\n Attachment Detected. Would you like to save the attachment, (Y)es or (N)o?').lower() + if uInput in ("y", 'yes'): - message = message[:fnPos] + '~~' + message[(attEndPos+4):] + this_attachment = message[attPos + 9:attEndPos] + saveFile(fileName, this_attachment) + + message = message[:fnPos] + '~~' + message[(attEndPos + 4):] else: break - - #End attachment Detection - - print '\n To:', getLabelForAddress(outboxMessages['sentMessages'][msgNum]['toAddress']) #Get the to address - print ' From:', getLabelForAddress(outboxMessages['sentMessages'][msgNum]['fromAddress']) #Get the from address - print ' Subject:', outboxMessages['sentMessages'][msgNum]['subject'].decode('base64') #Get the subject - print ' Status:', outboxMessages['sentMessages'][msgNum]['status'] #Get the subject - print ' Last Action Time:', datetime.datetime.fromtimestamp(float(outboxMessages['sentMessages'][msgNum]['lastActionTime'])).strftime('%Y-%m-%d %H:%M:%S') - print ' Message:\n' - print message #inboxMessages['inboxMessages'][msgNum]['message'].decode('base64') - print ' ' - -def readMsg(msgNum): #Opens a message for reading + + # End attachment Detection + + print('\n To:', getLabelForAddress(outboxMessages['sentMessages'][msgNum]['toAddress'])) # Get the to address) + # Get the from address + print(' From:', getLabelForAddress(outboxMessages['sentMessages'][msgNum]['fromAddress'])) + print(' Subject:', outboxMessages['sentMessages'][msgNum]['subject'].decode('base64')) # Get the subject) + print(' Status:', outboxMessages['sentMessages'][msgNum]['status']) # Get the subject) + print(''.join([ + ' Last Action Time:', + datetime.datetime.fromtimestamp( + float(outboxMessages['sentMessages'][msgNum]['lastActionTime'])).strftime('%Y-%m-%d %H:%M:%S'), + ])) + print(' Message:\n') + print(message) # inboxMessages['inboxMessages'][msgNum]['message'].decode('base64')) + print(' ') + + +def readMsg(msgNum): + """Open a message for reading""" global usrPrompt try: inboxMessages = json.loads(api.getAllInboxMessages()) numMessages = len(inboxMessages['inboxMessages']) - except: - print '\n Connection Error\n' + except: # noqa:E722 + print('\n Connection Error\n') usrPrompt = 0 main() - if (msgNum >= numMessages): - print '\n Invalid Message Number.\n' + if msgNum >= numMessages: + print('\n Invalid Message Number.\n') main() - #Begin attachment detection + # Begin attachment detection message = inboxMessages['inboxMessages'][msgNum]['message'].decode('base64') - while True: #Allows multiple messages to be downloaded/saved - if (';base64,' in message): #Found this text in the message, there is probably an attachment. - attPos= message.index(";base64,") #Finds the attachment position - attEndPos = message.index("' />") #Finds the end of the attachment - #attLen = attEndPos - attPos #Finds the length of the message + while True: # Allows multiple messages to be downloaded/saved + if ';base64,' in message: # Found this text in the message, there is probably an attachment. + attPos = message.index(";base64,") # Finds the attachment position + attEndPos = message.index("' />") # Finds the end of the attachment + # attLen = attEndPos - attPos #Finds the length of the message + if 'alt = "' in message: # We can get the filename too + fnPos = message.index('alt = "') # Finds position of the filename + fnEndPos = message.index('" src=') # Finds the end position + # fnLen = fnEndPos - fnPos #Finds the length of the filename - if ('alt = "' in message): #We can get the filename too - fnPos = message.index('alt = "') #Finds position of the filename - fnEndPos = message.index('" src=') #Finds the end position - #fnLen = fnEndPos - fnPos #Finds the length of the filename - - fileName = message[fnPos+7:fnEndPos] + fileName = message[fnPos + 7:fnEndPos] else: fnPos = attPos fileName = 'Attachment' - uInput = userInput('\n Attachment Detected. Would you like to save the attachment, (Y)es or (N)o?').lower() - if (uInput == "y" or uInput == 'yes'): - - attachment = message[attPos+9:attEndPos] - saveFile(fileName,attachment) + uInput = userInput( + '\n Attachment Detected. Would you like to save the attachment, (Y)es or (N)o?').lower() + if uInput in ("y", 'yes'): + + this_attachment = message[attPos + 9:attEndPos] + saveFile(fileName, this_attachment) - message = message[:fnPos] + '~~' + message[(attEndPos+4):] + message = message[:fnPos] + '~~' + message[attEndPos + 4:] else: break - - #End attachment Detection - print '\n To:', getLabelForAddress(inboxMessages['inboxMessages'][msgNum]['toAddress']) #Get the to address - print ' From:', getLabelForAddress(inboxMessages['inboxMessages'][msgNum]['fromAddress']) #Get the from address - print ' Subject:', inboxMessages['inboxMessages'][msgNum]['subject'].decode('base64') #Get the subject - print ' Received:',datetime.datetime.fromtimestamp(float(inboxMessages['inboxMessages'][msgNum]['receivedTime'])).strftime('%Y-%m-%d %H:%M:%S') - print ' Message:\n' - print message #inboxMessages['inboxMessages'][msgNum]['message'].decode('base64') - print ' ' + + # End attachment Detection + print('\n To:', getLabelForAddress(inboxMessages['inboxMessages'][msgNum]['toAddress'])) # Get the to address) + # Get the from address + print(' From:', getLabelForAddress(inboxMessages['inboxMessages'][msgNum]['fromAddress'])) + print(' Subject:', inboxMessages['inboxMessages'][msgNum]['subject'].decode('base64')) # Get the subject) + print(''.join([ + ' Received:', datetime.datetime.fromtimestamp( + float(inboxMessages['inboxMessages'][msgNum]['receivedTime'])).strftime('%Y-%m-%d %H:%M:%S'), + ])) + print(' Message:\n') + print(message) # inboxMessages['inboxMessages'][msgNum]['message'].decode('base64')) + print(' ') return inboxMessages['inboxMessages'][msgNum]['msgid'] -def replyMsg(msgNum,forwardORreply): #Allows you to reply to the message you are currently on. Saves typing in the addresses and subject. + +def replyMsg(msgNum, forwardORreply): + """Allows you to reply to the message you are currently on. Saves typing in the addresses and subject.""" + global usrPrompt - forwardORreply = forwardORreply.lower() #makes it lowercase + forwardORreply = forwardORreply.lower() # makes it lowercase try: inboxMessages = json.loads(api.getAllInboxMessages()) - except: - print '\n Connection Error\n' + except: # noqa:E722 + print('\n Connection Error\n') usrPrompt = 0 main() - - fromAdd = inboxMessages['inboxMessages'][msgNum]['toAddress']#Address it was sent To, now the From address - message = inboxMessages['inboxMessages'][msgNum]['message'].decode('base64') #Message that you are replying too. - + + fromAdd = inboxMessages['inboxMessages'][msgNum]['toAddress'] # Address it was sent To, now the From address + message = inboxMessages['inboxMessages'][msgNum]['message'].decode('base64') # Message that you are replying too. + subject = inboxMessages['inboxMessages'][msgNum]['subject'] subject = subject.decode('base64') - - if (forwardORreply == 'reply'): - toAdd = inboxMessages['inboxMessages'][msgNum]['fromAddress'] #Address it was From, now the To address + + if forwardORreply == 'reply': + toAdd = inboxMessages['inboxMessages'][msgNum]['fromAddress'] # Address it was From, now the To address subject = "Re: " + subject - - elif (forwardORreply == 'forward'): + + elif forwardORreply == 'forward': subject = "Fwd: " + subject - + while True: toAdd = userInput("What is the To Address?") - if (toAdd == "c"): + if toAdd == "c": usrPrompt = 1 - print ' ' + print(' ') main() - elif (validAddress(toAdd)== False): - print '\n Invalid Address. "c" to cancel. Please try again.\n' + elif validAddress(toAdd) is False: + print('\n Invalid Address. "c" to cancel. Please try again.\n') else: break else: - print '\n Invalid Selection. Reply or Forward only' + print('\n Invalid Selection. Reply or Forward only') usrPrompt = 0 main() - + subject = subject.encode('base64') - + newMessage = userInput("Enter your Message.") uInput = userInput('Would you like to add an attachment, (Y)es or (N)o?').lower() if uInput == "y": newMessage = newMessage + '\n\n' + attachment() - + newMessage = newMessage + '\n\n------------------------------------------------------\n' newMessage = newMessage + message newMessage = newMessage.encode('base64') sendMsg(toAdd, fromAdd, subject, newMessage) - + main() -def delMsg(msgNum): #Deletes a specified message from the inbox + +def delMsg(msgNum): + """Deletes a specified message from the inbox""" + global usrPrompt try: inboxMessages = json.loads(api.getAllInboxMessages()) - msgId = inboxMessages['inboxMessages'][int(msgNum)]['msgid'] #gets the message ID via the message index number - + # gets the message ID via the message index number + msgId = inboxMessages['inboxMessages'][int(msgNum)]['msgid'] + msgAck = api.trashMessage(msgId) - except: - print '\n Connection Error\n' + except: # noqa:E722 + print('\n Connection Error\n') usrPrompt = 0 main() - + return msgAck -def delSentMsg(msgNum): #Deletes a specified message from the outbox + +def delSentMsg(msgNum): + """Deletes a specified message from the outbox""" + global usrPrompt try: outboxMessages = json.loads(api.getAllSentMessages()) - msgId = outboxMessages['sentMessages'][int(msgNum)]['msgid'] #gets the message ID via the message index number + # gets the message ID via the message index number + msgId = outboxMessages['sentMessages'][int(msgNum)]['msgid'] msgAck = api.trashSentMessage(msgId) - except: - print '\n Connection Error\n' + except: # noqa:E722 + print('\n Connection Error\n') usrPrompt = 0 main() - + return msgAck + def getLabelForAddress(address): + """Get label for an address""" + if address in knownAddresses: return knownAddresses[address] else: @@ -1151,18 +1225,24 @@ def getLabelForAddress(address): return address + def buildKnownAddresses(): + """Build known addresses""" + + global usrPrompt + # add from address book try: response = api.listAddressBookEntries() # if api is too old then fail - if "API Error 0020" in response: return + if "API Error 0020" in response: + return addressBook = json.loads(response) for entry in addressBook['addresses']: if entry['address'] not in knownAddresses: knownAddresses[entry['address']] = "%s (%s)" % (entry['label'].decode('base64'), entry['address']) - except: - print '\n Connection Error\n' + except: # noqa:E722 + print('\n Connection Error\n') usrPrompt = 0 main() @@ -1170,274 +1250,314 @@ def buildKnownAddresses(): try: response = api.listAddresses2() # if api is too old just return then fail - if "API Error 0020" in response: return + if "API Error 0020" in response: + return addresses = json.loads(response) for entry in addresses['addresses']: if entry['address'] not in knownAddresses: knownAddresses[entry['address']] = "%s (%s)" % (entry['label'].decode('base64'), entry['address']) - except: - print '\n Connection Error\n' + except: # noqa:E722 + print('\n Connection Error\n') usrPrompt = 0 main() + def listAddressBookEntries(): + """List addressbook entries""" + + global usrPrompt + try: response = api.listAddressBookEntries() if "API Error" in response: return getAPIErrorCode(response) addressBook = json.loads(response) - print - print ' --------------------------------------------------------------' - print ' | Label | Address |' - print ' |--------------------|---------------------------------------|' + print(' --------------------------------------------------------------') + print(' | Label | Address |') + print(' |--------------------|---------------------------------------|') for entry in addressBook['addresses']: label = entry['label'].decode('base64') address = entry['address'] - if (len(label) > 19): label = label[:16] + '...' - print ' | ' + label.ljust(19) + '| ' + address.ljust(37) + ' |' - print ' --------------------------------------------------------------' - print - - except: - print '\n Connection Error\n' + if len(label) > 19: + label = label[:16] + '...' + print(' | ' + label.ljust(19) + '| ' + address.ljust(37) + ' |') + print(' --------------------------------------------------------------') + except: # noqa:E722 + print('\n Connection Error\n') usrPrompt = 0 main() + def addAddressToAddressBook(address, label): + """Add an address to an addressbook""" + + global usrPrompt + try: response = api.addAddressBookEntry(address, label.encode('base64')) if "API Error" in response: return getAPIErrorCode(response) - except: - print '\n Connection Error\n' + except: # noqa:E722 + print('\n Connection Error\n') usrPrompt = 0 main() + def deleteAddressFromAddressBook(address): + """Delete an address from an addressbook""" + + global usrPrompt + try: response = api.deleteAddressBookEntry(address) if "API Error" in response: return getAPIErrorCode(response) - except: - print '\n Connection Error\n' + except: # noqa:E722 + print('\n Connection Error\n') usrPrompt = 0 main() + def getAPIErrorCode(response): + """Get API error code""" + if "API Error" in response: # if we got an API error return the number by getting the number # after the second space and removing the trailing colon return int(response.split()[2][:-1]) + def markMessageRead(messageID): + """Mark a message as read""" + + global usrPrompt + try: response = api.getInboxMessageByID(messageID, True) if "API Error" in response: return getAPIErrorCode(response) - except: - print '\n Connection Error\n' + except: # noqa:E722 + print('\n Connection Error\n') usrPrompt = 0 main() + def markMessageUnread(messageID): + """Mark a mesasge as unread""" + + global usrPrompt + try: response = api.getInboxMessageByID(messageID, False) if "API Error" in response: return getAPIErrorCode(response) - except: - print '\n Connection Error\n' + except: # noqa:E722 + print('\n Connection Error\n') usrPrompt = 0 main() + def markAllMessagesRead(): + """Mark all messages as read""" + + global usrPrompt + try: inboxMessages = json.loads(api.getAllInboxMessages())['inboxMessages'] - except: - print '\n Connection Error\n' + except: # noqa:E722 + print('\n Connection Error\n') usrPrompt = 0 main() for message in inboxMessages: if not message['read']: markMessageRead(message['msgid']) + def markAllMessagesUnread(): + """Mark all messages as unread""" + + global usrPrompt + try: inboxMessages = json.loads(api.getAllInboxMessages())['inboxMessages'] - except: - print '\n Connection Error\n' + except: # noqa:E722 + print('\n Connection Error\n') usrPrompt = 0 main() for message in inboxMessages: if message['read']: markMessageUnread(message['msgid']) + def clientStatus(): + """Print (the client status""" + + global usrPrompt + try: - clientStatus = json.loads(api.clientStatus()) - except: - print '\n Connection Error\n' + client_status = json.loads(api.clientStatus()) + except: # noqa:E722 + print('\n Connection Error\n') usrPrompt = 0 main() - print "\nnetworkStatus: " + clientStatus['networkStatus'] + "\n" - print "\nnetworkConnections: " + str(clientStatus['networkConnections']) + "\n" - print "\nnumberOfPubkeysProcessed: " + str(clientStatus['numberOfPubkeysProcessed']) + "\n" - print "\nnumberOfMessagesProcessed: " + str(clientStatus['numberOfMessagesProcessed']) + "\n" - print "\nnumberOfBroadcastsProcessed: " + str(clientStatus['numberOfBroadcastsProcessed']) + "\n" + + print("\nnetworkStatus: " + client_status['networkStatus'] + "\n") + print("\nnetworkConnections: " + str(client_status['networkConnections']) + "\n") + print("\nnumberOfPubkeysProcessed: " + str(client_status['numberOfPubkeysProcessed']) + "\n") + print("\nnumberOfMessagesProcessed: " + str(client_status['numberOfMessagesProcessed']) + "\n") + print("\nnumberOfBroadcastsProcessed: " + str(client_status['numberOfBroadcastsProcessed']) + "\n") + def shutdown(): + """Shutdown the API""" + try: api.shutdown() except socket.error: pass - print "\nShutdown command relayed\n" + print("\nShutdown command relayed\n") + +def UI(usrInput): + """Main user menu""" -def UI(usrInput): #Main user menu global usrPrompt - - if usrInput == "help" or usrInput == "h" or usrInput == "?": - print ' ' - print ' -------------------------------------------------------------------------' - print ' | https://github.com/Dokument/PyBitmessage-Daemon |' - print ' |-----------------------------------------------------------------------|' - print ' | Command | Description |' - print ' |------------------------|----------------------------------------------|' - print ' | help | This help file. |' - print ' | apiTest | Tests the API |' - print ' | addInfo | Returns address information (If valid) |' - print ' | bmSettings | BitMessage settings |' - print ' | exit | Use anytime to return to main menu |' - print ' | quit | Quits the program |' - print ' |------------------------|----------------------------------------------|' - print ' | listAddresses | Lists all of the users addresses |' - print ' | generateAddress | Generates a new address |' - print ' | getAddress | Get determinist address from passphrase |' - print ' |------------------------|----------------------------------------------|' - print ' | listAddressBookEntries | Lists entries from the Address Book |' - print ' | addAddressBookEntry | Add address to the Address Book |' - print ' | deleteAddressBookEntry | Deletes address from the Address Book |' - print ' |------------------------|----------------------------------------------|' - print ' | subscribe | Subscribes to an address |' - print ' | unsubscribe | Unsubscribes from an address |' - #print' | listSubscriptions | Lists all of the subscriptions. |' - print ' |------------------------|----------------------------------------------|' - print ' | create | Creates a channel |' - print ' | join | Joins a channel |' - print ' | leave | Leaves a channel |' - print ' |------------------------|----------------------------------------------|' - print ' | inbox | Lists the message information for the inbox |' - print ' | outbox | Lists the message information for the outbox |' - print ' | send | Send a new message or broadcast |' - print ' | unread | Lists all unread inbox messages |' - print ' | read | Reads a message from the inbox or outbox |' - print ' | save | Saves message to text file |' - print ' | delete | Deletes a message or all messages |' - print ' -------------------------------------------------------------------------' - print ' ' + + if usrInput in ("help", "h", "?"): + print(' ') + print(' -------------------------------------------------------------------------') + print(' | https://github.com/Dokument/PyBitmessage-Daemon |') + print(' |-----------------------------------------------------------------------|') + print(' | Command | Description |') + print(' |------------------------|----------------------------------------------|') + print(' | help | This help file. |') + print(' | apiTest | Tests the API |') + print(' | addInfo | Returns address information (If valid) |') + print(' | bmSettings | BitMessage settings |') + print(' | exit | Use anytime to return to main menu |') + print(' | quit | Quits the program |') + print(' |------------------------|----------------------------------------------|') + print(' | listAddresses | Lists all of the users addresses |') + print(' | generateAddress | Generates a new address |') + print(' | getAddress | Get determinist address from passphrase |') + print(' |------------------------|----------------------------------------------|') + print(' | listAddressBookEntries | Lists entries from the Address Book |') + print(' | addAddressBookEntry | Add address to the Address Book |') + print(' | deleteAddressBookEntry | Deletes address from the Address Book |') + print(' |------------------------|----------------------------------------------|') + print(' | subscribe | Subscribes to an address |') + print(' | unsubscribe | Unsubscribes from an address |') + print(' |------------------------|----------------------------------------------|') + print(' | create | Creates a channel |') + print(' | join | Joins a channel |') + print(' | leave | Leaves a channel |') + print(' |------------------------|----------------------------------------------|') + print(' | inbox | Lists the message information for the inbox |') + print(' | outbox | Lists the message information for the outbox |') + print(' | send | Send a new message or broadcast |') + print(' | unread | Lists all unread inbox messages |') + print(' | read | Reads a message from the inbox or outbox |') + print(' | save | Saves message to text file |') + print(' | delete | Deletes a message or all messages |') + print(' -------------------------------------------------------------------------') + print(' ') main() - elif usrInput == "apitest": #tests the API Connection. - if (apiTest() == True): - print '\n API connection test has: PASSED\n' + elif usrInput == "apitest": # tests the API Connection. + if apiTest(): + print('\n API connection test has: PASSED\n') else: - print '\n API connection test has: FAILED\n' + print('\n API connection test has: FAILED\n') main() elif usrInput == "addinfo": tmp_address = userInput('\nEnter the Bitmessage Address.') address_information = json.loads(api.decodeAddress(tmp_address)) - print '\n------------------------------' - + print('\n------------------------------') + if 'success' in str(address_information['status']).lower(): - print ' Valid Address' - print ' Address Version: %s' % str(address_information['addressVersion']) - print ' Stream Number: %s' % str(address_information['streamNumber']) + print(' Valid Address') + print(' Address Version: %s' % str(address_information['addressVersion'])) + print(' Stream Number: %s' % str(address_information['streamNumber'])) else: - print ' Invalid Address !' + print(' Invalid Address !') - print '------------------------------\n' + print('------------------------------\n') main() - - elif usrInput == "bmsettings": #tests the API Connection. + + elif usrInput == "bmsettings": # tests the API Connection. bmSettings() - print ' ' + print(' ') main() - - elif usrInput == "quit": #Quits the application - print '\n Bye\n' - sys.exit() - os._exit() - - elif usrInput == "listaddresses": #Lists all of the identities in the addressbook + + elif usrInput == "quit": # Quits the application + print('\n Bye\n') + sys.exit(0) + + elif usrInput == "listaddresses": # Lists all of the identities in the addressbook listAdd() main() - - elif usrInput == "generateaddress": #Generates a new address + + elif usrInput == "generateaddress": # Generates a new address uInput = userInput('\nWould you like to create a (D)eterministic or (R)andom address?').lower() - if uInput == "d" or uInput == "determinstic": #Creates a deterministic address + if uInput in ("d", "deterministic"): # Creates a deterministic address deterministic = True - #lbl = raw_input('Label the new address:') #currently not possible via the api lbl = '' - passphrase = userInput('Enter the Passphrase.')#.encode('base64') + passphrase = userInput('Enter the Passphrase.') # .encode('base64') numOfAdd = int(userInput('How many addresses would you like to generate?')) - #addVNum = int(raw_input('Address version number (default "0"):')) - #streamNum = int(raw_input('Stream number (default "0"):')) addVNum = 3 streamNum = 1 isRipe = userInput('Shorten the address, (Y)es or (N)o?').lower() if isRipe == "y": ripe = True - print genAdd(lbl,deterministic, passphrase, numOfAdd, addVNum, streamNum, ripe) + print(genAdd(lbl, deterministic, passphrase, numOfAdd, addVNum, streamNum, ripe)) main() elif isRipe == "n": ripe = False - print genAdd(lbl, deterministic, passphrase, numOfAdd, addVNum, streamNum, ripe) + print(genAdd(lbl, deterministic, passphrase, numOfAdd, addVNum, streamNum, ripe)) main() elif isRipe == "exit": usrPrompt = 1 main() else: - print '\n Invalid input\n' + print('\n Invalid input\n') main() - - elif uInput == "r" or uInput == "random": #Creates a random address with user-defined label + elif uInput == "r" or uInput == "random": # Creates a random address with user-defined label deterministic = False null = '' lbl = userInput('Enter the label for the new address.') - - print genAdd(lbl,deterministic, null,null, null, null, null) + + print(genAdd(lbl, deterministic, null, null, null, null, null)) main() - + else: - print '\n Invalid input\n' + print('\n Invalid input\n') main() - - elif usrInput == "getaddress": #Gets the address for/from a passphrase - phrase = userInput("Enter the address passphrase.") - print '\n Working...\n' - #vNumber = int(raw_input("Enter the address version number:")) - #sNumber = int(raw_input("Enter the address stream number:")) - - address = getAddress(phrase,4,1)#,vNumber,sNumber) - print ('\n Address: ' + address + '\n') + elif usrInput == "getaddress": # Gets the address for/from a passphrase + phrase = userInput("Enter the address passphrase.") + print('\n Working...\n') + address = getAddress(phrase, 4, 1) # ,vNumber,sNumber) + print('\n Address: ' + address + '\n') usrPrompt = 1 main() - elif usrInput == "subscribe": #Subsribe to an address + elif usrInput == "subscribe": # Subsribe to an address subscribe() usrPrompt = 1 main() - elif usrInput == "unsubscribe": #Unsubscribe from an address + + elif usrInput == "unsubscribe": # Unsubscribe from an address unsubscribe() usrPrompt = 1 main() - elif usrInput == "listsubscriptions": #Unsubscribe from an address + + elif usrInput == "listsubscriptions": # Unsubscribe from an address listSubscriptions() usrPrompt = 1 main() @@ -1451,233 +1571,239 @@ def UI(usrInput): #Main user menu joinChan() usrPrompt = 1 main() - + elif usrInput == "leave": leaveChan() usrPrompt = 1 main() - + elif usrInput == "inbox": - print '\n Loading...\n' + print('\n Loading...\n') inbox() main() elif usrInput == "unread": - print '\n Loading...\n' + print('\n Loading...\n') inbox(True) main() elif usrInput == "outbox": - print '\n Loading...\n' + print('\n Loading...\n') outbox() main() - elif usrInput == 'send': #Sends a message or broadcast + elif usrInput == 'send': # Sends a message or broadcast uInput = userInput('Would you like to send a (M)essage or (B)roadcast?').lower() - if (uInput == 'm' or uInput == 'message'): + if uInput in ('m', 'message'): null = '' - sendMsg(null,null,null,null) + sendMsg(null, null, null, null) main() - elif (uInput =='b' or uInput == 'broadcast'): + elif uInput in ('b', 'broadcast'): null = '' - sendBrd(null,null,null) + sendBrd(null, null, null) main() + elif usrInput == "read": # Opens a message from the inbox for viewing. - elif usrInput == "read": #Opens a message from the inbox for viewing. - uInput = userInput("Would you like to read a message from the (I)nbox or (O)utbox?").lower() - if (uInput != 'i' and uInput != 'inbox' and uInput != 'o' and uInput != 'outbox'): - print '\n Invalid Input.\n' + if uInput not in ('i', 'inbox', 'o', 'outbox'): + print('\n Invalid Input.\n') usrPrompt = 1 main() msgNum = int(userInput("What is the number of the message you wish to open?")) - if (uInput == 'i' or uInput == 'inbox'): - print '\n Loading...\n' + if uInput in ('i', 'inbox'): + print('\n Loading...\n') messageID = readMsg(msgNum) uInput = userInput("\nWould you like to keep this message unread, (Y)es or (N)o?").lower() - if not (uInput == 'y' or uInput == 'yes'): + if uInput not in ('y', 'yes'): markMessageRead(messageID) usrPrompt = 1 uInput = userInput("\nWould you like to (D)elete, (F)orward, (R)eply to, or (Exit) this message?").lower() - if (uInput == 'r' or uInput == 'reply'): - print '\n Loading...\n' - print ' ' - replyMsg(msgNum,'reply') + if uInput in ('r', 'reply'): + print('\n Loading...\n') + print(' ') + replyMsg(msgNum, 'reply') usrPrompt = 1 - - elif (uInput == 'f' or uInput == 'forward'): - print '\n Loading...\n' - print ' ' - replyMsg(msgNum,'forward') + + elif uInput in ('f', 'forward'): + print('\n Loading...\n') + print(' ') + replyMsg(msgNum, 'forward') usrPrompt = 1 - elif (uInput == "d" or uInput == 'delete'): - uInput = userInput("Are you sure, (Y)es or (N)o?").lower()#Prevent accidental deletion + elif uInput in ("d", 'delete'): + uInput = userInput("Are you sure, (Y)es or (N)o?").lower() # Prevent accidental deletion if uInput == "y": delMsg(msgNum) - print '\n Message Deleted.\n' + print('\n Message Deleted.\n') usrPrompt = 1 else: usrPrompt = 1 else: - print '\n Invalid entry\n' + print('\n Invalid entry\n') usrPrompt = 1 - - elif (uInput == 'o' or uInput == 'outbox'): + + elif uInput in ('o', 'outbox'): readSentMsg(msgNum) - uInput = userInput("Would you like to (D)elete, or (Exit) this message?").lower() #Gives the user the option to delete the message + # Gives the user the option to delete the message + uInput = userInput("Would you like to (D)elete, or (Exit) this message?").lower() - if (uInput == "d" or uInput == 'delete'): - uInput = userInput('Are you sure, (Y)es or (N)o?').lower() #Prevent accidental deletion + if uInput in ("d", 'delete'): + uInput = userInput('Are you sure, (Y)es or (N)o?').lower() # Prevent accidental deletion if uInput == "y": delSentMsg(msgNum) - print '\n Message Deleted.\n' + print('\n Message Deleted.\n') usrPrompt = 1 else: usrPrompt = 1 else: - print '\n Invalid Entry\n' + print('\n Invalid Entry\n') usrPrompt = 1 - + main() - + elif usrInput == "save": - + uInput = userInput("Would you like to save a message from the (I)nbox or (O)utbox?").lower() - if (uInput != 'i' and uInput == 'inbox' and uInput != 'o' and uInput == 'outbox'): - print '\n Invalid Input.\n' + if uInput not in ('i', 'inbox', 'o', 'outbox'): + print('\n Invalid Input.\n') usrPrompt = 1 main() - if (uInput == 'i' or uInput == 'inbox'): + if uInput in ('i', 'inbox'): inboxMessages = json.loads(api.getAllInboxMessages()) numMessages = len(inboxMessages['inboxMessages']) while True: msgNum = int(userInput("What is the number of the message you wish to save?")) - if (msgNum >= numMessages): - print '\n Invalid Message Number.\n' + if msgNum >= numMessages: + print('\n Invalid Message Number.\n') else: break - - subject = inboxMessages['inboxMessages'][msgNum]['subject'].decode('base64') - message = inboxMessages['inboxMessages'][msgNum]['message']#Don't decode since it is done in the saveFile function - - elif (uInput == 'o' or uInput == 'outbox'): + + subject = inboxMessages['inboxMessages'][msgNum]['subject'].decode('base64') + # Don't decode since it is done in the saveFile function + message = inboxMessages['inboxMessages'][msgNum]['message'] + + elif uInput == 'o' or uInput == 'outbox': outboxMessages = json.loads(api.getAllSentMessages()) numMessages = len(outboxMessages['sentMessages']) while True: msgNum = int(userInput("What is the number of the message you wish to save?")) - if (msgNum >= numMessages): - print '\n Invalid Message Number.\n' + if msgNum >= numMessages: + print('\n Invalid Message Number.\n') else: break - - subject = outboxMessages['sentMessages'][msgNum]['subject'].decode('base64') - message = outboxMessages['sentMessages'][msgNum]['message']#Don't decode since it is done in the saveFile function - - subject = subject +'.txt' - saveFile(subject,message) - + + subject = outboxMessages['sentMessages'][msgNum]['subject'].decode('base64') + # Don't decode since it is done in the saveFile function + message = outboxMessages['sentMessages'][msgNum]['message'] + + subject = subject + '.txt' + saveFile(subject, message) + usrPrompt = 1 main() - - elif usrInput == "delete": #will delete a message from the system, not reflected on the UI. - + + elif usrInput == "delete": # will delete a message from the system, not reflected on the UI. + uInput = userInput("Would you like to delete a message from the (I)nbox or (O)utbox?").lower() - if (uInput == 'i' or uInput == 'inbox'): + if uInput in ('i', 'inbox'): inboxMessages = json.loads(api.getAllInboxMessages()) numMessages = len(inboxMessages['inboxMessages']) - - while True: - msgNum = userInput('Enter the number of the message you wish to delete or (A)ll to empty the inbox.').lower() - if (msgNum == 'a' or msgNum == 'all'): + while True: + msgNum = userInput( + 'Enter the number of the message you wish to delete or (A)ll to empty the inbox.').lower() + + if msgNum == 'a' or msgNum == 'all': break - elif (int(msgNum) >= numMessages): - print '\n Invalid Message Number.\n' + elif int(msgNum) >= numMessages: + print('\n Invalid Message Number.\n') else: break - - uInput = userInput("Are you sure, (Y)es or (N)o?").lower()#Prevent accidental deletion + + uInput = userInput("Are you sure, (Y)es or (N)o?").lower() # Prevent accidental deletion if uInput == "y": - if (msgNum == 'a' or msgNum == 'all'): - print ' ' - for msgNum in range (0, numMessages): #processes all of the messages in the inbox - print ' Deleting message ', msgNum+1, ' of ', numMessages + if msgNum in ('a', 'all'): + print(' ') + for msgNum in range(0, numMessages): # processes all of the messages in the inbox + print(' Deleting message ', msgNum + 1, ' of ', numMessages) delMsg(0) - print '\n Inbox is empty.' + print('\n Inbox is empty.') usrPrompt = 1 else: delMsg(int(msgNum)) - - print '\n Notice: Message numbers may have changed.\n' + + print('\n Notice: Message numbers may have changed.\n') main() else: usrPrompt = 1 - elif (uInput == 'o' or uInput == 'outbox'): + + elif uInput in ('o', 'outbox'): outboxMessages = json.loads(api.getAllSentMessages()) numMessages = len(outboxMessages['sentMessages']) - + while True: - msgNum = userInput('Enter the number of the message you wish to delete or (A)ll to empty the inbox.').lower() + msgNum = userInput( + 'Enter the number of the message you wish to delete or (A)ll to empty the inbox.').lower() - if (msgNum == 'a' or msgNum == 'all'): + if msgNum in ('a', 'all'): break - elif (int(msgNum) >= numMessages): - print '\n Invalid Message Number.\n' + elif int(msgNum) >= numMessages: + print('\n Invalid Message Number.\n') else: break - uInput = userInput("Are you sure, (Y)es or (N)o?").lower()#Prevent accidental deletion + uInput = userInput("Are you sure, (Y)es or (N)o?").lower() # Prevent accidental deletion if uInput == "y": - if (msgNum == 'a' or msgNum == 'all'): - print ' ' - for msgNum in range (0, numMessages): #processes all of the messages in the outbox - print ' Deleting message ', msgNum+1, ' of ', numMessages + if msgNum in ('a', 'all'): + print(' ') + for msgNum in range(0, numMessages): # processes all of the messages in the outbox + print(' Deleting message ', msgNum + 1, ' of ', numMessages) delSentMsg(0) - print '\n Outbox is empty.' + print('\n Outbox is empty.') usrPrompt = 1 else: delSentMsg(int(msgNum)) - print '\n Notice: Message numbers may have changed.\n' + print('\n Notice: Message numbers may have changed.\n') main() else: usrPrompt = 1 else: - print '\n Invalid Entry.\n' + print('\n Invalid Entry.\n') usrPrompt = 1 main() elif usrInput == "exit": - print '\n You are already at the main menu. Use "quit" to quit.\n' + print('\n You are already at the main menu. Use "quit" to quit.\n') usrPrompt = 1 main() elif usrInput == "listaddressbookentries": res = listAddressBookEntries() - if res == 20: print '\n Error: API function not supported.\n' + if res == 20: + print('\n Error: API function not supported.\n') usrPrompt = 1 main() @@ -1685,15 +1811,18 @@ def UI(usrInput): #Main user menu address = userInput('Enter address') label = userInput('Enter label') res = addAddressToAddressBook(address, label) - if res == 16: print '\n Error: Address already exists in Address Book.\n' - if res == 20: print '\n Error: API function not supported.\n' + if res == 16: + print('\n Error: Address already exists in Address Book.\n') + if res == 20: + print('\n Error: API function not supported.\n') usrPrompt = 1 main() elif usrInput == "deleteaddressbookentry": address = userInput('Enter address') res = deleteAddressFromAddressBook(address) - if res == 20: print '\n Error: API function not supported.\n' + if res == 20: + print('\n Error: API function not supported.\n') usrPrompt = 1 main() @@ -1717,49 +1846,37 @@ def UI(usrInput): #Main user menu usrPrompt = 1 main() - elif usrInput == "million+": - genMilAddr() - usrPrompt = 1 - main() - - elif usrInput == "million-": - delMilAddr() - usrPrompt = 1 - main() - else: - print '\n "',usrInput,'" is not a command.\n' + print('\n "', usrInput, '" is not a command.\n') usrPrompt = 1 main() - + + def main(): + """Entrypoint for the CLI app""" + global api global usrPrompt - - if (usrPrompt == 0): - print '\n ------------------------------' - print ' | Bitmessage Daemon by .dok |' - print ' | Version 0.3.1 for BM 0.6.2 |' - print ' ------------------------------' - api = xmlrpclib.ServerProxy(apiData()) #Connect to BitMessage using these api credentials - - if (apiTest() == False): - print '\n ****************************************************************' - print ' WARNING: You are not connected to the Bitmessage client.' - print ' Either Bitmessage is not running or your settings are incorrect.' - print ' Use the command "apiTest" or "bmSettings" to resolve this issue.' - print ' ****************************************************************\n' - - print 'Type (H)elp for a list of commands.' #Startup message + + if usrPrompt == 0: + print('\n ------------------------------') + print(' | Bitmessage Daemon by .dok |') + print(' | Version 0.3.1 for BM 0.6.2 |') + print(' ------------------------------') + api = xmlrpclib.ServerProxy(apiData()) # Connect to BitMessage using these api credentials + + if apiTest() is False: + print('\n ****************************************************************') + print(' WARNING: You are not connected to the Bitmessage client.') + print(' Either Bitmessage is not running or your settings are incorrect.') + print(' Use the command "apiTest" or "bmSettings" to resolve this issue.') + print(' ****************************************************************\n') + + print('Type (H)elp for a list of commands.') # Startup message) usrPrompt = 2 - - #if (apiTest() == False):#Preform a connection test #taken out until I get the error handler working - # print '*************************************' - # print 'WARNING: No connection to Bitmessage.' - # print '*************************************' - # print ' ' - elif (usrPrompt == 1): - print '\nType (H)elp for a list of commands.' #Startup message + + elif usrPrompt == 1: + print('\nType (H)elp for a list of commands.') # Startup message) usrPrompt = 2 try: @@ -1767,5 +1884,6 @@ def main(): except EOFError: UI("quit") + if __name__ == "__main__": - main() + main() diff --git a/src/bitmessagecurses/__init__.py b/src/bitmessagecurses/__init__.py index fc1d74b213..64fd735b45 100644 --- a/src/bitmessagecurses/__init__.py +++ b/src/bitmessagecurses/__init__.py @@ -1,38 +1,40 @@ +""" +Bitmessage commandline interface +""" # Copyright (c) 2014 Luke Montalvo # This file adds a alternative commandline interface, feel free to critique and fork -# +# # This has only been tested on Arch Linux and Linux Mint # Dependencies: # * from python2-pip # * python2-pythondialog # * dialog +import ConfigParser +import curses import os import sys -import StringIO -from textwrap import * - import time -from time import strftime, localtime +from textwrap import fill from threading import Timer -import curses -import dialog from dialog import Dialog -from helper_sql import * -from helper_ackPayload import genAckPayload - -from addresses import * -import ConfigParser -from bmconfigparser import BMConfigParser -from inventory import Inventory +import helper_sent import l10n -from pyelliptic.openssl import OpenSSL +import network.stats import queues import shared import shutdown +import state + +from addresses import addBMIfNotPresent, decodeAddress +from bmconfigparser import config +from helper_sql import sqlExecute, sqlQuery + +# pylint: disable=global-statement + -quit = False +quit_ = False menutab = 1 menu = ["Inbox", "Send", "Sent", "Your Identities", "Subscriptions", "Address Book", "Blacklist", "Network Status"] naptime = 100 @@ -58,163 +60,206 @@ BROADCAST_STR = "[Broadcast subscribers]" -class printLog: + +class printLog(object): + """Printing logs""" + # pylint: disable=no-self-use + def write(self, output): + """Write logs""" global log log += output + def flush(self): + """Flush logs""" pass -class errLog: + + +class errLog(object): + """Error logs""" + # pylint: disable=no-self-use + def write(self, output): + """Write error logs""" global log - log += "!"+output + log += "!" + output + def flush(self): + """Flush error logs""" pass + + printlog = printLog() errlog = errLog() def cpair(a): + """Color pairs""" r = curses.color_pair(a) - if r not in range(1, curses.COLOR_PAIRS-1): + if r not in range(1, curses.COLOR_PAIRS - 1): r = curses.color_pair(0) return r + + def ascii(s): + """ASCII values""" r = "" for c in s: if ord(c) in range(128): r += c return r + def drawmenu(stdscr): + """Creating menu's""" menustr = " " - for i in range(0, len(menu)): - if menutab == i+1: + for i, _ in enumerate(menu): + if menutab == i + 1: menustr = menustr[:-1] menustr += "[" - menustr += str(i+1)+menu[i] - if menutab == i+1: + menustr += str(i + 1) + menu[i] + if menutab == i + 1: menustr += "] " - elif i != len(menu)-1: + elif i != len(menu) - 1: menustr += " " stdscr.addstr(2, 5, menustr, curses.A_UNDERLINE) + def set_background_title(d, title): + """Setting background title""" try: d.set_background_title(title) - except: + except: # noqa:E722 d.add_persistent_args(("--backtitle", title)) + def scrollbox(d, text, height=None, width=None): + """Setting scroll box""" try: - d.scrollbox(text, height, width, exit_label = "Continue") - except: - d.msgbox(text, height or 0, width or 0, ok_label = "Continue") + d.scrollbox(text, height, width, exit_label="Continue") + except: # noqa:E722 + d.msgbox(text, height or 0, width or 0, ok_label="Continue") + 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() + + def drawtab(stdscr): - if menutab in range(1, len(menu)+1): - if menutab == 1: # Inbox + """Method for drawing different tabs""" + # pylint: disable=too-many-branches, too-many-statements + if menutab in range(1, len(menu) + 1): + if menutab == 1: # Inbox stdscr.addstr(3, 5, "To", curses.A_BOLD) stdscr.addstr(3, 40, "From", curses.A_BOLD) stdscr.addstr(3, 80, "Subject", curses.A_BOLD) stdscr.addstr(3, 120, "Time Received", curses.A_BOLD) stdscr.hline(4, 5, '-', 121) - for i, item in enumerate(inbox[max(min(len(inbox)-curses.LINES+6, inboxcur-5), 0):]): - if 6+i < curses.LINES: + for i, item in enumerate(inbox[max(min(len(inbox) - curses.LINES + 6, inboxcur - 5), 0):]): + if 6 + i < curses.LINES: a = 0 - if i == inboxcur - max(min(len(inbox)-curses.LINES+6, inboxcur-5), 0): # Highlight current address + if i == inboxcur - max(min(len(inbox) - curses.LINES + 6, inboxcur - 5), 0): + # Highlight current address a = a | curses.A_REVERSE - if item[7] == False: # If not read, highlight + if item[7] is False: # If not read, highlight a = a | curses.A_BOLD - stdscr.addstr(5+i, 5, item[1][:34], a) - stdscr.addstr(5+i, 40, item[3][:39], a) - stdscr.addstr(5+i, 80, item[5][:39], a) - stdscr.addstr(5+i, 120, item[6][:39], a) - elif menutab == 3: # Sent + stdscr.addstr(5 + i, 5, item[1][:34], a) + stdscr.addstr(5 + i, 40, item[3][:39], a) + stdscr.addstr(5 + i, 80, item[5][:39], a) + stdscr.addstr(5 + i, 120, item[6][:39], a) + elif menutab == 3: # Sent stdscr.addstr(3, 5, "To", curses.A_BOLD) stdscr.addstr(3, 40, "From", curses.A_BOLD) stdscr.addstr(3, 80, "Subject", curses.A_BOLD) stdscr.addstr(3, 120, "Status", curses.A_BOLD) stdscr.hline(4, 5, '-', 121) - for i, item in enumerate(sentbox[max(min(len(sentbox)-curses.LINES+6, sentcur-5), 0):]): - if 6+i < curses.LINES: + for i, item in enumerate(sentbox[max(min(len(sentbox) - curses.LINES + 6, sentcur - 5), 0):]): + if 6 + i < curses.LINES: a = 0 - if i == sentcur - max(min(len(sentbox)-curses.LINES+6, sentcur-5), 0): # Highlight current address + if i == sentcur - max(min(len(sentbox) - curses.LINES + 6, sentcur - 5), 0): + # Highlight current address a = a | curses.A_REVERSE - stdscr.addstr(5+i, 5, item[0][:34], a) - stdscr.addstr(5+i, 40, item[2][:39], a) - stdscr.addstr(5+i, 80, item[4][:39], a) - stdscr.addstr(5+i, 120, item[5][:39], a) - elif menutab == 2 or menutab == 4: # Send or Identities + stdscr.addstr(5 + i, 5, item[0][:34], a) + stdscr.addstr(5 + i, 40, item[2][:39], a) + stdscr.addstr(5 + i, 80, item[4][:39], a) + stdscr.addstr(5 + i, 120, item[5][:39], a) + elif menutab == 2 or menutab == 4: # Send or Identities stdscr.addstr(3, 5, "Label", curses.A_BOLD) stdscr.addstr(3, 40, "Address", curses.A_BOLD) stdscr.addstr(3, 80, "Stream", curses.A_BOLD) stdscr.hline(4, 5, '-', 81) - for i, item in enumerate(addresses[max(min(len(addresses)-curses.LINES+6, addrcur-5), 0):]): - if 6+i < curses.LINES: + for i, item in enumerate(addresses[max(min(len(addresses) - curses.LINES + 6, addrcur - 5), 0):]): + if 6 + i < curses.LINES: a = 0 - if i == addrcur - max(min(len(addresses)-curses.LINES+6, addrcur-5), 0): # Highlight current address + if i == addrcur - max(min(len(addresses) - curses.LINES + 6, addrcur - 5), 0): + # Highlight current address a = a | curses.A_REVERSE - if item[1] == True and item[3] not in [8,9]: # Embolden enabled, non-special addresses + if item[1] and item[3] not in [8, 9]: # Embolden enabled, non-special addresses a = a | curses.A_BOLD - stdscr.addstr(5+i, 5, item[0][:34], a) - stdscr.addstr(5+i, 40, item[2][:39], cpair(item[3]) | a) - stdscr.addstr(5+i, 80, str(1)[:39], a) - elif menutab == 5: # Subscriptions + stdscr.addstr(5 + i, 5, item[0][:34], a) + stdscr.addstr(5 + i, 40, item[2][:39], cpair(item[3]) | a) + stdscr.addstr(5 + i, 80, str(1)[:39], a) + elif menutab == 5: # Subscriptions stdscr.addstr(3, 5, "Label", curses.A_BOLD) stdscr.addstr(3, 80, "Address", curses.A_BOLD) stdscr.addstr(3, 120, "Enabled", curses.A_BOLD) stdscr.hline(4, 5, '-', 121) - for i, item in enumerate(subscriptions[max(min(len(subscriptions)-curses.LINES+6, subcur-5), 0):]): - if 6+i < curses.LINES: + for i, item in enumerate(subscriptions[max(min(len(subscriptions) - curses.LINES + 6, subcur - 5), 0):]): + if 6 + i < curses.LINES: a = 0 - if i == subcur - max(min(len(subscriptions)-curses.LINES+6, subcur-5), 0): # Highlight current address + if i == subcur - max(min(len(subscriptions) - curses.LINES + 6, subcur - 5), 0): + # Highlight current address a = a | curses.A_REVERSE - if item[2] == True: # Embolden enabled subscriptions + if item[2]: # Embolden enabled subscriptions a = a | curses.A_BOLD - stdscr.addstr(5+i, 5, item[0][:74], a) - stdscr.addstr(5+i, 80, item[1][:39], a) - stdscr.addstr(5+i, 120, str(item[2]), a) - elif menutab == 6: # Address book + stdscr.addstr(5 + i, 5, item[0][:74], a) + stdscr.addstr(5 + i, 80, item[1][:39], a) + stdscr.addstr(5 + i, 120, str(item[2]), a) + elif menutab == 6: # Address book stdscr.addstr(3, 5, "Label", curses.A_BOLD) stdscr.addstr(3, 40, "Address", curses.A_BOLD) stdscr.hline(4, 5, '-', 41) - for i, item in enumerate(addrbook[max(min(len(addrbook)-curses.LINES+6, abookcur-5), 0):]): - if 6+i < curses.LINES: + for i, item in enumerate(addrbook[max(min(len(addrbook) - curses.LINES + 6, abookcur - 5), 0):]): + if 6 + i < curses.LINES: a = 0 - if i == abookcur - max(min(len(addrbook)-curses.LINES+6, abookcur-5), 0): # Highlight current address + if i == abookcur - max(min(len(addrbook) - curses.LINES + 6, abookcur - 5), 0): + # Highlight current address a = a | curses.A_REVERSE - stdscr.addstr(5+i, 5, item[0][:34], a) - stdscr.addstr(5+i, 40, item[1][:39], a) - elif menutab == 7: # Blacklist - stdscr.addstr(3, 5, "Type: "+bwtype) + stdscr.addstr(5 + i, 5, item[0][:34], a) + stdscr.addstr(5 + i, 40, item[1][:39], a) + elif menutab == 7: # Blacklist + stdscr.addstr(3, 5, "Type: " + bwtype) stdscr.addstr(4, 5, "Label", curses.A_BOLD) stdscr.addstr(4, 80, "Address", curses.A_BOLD) stdscr.addstr(4, 120, "Enabled", curses.A_BOLD) stdscr.hline(5, 5, '-', 121) - for i, item in enumerate(blacklist[max(min(len(blacklist)-curses.LINES+6, blackcur-5), 0):]): - if 7+i < curses.LINES: + for i, item in enumerate(blacklist[max(min(len(blacklist) - curses.LINES + 6, blackcur - 5), 0):]): + if 7 + i < curses.LINES: a = 0 - if i == blackcur - max(min(len(blacklist)-curses.LINES+6, blackcur-5), 0): # Highlight current address + if i == blackcur - max(min(len(blacklist) - curses.LINES + 6, blackcur - 5), 0): + # Highlight current address a = a | curses.A_REVERSE - if item[2] == True: # Embolden enabled subscriptions + if item[2]: # Embolden enabled subscriptions a = a | curses.A_BOLD - stdscr.addstr(6+i, 5, item[0][:74], a) - stdscr.addstr(6+i, 80, item[1][:39], a) - stdscr.addstr(6+i, 120, str(item[2]), a) - elif menutab == 8: # Network status + stdscr.addstr(6 + i, 5, item[0][:74], a) + stdscr.addstr(6 + i, 80, item[1][:39], a) + stdscr.addstr(6 + i, 120, str(item[2]), a) + elif menutab == 8: # Network status # Connection data - stdscr.addstr(4, 5, "Total Connections: "+str(len(shared.connectedHostsList)).ljust(2)) + connected_hosts = network.stats.connectedHostsList() + stdscr.addstr( + 4, 5, "Total Connections: " + + str(len(connected_hosts)).ljust(2) + ) stdscr.addstr(6, 6, "Stream #", curses.A_BOLD) stdscr.addstr(6, 18, "Connections", curses.A_BOLD) stdscr.hline(7, 6, '-', 23) streamcount = [] - for host, stream in shared.connectedHostsList.items(): + for host, stream in connected_hosts: if stream >= len(streamcount): streamcount.append(1) else: @@ -222,73 +267,97 @@ def drawtab(stdscr): for i, item in enumerate(streamcount): if i < 4: if i == 0: - stdscr.addstr(8+i, 6, "?") + stdscr.addstr(8 + i, 6, "?") else: - stdscr.addstr(8+i, 6, str(i)) - stdscr.addstr(8+i, 18, str(item).ljust(2)) - + stdscr.addstr(8 + i, 6, str(i)) + stdscr.addstr(8 + i, 18, str(item).ljust(2)) + # Uptime and processing data - stdscr.addstr(6, 35, "Since startup on "+l10n.formatTimestamp(startuptime, False)) - stdscr.addstr(7, 40, "Processed "+str(shared.numberOfMessagesProcessed).ljust(4)+" person-to-person messages.") - stdscr.addstr(8, 40, "Processed "+str(shared.numberOfBroadcastsProcessed).ljust(4)+" broadcast messages.") - stdscr.addstr(9, 40, "Processed "+str(shared.numberOfPubkeysProcessed).ljust(4)+" public keys.") - + stdscr.addstr( + 6, 35, "Since startup on " + l10n.formatTimestamp(startuptime)) + stdscr.addstr(7, 40, "Processed " + str( + state.numberOfMessagesProcessed).ljust(4) + " person-to-person messages.") + stdscr.addstr(8, 40, "Processed " + str( + state.numberOfBroadcastsProcessed).ljust(4) + " broadcast messages.") + stdscr.addstr(9, 40, "Processed " + str( + state.numberOfPubkeysProcessed).ljust(4) + " public keys.") + # Inventory data - stdscr.addstr(11, 35, "Inventory lookups per second: "+str(inventorydata).ljust(3)) - + stdscr.addstr(11, 35, "Inventory lookups per second: " + str(inventorydata).ljust(3)) + # Log stdscr.addstr(13, 6, "Log", curses.A_BOLD) n = log.count('\n') if n > 0: - l = log.split('\n') + lg = log.split('\n') if n > 512: - del l[:(n-256)] + del lg[:(n - 256)] logpad.erase() - n = len(l) - for i, item in enumerate(l): + n = len(lg) + for i, item in enumerate(lg): a = 0 - if len(item) > 0 and item[0] == '!': + if item and item[0] == '!': a = curses.color_pair(1) item = item[1:] logpad.addstr(i, 0, item, a) - logpad.refresh(n-curses.LINES+2, 0, 14, 6, curses.LINES-2, curses.COLS-7) + logpad.refresh(n - curses.LINES + 2, 0, 14, 6, curses.LINES - 2, curses.COLS - 7) stdscr.refresh() + def redraw(stdscr): + """Redraw menu""" stdscr.erase() stdscr.border() drawmenu(stdscr) stdscr.refresh() + + def dialogreset(stdscr): + """Resetting dialogue""" stdscr.clear() stdscr.keypad(1) curses.curs_set(0) + + +# pylint: disable=too-many-branches, too-many-statements def handlech(c, stdscr): + """Handle character given on the command-line interface""" + # pylint: disable=redefined-outer-name, too-many-nested-blocks, too-many-locals if c != curses.ERR: global inboxcur, addrcur, sentcur, subcur, abookcur, blackcur - if c in range(256): + if c in range(256): if chr(c) in '12345678': global menutab menutab = int(chr(c)) elif chr(c) == 'q': - global quit - quit = True + global quit_ + quit_ = True elif chr(c) == '\n': curses.curs_set(1) d = Dialog(dialog="dialog") if menutab == 1: set_background_title(d, "Inbox Message Dialog Box") - r, t = d.menu("Do what with \""+inbox[inboxcur][5]+"\" from \""+inbox[inboxcur][3]+"\"?", - choices=[("1", "View message"), + r, t = d.menu( + "Do what with \"" + inbox[inboxcur][5] + "\" from \"" + inbox[inboxcur][3] + "\"?", + choices=[ + ("1", "View message"), ("2", "Mark message as unread"), ("3", "Reply"), ("4", "Add sender to Address Book"), ("5", "Save message as text file"), ("6", "Move to trash")]) if r == d.DIALOG_OK: - if t == "1": # View - set_background_title(d, "\""+inbox[inboxcur][5]+"\" from \""+inbox[inboxcur][3]+"\" to \""+inbox[inboxcur][1]+"\"") - data = "" + if t == "1": # View + set_background_title( + d, + "\"" + + inbox[inboxcur][5] + + "\" from \"" + + inbox[inboxcur][3] + + "\" to \"" + + inbox[inboxcur][1] + + "\"") + data = "" # pyint: disable=redefined-outer-name ret = sqlQuery("SELECT message FROM inbox WHERE msgid=?", inbox[inboxcur][0]) if ret != []: for row in ret: @@ -296,16 +365,16 @@ def handlech(c, stdscr): data = shared.fixPotentiallyInvalidUTF8Data(data) msg = "" for i, item in enumerate(data.split("\n")): - msg += fill(item, replace_whitespace=False)+"\n" + msg += fill(item, replace_whitespace=False) + "\n" scrollbox(d, unicode(ascii(msg)), 30, 80) sqlExecute("UPDATE inbox SET read=1 WHERE msgid=?", inbox[inboxcur][0]) inbox[inboxcur][7] = 1 else: scrollbox(d, unicode("Could not fetch message.")) - elif t == "2": # Mark unread + elif t == "2": # Mark unread sqlExecute("UPDATE inbox SET read=0 WHERE msgid=?", inbox[inboxcur][0]) inbox[inboxcur][7] = 0 - elif t == "3": # Reply + elif t == "3": # Reply curses.curs_set(1) m = inbox[inboxcur] fromaddr = m[4] @@ -314,29 +383,31 @@ def handlech(c, stdscr): if fromaddr == item[2] and item[3] != 0: ischan = True break - if not addresses[i][1]: - scrollbox(d, unicode("Sending address disabled, please either enable it or choose a different address.")) + if not addresses[i][1]: # pylint: disable=undefined-loop-variable + scrollbox(d, unicode( + "Sending address disabled, please either enable it" + "or choose a different address.")) return toaddr = m[2] if ischan: toaddr = fromaddr - + subject = m[5] if not m[5][:4] == "Re: ": - subject = "Re: "+m[5] + subject = "Re: " + m[5] body = "" ret = sqlQuery("SELECT message FROM inbox WHERE msgid=?", m[0]) if ret != []: body = "\n\n------------------------------------------------------\n" for row in ret: body, = row - + sendMessage(fromaddr, toaddr, ischan, subject, body, True) dialogreset(stdscr) - elif t == "4": # Add to Address Book + elif t == "4": # Add to Address Book addr = inbox[inboxcur][4] - if addr not in [item[1] for i,item in enumerate(addrbook)]: - r, t = d.inputbox("Label for address \""+addr+"\"") + if addr not in [item[1] for i, item in enumerate(addrbook)]: + r, t = d.inputbox("Label for address \"" + addr + "\"") if r == d.DIALOG_OK: label = t sqlExecute("INSERT INTO addressbook VALUES (?,?)", label, addr) @@ -346,61 +417,85 @@ def handlech(c, stdscr): addrbook.reverse() else: scrollbox(d, unicode("The selected address is already in the Address Book.")) - elif t == "5": # Save message - set_background_title(d, "Save \""+inbox[inboxcur][5]+"\" as text file") - r, t = d.inputbox("Filename", init=inbox[inboxcur][5]+".txt") + elif t == "5": # Save message + set_background_title(d, "Save \"" + inbox[inboxcur][5] + "\" as text file") + r, t = d.inputbox("Filename", init=inbox[inboxcur][5] + ".txt") if r == d.DIALOG_OK: msg = "" ret = sqlQuery("SELECT message FROM inbox WHERE msgid=?", inbox[inboxcur][0]) if ret != []: for row in ret: msg, = row - fh = open(t, "a") # Open in append mode just in case + fh = open(t, "a") # Open in append mode just in case fh.write(msg) fh.close() else: scrollbox(d, unicode("Could not fetch message.")) - elif t == "6": # Move to trash + elif t == "6": # Move to trash sqlExecute("UPDATE inbox SET folder='trash' WHERE msgid=?", inbox[inboxcur][0]) del inbox[inboxcur] - scrollbox(d, unicode("Message moved to trash. There is no interface to view your trash, \nbut the message is still on disk if you are desperate to recover it.")) + scrollbox(d, unicode( + "Message moved to trash. There is no interface to view your trash," + " \nbut the message is still on disk if you are desperate to recover it.")) elif menutab == 2: a = "" - if addresses[addrcur][3] != 0: # if current address is a chan + if addresses[addrcur][3] != 0: # if current address is a chan a = addresses[addrcur][2] sendMessage(addresses[addrcur][2], a) elif menutab == 3: set_background_title(d, "Sent Messages Dialog Box") - r, t = d.menu("Do what with \""+sentbox[sentcur][4]+"\" to \""+sentbox[sentcur][0]+"\"?", - choices=[("1", "View message"), + r, t = d.menu( + "Do what with \"" + sentbox[sentcur][4] + "\" to \"" + sentbox[sentcur][0] + "\"?", + choices=[ + ("1", "View message"), ("2", "Move to trash")]) if r == d.DIALOG_OK: - if t == "1": # View - set_background_title(d, "\""+sentbox[sentcur][4]+"\" from \""+sentbox[sentcur][3]+"\" to \""+sentbox[sentcur][1]+"\"") + if t == "1": # View + set_background_title( + d, + "\"" + + sentbox[sentcur][4] + + "\" from \"" + + sentbox[sentcur][3] + + "\" to \"" + + sentbox[sentcur][1] + + "\"") data = "" - ret = sqlQuery("SELECT message FROM sent WHERE subject=? AND ackdata=?", sentbox[sentcur][4], sentbox[sentcur][6]) + ret = sqlQuery( + "SELECT message FROM sent WHERE subject=? AND ackdata=?", + sentbox[sentcur][4], + sentbox[sentcur][6]) if ret != []: for row in ret: data, = row data = shared.fixPotentiallyInvalidUTF8Data(data) msg = "" for i, item in enumerate(data.split("\n")): - msg += fill(item, replace_whitespace=False)+"\n" + msg += fill(item, replace_whitespace=False) + "\n" scrollbox(d, unicode(ascii(msg)), 30, 80) else: scrollbox(d, unicode("Could not fetch message.")) - elif t == "2": # Move to trash - sqlExecute("UPDATE sent SET folder='trash' WHERE subject=? AND ackdata=?", sentbox[sentcur][4], sentbox[sentcur][6]) + elif t == "2": # Move to trash + sqlExecute( + "UPDATE sent SET folder='trash' WHERE subject=? AND ackdata=?", + sentbox[sentcur][4], + sentbox[sentcur][6]) del sentbox[sentcur] - scrollbox(d, unicode("Message moved to trash. There is no interface to view your trash, \nbut the message is still on disk if you are desperate to recover it.")) + scrollbox(d, unicode( + "Message moved to trash. There is no interface to view your trash" + " \nbut the message is still on disk if you are desperate to recover it.")) elif menutab == 4: set_background_title(d, "Your Identities Dialog Box") if len(addresses) <= addrcur: - r, t = d.menu("Do what with addresses?", - choices=[("1", "Create new address")]) + r, t = d.menu( + "Do what with addresses?", + choices=[ + ("1", "Create new address")]) else: - r, t = d.menu("Do what with \""+addresses[addrcur][0]+"\" : \""+addresses[addrcur][2]+"\"?", - choices=[("1", "Create new address"), + r, t = d.menu( + "Do what with \"" + addresses[addrcur][0] + "\" : \"" + addresses[addrcur][2] + "\"?", + choices=[ + ("1", "Create new address"), ("2", "Send a message from this address"), ("3", "Rename"), ("4", "Enable"), @@ -408,31 +503,41 @@ def handlech(c, stdscr): ("6", "Delete"), ("7", "Special address behavior")]) if r == d.DIALOG_OK: - if t == "1": # Create new address + if t == "1": # Create new address set_background_title(d, "Create new address") - scrollbox(d, unicode("Here you may generate as many addresses as you like.\n" - "Indeed, creating and abandoning addresses is encouraged.\n" - "Deterministic addresses have several pros and cons:\n" - "\nPros:\n" - " * You can recreate your addresses on any computer from memory\n" - " * You need not worry about backing up your keys.dat file as long as you \n can remember your passphrase\n" - "Cons:\n" - " * You must remember (or write down) your passphrase in order to recreate \n your keys if they are lost\n" - " * You must also remember the address version and stream numbers\n" - " * If you choose a weak passphrase someone may be able to brute-force it \n and then send and receive messages as you")) - r, t = d.menu("Choose an address generation technique", - choices=[("1", "Use a random number generator"), + scrollbox( + d, unicode( + "Here you may generate as many addresses as you like.\n" + "Indeed, creating and abandoning addresses is encouraged.\n" + "Deterministic addresses have several pros and cons:\n" + "\nPros:\n" + " * You can recreate your addresses on any computer from memory\n" + " * You need not worry about backing up your keys.dat file as long as you" + " \n can remember your passphrase\n" + "Cons:\n" + " * You must remember (or write down) your passphrase in order to recreate" + " \n your keys if they are lost\n" + " * You must also remember the address version and stream numbers\n" + " * If you choose a weak passphrase someone may be able to brute-force it" + " \n and then send and receive messages as you")) + r, t = d.menu( + "Choose an address generation technique", + choices=[ + ("1", "Use a random number generator"), ("2", "Use a passphrase")]) if r == d.DIALOG_OK: if t == "1": set_background_title(d, "Randomly generate address") r, t = d.inputbox("Label (not shown to anyone except you)") label = "" - if r == d.DIALOG_OK and len(t) > 0: + if r == d.DIALOG_OK and t: label = t - r, t = d.menu("Choose a stream", - choices=[("1", "Use the most available stream"),("", "(Best if this is the first of many addresses you will create)"), - ("2", "Use the same stream as an existing address"),("", "(Saves you some bandwidth and processing power)")]) + r, t = d.menu( + "Choose a stream", + choices=[("1", "Use the most available stream"), + ("", "(Best if this is the first of many addresses you will create)"), + ("2", "Use the same stream as an existing address"), + ("", "(Saves you some bandwidth and processing power)")]) if r == d.DIALOG_OK: if t == "1": stream = 1 @@ -444,117 +549,151 @@ def handlech(c, stdscr): if r == d.DIALOG_OK: stream = decodeAddress(addrs[int(t)][1])[2] shorten = False - r, t = d.checklist("Miscellaneous options", - choices=[("1", "Spend time shortening the address", 1 if shorten else 0)]) + r, t = d.checklist( + "Miscellaneous options", + choices=[( + "1", + "Spend time shortening the address", + 1 if shorten else 0)]) if r == d.DIALOG_OK and "1" in t: shorten = True - queues.addressGeneratorQueue.put(("createRandomAddress", 4, stream, label, 1, "", shorten)) + queues.addressGeneratorQueue.put(( + "createRandomAddress", + 4, + stream, + label, + 1, + "", + shorten)) elif t == "2": set_background_title(d, "Make deterministic addresses") - r, t = d.passwordform("Enter passphrase", - [("Passphrase", 1, 1, "", 2, 1, 64, 128), - ("Confirm passphrase", 3, 1, "", 4, 1, 64, 128)], + r, t = d.passwordform( + "Enter passphrase", + [ + ("Passphrase", 1, 1, "", 2, 1, 64, 128), + ("Confirm passphrase", 3, 1, "", 4, 1, 64, 128)], form_height=4, insecure=True) if r == d.DIALOG_OK: if t[0] == t[1]: passphrase = t[0] - r, t = d.rangebox("Number of addresses to generate", - width=48, min=1, max=99, init=8) + r, t = d.rangebox( + "Number of addresses to generate", + width=48, + min=1, + max=99, + init=8) if r == d.DIALOG_OK: number = t stream = 1 shorten = False - r, t = d.checklist("Miscellaneous options", - choices=[("1", "Spend time shortening the address", 1 if shorten else 0)]) + r, t = d.checklist( + "Miscellaneous options", + choices=[( + "1", + "Spend time shortening the address", + 1 if shorten else 0)]) if r == d.DIALOG_OK and "1" in t: shorten = True - scrollbox(d, unicode("In addition to your passphrase, be sure to remember the following numbers:\n" - "\n * Address version number: "+str(4)+"\n" - " * Stream number: "+str(stream))) - queues.addressGeneratorQueue.put(('createDeterministicAddresses', 4, stream, "unused deterministic address", number, str(passphrase), shorten)) + scrollbox( + d, unicode( + "In addition to your passphrase, be sure to remember the" + " following numbers:\n" + "\n * Address version number: " + str(4) + "\n" + " * Stream number: " + str(stream))) + queues.addressGeneratorQueue.put( + ('createDeterministicAddresses', 4, stream, + "unused deterministic address", number, + str(passphrase), shorten)) else: scrollbox(d, unicode("Passphrases do not match")) - elif t == "2": # Send a message + elif t == "2": # Send a message a = "" - if addresses[addrcur][3] != 0: # if current address is a chan + if addresses[addrcur][3] != 0: # if current address is a chan a = addresses[addrcur][2] sendMessage(addresses[addrcur][2], a) - elif t == "3": # Rename address label + elif t == "3": # Rename address label a = addresses[addrcur][2] label = addresses[addrcur][0] 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 + 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'): - addresses[addrcur][3] = 9 # orange - elif BMConfigParser().safeGetBoolean(a, 'mailinglist'): - addresses[addrcur][3] = 5 # magenta + if config.safeGetBoolean(a, 'chan'): + addresses[addrcur][3] = 9 # orange + elif config.safeGetBoolean(a, 'mailinglist'): + addresses[addrcur][3] = 5 # magenta else: - addresses[addrcur][3] = 0 # black + addresses[addrcur][3] = 0 # black addresses[addrcur][1] = True - shared.reloadMyAddressHashes() # Reload address hashes - elif t == "5": # Disable address + shared.reloadMyAddressHashes() # Reload address hashes + elif t == "5": # Disable address a = addresses[addrcur][2] - BMConfigParser().set(a, "enabled", "false") # Set config - addresses[addrcur][3] = 8 # Set color to gray + 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 + 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() - del addresses[addrcur] - elif t == "7": # Special address behavior + 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"): - scrollbox(d, unicode("This is a chan address. You cannot use it as a pseudo-mailing list.")) + 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") - r, t = d.radiolist("Select address behavior", - choices=[("1", "Behave as a normal address", not m), + m = config.safeGetBoolean(a, "mailinglist") + r, t = d.radiolist( + "Select address behavior", + choices=[ + ("1", "Behave as a normal address", not m), ("2", "Behave as a pseudo-mailing-list address", m)]) if r == d.DIALOG_OK: - if t == "1" and m == True: - BMConfigParser().set(a, "mailinglist", "false") + if t == "1" and m: + config.set(a, "mailinglist", "false") if addresses[addrcur][1]: - addresses[addrcur][3] = 0 # Set color to black + addresses[addrcur][3] = 0 # Set color to black else: - addresses[addrcur][3] = 8 # Set color to gray - elif t == "2" and m == False: + 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 = "" + 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) - addresses[addrcur][3] = 6 # Set color to magenta + 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: - r, t = d.menu("Do what with subscription to \""+subscriptions[subcur][0]+"\"?", - choices=[("1", "Add new subscription")]) + r, t = d.menu( + "Do what with subscription to \"" + subscriptions[subcur][0] + "\"?", + choices=[ + ("1", "Add new subscription")]) else: - r, t = d.menu("Do what with subscription to \""+subscriptions[subcur][0]+"\"?", - choices=[("1", "Add new subscription"), + r, t = d.menu( + "Do what with subscription to \"" + subscriptions[subcur][0] + "\"?", + choices=[ + ("1", "Add new subscription"), ("2", "Delete this subscription"), ("3", "Enable"), ("4", "Disable")]) @@ -575,27 +714,39 @@ def handlech(c, stdscr): sqlExecute("INSERT INTO subscriptions VALUES (?,?,?)", label, addr, True) shared.reloadBroadcastSendersForWhichImWatching() elif t == "2": - r, t = d.inpuxbox("Type in \"I want to delete this subscription\"") + r, t = d.inputbox("Type in \"I want to delete this subscription\"") if r == d.DIALOG_OK and t == "I want to delete this subscription": - sqlExecute("DELETE FROM subscriptions WHERE label=? AND address=?", subscriptions[subcur][0], subscriptions[subcur][1]) - shared.reloadBroadcastSendersForWhichImWatching() - del subscriptions[subcur] + sqlExecute( + "DELETE FROM subscriptions WHERE label=? AND address=?", + subscriptions[subcur][0], + subscriptions[subcur][1]) + shared.reloadBroadcastSendersForWhichImWatching() + del subscriptions[subcur] elif t == "3": - sqlExecute("UPDATE subscriptions SET enabled=1 WHERE label=? AND address=?", subscriptions[subcur][0], subscriptions[subcur][1]) + sqlExecute( + "UPDATE subscriptions SET enabled=1 WHERE label=? AND address=?", + subscriptions[subcur][0], + subscriptions[subcur][1]) shared.reloadBroadcastSendersForWhichImWatching() subscriptions[subcur][2] = True elif t == "4": - sqlExecute("UPDATE subscriptions SET enabled=0 WHERE label=? AND address=?", subscriptions[subcur][0], subscriptions[subcur][1]) + sqlExecute( + "UPDATE subscriptions SET enabled=0 WHERE label=? AND address=?", + subscriptions[subcur][0], + subscriptions[subcur][1]) shared.reloadBroadcastSendersForWhichImWatching() subscriptions[subcur][2] = False elif menutab == 6: set_background_title(d, "Address Book Dialog Box") if len(addrbook) <= abookcur: - r, t = d.menu("Do what with addressbook?", + r, t = d.menu( + "Do what with addressbook?", choices=[("3", "Add new address to Address Book")]) else: - r, t = d.menu("Do what with \""+addrbook[abookcur][0]+"\" : \""+addrbook[abookcur][1]+"\"", - choices=[("1", "Send a message to this address"), + r, t = d.menu( + "Do what with \"" + addrbook[abookcur][0] + "\" : \"" + addrbook[abookcur][1] + "\"", + choices=[ + ("1", "Send a message to this address"), ("2", "Subscribe to this address"), ("3", "Add new address to Address Book"), ("4", "Delete this address")]) @@ -617,8 +768,8 @@ def handlech(c, stdscr): r, t = d.inputbox("Input new address") if r == d.DIALOG_OK: addr = t - if addr not in [item[1] for i,item in enumerate(addrbook)]: - r, t = d.inputbox("Label for address \""+addr+"\"") + if addr not in [item[1] for i, item in enumerate(addrbook)]: + r, t = d.inputbox("Label for address \"" + addr + "\"") if r == d.DIALOG_OK: sqlExecute("INSERT INTO addressbook VALUES (?,?)", t, addr) # Prepend entry @@ -630,25 +781,39 @@ def handlech(c, stdscr): elif t == "4": r, t = d.inputbox("Type in \"I want to delete this Address Book entry\"") if r == d.DIALOG_OK and t == "I want to delete this Address Book entry": - sqlExecute("DELETE FROM addressbook WHERE label=? AND address=?", addrbook[abookcur][0], addrbook[abookcur][1]) + sqlExecute( + "DELETE FROM addressbook WHERE label=? AND address=?", + addrbook[abookcur][0], + addrbook[abookcur][1]) del addrbook[abookcur] elif menutab == 7: set_background_title(d, "Blacklist Dialog Box") - r, t = d.menu("Do what with \""+blacklist[blackcur][0]+"\" : \""+blacklist[blackcur][1]+"\"?", - choices=[("1", "Delete"), + r, t = d.menu( + "Do what with \"" + blacklist[blackcur][0] + "\" : \"" + blacklist[blackcur][1] + "\"?", + choices=[ + ("1", "Delete"), ("2", "Enable"), ("3", "Disable")]) if r == d.DIALOG_OK: if t == "1": r, t = d.inputbox("Type in \"I want to delete this Blacklist entry\"") if r == d.DIALOG_OK and t == "I want to delete this Blacklist entry": - sqlExecute("DELETE FROM blacklist WHERE label=? AND address=?", blacklist[blackcur][0], blacklist[blackcur][1]) + sqlExecute( + "DELETE FROM blacklist WHERE label=? AND address=?", + blacklist[blackcur][0], + blacklist[blackcur][1]) del blacklist[blackcur] elif t == "2": - sqlExecute("UPDATE blacklist SET enabled=1 WHERE label=? AND address=?", blacklist[blackcur][0], blacklist[blackcur][1]) + sqlExecute( + "UPDATE blacklist SET enabled=1 WHERE label=? AND address=?", + blacklist[blackcur][0], + blacklist[blackcur][1]) blacklist[blackcur][2] = True - elif t== "3": - sqlExecute("UPDATE blacklist SET enabled=0 WHERE label=? AND address=?", blacklist[blackcur][0], blacklist[blackcur][1]) + elif t == "3": + sqlExecute( + "UPDATE blacklist SET enabled=0 WHERE label=? AND address=?", + blacklist[blackcur][0], + blacklist[blackcur][1]) blacklist[blackcur][2] = False dialogreset(stdscr) else: @@ -666,17 +831,17 @@ def handlech(c, stdscr): if menutab == 7 and blackcur > 0: blackcur -= 1 elif c == curses.KEY_DOWN: - if menutab == 1 and inboxcur < len(inbox)-1: + if menutab == 1 and inboxcur < len(inbox) - 1: inboxcur += 1 - if (menutab == 2 or menutab == 4) and addrcur < len(addresses)-1: + if (menutab == 2 or menutab == 4) and addrcur < len(addresses) - 1: addrcur += 1 - if menutab == 3 and sentcur < len(sentbox)-1: + if menutab == 3 and sentcur < len(sentbox) - 1: sentcur += 1 - if menutab == 5 and subcur < len(subscriptions)-1: + if menutab == 5 and subcur < len(subscriptions) - 1: subcur += 1 - if menutab == 6 and abookcur < len(addrbook)-1: + if menutab == 6 and abookcur < len(addrbook) - 1: abookcur += 1 - if menutab == 7 and blackcur < len(blacklist)-1: + if menutab == 7 and blackcur < len(blacklist) - 1: blackcur += 1 elif c == curses.KEY_HOME: if menutab == 1: @@ -693,38 +858,47 @@ def handlech(c, stdscr): blackcur = 0 elif c == curses.KEY_END: if menutab == 1: - inboxcur = len(inbox)-1 + inboxcur = len(inbox) - 1 if menutab == 2 or menutab == 4: - addrcur = len(addresses)-1 + addrcur = len(addresses) - 1 if menutab == 3: - sentcur = len(sentbox)-1 + sentcur = len(sentbox) - 1 if menutab == 5: - subcur = len(subscriptions)-1 + subcur = len(subscriptions) - 1 if menutab == 6: - abookcur = len(addrbook)-1 + abookcur = len(addrbook) - 1 if menutab == 7: - blackcur = len(blackcur)-1 + blackcur = len(blackcur) - 1 redraw(stdscr) + + +# pylint: disable=too-many-locals, too-many-arguments def sendMessage(sender="", recv="", broadcast=None, subject="", body="", reply=False): + """Method for message sending""" if sender == "": return d = Dialog(dialog="dialog") set_background_title(d, "Send a message") if recv == "": - r, t = d.inputbox("Recipient address (Cancel to load from the Address Book or leave blank to broadcast)", 10, 60) + r, t = d.inputbox( + "Recipient address (Cancel to load from the Address Book or leave blank to broadcast)", + 10, + 60) if r != d.DIALOG_OK: global menutab menutab = 6 return recv = t - if broadcast == None and sender != recv: - r, t = d.radiolist("How to send the message?", - choices=[("1", "Send to one or more specific people", 1), + if broadcast is None and sender != recv: + r, t = d.radiolist( + "How to send the message?", + choices=[ + ("1", "Send to one or more specific people", 1), ("2", "Broadcast to everyone who is subscribed to your address", 0)]) if r != d.DIALOG_OK: return broadcast = False - if t == "2": # Broadcast + if t == "2": # Broadcast broadcast = True if subject == "" or reply: r, t = d.inputbox("Message subject", width=60, init=subject) @@ -740,12 +914,12 @@ def sendMessage(sender="", recv="", broadcast=None, subject="", body="", reply=F if not broadcast: recvlist = [] - for i, item in enumerate(recv.replace(",", ";").split(";")): + for _, item in enumerate(recv.replace(",", ";").split(";")): recvlist.append(item.strip()) - list(set(recvlist)) # Remove exact duplicates + list(set(recvlist)) # Remove exact duplicates for addr in recvlist: if addr != "": - status, version, stream, ripe = decodeAddress(addr) + status, version, stream = decodeAddress(addr)[:3] if status != "success": set_background_title(d, "Recipient address error") err = "Could not decode" + addr + " : " + status + "\n\n" @@ -756,13 +930,17 @@ def sendMessage(sender="", recv="", broadcast=None, subject="", body="", reply=F elif status == "invalidcharacters": err += "The address contains invalid characters." elif status == "versiontoohigh": - err += "The address version is too high. Either you need to upgrade your Bitmessage software or your acquaintance is doing something clever." + err += ("The address version is too high. Either you need to upgrade your Bitmessage software" + " or your acquaintance is doing something clever.") elif status == "ripetooshort": - err += "Some data encoded in the address is too short. There might be something wrong with the software of your acquaintance." + err += ("Some data encoded in the address is too short. There might be something wrong with" + " the software of your acquaintance.") elif status == "ripetoolong": - err += "Some data encoded in the address is too long. There might be something wrong with the software of your acquaintance." + err += ("Some data encoded in the address is too long. There might be something wrong with" + " the software of your acquaintance.") elif status == "varintmalformed": - err += "Some data encoded in the address is malformed. There might be something wrong with the software of your acquaintance." + err += ("Some data encoded in the address is malformed. There might be something wrong with" + " the software of your acquaintance.") else: err += "It is unknown what is wrong with the address." scrollbox(d, unicode(err)) @@ -770,68 +948,44 @@ def sendMessage(sender="", recv="", broadcast=None, subject="", body="", reply=F addr = addBMIfNotPresent(addr) if version > 4 or version <= 1: set_background_title(d, "Recipient address error") - scrollbox(d, unicode("Could not understand version number " + version + "of address" + addr + ".")) + scrollbox(d, unicode( + "Could not understand version number " + + version + + "of address" + + addr + + ".")) continue if stream > 1 or stream == 0: set_background_title(d, "Recipient address error") - scrollbox(d, unicode("Bitmessage currently only supports stream numbers of 1, unlike as requested for address " + addr + ".")) + scrollbox(d, unicode( + "Bitmessage currently only supports stream numbers of 1," + "unlike as requested for address " + addr + ".")) continue - if len(shared.connectedHostsList) == 0: + if not network.stats.connectedHostsList(): set_background_title(d, "Not connected warning") scrollbox(d, unicode("Because you are not currently connected to the network, ")) - stealthLevel = BMConfigParser().safeGetInt('bitmessagesettings', 'ackstealthlevel') - ackdata = genAckPayload(streamNumber, stealthLevel) - sqlExecute( - "INSERT INTO sent VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", - "", - addr, - ripe, - sender, - subject, - body, - ackdata, - int(time.time()), # sentTime (this will never change) - int(time.time()), # lastActionTime - 0, # sleepTill time. This will get set when the POW gets done. - "msgqueued", - 0, # retryNumber - "sent", - 2, # encodingType - BMConfigParser().getint('bitmessagesettings', 'ttl')) + helper_sent.insert( + toAddress=addr, fromAddress=sender, subject=subject, message=body) queues.workerQueue.put(("sendmessage", addr)) - else: # Broadcast + else: # Broadcast if recv == "": set_background_title(d, "Empty sender error") scrollbox(d, unicode("You must specify an address to send the message from.")) else: # dummy ackdata, no need for stealth - ackdata = genAckPayload(streamNumber, 0) - recv = BROADCAST_STR - ripe = "" - sqlExecute( - "INSERT INTO sent VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", - "", - recv, - ripe, - sender, - subject, - body, - ackdata, - int(time.time()), # sentTime (this will never change) - int(time.time()), # lastActionTime - 0, # sleepTill time. This will get set when the POW gets done. - "broadcastqueued", - 0, # retryNumber - "sent", # folder - 2, # encodingType - BMConfigParser().getint('bitmessagesettings', 'ttl')) + helper_sent.insert( + fromAddress=sender, subject=subject, + message=body, status='broadcastqueued') queues.workerQueue.put(('sendbroadcast', '')) + +# pylint: disable=redefined-outer-name, too-many-locals def loadInbox(): + """Load the list of messages""" sys.stdout = sys.__stdout__ print("Loading inbox messages...") sys.stdout = printlog - + where = "toaddress || fromaddress || subject || message" what = "%%" ret = sqlQuery("""SELECT msgid, toaddress, fromaddress, subject, received, read @@ -841,29 +995,29 @@ def loadInbox(): for row in ret: msgid, toaddr, fromaddr, subject, received, read = row subject = ascii(shared.fixPotentiallyInvalidUTF8Data(subject)) - + # Set label for to address try: if toaddr == BROADCAST_STR: tolabel = BROADCAST_STR else: - tolabel = BMConfigParser().get(toaddr, "label") - except: + tolabel = config.get(toaddr, "label") + except: # noqa:E722 tolabel = "" if tolabel == "": tolabel = toaddr tolabel = shared.fixPotentiallyInvalidUTF8Data(tolabel) - + # Set label for from address fromlabel = "" - if BMConfigParser().has_section(fromaddr): - fromlabel = BMConfigParser().get(fromaddr, "label") - if fromlabel == "": # Check Address Book + 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 != []: for r in qr: fromlabel, = r - if fromlabel == "": # Check Subscriptions + if fromlabel == "": # Check Subscriptions qr = sqlQuery("SELECT label FROM subscriptions WHERE address=?", fromaddr) if qr != []: for r in qr: @@ -871,16 +1025,20 @@ def loadInbox(): if fromlabel == "": fromlabel = fromaddr fromlabel = shared.fixPotentiallyInvalidUTF8Data(fromlabel) - + # Load into array - inbox.append([msgid, tolabel, toaddr, fromlabel, fromaddr, subject, - l10n.formatTimestamp(received, False), read]) + inbox.append([ + msgid, tolabel, toaddr, fromlabel, fromaddr, subject, + l10n.formatTimestamp(received), read]) inbox.reverse() + + def loadSent(): + """Load the messages that sent""" sys.stdout = sys.__stdout__ print("Loading sent messages...") sys.stdout = printlog - + where = "toaddress || fromaddress || subject || message" what = "%%" ret = sqlQuery("""SELECT toaddress, fromaddress, subject, status, ackdata, lastactiontime @@ -890,7 +1048,7 @@ def loadSent(): for row in ret: toaddr, fromaddr, subject, status, ackdata, lastactiontime = row subject = ascii(shared.fixPotentiallyInvalidUTF8Data(subject)) - + # Set label for to address tolabel = "" qr = sqlQuery("SELECT label FROM addressbook WHERE address=?", toaddr) @@ -903,18 +1061,18 @@ 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 - + # Set status string if status == "awaitingpubkey": statstr = "Waiting for their public key. Will request it again soon" @@ -923,21 +1081,21 @@ def loadSent(): elif status == "msgqueued": statstr = "Message queued" elif status == "msgsent": - t = l10n.formatTimestamp(lastactiontime, False) - statstr = "Message sent at "+t+".Waiting for acknowledgement." + t = l10n.formatTimestamp(lastactiontime) + statstr = "Message sent at " + t + ".Waiting for acknowledgement." elif status == "msgsentnoackexpected": - t = l10n.formatTimestamp(lastactiontime, False) - statstr = "Message sent at "+t+"." + t = l10n.formatTimestamp(lastactiontime) + statstr = "Message sent at " + t + "." elif status == "doingmsgpow": statstr = "The proof of work required to send the message has been queued." elif status == "ackreceived": - t = l10n.formatTimestamp(lastactiontime, False) - statstr = "Acknowledgment of the message received at "+t+"." + t = l10n.formatTimestamp(lastactiontime) + statstr = "Acknowledgment of the message received at " + t + "." elif status == "broadcastqueued": statstr = "Broadcast queued." elif status == "broadcastsent": - t = l10n.formatTimestamp(lastactiontime, False) - statstr = "Broadcast sent at "+t+"." + t = l10n.formatTimestamp(lastactiontime) + statstr = "Broadcast sent at " + t + "." elif status == "forcepow": statstr = "Forced difficulty override. Message will start sending soon." elif status == "badkey": @@ -945,33 +1103,49 @@ def loadSent(): elif status == "toodifficult": statstr = "Error: The work demanded by the recipient is more difficult than you are willing to do." else: - t = l10n.formatTimestamp(lastactiontime, False) - statstr = "Unknown status "+status+" at "+t+"." - + t = l10n.formatTimestamp(lastactiontime) + statstr = "Unknown status " + status + " at " + t + "." + # Load into array - sentbox.append([tolabel, toaddr, fromlabel, fromaddr, subject, statstr, ackdata, - l10n.formatTimestamp(lastactiontime, False)]) + sentbox.append([ + tolabel, + toaddr, + fromlabel, + fromaddr, + subject, + statstr, + ackdata, + l10n.formatTimestamp(lastactiontime)]) sentbox.reverse() + + def loadAddrBook(): + """Load address book""" sys.stdout = sys.__stdout__ print("Loading address book...") sys.stdout = printlog - + ret = sqlQuery("SELECT label, address FROM addressbook") for row in ret: label, addr = row label = shared.fixPotentiallyInvalidUTF8Data(label) addrbook.append([label, addr]) addrbook.reverse() + + def loadSubscriptions(): + """Load subscription functionality""" ret = sqlQuery("SELECT label, address, enabled FROM subscriptions") for row in ret: label, address, enabled = row subscriptions.append([label, address, enabled]) subscriptions.reverse() + + 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: @@ -981,79 +1155,83 @@ def loadBlackWhiteList(): blacklist.append([label, address, enabled]) blacklist.reverse() + def runwrapper(): + """Main method""" sys.stdout = printlog - #sys.stderr = errlog - - # Load messages from database + # sys.stderr = errlog + loadInbox() loadSent() loadAddrBook() loadSubscriptions() loadBlackWhiteList() - + stdscr = curses.initscr() - + global logpad logpad = curses.newpad(1024, curses.COLS) - + stdscr.nodelay(0) curses.curs_set(0) stdscr.timeout(1000) - + curses.wrapper(run) doShutdown() + def run(stdscr): + """Main loop""" # Schedule inventory lookup data resetlookups() - + # Init color pairs if curses.has_colors(): - curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK) # red - curses.init_pair(2, curses.COLOR_GREEN, curses.COLOR_BLACK) # green - curses.init_pair(3, curses.COLOR_YELLOW, curses.COLOR_BLACK) # yellow - curses.init_pair(4, curses.COLOR_BLUE, curses.COLOR_BLACK) # blue - curses.init_pair(5, curses.COLOR_MAGENTA, curses.COLOR_BLACK) # magenta - curses.init_pair(6, curses.COLOR_CYAN, curses.COLOR_BLACK) # cyan - curses.init_pair(7, curses.COLOR_WHITE, curses.COLOR_BLACK) # white + curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK) # red + curses.init_pair(2, curses.COLOR_GREEN, curses.COLOR_BLACK) # green + curses.init_pair(3, curses.COLOR_YELLOW, curses.COLOR_BLACK) # yellow + curses.init_pair(4, curses.COLOR_BLUE, curses.COLOR_BLACK) # blue + curses.init_pair(5, curses.COLOR_MAGENTA, curses.COLOR_BLACK) # magenta + curses.init_pair(6, curses.COLOR_CYAN, curses.COLOR_BLACK) # cyan + curses.init_pair(7, curses.COLOR_WHITE, curses.COLOR_BLACK) # white if curses.can_change_color(): - curses.init_color(8, 500, 500, 500) # gray + curses.init_color(8, 500, 500, 500) # gray curses.init_pair(8, 8, 0) - curses.init_color(9, 844, 465, 0) # orange + curses.init_color(9, 844, 465, 0) # orange curses.init_pair(9, 9, 0) else: - curses.init_pair(8, curses.COLOR_WHITE, curses.COLOR_BLACK) # grayish - curses.init_pair(9, curses.COLOR_YELLOW, curses.COLOR_BLACK) # orangish - + curses.init_pair(8, curses.COLOR_WHITE, curses.COLOR_BLACK) # grayish + curses.init_pair(9, curses.COLOR_YELLOW, curses.COLOR_BLACK) # orangish + # Init list of address in 'Your Identities' tab - configSections = BMConfigParser().addressses() + 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'): - addresses[len(addresses)-1].append(9) # orange - elif BMConfigParser().safeGetBoolean(addressInKeysFile, 'mailinglist'): - addresses[len(addresses)-1].append(5) # magenta + addresses[len(addresses) - 1].append(8) # gray + elif config.safeGetBoolean(addressInKeysFile, 'chan'): + addresses[len(addresses) - 1].append(9) # orange + elif config.safeGetBoolean(addressInKeysFile, 'mailinglist'): + addresses[len(addresses) - 1].append(5) # magenta else: - addresses[len(addresses)-1].append(0) # black + addresses[len(addresses) - 1].append(0) # black addresses.reverse() - + stdscr.clear() redraw(stdscr) - while quit == False: + while quit_ is False: drawtab(stdscr) handlech(stdscr.getch(), stdscr) + def doShutdown(): + """Shutting the app down""" sys.stdout = sys.__stdout__ print("Shutting down...") sys.stdout = printlog shutdown.doCleanShutdown() sys.stdout = sys.__stdout__ sys.stderr = sys.__stderr__ - - os._exit(0) + os._exit(0) # pylint: disable=protected-access diff --git a/src/bitmessagekivy/__init__.py b/src/bitmessagekivy/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/bitmessagekivy/base_navigation.py b/src/bitmessagekivy/base_navigation.py new file mode 100644 index 0000000000..5f6b1aa527 --- /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/bitmessagekivy/baseclass/__init__.py b/src/bitmessagekivy/baseclass/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/bitmessagekivy/baseclass/addressbook.py b/src/bitmessagekivy/baseclass/addressbook.py new file mode 100644 index 0000000000..b25f061369 --- /dev/null +++ b/src/bitmessagekivy/baseclass/addressbook.py @@ -0,0 +1,168 @@ +# 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') + +INITIAL_SCROLL_POSITION = 1.0 +SCROLL_THRESHOLD = -0.0 +SCROLL_RESET_POSITION = 0.06 +INITIAL_LOAD_COUNT = 20 +ADDRESS_INCREMENT = 5 +POPUP_WIDTH_ANDROID = 0.9 +POPUP_WIDTH_OTHER = 0.8 + + +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().__init__(*args, **kwargs) # pylint: disable=missing-super-argument + self.addbook_popup = None + self.kivy_state = kivy_state_variables() + + def loadAddresslist(self, account, where="", what=""): + """Load address list with optional search filters""" + if self.kivy_state.searching_text: + self.ids.scroll_y.scroll_y = INITIAL_SCROLL_POSITION + where = ['label', 'address'] + what = self.kivy_state.searching_text + + xAddress = '' + self.ids.tag_label.text = '' + self.queryreturn = list(reversed( + kivy_helper_search.search_sql(xAddress, account, "addressbook", where, what, False) + )) + + if self.queryreturn: + self.ids.tag_label.text = 'Address Book' + self.has_refreshed = True + self.set_mdList(0, INITIAL_LOAD_COUNT) + 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): + """Create 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.image_dir, "text_images", + f"{avatar_image_first_letter(item[0].strip())}.png" # noqa: E999 + ) + 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, _): + """Load more data on scroll down""" + if self.ids.scroll_y.scroll_y <= SCROLL_THRESHOLD and self.has_refreshed: + self.ids.scroll_y.scroll_y = SCROLL_RESET_POSITION + exist_addresses = len(self.ids.ml.children) + if exist_addresses != len(self.queryreturn): + self.update_addressBook_on_scroll(exist_addresses) + self.has_refreshed = exist_addresses != len(self.queryreturn) + + def update_addressBook_on_scroll(self, exist_addresses): + """Load more data on scroll""" + self.set_mdList(exist_addresses, exist_addresses + ADDRESS_INCREMENT) + + @staticmethod + def refreshs(*args): + """Refresh the Widget""" + + def addBook_detail(self, address, label, instance, *args): + """Display 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 = POPUP_WIDTH_ANDROID if platform == 'android' else POPUP_WIDTH_OTHER + 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 address from the address book""" + self.ids.ml.remove_widget(instance.parent.parent) + if self.ids.ml.children: + self.ids.tag_label.text = '' + sqlExecute("DELETE FROM addressbook WHERE address = ?", address) + toast('Address Deleted') + + def close_pop(self, instance): + """Cancel and close the popup""" + self.addbook_popup.dismiss() + toast('Canceled') + + def update_addbook_label(self, instance): + """Update the label of the address book""" + 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 = App.get_running_app() + app.root.ids.id_addressbook.ids.ml.clear_widgets() + app.root.ids.id_addressbook.loadAddresslist(None, 'All', '') + self.addbook_popup.dismiss() + toast('Saved') + + def send_message_to(self, instance): + """Fill the to_address of the 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 0000000000..7c2872ffa6 --- /dev/null +++ b/src/bitmessagekivy/baseclass/addressbook_widgets.py @@ -0,0 +1,64 @@ +# pylint: disable=no-member, too-many-arguments, too-few-public-methods, no-init + +"""Addressbook widgets are here.""" + +from kivy.app import App +from kivymd.uix.button import MDRaisedButton +from kivymd.uix.dialog import MDDialog + +POPUP_HEIGHT_PROPORTION = 0.25 + + +class HelperAddressBook(object): + """Widget utilities for Addressbook.""" + + @staticmethod + def address_detail_popup(obj, send_message, update_address, close_popup, width): + """ + Shows address details in a popup with clear actions. + + Args: + obj: The widget containing the address details to display. + send_message: The function to call when the "Send message" button is pressed. + update_address: The function to call when the "Save" button is pressed. + close_popup: The function to call when the "Cancel" button is pressed or the popup is closed. + width: The desired width of the popup as a proportion of the screen. + """ + + buttons = [ + MDRaisedButton(text="Send message", on_release=send_message), + MDRaisedButton(text="Update Address", on_release=update_address), + MDRaisedButton(text="Cancel", on_release=close_popup), + ] + + return MDDialog( + type="custom", + size_hint=(width, POPUP_HEIGHT_PROPORTION), + content_cls=obj, + buttons=buttons, + ) + + @staticmethod + def compose_message(from_addr=None, to_addr=None): + """ + Composes a new message (UI-independent). + + Args: + from_addr (str, optional): The address to set in the "From" field. Defaults to None. + to_addr (str, optional): The address to set in the "To" field. Defaults to None. + """ + + app = App.get_running_app() + + ids = app.root.ids + create_screen = ids.id_create.children[1].ids + + # Reset fields + create_screen.txt_input.text = to_addr if to_addr else from_addr + create_screen.ti.text = "" + create_screen.composer_dropdown.text = "Select" + create_screen.subject.text = "" + create_screen.body.text = "" + + # Navigate to create screen + ids.scr_mngr.current = "create" diff --git a/src/bitmessagekivy/baseclass/allmail.py b/src/bitmessagekivy/baseclass/allmail.py new file mode 100644 index 0000000000..12104c57cc --- /dev/null +++ b/src/bitmessagekivy/baseclass/allmail.py @@ -0,0 +1,70 @@ +# 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 +""" + +import logging + +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, +) + +logger = logging.getLogger('default') + + +class AllMails(Screen): + """AllMails Screen for Kivy UI""" + data = ListProperty() + has_refreshed = True + all_mails = ListProperty() + account = StringProperty() + label_str = 'No messages for this account.' + + def __init__(self, *args, **kwargs): + """Initialize the AllMails screen.""" + super().__init__(*args, **kwargs) # pylint: disable=missing-super-argument + self.kivy_state = kivy_state_variables() + self._initialize_selected_address() + Clock.schedule_once(self.init_ui, 0) + + def _initialize_selected_address(self): + """Initialize the selected address from the identity list.""" + if not self.kivy_state.selected_address and App.get_running_app().identity_list: + self.kivy_state.selected_address = App.get_running_app().identity_list[0] + + def init_ui(self, dt=0): + """Initialize the UI by loading the message list.""" + self.load_message_list() + logger.debug("UI initialized after %s seconds.", dt) + + def load_message_list(self): + """Load the Inbox, Sent, and Draft message lists.""" + self.account = self.kivy_state.selected_address + self.ids.tag_label.text = 'All Mails' if self.all_mails else '' + self._update_mail_count() + + def _update_mail_count(self): + """Update the mail count and handle empty states.""" + if self.all_mails: + total_count = int(self.kivy_state.sent_count) + int(self.kivy_state.inbox_count) + self.kivy_state.all_count = str(total_count) + self.set_all_mail_count(self.kivy_state.all_count) + else: + self.set_all_mail_count('0') + self.ids.ml.add_widget(empty_screen_label(self.label_str)) + + @staticmethod + def set_all_mail_count(count): + """Set the message count for all mails.""" + allmail_count_widget = App.get_running_app().root.ids.content_drawer.ids.allmail_cnt + allmail_count_widget.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 0000000000..c5f94b8ad1 --- /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 0000000000..cd0d2d7a9f --- /dev/null +++ b/src/bitmessagekivy/baseclass/common.py @@ -0,0 +1,256 @@ +# pylint: disable=no-name-in-module, attribute-defined-outside-init, import-error, unused-argument +# pylint: disable=no-init, too-few-public-methods, useless-object-inheritance + +""" + All Common widgets of kivy are managed here. +""" + +import os +from datetime import datetime + +from kivy.app import App +from kivy.core.window import Window +from kivy.metrics import dp +from kivy.properties import ListProperty, NumericProperty, StringProperty +from kivy.uix.image import Image +from kivymd.toast import kivytoast +from kivymd.uix.button import MDFlatButton +from kivymd.uix.card import MDCardSwipe +from kivymd.uix.chip import MDChip +from kivymd.uix.dialog import MDDialog +from kivymd.uix.label import MDLabel +from kivymd.uix.list import ILeftBody, IRightBodyTouch + +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", + }, +} + + +class ChipProperties(object): + """ChipProperties class for kivy UI""" + CENTER_X_ANDROID = 0.91 + CENTER_X_OTHER = 0.94 + CENTER_Y = 0.3 + HEIGHT_DP = 18 + RADIUS = [8] + TEXT_COLOR = (1, 1, 1, 1) + SIZE_HINT_ANDROID = (0.16, None) + SIZE_HINT_OTHER = (0.08, None) + + +class BadgeProperties(object): + """BadgeProperties class for kivy UI""" + SIZE_ANDROID = [120, 140] + SIZE_OTHER = [64, 80] + FONT_SIZE = "11sp" + + +DIALOG_WIDTH_ANDROID = 0.8 +DIALOG_WIDTH_OTHER = 0.55 +DIALOG_HEIGHT = 0.25 +LONG_PRESS_DURATION = 1 +DELETE_DELAY = 4 + + +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 0000000000..78ae88d570 --- /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 0000000000..50f883d64c --- /dev/null +++ b/src/bitmessagekivy/baseclass/draft.py @@ -0,0 +1,54 @@ +# pylint: disable=unused-argument, import-error, too-many-arguments +# pylint: disable=unnecessary-comprehension, no-member, no-name-in-module + +""" +draft.py +============== + +Draft screen for managing draft messages in Kivy UI. +""" +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): + """Initialize the Draft screen and set the default account""" + super().__init__(*args, **kwargs) + self.kivy_state = kivy_state_variables() + if not 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): + """Initialize the UI and load draft messages""" + self.load_draft() + logger.debug(f"UI initialized with dt: {dt}") # noqa: E999 + + def load_draft(self, where="", what=""): + """Load the list of 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 messages in the UI""" + draft_count_obj = App.get_running_app().root.ids.content_drawer.ids.draft_cnt + draft_count_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 0000000000..17ea9a9bde --- /dev/null +++ b/src/bitmessagekivy/baseclass/inbox.py @@ -0,0 +1,59 @@ +# 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 and set up the UI""" + super().__init__(*args, **kwargs) # pylint: disable=missing-super-argument + 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_default_address(self): + """Set the default address if none is selected""" + if not self.kivy_state.selected_address and self.kivy_running_app.identity_list: + self.kivy_state.selected_address = self.kivy_running_app.identity_list[0] + + def init_ui(self, dt=0): + """Initialize UI and load message list""" + self.loadMessagelist() + + def loadMessagelist(self, where="", what=""): + """Load inbox messages""" + self.set_default_address() + self.account = self.kivy_state.selected_address + + def refresh_callback(self, *args): + """Refresh the inbox messages while showing a loading spinner""" + + def refresh_on_scroll_down(interval): + """Reset search fields and reload data on scroll""" + 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 0000000000..addb1aaf19 --- /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().load_my_address_screen(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().load_my_address_screen(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 0000000000..de58e8c10a --- /dev/null +++ b/src/bitmessagekivy/baseclass/maildetail.py @@ -0,0 +1,256 @@ +# 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 +# pylint: disable=broad-except + +""" +MailDetail screen for inbox, sent, draft and trash. +""" + +import os +from datetime import datetime + +from kivy.app import App +from kivy.clock import Clock +from kivy.core.clipboard import Clipboard +from kivy.factory import Factory +from kivy.properties import NumericProperty, StringProperty +from kivy.uix.screenmanager import Screen +from kivymd.uix.button import MDFlatButton, MDIconButton +from kivymd.uix.dialog import MDDialog +from kivymd.uix.list import IRightBodyTouch, OneLineListItem + +from pybitmessage.bitmessagekivy.baseclass.common import ( + avatar_image_first_letter, kivy_state_variables, show_time_history, toast, + DIALOG_WIDTH_ANDROID, DIALOG_WIDTH_OTHER, DIALOG_HEIGHT, LONG_PRESS_DURATION, + DELETE_DELAY, ChipProperties, BadgeProperties) +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(LONG_PRESS_DURATION) + + def on_state(self, instance, value): + """Handle state change for long press.""" + if value == 'down': + self._clock_event = Clock.schedule_once(self._do_long_press, self.long_press_time) + else: + self._clock_event.cancel() + + def _do_long_press(self, dt): + """Trigger the long press event.""" + self.dispatch('on_long_press') + + def on_long_press(self, *args): + """Handle long press and display message title.""" + self.copy_message_title(self.text) + + def copy_message_title(self, title_text): + """Display dialog box with options to copy the message title.""" + self.title_text = title_text + width = DIALOG_WIDTH_ANDROID if platform == 'android' else DIALOG_WIDTH_OTHER + self.dialog_box = MDDialog( + text=title_text, + size_hint=(width, DIALOG_HEIGHT), + buttons=[ + MDFlatButton(text="Copy", on_release=self.copy_title_callback), + MDFlatButton(text="Cancel", on_release=self.copy_title_callback), + ], + ) + self.dialog_box.open() + + def copy_title_callback(self, instance): + """Handle dialog box button callback.""" + 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() + avatar_image = StringProperty() + no_subject = '(no subject)' + chipProperties = ChipProperties() + badgeProperties = BadgeProperties() + + def __init__(self, *args, **kwargs): + """Initialize MailDetail screen.""" + super().__init__(*args, **kwargs) # pylint: disable=missing-super-argument + self.kivy_state = kivy_state_variables() + Clock.schedule_once(self.init_ui, 0) + + def init_ui(self, dt=0): + """Initialize UI elements based on page type.""" + self.page_type = self.kivy_state.detail_page_type or '' + try: + if self.page_type in ('sent', 'draft'): + App.get_running_app().set_mail_detail_header() + elif self.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: + print("Error during MailDetail initalization") + + def assign_mail_details(self, data): + """Assign mail details from query result.""" + subject = data[0][2].decode() if isinstance(data[0][2], bytes) else data[0][2] + body = data[0][3].decode() if isinstance(data[0][3], 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() or 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.page_type == 'inbox' + else show_time_history(data[0][6]) + ) + self.avatar_image = ( + os.path.join(self.kivy_state.image_dir, 'draft-icon.png') + if self.page_type == 'draft' + else os.path.join( + self.kivy_state.image_dir, 'text_images', + f'{avatar_image_first_letter(self.subject.strip())}.png' # noqa: E999 + ) + ) + self.timeinseconds = data[0][4] if self.page_type == 'inbox' else data[0][6] + + def delete_mail(self): + """Delete the current mail and update UI.""" + 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.page_type == 'sent': + self._update_sent_mail(msg_count_objs) + elif self.page_type == 'inbox': + self._update_inbox_mail(msg_count_objs) + elif self.page_type == 'draft': + self._update_draft_mail(msg_count_objs) + + if self.page_type != 'draft': + self._update_mail_counts(msg_count_objs) + + Clock.schedule_once(self.callback_for_delete, DELETE_DELAY) + + def _update_sent_mail(self, msg_count_objs): + """Update UI for sent mail.""" + 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) + + def _update_inbox_mail(self, msg_count_objs): + """Update UI for inbox mail.""" + 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) + + def _update_draft_mail(self, msg_count_objs): + """Update UI for draft mail.""" + 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()) + + def _update_mail_counts(self, msg_count_objs): + """Update mail counts and refresh relevant screens.""" + 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()) + + def callback_for_delete(self, dt=0): + """Handle post-deletion operations.""" + if self.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.page_type + self.kivy_state.detail_page_type = '' + toast('Deleted') + + def get_message_details_to_reply(self, data): + """Prepare message details for 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) + subject_text = split_subject[1] if len(split_subject) > 1 else split_subject[0] + composer_obj.subject.text = 'Re: ' + subject_text + 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): + """Prepare for replying to an inbox message.""" + 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): + """Prepare message details for a 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, nav_app): + """Switch to draft mail composition.""" + self.kivy_state.send_draft_mail = self.kivy_state.mail_id + self.parent.current = 'create' + nav_app.set_navbar_for_composer() + + def detailed_popup(self): + """Show detailed sender information popup.""" + obj = SenderDetailPopup() + obj.open() + obj.assignDetail(self.to_addr, self.from_addr, self.timeinseconds) + + @staticmethod + def callback_for_menu_items(text_item, *args): + """Handle menu item callback.""" + toast(text_item) diff --git a/src/bitmessagekivy/baseclass/msg_composer.py b/src/bitmessagekivy/baseclass/msg_composer.py new file mode 100644 index 0000000000..a36996e0e4 --- /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 0000000000..0a46bae987 --- /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 0000000000..b5248690ec --- /dev/null +++ b/src/bitmessagekivy/baseclass/myaddress_widgets.py @@ -0,0 +1,68 @@ +# pylint: disable=too-many-arguments, no-name-in-module, import-error, no-init +# pylint: disable=too-few-public-methods, no-member, too-many-ancestors, useless-object-inheritance + +""" +Widgets for the MyAddress module. +""" + +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): + """Class representing a badge text in the UI.""" + + +class HelperMyAddress(object): + """Helper class to manage MyAddress widgets and dialogs.""" + + dialog_height = 0.25 # Consistent decimal notation + + @staticmethod + def is_active_badge(): + """Return a label showing 'Active' status for the address.""" + active_status = 'Active' + badge_width = 90 if platform == 'android' else 50 + badge_height = 60 + + return BadgeText( + size_hint=(None, None), + size=[badge_width, badge_height], + text=active_status, + halign='center', + font_style='Body1', + theme_text_color='Custom', + text_color=ThemeClsColor, + font_size='13sp' + ) + + @staticmethod + def myaddress_detail_popup(obj, width): + """Show address details in a popup dialog.""" + return MDDialog( + type="custom", + size_hint=(width, HelperMyAddress.dialog_height), + content_cls=obj, + ) + + @staticmethod + def inactive_address_popup(width, callback_for_menu_items): + """Show a warning dialog when the address is inactive.""" + dialog_text = ( + 'Address is not currently active. Please click the Toggle button to activate it.' + ) + + return MDDialog( + text=dialog_text, + size_hint=(width, HelperMyAddress.dialog_height), + buttons=[ + MDFlatButton( + text="Ok", on_release=lambda x: callback_for_menu_items("Ok") + ), + ], + ) diff --git a/src/bitmessagekivy/baseclass/network.py b/src/bitmessagekivy/baseclass/network.py new file mode 100644 index 0000000000..5001cf784f --- /dev/null +++ b/src/bitmessagekivy/baseclass/network.py @@ -0,0 +1,43 @@ +# 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 + object_tracker = kivy_main.network.objectracker +else: + from pybitmessage.network import stats, objectracker as object_tracker + + +class NetworkStat(Screen): + """NetworkStat class for Kivy UI""" + + text_variable_1 = StringProperty(f'Total Connections::0') # noqa: E999 + text_variable_2 = StringProperty(f'Processed 0 peer-to-peer messages') + text_variable_3 = StringProperty(f'Processed 0 broadcast messages') + text_variable_4 = StringProperty(f'Processed 0 public keys') + text_variable_5 = StringProperty(f'Processed 0 objects to be synced') + + def __init__(self, **kwargs): + super().__init__(**kwargs) # pylint: disable=missing-super-argument + Clock.schedule_interval(self.update_stats, 1) + + def update_stats(self, dt): + """Update network statistics""" + self.text_variable_1 = f'Total Connections::{len(stats.connectedHostsList())}' + self.text_variable_2 = f'Processed {state.numberOfMessagesProcessed} peer-to-peer messages' + self.text_variable_3 = f'Processed {state.numberOfBroadcastsProcessed} broadcast messages' + self.text_variable_4 = f'Processed {state.numberOfPubkeysProcessed} public keys' + self.text_variable_5 = f'Processed {object_tracker.missingObjects} objects' diff --git a/src/bitmessagekivy/baseclass/payment.py b/src/bitmessagekivy/baseclass/payment.py new file mode 100644 index 0000000000..9c33bdff56 --- /dev/null +++ b/src/bitmessagekivy/baseclass/payment.py @@ -0,0 +1,61 @@ +# 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().__init__(*args, **kwargs) # pylint: disable=missing-super-argument + 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""" + 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 = f'{self.kivy_state.available_credit}' # noqa: E999 + + +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 0000000000..2e74741dd7 --- /dev/null +++ b/src/bitmessagekivy/baseclass/popup.py @@ -0,0 +1,241 @@ +# pylint: disable=import-error, attribute-defined-outside-init +# pylint: disable=no-member, no-name-in-module, unused-argument, too-few-public-methods + +""" +All the popups 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') + + +def format_time(timeinseconds): + """Format the timestamp into a readable string""" + time_obj = datetime.fromtimestamp(int(timeinseconds)) + return time_obj.strftime("%d %b %Y, %I:%M %p") + + +class AddressChangingLoader(Popup): + """Run a Screen Loader when changing the Identity for Kivy UI""" + + def __init__(self, **kwargs): + super().__init__(**kwargs) # pylint: disable=missing-super-argument + Clock.schedule_once(self.dismiss_popup, 0.5) + + def dismiss_popup(self, dt): + """Dismiss popup""" + self.dismiss() + + +class AddAddressPopup(BoxLayout): + """Popup for adding new address to address book""" + + 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().__init__(**kwargs) # pylint: disable=missing-super-argument + + def checkAddress_valid(self, instance): + """Check if the 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 address book.' + elif entered_text in my_addresses: + text = 'You cannot 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): + """Check if the 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): + """Popup for saved address details for Kivy UI""" + + address_label = StringProperty() + address = StringProperty() + + def __init__(self, **kwargs): + """Set screen of address detail page""" + super().__init__(**kwargs) # pylint: disable=missing-super-argument + + def checkLabel_valid(self, instance): + """Check if the address label is unique or 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): + """Popup for my address details for Kivy UI""" + + address_label = StringProperty() + address = StringProperty() + + def __init__(self, **kwargs): + """Set screen of my address details""" + super().__init__(**kwargs) # pylint: disable=missing-super-argument + + def send_message_from(self): + """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): + """Cancel the popup""" + self.parent.parent.parent.dismiss() + toast('Cancelled') + + +class AppClosingPopup(Popup): + """Popup for closing the application for Kivy UI""" + + def __init__(self, **kwargs): + super().__init__(**kwargs) # pylint: disable=missing-super-argument + + 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): + """Popup for sender details for Kivy UI""" + + to_addr = StringProperty() + from_addr = StringProperty() + time_tag = StringProperty() + + def __init__(self, **kwargs): + """Initialize the send message detail popup""" + super().__init__(**kwargs) # pylint: disable=missing-super-argument + + def assignDetail(self, to_addr, from_addr, timeinseconds): + """Assign details to the popup""" + self.to_addr = to_addr + self.from_addr = from_addr + self.time_tag = format_time(timeinseconds) + self.adjust_popup_height(to_addr) + + def adjust_popup_height(self, to_addr): + """Adjust the height of the popup based on the address length""" + 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): + """BoxLayout for displaying the to address""" + + to_addr = StringProperty() + + def set_toAddress(self, to_addr): + """Set the to address""" + self.to_addr = to_addr + + +class ToAddressTitle(BoxLayout): + """BoxLayout for displaying the to address title""" diff --git a/src/bitmessagekivy/baseclass/qrcode.py b/src/bitmessagekivy/baseclass/qrcode.py new file mode 100644 index 0000000000..8cdb68656a --- /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_qr_code() + 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 0000000000..c911afa6f8 --- /dev/null +++ b/src/bitmessagekivy/baseclass/scan_screen.py @@ -0,0 +1,119 @@ +# 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() + self.zbarcam = None + + 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) + self.zbarcam = self.children[0].ids.zbarcam + 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.zbarcam.start() + Clock.schedule_interval(self.check_symbol, 0.5) + + def check_symbol(self, *args): + """Check if the symbol is detected""" + if not self.zbarcam.symbols: + return True + + for symbol in self.zbarcam.symbols: + # ZBarSymbol.QRCODE is an integer, QRCODE corresponds to 64 + if symbol.type == 'QRCODE': + self.stop_camera() + self.pop_up_instance.content_cls.address.text = symbol.data.decode("utf-8") + self.manager.current = self.previous_open_screen + # changing screen closes popup, so need to open again with filled address + self.pop_up_instance.open() + return False + + return True + + def stop_camera(self, *args): + """Its used to stop the camera""" + self.zbarcam.stop() + + 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 0000000000..59db0ab9df --- /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 0000000000..1ceb35ee68 --- /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 0000000000..eb62fdaaf5 --- /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 0000000000..654b31f4a0 --- /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 0000000000..2e2f2e9346 --- /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 0000000000..c48ca3adfb --- /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 0000000000..42051ff94e --- /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 0000000000..73b4c1ef98 --- /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 0000000000..3df69e05e7 --- /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 0000000000..e21ed5031b --- /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 0000000000..e59c32d754 --- /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 0000000000..40843c472b --- /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 0000000000..275bd12c41 --- /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 0000000000..1680d6f0d7 --- /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 0000000000..56682d2b9b --- /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 0000000000..b9cc8566b1 --- /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 0000000000..d3e2f7f9f6 --- /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 0000000000..50bb886dd6 --- /dev/null +++ b/src/bitmessagekivy/kv/maildetail.kv @@ -0,0 +1,74 @@ +#:set color_transparent (0,0,0,0) # transparent black +#:set color_black (0,0,0,1) # black + +: + 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) + OneLineListTitle: + id: subj + text: app.tr._(root.subject) + divider: None + font_style: 'H5' + theme_text_color: 'Primary' + _no_ripple_effect: True + long_press_time: self.long_press_time + TwoLineAvatarIconListItem: + id: subaft + text: app.tr._(root.from_addr) + secondary_text: app.tr._('to ' + root.to_addr) + divider: None + on_press: root.detailed_popup() + BadgeText: + size_hint:(None, None) + size: root.badgeProperties.SIZE_ANDROID if app.app_platform == 'android' else root.badgeProperties.SIZE_OTHER + text: app.tr._(root.time_tag) + halign:'center' + font_style:'Caption' + pos_hint: {'center_y': .8} + _txt_right_pad: dp(70) + font_size: root.badgeProperties.FONT_SIZE + MDChip: + size_hint: root.chipProperties.SIZE_HINT_ANDROID if app.app_platform == 'android' else root.chipProperties.SIZE_HINT_OTHER + text: app.tr._(root.page_type) + icon: '' + text_color: root.chipProperties.TEXT_COLOR + pos_hint: {'center_x': root.chipProperties.CENTER_X_ANDROID if app.app_platform == 'android' else root.chipProperties.CENTER_X_OTHER, 'center_y': root.chipProperties.CENTER_Y} + radius: root.chipProperties.RADIUS + height: self.parent.height/4 + AvatarSampleWidget: + source: root.avatar_image + MDLabel: + text: root.status + disabled: True + font_style: 'Body2' + halign:'left' + padding_x: 20 + MyMDTextField: + id: bod + size_hint_y: None + font_style: 'Subtitle2' + text: root.message + multiline: True + readonly: True + line_color_normal: color_transparent + _current_line_color: color_transparent + line_color_focus: color_transparent + markup: True + font_size: '15sp' + canvas.before: + Color: + rgba: color_black + Loader: + + +: + canvas.before: + Color: + rgba: color_black diff --git a/src/bitmessagekivy/kv/msg_composer.kv b/src/bitmessagekivy/kv/msg_composer.kv new file mode 100644 index 0000000000..13db4f4e44 --- /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 0000000000..71dde54a6a --- /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 0000000000..17211e9817 --- /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 0000000000..6d475f5613 --- /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 0000000000..b1fb2b3408 --- /dev/null +++ b/src/bitmessagekivy/kv/popup.kv @@ -0,0 +1,335 @@ +: + 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 + address: address + label: label + 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 0000000000..cadaa9967a --- /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 0000000000..dbcff5a148 --- /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 0000000000..9505c3ab8a --- /dev/null +++ b/src/bitmessagekivy/kv/scanner.kv @@ -0,0 +1,9 @@ +#:import ZBarSymbol pyzbar.pyzbar.ZBarSymbol +#:import ZBarCam kivy_garden.zbarcam.ZBarCam + +BoxLayout: + orientation: 'vertical' + ZBarCam: + id: zbarcam + # optional, by default checks all types + code_types: ZBarSymbol.QRCODE, ZBarSymbol.EAN13 diff --git a/src/bitmessagekivy/kv/sent.kv b/src/bitmessagekivy/kv/sent.kv new file mode 100644 index 0000000000..11477ed610 --- /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 0000000000..f579606021 --- /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 0000000000..97bcf7d779 --- /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 0000000000..b413292721 --- /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 0000000000..5e38f2f003 --- /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.get_default_account_data(self) + color: color_font + background_normal: '' + background_color: app.theme_cls.primary_color + on_text: app.get_current_account_data(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.set_screen('draft') + on_release: root.parent.set_state() + NavigationItem: + id: trash_cnt + text: app.tr._('Trash') + icon: 'delete' + divider: None + on_release: app.set_screen('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.set_screen('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.set_screen('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.set_screen('addressbook') + on_release: root.parent.set_state() + NavigationItem: + text: app.tr._('Settings') + icon: 'application-settings' + divider: None + on_release: app.set_screen('set') + on_release: root.parent.set_state() + NavigationItem: + text: app.tr._('Payment plan') + icon: 'shopping' + divider: None + on_release: app.set_screen('payment') + on_release: root.parent.set_state() + NavigationItem: + text: app.tr._('New address') + icon: 'account-plus' + divider: None + on_release: app.set_screen('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.set_screen('networkstat') + on_release: root.parent.set_state() + NavigationItem: + text: app.tr._('My addresses') + icon: 'account-multiple' + divider: None + on_release: app.set_screen('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 new file mode 100644 index 0000000000..b1822cd213 --- /dev/null +++ b/src/bitmessagekivy/mpybit.py @@ -0,0 +1,508 @@ +# pylint: disable=too-many-public-methods, unused-variable, too-many-ancestors +# pylint: disable=too-few-public-methods, unused-argument +# pylint: disable=attribute-defined-outside-init, too-many-instance-attributes +# pylint: disable=broad-exception-caught, no-self-use + +""" +Bitmessage android(mobile) interface +""" + +import logging +import os +import sys +from functools import partial + +from kivy.clock import Clock +from kivy.core.clipboard import Clipboard +from kivy.core.window import Window +from kivy.lang import Builder +from kivy.uix.boxlayout import BoxLayout +from kivymd.app import MDApp +from kivymd.uix.bottomsheet import MDCustomBottomSheet +from kivymd.uix.button import MDRaisedButton +from kivymd.uix.dialog import MDDialog +from kivymd.uix.filemanager import MDFileManager +from kivymd.uix.label import MDLabel +from kivymd.uix.list import IRightBodyTouch +from PIL import Image as PilImage + +from pybitmessage.bitmessagekivy import identiconGeneration +from pybitmessage.bitmessagekivy.base_navigation import ( + BaseContentNavigationDrawer, BaseIdentitySpinner, BaseLanguage, + BaseNavigationDrawerDivider, BaseNavigationDrawerSubheader, + BaseNavigationItem) +from pybitmessage.bitmessagekivy.baseclass.common import (get_identity_list, + load_image_path, + toast) +from pybitmessage.bitmessagekivy.baseclass.popup import (AddAddressPopup, + AddressChangingLoader, + AppClosingPopup) +from pybitmessage.bitmessagekivy.get_platform import platform +from pybitmessage.bitmessagekivy.kivy_state import KivyStateVariables +from pybitmessage.bitmessagekivy.load_kivy_screens_data import load_screen_json +from pybitmessage.bitmessagekivy.uikivysignaler import UIkivySignaler +from pybitmessage.bmconfigparser import config # noqa: F401 +from pybitmessage.mockbm.helper_startup import ( + loadConfig, total_encrypted_messages_per_month) + +logger = logging.getLogger('default') + +# Define constants for magic numbers +DIALOG_WIDTH_ANDROID = 0.85 +DIALOG_WIDTH_OTHER = 0.8 +DIALOG_HEIGHT = 0.23 +LOADER_DELAY = 1 +IMAGE_SIZE = (300, 300) +MAX_LABEL_LENGTH = 15 +TRUNCATE_STRING = '...' + + +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""" + 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 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): + """Dialog for saving address""" + width = DIALOG_WIDTH_ANDROID if platform == 'android' else DIALOG_WIDTH_OTHER + self.add_popup = MDDialog( + title='Add contact', + type="custom", + size_hint=(width, DIALOG_HEIGHT), + 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 available!' + 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 load_my_address_screen(self, action): + """load_my_address_screen 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), LOADER_DELAY) + + 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() + formatted_label = f_name[0][:MAX_LABEL_LENGTH - 1].capitalize() + TRUNCATE_STRING if len( + f_name[0]) > MAX_LABEL_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 get_default_account_data(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 get_current_account_data(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.file_manager_setting() + Clock.schedule_once(self.set_current_account_data, 0.5) + + def set_current_account_data(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 file_manager_setting(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): + """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 (Permission, check_permission, + request_permissions) + 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: + new_image = PilImage.open(path).resize(IMAGE_SIZE) + 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')) + new_image.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')) + new_image.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, curerent_addr): + """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(curerent_addr) + ) + 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 None + 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_qr_code(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) + + def copy_composer_text(self, text): + """Copy text to clipboard""" + Clipboard.copy(text) + + +class PaymentMethodLayout(BoxLayout): + """PaymentMethodLayout class for kivy Ui""" + + +if __name__ == '__main__': + NavigateApp().run() diff --git a/src/bitmessagekivy/screens_data.json b/src/bitmessagekivy/screens_data.json new file mode 100644 index 0000000000..974ef1c4df --- /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/__init__.py b/src/bitmessagekivy/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/bitmessagekivy/tests/common.py b/src/bitmessagekivy/tests/common.py new file mode 100644 index 0000000000..553fa02072 --- /dev/null +++ b/src/bitmessagekivy/tests/common.py @@ -0,0 +1,37 @@ +""" +This module is used for running test cases ui order. +""" +import unittest + +from pybitmessage import state + + +def make_ordered_test(): + """this method is for comparing and arranging in order""" + order = {} + + def ordered_method(f): + """method for ordering""" + order[f.__name__] = len(order) + return f + + def compare_method(a, b): + """method for comparing order of methods""" + return [1, -1][order[a] < order[b]] + + return ordered_method, compare_method + + +ordered, compare = make_ordered_test() +unittest.defaultTestLoader.sortTestMethodsUsing = compare + + +def skip_screen_checks(x): + """This methos is skipping current screen checks""" + def inner(y): + """Inner function""" + if not state.enableKivy: + return unittest.skip('Kivy not enabled') + else: + x(y) + return inner diff --git a/src/bitmessagekivy/tests/sampleData/keys.dat b/src/bitmessagekivy/tests/sampleData/keys.dat new file mode 100644 index 0000000000..940b3e147c --- /dev/null +++ b/src/bitmessagekivy/tests/sampleData/keys.dat @@ -0,0 +1,59 @@ +[bitmessagesettings] +settingsversion = 0 +port = 8444 +timeformat = %%c +blackwhitelist = black +startonlogon = false +minimizetotray = false +showtraynotifications = true +startintray = false +socksproxytype = none +sockshostname = localhost +socksport = 9050 +socksauthentication = false +socksusername = +sockspassword = +keysencrypted = false +messagesencrypted = false +defaultnoncetrialsperbyte = 1000 +defaultpayloadlengthextrabytes = 1000 +minimizeonclose = false +replybelow = False +maxdownloadrate = 0 +maxuploadrate = 0 +stopresendingafterxdays = +stopresendingafterxmonths = +sockslisten = false +userlocale = system +sendoutgoingconnections = True +useidenticons = True +identiconsuffix = qcqQGW6sQtZK +maxacceptablenoncetrialsperbyte = 20000000000 +maxacceptablepayloadlengthextrabytes = 20000000000 +onionhostname = +onionport = 8444 +onionbindip = 127.0.0.1 +smtpdeliver = +hidetrayconnectionnotifications = false +ttl = 367200 + +[BM-2cTpgCn57rYUgqm5BrgmykuV9gK1Ak1THF] +label = test1 +enabled = true +decoy = false +noncetrialsperbyte = 1000 +payloadlengthextrabytes = 1000 +privsigningkey = 5KYCPJ4Vp31UD6k5NWmDKtHhfapW25UJ7V2MjctYxcgL3BpWGA3 +privencryptionkey = 5JLER8q2zyj3KDEgGMv682en2SRUkkWWhUrNuqVYfGNNhHJmdkJ +lastpubkeysendtime = 1623160189 + +[BM-2cVrdzQjCQRqUuET6dc3byVyRTjZcgcJXj] +label = test2 +enabled = true +decoy = false +noncetrialsperbyte = 1000 +payloadlengthextrabytes = 1000 +privsigningkey = 5KhryWvNowFWWA9JRjQnLVStYKwhpKpAG4RtWwzyaQqmK2fTMue +privencryptionkey = 5JKQ9NqX2LRzHBCgyxc1GAL3rDvyDTHPifpL22a6UNN7K6y9BmL +lastpubkeysendtime = 1623160221 + diff --git a/src/bitmessagekivy/tests/sampleData/knownnodes.dat b/src/bitmessagekivy/tests/sampleData/knownnodes.dat new file mode 100644 index 0000000000..41ba8a282d --- /dev/null +++ b/src/bitmessagekivy/tests/sampleData/knownnodes.dat @@ -0,0 +1,110 @@ +[ + { + "stream": 1, + "peer": { + "host": "5.45.99.75", + "port": 8444 + }, + "info": { + "lastseen": 1620741290.255359, + "rating": 0, + "self": false + } + }, + { + "stream": 1, + "peer": { + "host": "75.167.159.54", + "port": 8444 + }, + "info": { + "lastseen": 1620741290.255359, + "rating": 0, + "self": false + } + }, + { + "stream": 1, + "peer": { + "host": "95.165.168.168", + "port": 8444 + }, + "info": { + "lastseen": 1620741290.255359, + "rating": 0, + "self": false + } + }, + { + "stream": 1, + "peer": { + "host": "85.180.139.241", + "port": 8444 + }, + "info": { + "lastseen": 1620741290.255359, + "rating": 0, + "self": false + } + }, + { + "stream": 1, + "peer": { + "host": "158.222.217.190", + "port": 8080 + }, + "info": { + "lastseen": 1620741290.255359, + "rating": 0, + "self": false + } + }, + { + "stream": 1, + "peer": { + "host": "178.62.12.187", + "port": 8448 + }, + "info": { + "lastseen": 1620741290.255359, + "rating": 0, + "self": false + } + }, + { + "stream": 1, + "peer": { + "host": "24.188.198.204", + "port": 8111 + }, + "info": { + "lastseen": 1620741290.255359, + "rating": 0, + "self": false + } + }, + { + "stream": 1, + "peer": { + "host": "109.147.204.113", + "port": 1195 + }, + "info": { + "lastseen": 1620741290.255359, + "rating": 0, + "self": false + } + }, + { + "stream": 1, + "peer": { + "host": "178.11.46.221", + "port": 8444 + }, + "info": { + "lastseen": 1620741290.255359, + "rating": 0, + "self": false + } + } +] \ No newline at end of file diff --git a/src/bitmessagekivy/tests/sampleData/messages.dat b/src/bitmessagekivy/tests/sampleData/messages.dat new file mode 100644 index 0000000000..7150b42b95 Binary files /dev/null and b/src/bitmessagekivy/tests/sampleData/messages.dat differ diff --git a/src/bitmessagekivy/tests/telenium_process.py b/src/bitmessagekivy/tests/telenium_process.py new file mode 100644 index 0000000000..209287e212 --- /dev/null +++ b/src/bitmessagekivy/tests/telenium_process.py @@ -0,0 +1,126 @@ +""" + Base class for telenium test cases which run kivy app as background process +""" + +import os +import shutil +import tempfile +from time import time, sleep + +from requests.exceptions import ChunkedEncodingError + +from telenium.tests import TeleniumTestCase +from telenium.client import TeleniumHttpException + + +_files = ( + 'keys.dat', 'debug.log', 'messages.dat', 'knownnodes.dat', + '.api_started', 'unittest.lock' +) + +tmp_db_file = ( + 'keys.dat', 'messages.dat' +) + + +def cleanup(files=_files): + """Cleanup application files""" + for pfile in files: + try: + os.remove(os.path.join(tempfile.gettempdir(), pfile)) + except OSError: + pass + + +class TeleniumTestProcess(TeleniumTestCase): + """Setting Screen Functionality Testing""" + cmd_entrypoint = [os.path.join(os.path.abspath(os.getcwd()), 'src', 'mockbm', 'kivy_main.py')] + + @classmethod + def setUpClass(cls): + """Setupclass is for setting temp environment""" + os.environ["BITMESSAGE_HOME"] = tempfile.gettempdir() + cls.populate_test_data() + super(TeleniumTestProcess, cls).setUpClass() + + @staticmethod + def populate_test_data(): + """Set temp data in tmp directory""" + for file_name in tmp_db_file: + old_source_file = os.path.join( + os.path.abspath(os.path.dirname(__file__)), 'sampleData', file_name) + new_destination_file = os.path.join(os.environ['BITMESSAGE_HOME'], file_name) + shutil.copyfile(old_source_file, new_destination_file) + + @classmethod + def tearDownClass(cls): + """Ensures that pybitmessage stopped and removes files""" + # pylint: disable=no-member + try: + super(TeleniumTestProcess, cls).tearDownClass() + except ChunkedEncodingError: + pass + cleanup() + + def assert_wait_no_except(self, selector, timeout=-1, value='inbox'): + """This method is to check the application is launched.""" + start = time() + deadline = start + timeout + while time() < deadline: + try: + if self.cli.getattr(selector, 'current') == value: + self.assertTrue(selector, value) + return + except TeleniumHttpException: + sleep(0.1) + continue + finally: + # 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(1) + + def assertCheckScrollDown(self, selector, timeout=-1): + """this method is for checking scroll""" + start = time() + while True: + scroll_distance = self.cli.getattr(selector, 'scroll_y') + if scroll_distance > 0.0: + self.assertGreaterEqual(scroll_distance, 0.0) + return True + if timeout == -1: + return False + if timeout > 0 and time() - start > timeout: + raise Exception("Timeout") + sleep(0.5) + + def assertCheckScrollUp(self, selector, timeout=-1): + """this method is for checking scroll UP""" + start = time() + while True: + scroll_distance = self.cli.getattr(selector, 'scroll_y') + if scroll_distance < 1.0: + self.assertGreaterEqual(scroll_distance, 0.0) + return True + if timeout == -1: + return False + if timeout > 0 and time() - start > timeout: + raise Exception("Timeout") + 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('//ActionTopAppBarButton[@icon~=\"menu\"]', timeout=5) + # this is for opening Nav drawer + 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 new file mode 100644 index 0000000000..e61fef2a11 --- /dev/null +++ b/src/bitmessagekivy/tests/test_addressbook.py @@ -0,0 +1,155 @@ +from .telenium_process import TeleniumTestProcess +from .common import skip_screen_checks +from .common import ordered + +test_address = { + 'invalid_address': 'BM-2cWmjntZ47WKEUtocrdvs19y5CivpKoi1', + 'autoresponder_address': 'BM-2cVWtdUzPwF7UNGDrZftWuHWiJ6xxBpiSP', + 'recipient': 'BM-2cVpswZo8rWLXDVtZEUNcDQvnvHJ6TLRYr', + 'sender': 'BM-2cVpswZo8rWLXDVtZEUNcDQvnvHJ6TLRYr' +} + + +class AddressBook(TeleniumTestProcess): + """AddressBook Screen Functionality Testing""" + test_label = 'Auto Responder' + test_subject = 'Test Subject' + test_body = 'Hey,This is draft Message Body from Address Book' + + # @skip_screen_checks + @ordered + def test_save_address(self): + """Saving a new Address On Address Book Screen/Window""" + # Checking current Screen(Inbox screen) + self.assert_wait_no_except('//ScreenManager[@current]', timeout=15, value='inbox') + # Method to open side navbar (written in telenium_process.py) + 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) + # this is for opening addressbook screen + self.cli.wait_click('//NavigationItem[@text=\"Address Book\"]', 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('//ActionTopAppBarButton[@icon=\"account-plus\"]', timeout=5) + # Click on "Account-Plus' Icon to open popup to save a new Address + self.cli.wait_click('//ActionTopAppBarButton[@icon=\"account-plus\"]', timeout=5) + # Checking the Label Field shows Validation for empty string + self.assertExists('//AddAddressPopup/BoxLayout[0]/MDTextField[@hint_text=\"Label\"][@text=\"\"]', timeout=5) + # Checking the Address Field shows Validation for empty string + self.assertExists('//AddAddressPopup/BoxLayout[0]/MDTextField[@hint_text=\"Address\"][@text=\"\"]', timeout=5) + # Add an address Label to label Field + self.cli.setattr('//AddAddressPopup/BoxLayout[0]/MDTextField[@hint_text=\"Label\"]', 'text', self.test_label) + # Checking the Label Field should not be empty + self.assertExists( + '//AddAddressPopup/BoxLayout[0]/MDTextField[0][@text=\"{}\"]'.format(self.test_label), timeout=5) + # Add Correct Address + self.cli.setattr( + '//AddAddressPopup/BoxLayout[0]/MDTextField[@hint_text=\"Address\"]', 'text', + test_address['autoresponder_address']) + # Checking the Address Field contains correct address + self.assertEqual( + self.cli.getattr('//AddAddressPopup/BoxLayout[0]/MDTextField[1][@text]', 'text'), + test_address['autoresponder_address']) + # Validating the Label field + self.assertExists( + '//AddAddressPopup/BoxLayout[0]/MDTextField[0][@text=\"{}\"]'.format(self.test_label), timeout=5) + # Validating the Valid Address is entered + self.assertExists( + '//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) + + @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('//AddAddressPopup', timeout=5) + # Checking the "Add account" Button is rendered + self.assertExists('//ActionTopAppBarButton[@icon=\"account-plus\"]', timeout=6) + # Click on Account-Plus Icon to open popup for add Address + self.cli.wait_click('//ActionTopAppBarButton[@icon=\"account-plus\"]', timeout=5) + # Add Label to label Field + self.cli.setattr('//AddAddressPopup/BoxLayout[0]/MDTextField[0]', 'text', 'test_label2') + # Checking the Label Field should not be empty + self.assertExists( + '//AddAddressPopup/BoxLayout[0]/MDTextField[0][@text=\"{}\"]'.format('test_label2'), timeout=5) + # Add Address to Address Field + self.cli.setattr( + '//AddAddressPopup/BoxLayout[0]/MDTextField[1]', 'text', test_address['recipient']) + # Checking the Address Field should not be empty + self.assertExists( + '//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=5) + # Check Current Screen (Address Book) + self.assertExists("//ScreenManager[@current=\"addressbook\"]", timeout=5) + + @skip_screen_checks + @ordered + 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=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=5) + # Checking the buttons are rendered + self.assertExists('//MDRaisedButton', timeout=5) + # Click on the Send to message Button + self.cli.wait_click('//MDRaisedButton[0]', timeout=5) + # Redirected to message composer screen(create) + self.assertExists("//ScreenManager[@current=\"create\"]", timeout=5) + # Checking the Address is populated to recipient field when we try to send message to saved address. + self.assertExists( + '//DropDownWidget/ScrollView[0]//MyTextInput[@text="{}"]'.format( + test_address['autoresponder_address']), timeout=5) + # CLICK BACK-BUTTON + self.cli.wait_click('//MDToolbar/BoxLayout[0]/MDActionTopAppBarButton[@icon=\"arrow-left\"]', timeout=5) + # After Back press, redirected to 'inbox' screen + self.assertExists("//ScreenManager[@current=\"inbox\"]", timeout=8) + + @skip_screen_checks + @ordered + def test_delete_address_from_saved_address(self): + """Delete a saved Address from Address Book""" + # 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=5) + # checking state of Nav drawer(closed) + self.assertExists("//MDNavigationDrawer[@state~=\"close\"]", timeout=5) + # Checking current screen + self.assertExists("//ScreenManager[@current=\"addressbook\"]", timeout=8) + # Checking the Address is rendered + self.assertExists('//SwipeToDeleteItem[0]//TwoLineAvatarIconListItem', timeout=5) + # Waiting for the trash icon to be rendered + self.cli.wait('//MDList[0]//MDIconButton[@icon=\"trash-can\"]', timeout=5) + # Enable the trash icon + self.cli.setattr('//MDList[0]//MDIconButton[@disabled]', 'disabled', False) + # Swiping over the Address to delete + self.cli.wait_drag('//MDList[0]//AvatarSampleWidget', '//MDList[0]//TimeTagRightSampleWidget', 2, timeout=5) + # Click on trash icon to delete the Address. + self.cli.wait_click('//MDList[0]//MDIconButton[@icon=\"trash-can\"]', timeout=5) + # Checking the deleted Address is disappeared + self.assertNotExists('//SwipeToDeleteItem[0]//TwoLineAvatarIconListItem', timeout=5) + # Address count should be zero + self.assertEqual(len(self.cli.select('//SwipeToDeleteItem[0]//TwoLineAvatarIconListItem[0]')), 0) + # After Deleting, Screen is redirected to Address Book screen + self.assertExists("//ScreenManager[@current=\"addressbook\"]", timeout=8) diff --git a/src/bitmessagekivy/tests/test_allmail_message.py b/src/bitmessagekivy/tests/test_allmail_message.py new file mode 100644 index 0000000000..b08e0f29ed --- /dev/null +++ b/src/bitmessagekivy/tests/test_allmail_message.py @@ -0,0 +1,35 @@ +from .telenium_process import TeleniumTestProcess +from .common import skip_screen_checks +from .common import ordered + + +class AllMailMessage(TeleniumTestProcess): + """AllMail Screen Functionality Testing""" + + # @skip_screen_checks + @ordered + def test_show_allmail_list(self): + """Show All Messages on Mail Screen/Window""" + # This is for checking Current 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 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) + + @skip_screen_checks + @ordered + def test_delete_message_from_allmail_list(self): + """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=5) + # Assert for checking Current Screen(Mail Detail) + self.assertExists("//ScreenManager[@current=\"mailDetail\"]", timeout=5) + # CLicking on Trash-Can icon to delete Message + self.cli.wait_click('//MDToolbar/BoxLayout[2]/MDActionTopAppBarButton[@icon=\"delete-forever\"]', timeout=5) + # After deleting msg, screen is redirected to All mail screen + self.assertExists("//ScreenManager[@current=\"allmails\"]", timeout=5) diff --git a/src/bitmessagekivy/tests/test_chat_screen.py b/src/bitmessagekivy/tests/test_chat_screen.py new file mode 100644 index 0000000000..98464bf3aa --- /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 new file mode 100644 index 0000000000..691a9d46ca --- /dev/null +++ b/src/bitmessagekivy/tests/test_create_random_address.py @@ -0,0 +1,129 @@ +""" + Test for creating new identity +""" + +from random import choice +from string import ascii_lowercase +from .telenium_process import TeleniumTestProcess +from .common import ordered + + +class CreateRandomAddress(TeleniumTestProcess): + """This is for testing randrom address creation""" + @staticmethod + def populate_test_data(): + pass + + @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 + def test_landing_screen(self): + """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') + # 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) + + @ordered + def test_generate_random_address_label(self): + """Creating New Adress For New User.""" + # Checking the Button is rendered + self.assertExists( + '//Random//RandomBoxlayout//MDTextField[@hint_text=\"Label\"]', timeout=5) + # Click on Label Text Field to give address Label + self.cli.wait_click( + '//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('//Random//MDTextField[0]', "text", random_label) + self.cli.sleep(0.1) + # Checking the Button is rendered + self.assertExists( + '//Random//RandomBoxlayout//MDFillRoundFlatIconButton[@text=\"Proceed Next\"]', timeout=5) + # Click on Proceed Next button to generate random Address + 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('//MyAddress//MDList[0]/CustomTwoLineAvatarIconListItem', timeout=10) + + @ordered + def test_set_default_address(self): + """Select First Address From Drawer-Box""" + # Checking current screen + self.assertExists("//ScreenManager[@current=\"myaddress\"]", timeout=5) + # This is for opening 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=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.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) + + 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 new file mode 100644 index 0000000000..e876f418e8 --- /dev/null +++ b/src/bitmessagekivy/tests/test_draft_message.py @@ -0,0 +1,158 @@ +from .telenium_process import TeleniumTestProcess +from .common import skip_screen_checks +from .common import ordered + +test_address = { + 'receiver': 'BM-2cVWtdUzPwF7UNGDrZftWuHWiJ6xxBpiSP' +} + + +class DraftMessage(TeleniumTestProcess): + """Draft Screen Functionality Testing""" + 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): + """ + Saving a message to draft box when click on back button + """ + # Checking current 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=5) + # Checking Message Composer Screen(Create) + self.assertExists("//ScreenManager[@current=\"create\"]", timeout=5) + # ADD SUBJECT + self.cli.setattr('//DropDownWidget/ScrollView[0]//MyMDTextField[0]', 'text', self.test_subject) + # Checking Subject Field is Entered + self.assertExists( + '//DropDownWidget/ScrollView[0]//MyMDTextField[@text=\"{}\"]'.format(self.test_subject), timeout=5) + # ADD MESSAGE BODY + self.cli.setattr( + '//DropDownWidget/ScrollView[0]/BoxLayout[0]/ScrollView[0]/MDTextField[@text]', + 'text', self.test_body) + # Checking Message body is Entered + self.assertExists( + '//DropDownWidget/ScrollView[0]/BoxLayout[0]/ScrollView[0]/MDTextField[@text=\"{}\"]'.format( + self.test_body), timeout=5) + # Click on "Send" Icon + self.cli.wait_click('//MDActionTopAppBarButton[@icon=\"send\"]', timeout=5) + # Checking validation Pop up is Opened + self.assertExists('//MDDialog[@open]', timeout=5) + # checking the button is rendered + self.assertExists('//MDFlatButton[@text=\"Ok\"]', timeout=5) + # Click "OK" button to dismiss the Popup + self.cli.wait_click('//MDFlatButton[@text=\"Ok\"]', timeout=5) + # Checking validation Pop up is Closed + self.assertNotExists('//MDDialog[@open]', timeout=5) + # RECEIVER FIELD + # Checking Receiver Address Field + self.assertExists('//DropDownWidget/ScrollView[0]//MyTextInput[@text=\"\"]', timeout=5) + # Entering Receiver Address + self.cli.setattr( + '//DropDownWidget/ScrollView[0]//MyTextInput[0]', "text", test_address['auto_responder']) + # Checking Receiver Address filled or not + self.assertExists( + '//DropDownWidget/ScrollView[0]//MyTextInput[@text=\"{}\"]'.format(test_address['auto_responder']), + timeout=5) + # Checking the sender's Field is empty + 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 + self.cli.wait_click('//Create//CustomSpinner[0]/ArrowImg[0]', timeout=5) + # Checking the status of dropdown + self.assertEqual(self.cli.getattr('//Create//CustomSpinner[@is_open]', 'is_open'), False) + # Checking the dropdown option is rendered + self.assertExists('//ComposerSpinnerOption[0]', timeout=5) + # Select Sender's Address from Dropdown options + self.cli.wait_click('//ComposerSpinnerOption[0]', timeout=5) + # Assert to check Sender address dropdown closed + self.assertEqual(self.cli.getattr('//Create//CustomSpinner[@is_open]', 'is_open'), False) + # 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=5) + # CLICK BACK-BUTTON + self.cli.wait_click('//MDToolbar/BoxLayout[0]/MDActionTopAppBarButton[@icon=\"arrow-left\"]', timeout=5) + # Checking current screen(Login) after "BACK" Press + self.assertExists("//ScreenManager[@current=\"inbox\"]", timeout=5) + + @skip_screen_checks + @ordered + def test_edit_and_resend_draft_messgae(self): + """Click on a Drafted message to send message""" + # OPEN NAVIGATION-DRAWER + # this is for opening Nav drawer + self.open_side_navbar() + # 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=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=5) + + # CLICK on EDIT(Pencil) BUTTON + self.cli.wait_click('//MDToolbar/BoxLayout[2]/MDActionTopAppBarButton[@icon=\"pencil\"]', timeout=5) + # Checking Current Screen 'Create'; composer screen. + self.assertExists("//ScreenManager[@current=\"create\"]", timeout=10) + # Checking the recipient is in the receiver field + self.assertExists( + '//DropDownWidget/ScrollView[0]//MyTextInput[@text=\"{}\"]'.format(test_address['auto_responder']), + 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=5) + # Checking the subject text is in the subject field + self.assertExists( + '//DropDownWidget/ScrollView[0]//MyMDTextField[@text=\"{}\"]'.format(self.test_subject), timeout=5) + # Checking the Body text is in the Body field + self.assertExists( + '//DropDownWidget/ScrollView[0]//ScrollView[0]/MDTextField[@text=\"{}\"]'.format(self.test_body), + timeout=5) + # CLICK BACK-BUTTON to autosave msg in draft screen + self.cli.wait_click('//MDToolbar/BoxLayout[0]/MDActionTopAppBarButton[@icon=\"arrow-left\"]', timeout=5) + # Checking current screen(Login) after BACK Press + self.assertExists("//ScreenManager[@current=\"draft\"]", timeout=5) + + @skip_screen_checks + @ordered + def test_delete_draft_message(self): + """Deleting a Drafted Message""" + # Checking current screen is Draft Screen + self.assertExists("//ScreenManager[@current=\"draft\"]", timeout=5) + # Cheking the Message is rendered + self.assertExists('//SwipeToDeleteItem[0]//TwoLineAvatarIconListItem[0]', timeout=5) + # Enable the trash icon + self.cli.setattr('//MDList[0]//MDIconButton[@disabled]', 'disabled', False) + # Waiting for the trash icon to be rendered + self.cli.wait('//MDList[0]//MDIconButton[@icon=\"trash-can\"]', timeout=5) + # Swiping over the message to delete + self.cli.wait_drag('//MDList[0]//AvatarSampleWidget', '//MDList[0]//TimeTagRightSampleWidget', 2, timeout=5) + # Click on trash icon to delete the message. + self.cli.wait_click('//MDList[0]//MDIconButton[@icon=\"trash-can\"]', timeout=5) + # Checking the deleted message is disappeared + self.assertNotExists('//SwipeToDeleteItem[0]//TwoLineAvatarIconListItem', timeout=5) + # Message count should be zero + self.assertEqual(len(self.cli.select('//SwipeToDeleteItem[0]//TwoLineAvatarIconListItem[0]')), 0) + # After Deleting, Screen is redirected to Draft screen + self.assertExists("//ScreenManager[@current=\"draft\"]", timeout=8) diff --git a/src/bitmessagekivy/tests/test_filemanager.py b/src/bitmessagekivy/tests/test_filemanager.py new file mode 100644 index 0000000000..6d25553c50 --- /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 0000000000..619daf25ef --- /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 0000000000..f46588c11f --- /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 the Toggle button to activate 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//BoxLayout[1]//MDLabel[@text=\"Show QR code\"]', timeout=5) + # Cick on 'Show QR code' button to view QR Code + self.cli.wait_click('//MyaddDetailPopup//BoxLayout[1]//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 new file mode 100644 index 0000000000..237a45c60a --- /dev/null +++ b/src/bitmessagekivy/tests/test_network_screen.py @@ -0,0 +1,50 @@ +# pylint: disable=too-few-public-methods +""" + Kivy Networkstat UI test +""" + +from .telenium_process import TeleniumTestProcess + + +class NetworkStatusScreen(TeleniumTestProcess): + """NetworkStatus Screen Functionality Testing""" + + 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=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=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=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 new file mode 100644 index 0000000000..4230900172 --- /dev/null +++ b/src/bitmessagekivy/tests/test_payment_subscription.py @@ -0,0 +1,70 @@ +from .telenium_process import TeleniumTestProcess +from .common import ordered + + +class PaymentScreen(TeleniumTestProcess): + """Payment Plan Screen Functionality Testing""" + + @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 + self.open_side_navbar() + # 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=10) + # this is for opening Payment screen + 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) + # 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( + '//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.wait_click( + '//PaymentMethodLayout//ScrollView[0]//ListItemWithLabel[0]', + timeout=10 + ) + # Check pop up is opened + self.assertExists('//PaymentMethodLayout[@disabled=false]', timeout=10) + # Click out side to dismiss the popup + 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 new file mode 100644 index 0000000000..a7fd576b72 --- /dev/null +++ b/src/bitmessagekivy/tests/test_sent_message.py @@ -0,0 +1,133 @@ +from .telenium_process import TeleniumTestProcess +from .common import skip_screen_checks +from .common import ordered + +test_address = {'autoresponder_address': 'BM-2cVWtdUzPwF7UNGDrZftWuHWiJ6xxBpiSP'} + + +class SendMessage(TeleniumTestProcess): + """Sent Screen Functionality Testing""" + test_subject = 'Test Subject' + test_body = 'Hello, \n Hope your are doing good.\n\t This is test message body' + + @skip_screen_checks + @ordered + def test_validate_empty_form(self): + """ + Sending Message From Inbox Screen + opens a pop-up(screen) which send message from sender to reciever + """ + # 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=5) + # Checking Message Composer Screen(Create) + 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=5) + # Checking State of Receiver's Address Input Field (should be Empty) + 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=5) + # Click on Send Icon to check validation working + 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 + self.assertExists('//MDFlatButton[@text=\"Ok\"]', timeout=5) + # Click "OK" button to dismiss the Popup + self.cli.wait_click('//MDFlatButton[@text=\"Ok\"]', timeout=5) + # Checking the pop is closed + self.assertNotExists('//MDDialog[@open]', timeout=5) + # Checking current screen after dialog dismiss + self.assertExists("//ScreenManager[@current=\"create\"]", timeout=10) + + @skip_screen_checks + @ordered + def test_validate_half_filled_form(self): + """ + 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=5) + # ADD SENDER'S ADDRESS + # Checking State of Sender's Address Input Field (Empty) + 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) + # Open Sender's Address DropDown + self.cli.wait_click('//Create//CustomSpinner[0]/ArrowImg[0]', timeout=5) + # 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=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) + # Checking the sender address is selected + 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=5) + # Assert check for empty Subject Field + self.assertExists('//DropDownWidget/ScrollView[0]//MyMDTextField[@text=\"\"]', timeout=5) + # ADD SUBJECT + self.cli.setattr('//DropDownWidget/ScrollView[0]//MyMDTextField[0]', 'text', self.test_subject) + # Checking Subject Field is Entered + 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=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=5) + # click on send icon + self.cli.wait_click('//MDActionTopAppBarButton[@icon=\"send\"]', timeout=5) + # Checking validation Pop up is Opened + self.assertExists('//MDDialog', timeout=5) + # clicked on 'Ok' button to close popup + self.cli.wait_click('//MDFlatButton[@text=\"Ok\"]', timeout=5) + # Checking current screen after dialog dismiss + self.assertExists("//ScreenManager[@current=\"create\"]", timeout=5) + + @skip_screen_checks + @ordered + def test_sending_msg_fully_filled_form(self): + """ + Sending message when all fields are filled + """ + # ADD RECEIVER ADDRESS + # Checking Receiver Address Field + 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']) + # Checking Receiver Address filled or not + self.assertExists( + '//DropDownWidget/ScrollView[0]//MyTextInput[@text=\"{}\"]'.format( + test_address['autoresponder_address']), timeout=5) + # Clicking on send icon + self.cli.wait_click('//MDActionTopAppBarButton[@icon=\"send\"]', timeout=5) + # Checking the current screen + self.assertExists("//ScreenManager[@current=\"inbox\"]", timeout=10) + + @skip_screen_checks + @ordered + def test_sent_box(self): + """ + Checking Message in Sent Screen after sending a Message. + """ + # this is for opening Nav drawer + self.open_side_navbar() + # Clicking on Sent Tab + self.cli.wait_click('//NavigationItem[@text=\"Sent\"]', timeout=5) + # Checking current screen; Sent + self.assertExists("//ScreenManager[@current=\"sent\"]", timeout=5) + # Checking number of Sent messages + total_sent_msgs = len(self.cli.select("//SwipeToDeleteItem")) + self.assertEqual(total_sent_msgs, 3) diff --git a/src/bitmessagekivy/tests/test_setting_screen.py b/src/bitmessagekivy/tests/test_setting_screen.py new file mode 100644 index 0000000000..4f3a0a5948 --- /dev/null +++ b/src/bitmessagekivy/tests/test_setting_screen.py @@ -0,0 +1,38 @@ +# pylint: disable=too-few-public-methods + +from .telenium_process import TeleniumTestProcess + + +class SettingScreen(TeleniumTestProcess): + """Setting Screen Functionality Testing""" + + def test_setting_screen(self): + """Show Setting Screen""" + # This is for checking Current 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) + # this is for opening setting screen + 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=5) + # Scrolling down currrent screen + self.cli.wait_drag( + '//MDTabs[0]//MDLabel[@text=\"Close to tray\"]', + '//MDTabs[0]//MDLabel[@text=\"Minimize to tray\"]', 1, timeout=5) + # Checking state of 'Network Settings' sub tab should be 'normal'(inactive) + self.assertExists('//MDTabs[0]//MDTabsLabel[@text=\"Network Settings\"][@state=\"normal\"]', timeout=5) + # Click on "Network Settings" subtab + self.cli.wait_click('//MDTabs[0]//MDTabsLabel[@text=\"Network Settings\"]', timeout=5) + # Checking state of 'Network Settings' sub tab should be 'down'(active) + self.assertExists('//MDTabs[0]//MDTabsLabel[@text=\"Network Settings\"][@state=\"down\"]', timeout=5) + # Scrolling down currrent screen + self.cli.wait_drag( + '//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) diff --git a/src/bitmessagekivy/tests/test_trash_message.py b/src/bitmessagekivy/tests/test_trash_message.py new file mode 100644 index 0000000000..d7cdb46721 --- /dev/null +++ b/src/bitmessagekivy/tests/test_trash_message.py @@ -0,0 +1,21 @@ +from .telenium_process import TeleniumTestProcess +from .common import ordered + + +class TrashMessage(TeleniumTestProcess): + """Trash Screen Functionality Testing""" + + @ordered + def test_delete_trash_message(self): + """Delete Trash message permanently from trash message listing""" + # 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 Trash screen + 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) + self.cli.sleep(0.5) diff --git a/src/bitmessagekivy/uikivysignaler.py b/src/bitmessagekivy/uikivysignaler.py new file mode 100644 index 0000000000..6f73247ea6 --- /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 fc014b0cdb..ab131a4c68 100755 --- a/src/bitmessagemain.py +++ b/src/bitmessagemain.py @@ -1,210 +1,97 @@ -#!/usr/bin/python2.7 +#!/usr/bin/env python +""" +The PyBitmessage startup script +""" # Copyright (c) 2012-2016 Jonathan Warren -# Copyright (c) 2012-2016 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. # Right now, PyBitmessage only support connecting to stream 1. It doesn't # yet contain logic to expand into further streams. - -# The software version variable is now held in shared.py - import os import sys -app_dir = os.path.dirname(os.path.abspath(__file__)) -os.chdir(app_dir) -sys.path.insert(0, app_dir) + +try: + import pathmagic +except ImportError: + from pybitmessage import pathmagic +app_dir = pathmagic.setup() import depends depends.check_dependencies() -import signal # Used to capture a Ctrl-C keypress so that Bitmessage can shutdown gracefully. -# The next 3 are used for the API -from singleinstance import singleinstance -import errno -import socket -import ctypes -from struct import pack -from subprocess import call -from time import sleep -from random import randint import getopt - -from api import MySimpleXMLRPCRequestHandler, StoppableXMLRPCServer -from helper_startup import isOurOperatingSystemLimitedToHavingVeryFewHalfOpenConnections +import multiprocessing +# Used to capture a Ctrl-C keypress so that Bitmessage can shutdown gracefully. +import signal +import threading +import time +import traceback import defaults -import shared -import knownnodes -import state +# Network subsystem +import network import shutdown -import threading - -# Classes -from class_sqlThread import sqlThread -from class_singleCleaner import singleCleaner -from class_objectProcessor import objectProcessor -from class_singleWorker import singleWorker -from class_addressGenerator import addressGenerator -from class_smtpDeliver import smtpDeliver -from class_smtpServer import smtpServer -from bmconfigparser import BMConfigParser +import state +from testmode_init import populate_api_test_data +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 - -from network.connectionpool import BMConnectionPool -from network.dandelion import Dandelion -from network.networkthread import BMNetworkThread -from network.receivequeuethread import ReceiveQueueThread -from network.announcethread import AnnounceThread -from network.invthread import InvThread -from network.addrthread import AddrThread -from network.downloadthread import DownloadThread - -# Helper Functions -import helper_bootstrap -import helper_generic -import helper_threading - - -def connectToStream(streamNumber): - state.streamsInWhichIAmParticipating.append(streamNumber) - selfInitiatedConnections[streamNumber] = {} - - if isOurOperatingSystemLimitedToHavingVeryFewHalfOpenConnections(): - # Some XP and Vista systems can only have 10 outgoing connections at a time. - state.maximumNumberOfHalfOpenConnections = 9 - else: - state.maximumNumberOfHalfOpenConnections = 64 - try: - # don't overload Tor - if BMConfigParser().get('bitmessagesettings', 'socksproxytype') != 'none': - state.maximumNumberOfHalfOpenConnections = 4 - except: - pass - - with knownnodes.knownNodesLock: - if streamNumber not in knownnodes.knownNodes: - knownnodes.knownNodes[streamNumber] = {} - if streamNumber*2 not in knownnodes.knownNodes: - knownnodes.knownNodes[streamNumber*2] = {} - if streamNumber*2+1 not in knownnodes.knownNodes: - knownnodes.knownNodes[streamNumber*2+1] = {} - - BMConnectionPool().connectToStream(streamNumber) - -def _fixSocket(): - if sys.platform.startswith('linux'): - socket.SO_BINDTODEVICE = 25 - - if not sys.platform.startswith('win'): +from singleinstance import singleinstance +# Synchronous threads +from threads import ( + set_thread_name, printLock, + addressGenerator, objectProcessor, singleCleaner, singleWorker, sqlThread) + + +def signal_handler(signum, frame): + """Single handler for any signal sent to pybitmessage""" + process = multiprocessing.current_process() + thread = threading.current_thread() + logger.error( + 'Got signal %i in %s/%s', + signum, process.name, thread.name + ) + if process.name == "RegExParser": + # on Windows this isn't triggered, but it's fine, + # it has its own process termination thing + raise SystemExit + if "PoolWorker" in process.name: + raise SystemExit + if thread.name not in ("PyBitmessage", "MainThread"): return + logger.error("Got signal %i", signum) + # there are possible non-UI variants to run bitmessage + # which should shutdown especially test-mode + if state.thisapp.daemon or not state.enableGUI: + shutdown.doCleanShutdown() + else: + print('# Thread: %s(%d)' % (thread.name, thread.ident)) + for filename, lineno, name, line in traceback.extract_stack(frame): + print('File: "%s", line %d, in %s' % (filename, lineno, name)) + if line: + print(' %s' % line.strip()) + print('Unfortunately you cannot use Ctrl+C when running the UI' + ' because the UI captures the signal.') - # Python 2 on Windows doesn't define a wrapper for - # socket.inet_ntop but we can make one ourselves using ctypes - if not hasattr(socket, 'inet_ntop'): - addressToString = ctypes.windll.ws2_32.WSAAddressToStringA - def inet_ntop(family, host): - if family == socket.AF_INET: - if len(host) != 4: - raise ValueError("invalid IPv4 host") - host = pack("hH4s8s", socket.AF_INET, 0, host, "\0" * 8) - elif family == socket.AF_INET6: - if len(host) != 16: - raise ValueError("invalid IPv6 host") - host = pack("hHL16sL", socket.AF_INET6, 0, 0, host, 0) - else: - raise ValueError("invalid address family") - buf = "\0" * 64 - lengthBuf = pack("I", len(buf)) - addressToString(host, len(host), None, buf, lengthBuf) - return buf[0:buf.index("\0")] - socket.inet_ntop = inet_ntop - - # Same for inet_pton - if not hasattr(socket, 'inet_pton'): - stringToAddress = ctypes.windll.ws2_32.WSAStringToAddressA - def inet_pton(family, host): - buf = "\0" * 28 - lengthBuf = pack("I", len(buf)) - if stringToAddress(str(host), - int(family), - None, - buf, - lengthBuf) != 0: - raise socket.error("illegal IP address passed to inet_pton") - if family == socket.AF_INET: - return buf[4:8] - elif family == socket.AF_INET6: - return buf[8:24] - else: - raise ValueError("invalid address family") - socket.inet_pton = inet_pton - - # These sockopts are needed on for IPv6 support - if not hasattr(socket, 'IPPROTO_IPV6'): - socket.IPPROTO_IPV6 = 41 - if not hasattr(socket, 'IPV6_V6ONLY'): - socket.IPV6_V6ONLY = 27 - -# This thread, of which there is only one, runs the API. -class singleAPI(threading.Thread, helper_threading.StoppableThread): - def __init__(self): - threading.Thread.__init__(self, name="singleAPI") - self.initStop() - - def stopThread(self): - super(singleAPI, self).stopThread() - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - try: - s.connect((BMConfigParser().get('bitmessagesettings', 'apiinterface'), BMConfigParser().getint( - 'bitmessagesettings', 'apiport'))) - s.shutdown(socket.SHUT_RDWR) - s.close() - except: - pass - def run(self): - port = BMConfigParser().getint('bitmessagesettings', 'apiport') - try: - from errno import WSAEADDRINUSE - except (ImportError, AttributeError): - errno.WSAEADDRINUSE = errno.EADDRINUSE - for attempt in range(50): - try: - if attempt > 0: - port = randint(32767, 65535) - se = StoppableXMLRPCServer((BMConfigParser().get('bitmessagesettings', 'apiinterface'), port), - MySimpleXMLRPCRequestHandler, True, True) - except socket.error as e: - if e.errno in (errno.EADDRINUSE, errno.WSAEADDRINUSE): - continue - else: - if attempt > 0: - BMConfigParser().set("bitmessagesettings", "apiport", str(port)) - BMConfigParser().save() - break - se.register_introspection_functions() - se.serve_forever() - -# This is a list of current connections (the thread pointers at least) -selfInitiatedConnections = {} - -if shared.useVeryEasyProofOfWorkForTesting: - defaults.networkDefaultProofOfWorkNonceTrialsPerByte = int( - defaults.networkDefaultProofOfWorkNonceTrialsPerByte / 100) - defaults.networkDefaultPayloadLengthExtraBytes = int( - defaults.networkDefaultPayloadLengthExtraBytes / 100) - -class Main: +class Main(object): + """Main PyBitmessage class""" def start(self): - _fixSocket() + """Start main application""" + # pylint: disable=too-many-statements,too-many-branches,too-many-locals + fixSocket() + adjustHalfOpenConnectionsLimit() - daemon = BMConfigParser().safeGetBoolean('bitmessagesettings', 'daemon') + daemon = config.safeGetBoolean('bitmessagesettings', 'daemon') try: - opts, args = getopt.getopt( + opts, _ = getopt.getopt( sys.argv[1:], "hcdt", ["help", "curses", "daemon", "test"]) @@ -212,188 +99,201 @@ def start(self): self.usage() sys.exit(2) - for opt, arg in opts: + for opt, _ in opts: if opt in ("-h", "--help"): self.usage() sys.exit() elif opt in ("-d", "--daemon"): daemon = True - state.enableGUI = False # run without a UI elif opt in ("-c", "--curses"): state.curses = True elif opt in ("-t", "--test"): - state.testmode = daemon = True - state.enableGUI = False # run without a UI + state.testmode = True + if os.path.isfile(os.path.join( + state.appdata, 'unittest.lock')): + daemon = True + state.enableGUI = False # run without a UI + # Fallback: in case when no api command was issued + state.last_api_response = time.time() + # Apply special settings + config.set( + 'bitmessagesettings', 'apienabled', 'true') + config.set( + 'bitmessagesettings', 'apiusername', 'username') + config.set( + 'bitmessagesettings', 'apipassword', 'password') + config.set( + 'bitmessagesettings', 'apivariant', 'legacy') + config.set( + 'bitmessagesettings', 'apinotifypath', + os.path.join(app_dir, 'tests', 'apinotify_handler.py') + ) + if daemon: + state.enableGUI = False # run without a UI + + if state.enableGUI and not state.curses and not depends.check_pyqt(): + sys.exit( + 'PyBitmessage requires PyQt unless you want' + ' to run it as a daemon and interact with it' + ' using the API. You can download PyQt from ' + 'http://www.riverbankcomputing.com/software/pyqt/download' + ' or by searching Google for \'PyQt Download\'.' + ' If you want to run in daemon mode, see ' + 'https://bitmessage.org/wiki/Daemon\n' + 'You can also run PyBitmessage with' + ' the new curses interface by providing' + ' \'-c\' as a commandline argument.' + ) # is the application already running? If yes then exit. - shared.thisapp = singleinstance("", daemon) + state.thisapp = singleinstance("", daemon) - if daemon and not state.testmode: - with shared.printLock: + if daemon: + with printLock: print('Running as a daemon. Send TERM signal to end.') self.daemonize() self.setSignalHandler() - helper_threading.set_thread_name("PyBitmessage") - - state.dandelion = BMConfigParser().safeGetInt('network', 'dandelion') - # dandelion requires outbound connections, without them, stem objects will get stuck forever - if state.dandelion and not BMConfigParser().safeGetBoolean('bitmessagesettings', 'sendoutgoingconnections'): - state.dandelion = 0 + set_thread_name("PyBitmessage") - helper_bootstrap.knownNodes() + if state.testmode or config.safeGetBoolean( + 'bitmessagesettings', 'extralowdifficulty'): + defaults.networkDefaultProofOfWorkNonceTrialsPerByte = int( + defaults.networkDefaultProofOfWorkNonceTrialsPerByte / 100) + defaults.networkDefaultPayloadLengthExtraBytes = int( + defaults.networkDefaultPayloadLengthExtraBytes / 100) - # 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() - addressGeneratorThread.daemon = True # close the main program even if there are threads left + # close the main program even if there are threads left + addressGeneratorThread.daemon = True addressGeneratorThread.start() # Start the thread that calculates POWs singleWorkerThread = singleWorker() - singleWorkerThread.daemon = True # close the main program even if there are threads left + # close the main program even if there are threads left + singleWorkerThread.daemon = True singleWorkerThread.start() - # Start the SQL thread - sqlLookup = sqlThread() - sqlLookup.daemon = False # DON'T close the main program even if there are threads left. The closeEvent should command this thread to exit gracefully. - sqlLookup.start() - - Inventory() # init - Dandelion() # init, needs to be early because other thread may access it early - - # 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 BMConfigParser().safeGet("bitmessagesettings", "smtpdeliver", '') != '': + if daemon and config.safeGet( + 'bitmessagesettings', 'smtpdeliver', '') != '': + from class_smtpDeliver import smtpDeliver smtpDeliveryThread = smtpDeliver() smtpDeliveryThread.start() # SMTP daemon thread - if daemon and BMConfigParser().safeGetBoolean("bitmessagesettings", "smtpd"): + if daemon and config.safeGetBoolean( + 'bitmessagesettings', 'smtpd'): + from class_smtpServer import smtpServer smtpServerThread = smtpServer() smtpServerThread.start() - # Start the thread that calculates POWs - objectProcessorThread = objectProcessor() - objectProcessorThread.daemon = False # DON'T close the main program even the thread remains. This thread checks the shutdown variable after processing each object. - objectProcessorThread.start() + # API is also objproc dependent + if config.safeGetBoolean('bitmessagesettings', 'apienabled'): + import api # pylint: disable=relative-import + singleAPIThread = api.singleAPI() + # close the main program even if there are threads left + singleAPIThread.daemon = True + singleAPIThread.start() # Start the cleanerThread singleCleanerThread = singleCleaner() - singleCleanerThread.daemon = True # close the main program even if there are threads left + # 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 BMConfigParser().safeGetBoolean('bitmessagesettings', 'apienabled'): - try: - apiNotifyPath = BMConfigParser().get( - 'bitmessagesettings', 'apinotifypath') - except: - apiNotifyPath = '' - if apiNotifyPath != '': - with shared.printLock: - print('Trying to call', apiNotifyPath) - - call([apiNotifyPath, "startingUp"]) - singleAPIThread = singleAPI() - singleAPIThread.daemon = True # close the main program even if there are threads left - singleAPIThread.start() - # start network components if networking is enabled if state.enableNetwork: - BMConnectionPool() - asyncoreThread = BMNetworkThread() - asyncoreThread.daemon = True - asyncoreThread.start() - for i in range(BMConfigParser().getint("threads", "receive")): - receiveQueueThread = ReceiveQueueThread(i) - receiveQueueThread.daemon = True - receiveQueueThread.start() - announceThread = AnnounceThread() - announceThread.daemon = True - 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() - - connectToStream(1) - - if BMConfigParser().safeGetBoolean('bitmessagesettings','upnp'): + start_proxyconfig() + 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) - - if daemon == False and state.enableGUI: # FIXME redundant? - if state.curses == False: - if not depends.check_pyqt(): - sys.exit( - 'PyBitmessage requires PyQt unless you want' - ' to run it as a daemon and interact with it' - ' using the API. You can download PyQt from ' - 'http://www.riverbankcomputing.com/software/pyqt/download' - ' or by searching Google for \'PyQt Download\'.' - ' If you want to run in daemon mode, see ' - 'https://bitmessage.org/wiki/Daemon' - 'You can also run PyBitmessage with' - ' the new curses interface by providing' - ' \'-c\' as a commandline argument.' - ) - + network.connectionpool.pool.connectToStream(1) + + if not daemon and state.enableGUI: + if state.curses: + if not depends.check_curses(): + sys.exit() + print('Running with curses') + import bitmessagecurses + bitmessagecurses.runwrapper() + else: import bitmessageqt bitmessageqt.run() - else: - if True: -# if depends.check_curses(): - print('Running with curses') - import bitmessagecurses - bitmessagecurses.runwrapper() else: - BMConfigParser().remove_option('bitmessagesettings', 'dontconnect') + config.remove_option('bitmessagesettings', 'dontconnect') + + if state.testmode: + populate_api_test_data() if daemon: - if state.testmode: - sleep(30) - # make testing - self.stop() while state.shutdown == 0: - sleep(1) - - def daemonize(self): + time.sleep(1) + if ( + state.testmode + and time.time() - state.last_api_response >= 30 + ): + self.stop() + elif not state.enableGUI: + state.enableGUI = True + try: + # pylint: disable=relative-import + from tests import core as test_core + except ImportError: + try: + from pybitmessage.tests import core as test_core + except ImportError: + self.stop() + return + + test_core_result = test_core.run() + self.stop() + test_core.cleanup() + sys.exit(not test_core_result.wasSuccessful()) + + @staticmethod + def daemonize(): + """Running as a daemon. Send signal in end.""" grandfatherPid = os.getpid() parentPid = None try: if os.fork(): # unlock - shared.thisapp.cleanup() + state.thisapp.cleanup() # wait until grandchild ready while True: - sleep(1) - os._exit(0) + time.sleep(1) + os._exit(0) # pylint: disable=protected-access except AttributeError: # fork not implemented pass else: parentPid = os.getpid() - shared.thisapp.lock() # relock + state.thisapp.lock() # relock + os.umask(0) try: os.setsid() @@ -403,23 +303,23 @@ def daemonize(self): try: if os.fork(): # unlock - shared.thisapp.cleanup() + state.thisapp.cleanup() # wait until child ready while True: - sleep(1) - os._exit(0) + time.sleep(1) + os._exit(0) # pylint: disable=protected-access except AttributeError: # fork not implemented pass else: - shared.thisapp.lock() # relock - shared.thisapp.lockPid = None # indicate we're the final child + state.thisapp.lock() # relock + state.thisapp.lockPid = None # indicate we're the final child sys.stdout.flush() sys.stderr.flush() if not sys.platform.startswith('win'): - si = file(os.devnull, 'r') - so = file(os.devnull, 'a+') - se = file(os.devnull, 'a+', 0) + si = open(os.devnull, 'r') + so = open(os.devnull, 'a+') + se = open(os.devnull, 'a+', 0) os.dup2(si.fileno(), sys.stdin.fileno()) os.dup2(so.fileno(), sys.stdout.fileno()) os.dup2(se.fileno(), sys.stderr.fileno()) @@ -428,14 +328,18 @@ def daemonize(self): os.kill(parentPid, signal.SIGTERM) os.kill(grandfatherPid, signal.SIGTERM) - def setSignalHandler(self): - signal.signal(signal.SIGINT, helper_generic.signal_handler) - signal.signal(signal.SIGTERM, helper_generic.signal_handler) + @staticmethod + def setSignalHandler(): + """Setting the Signal Handler""" + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) # signal.signal(signal.SIGINT, signal.SIG_DFL) - def usage(self): - print 'Usage: ' + sys.argv[0] + ' [OPTIONS]' - print ''' + @staticmethod + def usage(): + """Displaying the usages""" + print('Usage: ' + sys.argv[0] + ' [OPTIONS]') + print(''' Options: -h, --help show this help message and exit -c, --curses use curses (text mode) interface @@ -443,27 +347,33 @@ def usage(self): -t, --test dryrun, make testing All parameters are optional. -''' +''') - def stop(self): - with shared.printLock: + @staticmethod + def stop(): + """Stop main application""" + with printLock: print('Stopping Bitmessage Deamon.') shutdown.doCleanShutdown() - - #TODO: nice function but no one is using this - def getApiAddress(self): - if not BMConfigParser().safeGetBoolean('bitmessagesettings', 'apienabled'): + # .. todo:: nice function but no one is using this + @staticmethod + def getApiAddress(): + """This function returns API address and port""" + if not config.safeGetBoolean( + 'bitmessagesettings', 'apienabled'): return None - address = BMConfigParser().get('bitmessagesettings', 'apiinterface') - port = BMConfigParser().getint('bitmessagesettings', 'apiport') - return {'address':address,'port':port} + address = config.get('bitmessagesettings', 'apiinterface') + port = config.getint('bitmessagesettings', 'apiport') + return {'address': address, 'port': port} def main(): + """Triggers main module""" mainprogram = Main() mainprogram.start() + if __name__ == "__main__": main() diff --git a/src/bitmessageqt/__init__.py b/src/bitmessageqt/__init__.py index bdd2beaf2e..1b1a78858e 100644 --- a/src/bitmessageqt/__init__.py +++ b/src/bitmessageqt/__init__.py @@ -1,64 +1,60 @@ -from debug import logger +""" +PyQt based UI for bitmessage, the main module +""" + +import hashlib +import locale +import os +import random +import string +import subprocess import sys +import textwrap +import threading +import time +from datetime import datetime, timedelta +from sqlite3 import register_adapter -try: - from PyQt4 import QtCore, QtGui - from PyQt4.QtNetwork import QLocalSocket, QLocalServer -except Exception as err: - logmsg = 'PyBitmessage requires PyQt unless you want to run it as a daemon and interact with it using the API. You can download it from http://www.riverbankcomputing.com/software/pyqt/download or by searching Google for \'PyQt Download\' (without quotes).' - logger.critical(logmsg, exc_info=True) - sys.exit() +from PyQt4 import QtCore, QtGui +from PyQt4.QtNetwork import QLocalSocket, QLocalServer +import shared +import state +from debug import logger from tr import _translate +from account import ( + accountClass, getSortedSubscriptions, + BMAccount, GatewayAccount, MailchuckAccount, AccountColor) from addresses import decodeAddress, addBMIfNotPresent -import shared from bitmessageui import Ui_MainWindow -from bmconfigparser import BMConfigParser -import defaults -from namecoin import namecoinConnection +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) -from settings import Ui_settingsDialog + Ui_AddressBookWidgetItemLabel, Ui_AddressBookWidgetItemAddress, + MessageList_TimeWidget) import settingsmixin import support -import locale -import time -import os -import hashlib -from pyelliptic.openssl import OpenSSL -import textwrap -import debug -import random -from sqlite3 import register_adapter -import string -from datetime import datetime, timedelta -from helper_ackPayload import genAckPayload from helper_sql import sqlQuery, sqlExecute, sqlExecuteChunked, sqlStoredProcedure +import helper_addressbook import helper_search import l10n -import openclpow from utils import str_broadcast_subscribers, avatarize -from account import ( - getSortedAccounts, getSortedSubscriptions, accountClass, BMAccount, - GatewayAccount, MailchuckAccount, AccountColor) import dialogs -from helper_generic import powQueueSize from network.stats import pendingDownload, pendingUpload from uisignaler import UISignaler -import knownnodes import paths from proofofwork import getPowType import queues import shutdown -import state from statusbar import BMStatusBar -from network.asyncore_pollchoose import set_rates import sound - +# This is needed for tray icon +import bitmessage_icons_rc # noqa:F401 pylint: disable=unused-import +import helper_sent try: from plugins.plugin import get_plugin, get_plugins @@ -66,59 +62,93 @@ get_plugins = False -def change_translation(newlocale): - global qmytranslator, qsystranslator - try: - if not qmytranslator.isEmpty(): - QtGui.QApplication.removeTranslator(qmytranslator) - except: - pass - try: - if not qsystranslator.isEmpty(): - QtGui.QApplication.removeTranslator(qsystranslator) - except: - pass - - qmytranslator = QtCore.QTranslator() - translationpath = os.path.join (paths.codePath(), 'translations', 'bitmessage_' + newlocale) - qmytranslator.load(translationpath) - QtGui.QApplication.installTranslator(qmytranslator) - - qsystranslator = QtCore.QTranslator() - if paths.frozen: - translationpath = os.path.join (paths.codePath(), 'translations', 'qt_' + newlocale) - else: - translationpath = os.path.join (str(QtCore.QLibraryInfo.location(QtCore.QLibraryInfo.TranslationsPath)), 'qt_' + newlocale) - qsystranslator.load(translationpath) - QtGui.QApplication.installTranslator(qsystranslator) - - lang = locale.normalize(l10n.getTranslationLanguage()) - langs = [lang.split(".")[0] + "." + l10n.encoding, lang.split(".")[0] + "." + 'UTF-8', lang] - if 'win32' in sys.platform or 'win64' in sys.platform: - langs = [l10n.getWindowsLocale(lang)] - for lang in langs: +is_windows = sys.platform.startswith('win') + + +# TODO: rewrite +def powQueueSize(): + """Returns the size of queues.workerQueue including current unfinished work""" + queue_len = queues.workerQueue.qsize() + for thread in threading.enumerate(): try: - l10n.setlocale(locale.LC_ALL, lang) - if 'win32' not in sys.platform and 'win64' not in sys.platform: - l10n.encoding = locale.nl_langinfo(locale.CODESET) - else: - l10n.encoding = locale.getlocale()[1] - logger.info("Successfully set locale to %s", lang) - break - except: - logger.error("Failed to set locale to %s", lang, exc_info=True) + if thread.name == "singleWorker": + queue_len += thread.busy + except Exception as err: + logger.info('Thread error %s', err) + return queue_len -class MyForm(settingsmixin.SMainWindow): +def openKeysFile(): + """Open keys file with an external editor""" + keysfile = os.path.join(state.appdata, 'keys.dat') + if 'linux' in sys.platform: + subprocess.call(["xdg-open", keysfile]) + elif is_windows: + os.startfile(keysfile) # pylint: disable=no-member - # the last time that a message arrival sound was played - lastSoundTime = datetime.now() - timedelta(days=1) + +class MyForm(settingsmixin.SMainWindow): # the maximum frequency of message sounds in seconds maxSoundFrequencySec = 60 REPLY_TYPE_SENDER = 0 REPLY_TYPE_CHAN = 1 + REPLY_TYPE_UPD = 2 + + def change_translation(self, newlocale=None): + """Change translation language for the application""" + if newlocale is None: + newlocale = l10n.getTranslationLanguage() + try: + if not self.qmytranslator.isEmpty(): + QtGui.QApplication.removeTranslator(self.qmytranslator) + except: + pass + try: + if not self.qsystranslator.isEmpty(): + QtGui.QApplication.removeTranslator(self.qsystranslator) + except: + pass + + self.qmytranslator = QtCore.QTranslator() + translationpath = os.path.join( + paths.codePath(), 'translations', 'bitmessage_' + newlocale) + self.qmytranslator.load(translationpath) + QtGui.QApplication.installTranslator(self.qmytranslator) + + self.qsystranslator = QtCore.QTranslator() + if paths.frozen: + translationpath = os.path.join( + paths.codePath(), 'translations', 'qt_' + newlocale) + else: + translationpath = os.path.join( + str(QtCore.QLibraryInfo.location( + QtCore.QLibraryInfo.TranslationsPath)), 'qt_' + newlocale) + self.qsystranslator.load(translationpath) + QtGui.QApplication.installTranslator(self.qsystranslator) + + # TODO: move this block into l10n + # FIXME: shouldn't newlocale be used here? + lang = locale.normalize(l10n.getTranslationLanguage()) + langs = [ + lang.split(".")[0] + "." + l10n.encoding, + lang.split(".")[0] + "." + 'UTF-8', + lang + ] + if 'win32' in sys.platform or 'win64' in sys.platform: + langs = [l10n.getWindowsLocale(lang)] + for lang in langs: + try: + l10n.setlocale(lang) + if 'win32' not in sys.platform and 'win64' not in sys.platform: + l10n.encoding = locale.nl_langinfo(locale.CODESET) + else: + l10n.encoding = locale.getlocale()[1] + logger.info("Successfully set locale to %s", lang) + break + except: + logger.error("Failed to set locale to %s", lang, exc_info=True) def init_file_menu(self): QtCore.QObject.connect(self.ui.actionExit, QtCore.SIGNAL( @@ -135,9 +165,10 @@ def init_file_menu(self): QtCore.SIGNAL( "triggered()"), self.click_actionRegenerateDeterministicAddresses) - QtCore.QObject.connect(self.ui.pushButtonAddChan, QtCore.SIGNAL( - "clicked()"), - self.click_actionJoinChan) # also used for creating chans. + QtCore.QObject.connect( + self.ui.pushButtonAddChan, + QtCore.SIGNAL("clicked()"), + self.click_actionJoinChan) # also used for creating chans. QtCore.QObject.connect(self.ui.pushButtonNewAddress, QtCore.SIGNAL( "clicked()"), self.click_NewAddressDialog) QtCore.QObject.connect(self.ui.pushButtonAddAddressBook, QtCore.SIGNAL( @@ -201,19 +232,19 @@ def init_inbox_popup_menu(self, connectSignal=True): if connectSignal: self.connect(self.ui.tableWidgetInbox, QtCore.SIGNAL( 'customContextMenuRequested(const QPoint&)'), - self.on_context_menuInbox) + self.on_context_menuInbox) self.ui.tableWidgetInboxSubscriptions.setContextMenuPolicy( QtCore.Qt.CustomContextMenu) if connectSignal: self.connect(self.ui.tableWidgetInboxSubscriptions, QtCore.SIGNAL( 'customContextMenuRequested(const QPoint&)'), - self.on_context_menuInbox) + self.on_context_menuInbox) self.ui.tableWidgetInboxChans.setContextMenuPolicy( QtCore.Qt.CustomContextMenu) if connectSignal: self.connect(self.ui.tableWidgetInboxChans, QtCore.SIGNAL( 'customContextMenuRequested(const QPoint&)'), - self.on_context_menuInbox) + self.on_context_menuInbox) def init_identities_popup_menu(self, connectSignal=True): # Popup menu for the Your Identities tab @@ -253,10 +284,12 @@ def init_identities_popup_menu(self, connectSignal=True): if connectSignal: self.connect(self.ui.treeWidgetYourIdentities, QtCore.SIGNAL( 'customContextMenuRequested(const QPoint&)'), - self.on_context_menuYourIdentities) + self.on_context_menuYourIdentities) # load all gui.menu plugins with prefix 'address' self.menu_plugins = {'address': []} + if not get_plugins: + return for plugin in get_plugins('gui.menu', 'address'): try: handler, title = plugin(self) @@ -268,8 +301,6 @@ def init_identities_popup_menu(self, connectSignal=True): )) def init_chan_popup_menu(self, connectSignal=True): - # Popup menu for the Channels tab - self.ui.addressContextMenuToolbar = QtGui.QToolBar() # Actions self.actionNew = self.ui.addressContextMenuToolbar.addAction(_translate( "MainWindow", "New"), self.on_action_YourIdentitiesNew) @@ -290,6 +321,9 @@ def init_chan_popup_menu(self, connectSignal=True): _translate( "MainWindow", "Copy address to clipboard"), self.on_action_Clipboard) + self.actionSend = self.ui.addressContextMenuToolbar.addAction( + _translate("MainWindow", "Send message to this chan"), + self.on_action_Send) self.actionSpecialAddressBehavior = self.ui.addressContextMenuToolbar.addAction( _translate( "MainWindow", "Special address behavior..."), @@ -300,7 +334,7 @@ def init_chan_popup_menu(self, connectSignal=True): if connectSignal: self.connect(self.ui.treeWidgetChans, QtCore.SIGNAL( 'customContextMenuRequested(const QPoint&)'), - self.on_context_menuChan) + self.on_context_menuChan) def init_addressbook_popup_menu(self, connectSignal=True): # Popup menu for the Address Book page @@ -337,11 +371,9 @@ def init_addressbook_popup_menu(self, connectSignal=True): if connectSignal: self.connect(self.ui.tableWidgetAddressBook, QtCore.SIGNAL( 'customContextMenuRequested(const QPoint&)'), - self.on_context_menuAddressBook) + self.on_context_menuAddressBook) def init_subscriptions_popup_menu(self, connectSignal=True): - # Popup menu for the Subscriptions page - self.ui.subscriptionsContextMenuToolbar = QtGui.QToolBar() # Actions self.actionsubscriptionsNew = self.ui.subscriptionsContextMenuToolbar.addAction( _translate("MainWindow", "New"), self.on_action_SubscriptionsNew) @@ -360,16 +392,17 @@ def init_subscriptions_popup_menu(self, connectSignal=True): self.actionsubscriptionsSetAvatar = self.ui.subscriptionsContextMenuToolbar.addAction( _translate("MainWindow", "Set avatar..."), self.on_action_TreeWidgetSetAvatar) + self.actionsubscriptionsSend = self.ui.addressContextMenuToolbar.addAction( + _translate("MainWindow", "Send message to this address"), + self.on_action_Send) self.ui.treeWidgetSubscriptions.setContextMenuPolicy( QtCore.Qt.CustomContextMenu) if connectSignal: self.connect(self.ui.treeWidgetSubscriptions, QtCore.SIGNAL( 'customContextMenuRequested(const QPoint&)'), - self.on_context_menuSubscriptions) + self.on_context_menuSubscriptions) def init_sent_popup_menu(self, connectSignal=True): - # Popup menu for the Sent page - self.ui.sentContextMenuToolbar = QtGui.QToolBar() # Actions self.actionTrashSentMessage = self.ui.sentContextMenuToolbar.addAction( _translate( @@ -381,6 +414,9 @@ def init_sent_popup_menu(self, connectSignal=True): self.actionForceSend = self.ui.sentContextMenuToolbar.addAction( _translate( "MainWindow", "Force send"), self.on_action_ForceSend) + self.actionSentReply = self.ui.sentContextMenuToolbar.addAction( + _translate("MainWindow", "Send update"), + self.on_action_SentReply) # self.popMenuSent = QtGui.QMenu( self ) # self.popMenuSent.addAction( self.actionSentClipboard ) # self.popMenuSent.addAction( self.actionTrashSentMessage ) @@ -395,13 +431,13 @@ def rerenderTabTreeSubscriptions(self): treeWidget.header().setSortIndicator( 0, QtCore.Qt.AscendingOrder) # init dictionary - + db = getSortedSubscriptions(True) for address in db: for folder in folders: - if not folder in db[address]: + if folder not in db[address]: db[address][folder] = {} - + if treeWidget.isSortingEnabled(): treeWidget.setSortingEnabled(False) @@ -413,8 +449,8 @@ def rerenderTabTreeSubscriptions(self): toAddress = widget.address else: toAddress = None - - if not toAddress in db: + + if toAddress not in db: treeWidget.takeTopLevelItem(i) # no increment continue @@ -444,10 +480,16 @@ def rerenderTabTreeSubscriptions(self): widget.setUnreadCount(unread) db.pop(toAddress, None) i += 1 - + i = 0 for toAddress in db: - widget = Ui_SubscriptionWidget(treeWidget, i, toAddress, db[toAddress]["inbox"]['count'], db[toAddress]["inbox"]['label'], db[toAddress]["inbox"]['enabled']) + widget = Ui_SubscriptionWidget( + treeWidget, + i, + toAddress, + db[toAddress]["inbox"]['count'], + db[toAddress]["inbox"]['label'], + db[toAddress]["inbox"]['enabled']) j = 0 unread = 0 for folder in folders: @@ -459,23 +501,22 @@ def rerenderTabTreeSubscriptions(self): j += 1 widget.setUnreadCount(unread) i += 1 - - treeWidget.setSortingEnabled(True) + treeWidget.setSortingEnabled(True) def rerenderTabTreeMessages(self): self.rerenderTabTree('messages') def rerenderTabTreeChans(self): self.rerenderTabTree('chan') - + def rerenderTabTree(self, tab): if tab == 'messages': treeWidget = self.ui.treeWidgetYourIdentities elif tab == 'chan': treeWidget = self.ui.treeWidgetChans folders = Ui_FolderWidget.folderWeight.keys() - + # sort ascending when creating if treeWidget.topLevelItemCount() == 0: treeWidget.header().setSortIndicator( @@ -483,13 +524,13 @@ def rerenderTabTree(self, tab): # init dictionary 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: @@ -502,12 +543,16 @@ def rerenderTabTree(self, tab): db[toAddress] = {} for folder in folders: db[toAddress][folder] = 0 - + enabled[toAddress] = isEnabled # get number of (unread) messages total = 0 - queryreturn = sqlQuery('SELECT toaddress, folder, count(msgid) as cnt FROM inbox WHERE read = 0 GROUP BY toaddress, folder') + queryreturn = sqlQuery( + "SELECT toaddress, folder, count(msgid) as cnt " + "FROM inbox " + "WHERE read = 0 " + "GROUP BY toaddress, folder") for row in queryreturn: toaddress, folder, cnt = row total += cnt @@ -520,10 +565,10 @@ def rerenderTabTree(self, tab): db[None]["sent"] = 0 db[None]["trash"] = 0 enabled[None] = True - + if treeWidget.isSortingEnabled(): treeWidget.setSortingEnabled(False) - + widgets = {} i = 0 while i < treeWidget.topLevelItemCount(): @@ -532,8 +577,8 @@ def rerenderTabTree(self, tab): toAddress = widget.address else: toAddress = None - - if not toAddress in db: + + if toAddress not in db: treeWidget.takeTopLevelItem(i) # no increment continue @@ -542,8 +587,9 @@ def rerenderTabTree(self, tab): while j < widget.childCount(): subwidget = widget.child(j) try: - subwidget.setUnreadCount(db[toAddress][subwidget.folderName]) - if subwidget.folderName not in ["new", "trash", "sent"]: + subwidget.setUnreadCount( + db[toAddress][subwidget.folderName]) + if subwidget.folderName not in ("new", "trash", "sent"): unread += db[toAddress][subwidget.folderName] db[toAddress].pop(subwidget.folderName, None) except: @@ -559,13 +605,13 @@ def rerenderTabTree(self, tab): if toAddress is not None and tab == 'messages' and folder == "new": continue subwidget = Ui_FolderWidget(widget, j, toAddress, f, c) - if subwidget.folderName not in ["new", "trash", "sent"]: + if subwidget.folderName not in ("new", "trash", "sent"): unread += c j += 1 widget.setUnreadCount(unread) db.pop(toAddress, None) i += 1 - + i = 0 for toAddress in db: widget = Ui_AddressWidget(treeWidget, i, toAddress, db[toAddress]["inbox"], enabled[toAddress]) @@ -575,12 +621,12 @@ def rerenderTabTree(self, tab): if toAddress is not None and tab == 'messages' and folder == "new": continue subwidget = Ui_FolderWidget(widget, j, toAddress, folder, db[toAddress][folder]) - if subwidget.folderName not in ["new", "trash", "sent"]: + if subwidget.folderName not in ("new", "trash", "sent"): unread += db[toAddress][folder] j += 1 widget.setUnreadCount(unread) i += 1 - + treeWidget.setSortingEnabled(True) def __init__(self, parent=None): @@ -588,47 +634,41 @@ def __init__(self, parent=None): self.ui = Ui_MainWindow() self.ui.setupUi(self) + self.qmytranslator = self.qsystranslator = None + self.indicatorUpdate = None + self.actionStatus = None + + # the last time that a message arrival sound was played + self.lastSoundTime = datetime.now() - timedelta(days=1) + # 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: displayMsg = _translate( - "MainWindow", "One of your addresses, %1, is an old version 1 address. Version 1 addresses are no longer supported. " - + "May we delete it now?").arg(addressInKeysFile) + "MainWindow", + "One of your addresses, %1, is an old version 1 address. " + "Version 1 addresses are no longer supported. " + "May we delete it now?").arg(addressInKeysFile) reply = QtGui.QMessageBox.question( self, 'Message', displayMsg, QtGui.QMessageBox.Yes, QtGui.QMessageBox.No) if reply == QtGui.QMessageBox.Yes: - BMConfigParser().remove_section(addressInKeysFile) - BMConfigParser().save() + config.remove_section(addressInKeysFile) + config.save() - # Configure Bitmessage to start on startup (or remove the - # configuration) based on the setting in the keys.dat file - if 'win32' in sys.platform or 'win64' in sys.platform: - # Auto-startup for Windows - RUN_PATH = "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" - self.settings = QtCore.QSettings(RUN_PATH, QtCore.QSettings.NativeFormat) - self.settings.remove( - "PyBitmessage") # In case the user moves the program and the registry entry is no longer valid, this will delete the old registry entry. - if BMConfigParser().getboolean('bitmessagesettings', 'startonlogon'): - self.settings.setValue("PyBitmessage", sys.argv[0]) - elif 'darwin' in sys.platform: - # startup for mac - pass - elif 'linux' in sys.platform: - # startup for linux - pass + self.change_translation() # e.g. for editing labels self.recurDepth = 0 - + # switch back to this when replying self.replyFromTab = None # so that quit won't loop - self.quitAccepted = False - + self.wait = self.quitAccepted = False + self.init_file_menu() self.init_inbox_popup_menu() self.init_identities_popup_menu() @@ -718,20 +758,18 @@ def __init__(self, parent=None): QtCore.QObject.connect(self.pushButtonStatusIcon, QtCore.SIGNAL( "clicked()"), self.click_pushButtonStatusIcon) - self.numberOfMessagesProcessed = 0 - self.numberOfBroadcastsProcessed = 0 - self.numberOfPubkeysProcessed = 0 self.unreadCount = 0 # Set the icon sizes for the identicons - identicon_size = 3*7 + identicon_size = 3 * 7 self.ui.tableWidgetInbox.setIconSize(QtCore.QSize(identicon_size, identicon_size)) self.ui.treeWidgetChans.setIconSize(QtCore.QSize(identicon_size, identicon_size)) self.ui.treeWidgetYourIdentities.setIconSize(QtCore.QSize(identicon_size, identicon_size)) self.ui.treeWidgetSubscriptions.setIconSize(QtCore.QSize(identicon_size, identicon_size)) self.ui.tableWidgetAddressBook.setIconSize(QtCore.QSize(identicon_size, identicon_size)) - + self.UISignalThread = UISignaler.get() + QtCore.QObject.connect(self.UISignalThread, QtCore.SIGNAL( "writeNewAddressToTable(PyQt_PyObject,PyQt_PyObject,PyQt_PyObject)"), self.writeNewAddressToTable) QtCore.QObject.connect(self.UISignalThread, QtCore.SIGNAL( @@ -741,9 +779,12 @@ def __init__(self, parent=None): QtCore.QObject.connect(self.UISignalThread, QtCore.SIGNAL( "updateSentItemStatusByAckdata(PyQt_PyObject,PyQt_PyObject)"), self.updateSentItemStatusByAckdata) QtCore.QObject.connect(self.UISignalThread, QtCore.SIGNAL( - "displayNewInboxMessage(PyQt_PyObject,PyQt_PyObject,PyQt_PyObject,PyQt_PyObject,PyQt_PyObject)"), self.displayNewInboxMessage) + "displayNewInboxMessage(PyQt_PyObject,PyQt_PyObject,PyQt_PyObject,PyQt_PyObject,PyQt_PyObject)"), + self.displayNewInboxMessage) QtCore.QObject.connect(self.UISignalThread, QtCore.SIGNAL( - "displayNewSentMessage(PyQt_PyObject,PyQt_PyObject,PyQt_PyObject,PyQt_PyObject,PyQt_PyObject,PyQt_PyObject)"), self.displayNewSentMessage) + "displayNewSentMessage(PyQt_PyObject,PyQt_PyObject,PyQt_PyObject,PyQt_PyObject," + "PyQt_PyObject,PyQt_PyObject)"), + self.displayNewSentMessage) QtCore.QObject.connect(self.UISignalThread, QtCore.SIGNAL( "setStatusIcon(PyQt_PyObject)"), self.setStatusIcon) QtCore.QObject.connect(self.UISignalThread, QtCore.SIGNAL( @@ -769,6 +810,9 @@ def __init__(self, parent=None): self.ui.treeWidgetSubscriptions.keyPressEvent = self.treeWidgetKeyPressEvent self.ui.treeWidgetChans.keyPressEvent = self.treeWidgetKeyPressEvent + # Key press in addressbook + self.ui.tableWidgetAddressBook.keyPressEvent = self.addressbookKeyPressEvent + # Key press in messagelist self.ui.tableWidgetInbox.keyPressEvent = self.tableWidgetKeyPressEvent self.ui.tableWidgetInboxSubscriptions.keyPressEvent = self.tableWidgetKeyPressEvent @@ -784,39 +828,72 @@ def __init__(self, parent=None): self.rerenderComboBoxSendFrom() 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 TTL = 28*24*60*60 self.ui.horizontalSliderTTL.setSliderPosition((TTL - 3600) ** (1/3.199)) self.updateHumanFriendlyTTLDescription(TTL) - + QtCore.QObject.connect(self.ui.horizontalSliderTTL, QtCore.SIGNAL( "valueChanged(int)"), self.updateTTL) self.initSettings() + self.resetNamecoinConnection() + self.sqlInit() + self.indicatorInit() + self.notifierInit() + self.updateStartOnLogon() - self.namecoin = namecoinConnection() + self.ui.updateNetworkSwitchMenuLabel() - # Check to see whether we can connect to namecoin. - # Hide the 'Fetch Namecoin ID' button if we can't. - if BMConfigParser().safeGetBoolean( - 'bitmessagesettings', 'dontconnect' - ) or self.namecoin.test()[0] == 'failed': - logger.warning( - 'There was a problem testing for a Namecoin daemon. Hiding the' - ' Fetch Namecoin ID button') - self.ui.pushButtonFetchNamecoinID.hide() + self._firstrun = config.safeGetBoolean( + 'bitmessagesettings', 'dontconnect') + + self._contact_selected = None + + def getContactSelected(self): + """Returns last selected contact once""" + try: + return self._contact_selected + except AttributeError: + pass + finally: + self._contact_selected = None + + def updateStartOnLogon(self): + """ + Configure Bitmessage to start on startup (or remove the + configuration) based on the setting in the keys.dat file + """ + startonlogon = config.safeGetBoolean( + 'bitmessagesettings', 'startonlogon') + if is_windows: # Auto-startup for Windows + RUN_PATH = "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" + settings = QtCore.QSettings( + RUN_PATH, QtCore.QSettings.NativeFormat) + # In case the user moves the program and the registry entry is + # no longer valid, this will delete the old registry entry. + if startonlogon: + settings.setValue("PyBitmessage", sys.argv[0]) + else: + settings.remove("PyBitmessage") + else: + try: # get desktop plugin if any + self.desktop = get_plugin('desktop')() + self.desktop.adjust_startonlogon(startonlogon) + except (NameError, TypeError): + self.desktop = False 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))) font = QtGui.QFont() @@ -832,7 +909,13 @@ def updateHumanFriendlyTTLDescription(self, TTL): font.setBold(True) else: numberOfDays = int(round(TTL / (24*60*60))) - self.ui.labelHumanFriendlyTTLDescription.setText(_translate("MainWindow", "%n day(s)", None, QtCore.QCoreApplication.CodecForTr, numberOfDays)) + self.ui.labelHumanFriendlyTTLDescription.setText( + _translate( + "MainWindow", + "%n day(s)", + None, + QtCore.QCoreApplication.CodecForTr, + numberOfDays)) font.setBold(False) self.ui.labelHumanFriendlyTTLDescription.setStyleSheet(stylesheet) self.ui.labelHumanFriendlyTTLDescription.setFont(font) @@ -866,7 +949,7 @@ def appIndicatorHide(self): self.appIndicatorShowOrHideWindow() def appIndicatorSwitchQuietMode(self): - BMConfigParser().set( + config.set( 'bitmessagesettings', 'showtraynotifications', str(not self.actionQuiet.isChecked()) ) @@ -926,91 +1009,106 @@ def updateUnreadStatus(self, widget, row, msgid, unread=True): Switch unread for item of msgid and related items in other STableWidgets "All Accounts" and "Chans" """ - related = [self.ui.tableWidgetInbox, self.ui.tableWidgetInboxChans] + status = widget.item(row, 0).unread + if status != unread: + return + + widgets = [self.ui.tableWidgetInbox, self.ui.tableWidgetInboxChans] + rrow = None try: - related.remove(widget) - related = related.pop() + widgets.remove(widget) + related = widgets.pop() except ValueError: - rrow = None - related = [] + pass else: # maybe use instead: # rrow = related.row(msgid), msgid should be QTableWidgetItem # related = related.findItems(msgid, QtCore.Qt.MatchExactly), # returns an empty list - for rrow in xrange(related.rowCount()): - if msgid == str(related.item(rrow, 3).data( - QtCore.Qt.UserRole).toPyObject()): + for rrow in range(related.rowCount()): + if related.item(rrow, 3).data() == msgid: break - else: - rrow = None - status = widget.item(row, 0).unread - if status == unread: - font = QtGui.QFont() - font.setBold(not status) - widget.item(row, 3).setFont(font) - for col in (0, 1, 2): - widget.item(row, col).setUnread(not status) - - try: - related.item(rrow, 3).setFont(font) - except (TypeError, AttributeError): - pass - else: - for col in (0, 1, 2): - related.item(rrow, col).setUnread(not status) - - def propagateUnreadCount(self, address = None, folder = "inbox", widget = None, type = 1): - widgets = [self.ui.treeWidgetYourIdentities, self.ui.treeWidgetSubscriptions, self.ui.treeWidgetChans] - queryReturn = sqlQuery("SELECT toaddress, folder, COUNT(msgid) AS cnt FROM inbox WHERE read = 0 GROUP BY toaddress, folder") + for col in range(widget.columnCount()): + widget.item(row, col).setUnread(not status) + if rrow: + related.item(rrow, col).setUnread(not status) + + # Here we need to update unread count for: + # - all widgets if there is no args + # - All accounts + # - corresponding account if current is "All accounts" + # - current account otherwise + def propagateUnreadCount(self, folder=None, widget=None): + queryReturn = sqlQuery( + 'SELECT toaddress, folder, COUNT(msgid) AS cnt' + ' FROM inbox WHERE read = 0 GROUP BY toaddress, folder') totalUnread = {} normalUnread = {} - for row in queryReturn: - normalUnread[row[0]] = {} - if row[1] in ["trash"]: - continue - normalUnread[row[0]][row[1]] = row[2] - if row[1] in totalUnread: - totalUnread[row[1]] += row[2] - else: - totalUnread[row[1]] = row[2] - queryReturn = sqlQuery("SELECT fromaddress, folder, COUNT(msgid) AS cnt FROM inbox WHERE read = 0 AND toaddress = ? GROUP BY fromaddress, folder", str_broadcast_subscribers) broadcastsUnread = {} - for row in queryReturn: - broadcastsUnread[row[0]] = {} - broadcastsUnread[row[0]][row[1]] = row[2] - + for addr, fld, count in queryReturn: + try: + normalUnread[addr][fld] = count + except KeyError: + normalUnread[addr] = {fld: count} + try: + totalUnread[fld] += count + except KeyError: + totalUnread[fld] = count + if widget in ( + self.ui.treeWidgetSubscriptions, self.ui.treeWidgetChans): + widgets = (self.ui.treeWidgetYourIdentities,) + else: + widgets = ( + self.ui.treeWidgetYourIdentities, + self.ui.treeWidgetSubscriptions, self.ui.treeWidgetChans + ) + queryReturn = sqlQuery( + 'SELECT fromaddress, folder, COUNT(msgid) AS cnt' + ' FROM inbox WHERE read = 0 AND toaddress = ?' + ' GROUP BY fromaddress, folder', str_broadcast_subscribers) + for addr, fld, count in queryReturn: + try: + broadcastsUnread[addr][fld] = count + except KeyError: + broadcastsUnread[addr] = {fld: count} + for treeWidget in widgets: root = treeWidget.invisibleRootItem() for i in range(root.childCount()): addressItem = root.child(i) - newCount = 0 if addressItem.type == AccountMixin.ALL: newCount = sum(totalUnread.itervalues()) self.drawTrayIcon(self.currentTrayIconFileName, newCount) - elif addressItem.type == AccountMixin.SUBSCRIPTION: - if addressItem.address in broadcastsUnread: - newCount = sum(broadcastsUnread[addressItem.address].itervalues()) - elif addressItem.address in normalUnread: - newCount = sum(normalUnread[addressItem.address].itervalues()) + else: + try: + newCount = sum(( + broadcastsUnread + if addressItem.type == AccountMixin.SUBSCRIPTION + else normalUnread + )[addressItem.address].itervalues()) + except KeyError: + newCount = 0 if newCount != addressItem.unreadCount: addressItem.setUnreadCount(newCount) - if addressItem.childCount == 0: - continue for j in range(addressItem.childCount()): folderItem = addressItem.child(j) - newCount = 0 folderName = folderItem.folderName if folderName == "new": folderName = "inbox" - if addressItem.type == AccountMixin.ALL and folderName in totalUnread: - newCount = totalUnread[folderName] - elif addressItem.type == AccountMixin.SUBSCRIPTION: - if addressItem.address in broadcastsUnread and folderName in broadcastsUnread[addressItem.address]: - newCount = broadcastsUnread[addressItem.address][folderName] - elif addressItem.address in normalUnread and folderName in normalUnread[addressItem.address]: - newCount = normalUnread[addressItem.address][folderName] + if folder and folderName != folder: + continue + if addressItem.type == AccountMixin.ALL: + newCount = totalUnread.get(folderName, 0) + else: + try: + newCount = ( + broadcastsUnread + if addressItem.type == AccountMixin.SUBSCRIPTION + else normalUnread + )[addressItem.address][folderName] + except KeyError: + newCount = 0 if newCount != folderItem.unreadCount: folderItem.setUnreadCount(newCount) @@ -1019,43 +1117,46 @@ def addMessageListItem(self, tableWidget, items): if sortingEnabled: tableWidget.setSortingEnabled(False) tableWidget.insertRow(0) - for i in range(len(items)): - tableWidget.setItem(0, i, items[i]) + for i, item in enumerate(items): + tableWidget.setItem(0, i, item) if sortingEnabled: tableWidget.setSortingEnabled(True) - def addMessageListItemSent(self, tableWidget, toAddress, fromAddress, subject, status, ackdata, lastactiontime): - acct = accountClass(fromAddress) - if acct is None: - acct = BMAccount(fromAddress) + def addMessageListItemSent( + self, tableWidget, toAddress, fromAddress, subject, + status, ackdata, lastactiontime + ): + acct = accountClass(fromAddress) or BMAccount(fromAddress) acct.parseMessage(toAddress, fromAddress, subject, "") - items = [] - MessageList_AddressWidget(items, str(toAddress), unicode(acct.toLabel, 'utf-8')) - MessageList_AddressWidget(items, str(fromAddress), unicode(acct.fromLabel, 'utf-8')) - MessageList_SubjectWidget(items, str(subject), unicode(acct.subject, 'utf-8', 'replace')) - if status == 'awaitingpubkey': statusText = _translate( - "MainWindow", "Waiting for their encryption key. Will request it again soon.") + "MainWindow", + "Waiting for their encryption key. Will request it again soon." + ) elif status == 'doingpowforpubkey': statusText = _translate( - "MainWindow", "Doing work necessary to request encryption key.") + "MainWindow", "Doing work necessary to request encryption key." + ) elif status == 'msgqueued': - statusText = _translate( - "MainWindow", "Queued.") + statusText = _translate("MainWindow", "Queued.") elif status == 'msgsent': - statusText = _translate("MainWindow", "Message sent. Waiting for acknowledgement. Sent at %1").arg( - l10n.formatTimestamp(lastactiontime)) + statusText = _translate( + "MainWindow", + "Message sent. Waiting for acknowledgement. Sent at %1" + ).arg(l10n.formatTimestamp(lastactiontime)) elif status == 'msgsentnoackexpected': - statusText = _translate("MainWindow", "Message sent. Sent at %1").arg( - l10n.formatTimestamp(lastactiontime)) + statusText = _translate( + "MainWindow", "Message sent. Sent at %1" + ).arg(l10n.formatTimestamp(lastactiontime)) elif status == 'doingmsgpow': statusText = _translate( "MainWindow", "Doing work necessary to send message.") elif status == 'ackreceived': - statusText = _translate("MainWindow", "Acknowledgement of the message received %1").arg( - l10n.formatTimestamp(lastactiontime)) + statusText = _translate( + "MainWindow", + "Acknowledgement of the message received %1" + ).arg(l10n.formatTimestamp(lastactiontime)) elif status == 'broadcastqueued': statusText = _translate( "MainWindow", "Broadcast queued.") @@ -1066,58 +1167,64 @@ def addMessageListItemSent(self, tableWidget, toAddress, fromAddress, subject, s statusText = _translate("MainWindow", "Broadcast on %1").arg( l10n.formatTimestamp(lastactiontime)) elif status == 'toodifficult': - statusText = _translate("MainWindow", "Problem: The work demanded by the recipient is more difficult than you are willing to do. %1").arg( - l10n.formatTimestamp(lastactiontime)) + statusText = _translate( + "MainWindow", + "Problem: The work demanded by the recipient is more" + " difficult than you are willing to do. %1" + ).arg(l10n.formatTimestamp(lastactiontime)) elif status == 'badkey': - statusText = _translate("MainWindow", "Problem: The recipient\'s encryption key is no good. Could not encrypt message. %1").arg( - l10n.formatTimestamp(lastactiontime)) + statusText = _translate( + "MainWindow", + "Problem: The recipient\'s encryption key is no good." + " Could not encrypt message. %1" + ).arg(l10n.formatTimestamp(lastactiontime)) elif status == 'forcepow': statusText = _translate( - "MainWindow", "Forced difficulty override. Send should start soon.") + "MainWindow", + "Forced difficulty override. Send should start soon.") else: - statusText = _translate("MainWindow", "Unknown status: %1 %2").arg(status).arg( + statusText = _translate( + "MainWindow", "Unknown status: %1 %2").arg(status).arg( l10n.formatTimestamp(lastactiontime)) - newItem = myTableWidgetItem(statusText) - newItem.setToolTip(statusText) - newItem.setData(QtCore.Qt.UserRole, QtCore.QByteArray(ackdata)) - newItem.setData(33, int(lastactiontime)) - newItem.setFlags( - QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) - items.append(newItem) + + items = [ + MessageList_AddressWidget( + toAddress, unicode(acct.toLabel, 'utf-8')), + MessageList_AddressWidget( + fromAddress, unicode(acct.fromLabel, 'utf-8')), + MessageList_SubjectWidget( + str(subject), unicode(acct.subject, 'utf-8', 'replace')), + MessageList_TimeWidget( + statusText, False, lastactiontime, ackdata)] self.addMessageListItem(tableWidget, items) + return acct - def addMessageListItemInbox(self, tableWidget, msgfolder, msgid, toAddress, fromAddress, subject, received, read): - font = QtGui.QFont() - font.setBold(True) + def addMessageListItemInbox( + self, tableWidget, toAddress, fromAddress, subject, + msgid, received, read + ): if toAddress == str_broadcast_subscribers: acct = accountClass(fromAddress) else: - acct = accountClass(toAddress) - if acct is None: - acct = accountClass(fromAddress) + acct = accountClass(toAddress) or accountClass(fromAddress) if acct is None: acct = BMAccount(fromAddress) acct.parseMessage(toAddress, fromAddress, subject, "") - - items = [] - #to - MessageList_AddressWidget(items, toAddress, unicode(acct.toLabel, 'utf-8'), not read) - # from - MessageList_AddressWidget(items, fromAddress, unicode(acct.fromLabel, 'utf-8'), not read) - # subject - MessageList_SubjectWidget(items, str(subject), unicode(acct.subject, 'utf-8', 'replace'), not read) - # time received - time_item = myTableWidgetItem(l10n.formatTimestamp(received)) - time_item.setToolTip(l10n.formatTimestamp(received)) - time_item.setData(QtCore.Qt.UserRole, QtCore.QByteArray(msgid)) - time_item.setData(33, int(received)) - time_item.setFlags( - QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) - if not read: - time_item.setFont(font) - items.append(time_item) + + items = [ + MessageList_AddressWidget( + toAddress, unicode(acct.toLabel, 'utf-8'), not read), + MessageList_AddressWidget( + fromAddress, unicode(acct.fromLabel, 'utf-8'), not read), + MessageList_SubjectWidget( + str(subject), unicode(acct.subject, 'utf-8', 'replace'), + not read), + MessageList_TimeWidget( + l10n.formatTimestamp(received), not read, received, msgid) + ] self.addMessageListItem(tableWidget, items) + return acct # Load Sent items from database @@ -1132,35 +1239,40 @@ def loadSent(self, tableWidget, account, where="", what=""): xAddress = 'both' else: tableWidget.setColumnHidden(0, False) - if account is None: - tableWidget.setColumnHidden(1, False) - else: - tableWidget.setColumnHidden(1, True) + tableWidget.setColumnHidden(1, bool(account)) xAddress = 'fromaddress' - tableWidget.setUpdatesEnabled(False) - tableWidget.setSortingEnabled(False) - tableWidget.setRowCount(0) - queryreturn = helper_search.search_sql(xAddress, account, "sent", where, what, False) + queryreturn = helper_search.search_sql( + xAddress, account, "sent", where, what, False) for row in queryreturn: - toAddress, fromAddress, subject, status, ackdata, lastactiontime = row - self.addMessageListItemSent(tableWidget, toAddress, fromAddress, subject, status, ackdata, lastactiontime) + self.addMessageListItemSent(tableWidget, *row) tableWidget.horizontalHeader().setSortIndicator( 3, QtCore.Qt.DescendingOrder) tableWidget.setSortingEnabled(True) - tableWidget.horizontalHeaderItem(3).setText(_translate("MainWindow", "Sent", None)) + tableWidget.horizontalHeaderItem(3).setText( + _translate("MainWindow", "Sent")) tableWidget.setUpdatesEnabled(True) # Load messages from database file - def loadMessagelist(self, tableWidget, account, folder="inbox", where="", what="", unreadOnly = False): + def loadMessagelist( + self, tableWidget, account, folder="inbox", where="", what="", + unreadOnly=False + ): + tableWidget.setUpdatesEnabled(False) + tableWidget.setSortingEnabled(False) + tableWidget.setRowCount(0) + if folder == 'sent': self.loadSent(tableWidget, account, where, what) return if tableWidget == self.ui.tableWidgetInboxSubscriptions: xAddress = "fromaddress" + if not what: + where = _translate("MainWindow", "To") + what = str_broadcast_subscribers else: xAddress = "toaddress" if account is not None: @@ -1170,21 +1282,21 @@ def loadMessagelist(self, tableWidget, account, folder="inbox", where="", what=" tableWidget.setColumnHidden(0, False) tableWidget.setColumnHidden(1, False) - tableWidget.setUpdatesEnabled(False) - tableWidget.setSortingEnabled(False) - tableWidget.setRowCount(0) + queryreturn = helper_search.search_sql( + xAddress, account, folder, where, what, unreadOnly) - queryreturn = helper_search.search_sql(xAddress, account, folder, where, what, unreadOnly) - for row in queryreturn: - msgfolder, msgid, toAddress, fromAddress, subject, received, read = row - self.addMessageListItemInbox(tableWidget, msgfolder, msgid, toAddress, fromAddress, subject, received, read) + toAddress, fromAddress, subject, _, msgid, received, read = row + self.addMessageListItemInbox( + tableWidget, toAddress, fromAddress, subject, + msgid, received, read) tableWidget.horizontalHeader().setSortIndicator( 3, QtCore.Qt.DescendingOrder) tableWidget.setSortingEnabled(True) tableWidget.selectRow(0) - tableWidget.horizontalHeaderItem(3).setText(_translate("MainWindow", "Received", None)) + tableWidget.horizontalHeaderItem(3).setText( + _translate("MainWindow", "Received")) tableWidget.setUpdatesEnabled(True) # create application indicator @@ -1208,7 +1320,7 @@ def appIndicatorInit(self, app): # show bitmessage self.actionShow = QtGui.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': @@ -1217,7 +1329,7 @@ def appIndicatorInit(self, app): # quiet mode self.actionQuiet = QtGui.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) @@ -1340,9 +1452,11 @@ def _choose_ext(basename): def sqlInit(self): register_adapter(QtCore.QByteArray, str) - # Try init the distro specific appindicator, - # for example the Ubuntu MessagingMenu def indicatorInit(self): + """ + Try init the distro specific appindicator, + for example the Ubuntu MessagingMenu + """ def _noop_update(*args, **kwargs): pass @@ -1391,6 +1505,15 @@ def notifierShow( def treeWidgetKeyPressEvent(self, event): return self.handleKeyPress(event, self.getCurrentTreeWidget()) + # addressbook + def addressbookKeyPressEvent(self, event): + """Handle keypress event in addressbook widget""" + if event.key() == QtCore.Qt.Key_Delete: + self.on_action_AddressBookDelete() + else: + return QtGui.QTableWidget.keyPressEvent( + self.ui.tableWidgetAddressBook, event) + # inbox / sent def tableWidgetKeyPressEvent(self, event): return self.handleKeyPress(event, self.getCurrentMessagelist()) @@ -1399,11 +1522,12 @@ def tableWidgetKeyPressEvent(self, event): def textEditKeyPressEvent(self, event): return self.handleKeyPress(event, self.getCurrentMessageTextedit()) - def handleKeyPress(self, event, focus = None): + def handleKeyPress(self, event, focus=None): + """This method handles keypress events for all widgets on MyForm""" messagelist = self.getCurrentMessagelist() - folder = self.getCurrentFolder() if event.key() == QtCore.Qt.Key_Delete: - if isinstance (focus, MessageView) or isinstance(focus, QtGui.QTableWidget): + if isinstance(focus, (MessageView, QtGui.QTableWidget)): + folder = self.getCurrentFolder() if folder == "sent": self.on_action_SentTrash() else: @@ -1439,17 +1563,18 @@ def handleKeyPress(self, event, focus = None): self.ui.lineEditTo.setFocus() event.ignore() elif event.key() == QtCore.Qt.Key_F: - searchline = self.getCurrentSearchLine(retObj = True) - if searchline: - searchline.setFocus() + try: + self.getCurrentSearchLine(retObj=True).setFocus() + except AttributeError: + pass event.ignore() if not event.isAccepted(): return - if isinstance (focus, MessageView): + if isinstance(focus, MessageView): return MessageView.keyPressEvent(focus, event) - elif isinstance (focus, QtGui.QTableWidget): + if isinstance(focus, QtGui.QTableWidget): return QtGui.QTableWidget.keyPressEvent(focus, event) - elif isinstance (focus, QtGui.QTreeWidget): + if isinstance(focus, QtGui.QTreeWidget): return QtGui.QTreeWidget.keyPressEvent(focus, event) # menu button 'manage keys' @@ -1460,36 +1585,81 @@ def click_actionManageKeys(self): # may manage your keys by editing the keys.dat file stored in # the same directory as this program. It is important that you # back up this file.', QMessageBox.Ok) - reply = QtGui.QMessageBox.information(self, 'keys.dat?', _translate( - "MainWindow", "You may manage your keys by editing the keys.dat file stored in the same directory as this program. It is important that you back up this file."), QtGui.QMessageBox.Ok) + reply = QtGui.QMessageBox.information( + self, + 'keys.dat?', + _translate( + "MainWindow", + "You may manage your keys by editing the keys.dat file stored in the same directory" + "as this program. It is important that you back up this file." + ), + QtGui.QMessageBox.Ok) else: - QtGui.QMessageBox.information(self, 'keys.dat?', _translate( - "MainWindow", "You may manage your keys by editing the keys.dat file stored in\n %1 \nIt is important that you back up this file.").arg(state.appdata), QtGui.QMessageBox.Ok) + QtGui.QMessageBox.information( + self, + 'keys.dat?', + _translate( + "MainWindow", + "You may manage your keys by editing the keys.dat file stored in" + "\n %1 \n" + "It is important that you back up this file." + ).arg(state.appdata), + QtGui.QMessageBox.Ok) elif sys.platform == 'win32' or sys.platform == 'win64': if state.appdata == '': - reply = QtGui.QMessageBox.question(self, _translate("MainWindow", "Open keys.dat?"), _translate( - "MainWindow", "You may manage your keys by editing the keys.dat file stored in the same directory as this program. It is important that you back up this file. Would you like to open the file now? (Be sure to close Bitmessage before making any changes.)"), QtGui.QMessageBox.Yes, QtGui.QMessageBox.No) + reply = QtGui.QMessageBox.question( + self, + _translate("MainWindow", "Open keys.dat?"), + _translate( + "MainWindow", + "You may manage your keys by editing the keys.dat file stored in the same directory as " + "this program. It is important that you back up this file. " + "Would you like to open the file now? " + "(Be sure to close Bitmessage before making any changes.)"), + QtGui.QMessageBox.Yes, + QtGui.QMessageBox.No) else: - reply = QtGui.QMessageBox.question(self, _translate("MainWindow", "Open keys.dat?"), _translate( - "MainWindow", "You may manage your keys by editing the keys.dat file stored in\n %1 \nIt is important that you back up this file. Would you like to open the file now? (Be sure to close Bitmessage before making any changes.)").arg(state.appdata), QtGui.QMessageBox.Yes, QtGui.QMessageBox.No) + reply = QtGui.QMessageBox.question( + self, + _translate("MainWindow", "Open keys.dat?"), + _translate( + "MainWindow", + "You may manage your keys by editing the keys.dat file stored in\n %1 \n" + "It is important that you back up this file. Would you like to open the file now?" + "(Be sure to close Bitmessage before making any changes.)").arg(state.appdata), + QtGui.QMessageBox.Yes, QtGui.QMessageBox.No) if reply == QtGui.QMessageBox.Yes: - shared.openKeysFile() + openKeysFile() # menu button 'delete all treshed messages' def click_actionDeleteAllTrashedMessages(self): - if QtGui.QMessageBox.question(self, _translate("MainWindow", "Delete trash?"), _translate("MainWindow", "Are you sure you want to delete all trashed messages?"), QtGui.QMessageBox.Yes, QtGui.QMessageBox.No) == QtGui.QMessageBox.No: + if QtGui.QMessageBox.question( + self, + _translate("MainWindow", "Delete trash?"), + _translate("MainWindow", "Are you sure you want to delete all trashed messages?"), + QtGui.QMessageBox.Yes, + QtGui.QMessageBox.No) == QtGui.QMessageBox.No: return sqlStoredProcedure('deleteandvacuume') self.rerenderTabTreeMessages() 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): @@ -1547,9 +1717,10 @@ def showConnectDialog(self): dialog = dialogs.ConnectDialog(self) if dialog.exec_(): if dialog.radioButtonConnectNow.isChecked(): - BMConfigParser().remove_option( + self.ui.updateNetworkSwitchMenuLabel(False) + config.remove_option( 'bitmessagesettings', 'dontconnect') - BMConfigParser().save() + config.save() elif dialog.radioButtonConfigureNetwork.isChecked(): self.click_actionSettings() else: @@ -1574,13 +1745,12 @@ def changeEvent(self, event): 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 # Normal/Maximised/FullScreen pass - # QtGui.QWidget.changeEvent(self, event) def __icon_activated(self, reason): if reason == QtGui.QSystemTrayIcon.Trigger: @@ -1591,68 +1761,65 @@ def __icon_activated(self, reason): 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")) - shared.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")) - shared.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")) - shared.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 @@ -1664,7 +1831,7 @@ def setTrayIconFile(self, iconFileName): self.drawTrayIcon(iconFileName, self.findInboxUnreadCount()) def calcTrayIcon(self, iconFileName, inboxUnreadCount): - pixmap = QtGui.QPixmap(":/newPrefix/images/"+iconFileName) + pixmap = QtGui.QPixmap(":/newPrefix/images/" + iconFileName) if inboxUnreadCount > 0: # choose font and calculate font parameters fontName = "Lucida" @@ -1676,7 +1843,8 @@ def calcTrayIcon(self, iconFileName, inboxUnreadCount): rect = fontMetrics.boundingRect(txt) # margins that we add in the top-right corner marginX = 2 - marginY = 0 # it looks like -2 is also ok due to the error of metric + # it looks like -2 is also ok due to the error of metric + marginY = 0 # if it renders too wide we need to change it to a plus symbol if rect.width() > 20: txt = "+" @@ -1716,11 +1884,18 @@ def findInboxUnreadCount(self, count=None): return self.unreadCount def updateSentItemStatusByToAddress(self, toAddress, textToDisplay): - for sent in [self.ui.tableWidgetInbox, self.ui.tableWidgetInboxSubscriptions, self.ui.tableWidgetInboxChans]: + for sent in ( + self.ui.tableWidgetInbox, + self.ui.tableWidgetInboxSubscriptions, + self.ui.tableWidgetInboxChans + ): treeWidget = self.widgetConvert(sent) if self.getCurrentFolder(treeWidget) != "sent": continue - if treeWidget in [self.ui.treeWidgetSubscriptions, self.ui.treeWidgetChans] and self.getCurrentAccount(treeWidget) != toAddress: + if treeWidget in ( + self.ui.treeWidgetSubscriptions, + self.ui.treeWidgetChans + ) and self.getCurrentAccount(treeWidget) != toAddress: continue for i in range(sent.rowCount()): @@ -1729,7 +1904,9 @@ def updateSentItemStatusByToAddress(self, toAddress, textToDisplay): sent.item(i, 3).setToolTip(textToDisplay) try: newlinePosition = textToDisplay.indexOf('\n') - except: # If someone misses adding a "_translate" to a string before passing it to this function, this function won't receive a qstring which will cause an exception. + except: + # If someone misses adding a "_translate" to a string before passing it to this function, + # this function won't receive a qstring which will cause an exception. newlinePosition = 0 if newlinePosition > 1: sent.item(i, 3).setText( @@ -1740,22 +1917,26 @@ def updateSentItemStatusByToAddress(self, toAddress, textToDisplay): def updateSentItemStatusByAckdata(self, ackdata, textToDisplay): if type(ackdata) is str: ackdata = QtCore.QByteArray(ackdata) - for sent in [self.ui.tableWidgetInbox, self.ui.tableWidgetInboxSubscriptions, self.ui.tableWidgetInboxChans]: + for sent in ( + self.ui.tableWidgetInbox, + self.ui.tableWidgetInboxSubscriptions, + self.ui.tableWidgetInboxChans + ): treeWidget = self.widgetConvert(sent) if self.getCurrentFolder(treeWidget) != "sent": continue for i in range(sent.rowCount()): - toAddress = sent.item( - i, 0).data(QtCore.Qt.UserRole) - tableAckdata = sent.item( - i, 3).data(QtCore.Qt.UserRole).toPyObject() + toAddress = sent.item(i, 0).data(QtCore.Qt.UserRole) + tableAckdata = sent.item(i, 3).data() status, addressVersionNumber, streamNumber, ripe = decodeAddress( toAddress) if ackdata == tableAckdata: sent.item(i, 3).setToolTip(textToDisplay) try: newlinePosition = textToDisplay.indexOf('\n') - except: # If someone misses adding a "_translate" to a string before passing it to this function, this function won't receive a qstring which will cause an exception. + except: + # If someone misses adding a "_translate" to a string before passing it to this function, + # this function won't receive a qstring which will cause an exception. newlinePosition = 0 if newlinePosition > 1: sent.item(i, 3).setText( @@ -1763,19 +1944,27 @@ def updateSentItemStatusByAckdata(self, ackdata, textToDisplay): else: sent.item(i, 3).setText(textToDisplay) - def removeInboxRowByMsgid(self, msgid): # msgid and inventoryHash are the same thing - for inbox in ([ + def removeInboxRowByMsgid(self, msgid): + # msgid and inventoryHash are the same thing + for inbox in ( self.ui.tableWidgetInbox, self.ui.tableWidgetInboxSubscriptions, - self.ui.tableWidgetInboxChans]): + self.ui.tableWidgetInboxChans + ): + i = None for i in range(inbox.rowCount()): - if msgid == str(inbox.item(i, 3).data(QtCore.Qt.UserRole).toPyObject()): - self.updateStatusBar( - _translate("MainWindow", "Message trashed")) - treeWidget = self.widgetConvert(inbox) - self.propagateUnreadCount(inbox.item(i, 1 if inbox.item(i, 1).type == AccountMixin.SUBSCRIPTION else 0).data(QtCore.Qt.UserRole), self.getCurrentFolder(treeWidget), treeWidget, 0) - inbox.removeRow(i) + if msgid == inbox.item(i, 3).data(): break + else: + continue + self.updateStatusBar(_translate("MainWindow", "Message trashed")) + treeWidget = self.widgetConvert(inbox) + self.propagateUnreadCount( + # wrong assumption about current folder here: + self.getCurrentFolder(treeWidget), treeWidget + ) + if i: + inbox.removeRow(i) def newVersionAvailable(self, version): self.notifiedNewVersion = ".".join(str(n) for n in version) @@ -1793,12 +1982,16 @@ def displayAlert(self, title, text, exitAfterUserClicksOk): 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() @@ -1827,10 +2020,9 @@ def addRow (address, label, type): label, address = row newRows[address] = [label, 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')): + if (account.type == AccountMixin.CHAN and config.safeGetBoolean(address, 'enabled')): newRows[address] = [account.getLabel(), AccountMixin.CHAN] # normal accounts queryreturn = sqlQuery('SELECT * FROM addressbook') @@ -1839,11 +2031,13 @@ def addRow (address, label, type): newRows[address] = [label, AccountMixin.NORMAL] completerList = [] - for address in sorted(oldRows, key = lambda x: oldRows[x][2], reverse = True): - if address in newRows: - completerList.append(unicode(newRows[address][0], encoding="UTF-8") + " <" + address + ">") - newRows.pop(address) - else: + for address in sorted( + oldRows, key=lambda x: oldRows[x][2], reverse=True + ): + try: + completerList.append( + newRows.pop(address)[0] + " <" + address + ">") + except KeyError: self.ui.tableWidgetAddressBook.removeRow(oldRows[address][2]) for address in newRows: addRow(address, newRows[address][0], newRows[address][1]) @@ -1859,16 +2053,21 @@ def rerenderSubscriptions(self): self.rerenderTabTreeSubscriptions() def click_pushButtonTTL(self): - QtGui.QMessageBox.information(self, 'Time To Live', _translate( - "MainWindow", """The TTL, or Time-To-Live is the length of time that the network will hold the message. - The recipient must get it during this time. If your Bitmessage client does not hear an acknowledgement, it - will resend the message automatically. The longer the Time-To-Live, the - more work your computer must do to send the message. A Time-To-Live of four or five days is often appropriate."""), QtGui.QMessageBox.Ok) + QtGui.QMessageBox.information( + self, + 'Time To Live', + _translate( + "MainWindow", """The TTL, or Time-To-Live is the length of time that the network will hold the message. + The recipient must get it during this time. If your Bitmessage client does not hear an acknowledgement + ,it will resend the message automatically. The longer the Time-To-Live, the + more work your computer must do to send the message. + A Time-To-Live of four or five days is often appropriate."""), + QtGui.QMessageBox.Ok) def click_pushButtonClear(self): self.ui.lineEditSubject.setText("") self.ui.lineEditTo.setText("") - self.ui.textEditMessage.setText("") + self.ui.textEditMessage.reset() self.ui.comboBoxSendFrom.setCurrentIndex(0) def click_pushButtonSend(self): @@ -1916,11 +2115,14 @@ def click_pushButtonSend(self): acct = accountClass(fromAddress) - if sendMessageToPeople: # To send a message to specific people (rather than broadcast) - toAddressesList = [s.strip() - for s in toAddresses.replace(',', ';').split(';')] - toAddressesList = list(set( - toAddressesList)) # remove duplicate addresses. If the user has one address with a BM- and the same address without the BM-, this will not catch it. They'll send the message to the person twice. + # To send a message to specific people (rather than broadcast) + if sendMessageToPeople: + toAddressesList = set([ + s.strip() for s in toAddresses.replace(',', ';').split(';') + ]) + # remove duplicate addresses. If the user has one address + # with a BM- and the same address without the BM-, this will + # not catch it. They'll send the message to the person twice. for toAddress in toAddressesList: if toAddress != '': # label plus address @@ -1933,19 +2135,26 @@ def click_pushButtonSend(self): subject = acct.subject toAddress = acct.toAddress else: - if QtGui.QMessageBox.question(self, "Sending an email?", _translate("MainWindow", - "You are trying to send an email instead of a bitmessage. This requires registering with a gateway. Attempt to register?"), - QtGui.QMessageBox.Yes|QtGui.QMessageBox.No) != QtGui.QMessageBox.Yes: + if QtGui.QMessageBox.question( + self, + "Sending an email?", + _translate( + "MainWindow", + "You are trying to send an email instead of a bitmessage. " + "This requires registering with a gateway. Attempt to register?"), + QtGui.QMessageBox.Yes|QtGui.QMessageBox.No) != QtGui.QMessageBox.Yes: continue email = acct.getLabel() - if email[-14:] != "@mailchuck.com": #attempt register + if email[-14:] != "@mailchuck.com": # attempt register # 12 character random email address - email = ''.join(random.SystemRandom().choice(string.ascii_lowercase) for _ in range(12)) + "@mailchuck.com" + email = ''.join( + random.SystemRandom().choice(string.ascii_lowercase) for _ in range(12) + ) + "@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" @@ -1955,8 +2164,7 @@ def click_pushButtonSend(self): ).arg(email) ) return - status, addressVersionNumber, streamNumber, ripe = decodeAddress( - toAddress) + status, addressVersionNumber, streamNumber = decodeAddress(toAddress)[:3] if status != 'success': try: toAddress = unicode(toAddress, 'utf-8', 'ignore') @@ -2031,15 +2239,27 @@ def click_pushButtonSend(self): toAddress = addBMIfNotPresent(toAddress) if addressVersionNumber > 4 or addressVersionNumber <= 1: - QtGui.QMessageBox.about(self, _translate("MainWindow", "Address version number"), _translate( - "MainWindow", "Concerning the address %1, Bitmessage cannot understand address version numbers of %2. Perhaps upgrade Bitmessage to the latest version.").arg(toAddress).arg(str(addressVersionNumber))) + QtGui.QMessageBox.about( + self, + _translate("MainWindow", "Address version number"), + _translate( + "MainWindow", + "Concerning the address %1, Bitmessage cannot understand address version numbers" + " of %2. Perhaps upgrade Bitmessage to the latest version." + ).arg(toAddress).arg(str(addressVersionNumber))) continue if streamNumber > 1 or streamNumber == 0: - QtGui.QMessageBox.about(self, _translate("MainWindow", "Stream number"), _translate( - "MainWindow", "Concerning the address %1, Bitmessage cannot handle stream numbers of %2. Perhaps upgrade Bitmessage to the latest version.").arg(toAddress).arg(str(streamNumber))) + QtGui.QMessageBox.about( + self, + _translate("MainWindow", "Stream number"), + _translate( + "MainWindow", + "Concerning the address %1, Bitmessage cannot handle stream numbers of %2." + " Perhaps upgrade Bitmessage to the latest version." + ).arg(toAddress).arg(str(streamNumber))) continue self.statusbar.clearMessage() - if shared.statusIconColor == 'red': + if state.statusIconColor == 'red': self.updateStatusBar(_translate( "MainWindow", "Warning: You are currently not connected." @@ -2047,29 +2267,9 @@ def click_pushButtonSend(self): " send the message but it won\'t send until" " you connect.") ) - stealthLevel = BMConfigParser().safeGetInt( - 'bitmessagesettings', 'ackstealthlevel') - ackdata = genAckPayload(streamNumber, stealthLevel) - t = () - sqlExecute( - '''INSERT INTO sent VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''', - '', - toAddress, - ripe, - fromAddress, - subject, - message, - ackdata, - int(time.time()), # sentTime (this will never change) - int(time.time()), # lastActionTime - 0, # sleepTill time. This will get set when the POW gets done. - 'msgqueued', - 0, # retryNumber - 'sent', # folder - encoding, # encodingtype - BMConfigParser().getint('bitmessagesettings', 'ttl') - ) - + ackdata = helper_sent.insert( + toAddress=toAddress, fromAddress=fromAddress, + subject=subject, message=message, encoding=encoding) toLabel = '' queryreturn = sqlQuery('''select label from addressbook where address=?''', toAddress) @@ -2081,10 +2281,7 @@ def click_pushButtonSend(self): toAddress, toLabel, fromAddress, subject, message, ackdata) queues.workerQueue.put(('sendmessage', toAddress)) - self.ui.comboBoxSendFrom.setCurrentIndex(0) - self.ui.lineEditTo.setText('') - self.ui.lineEditSubject.setText('') - self.ui.textEditMessage.reset() + self.click_pushButtonClear() if self.replyFromTab is not None: self.ui.tabWidget.setCurrentIndex(self.replyFromTab) self.replyFromTab = None @@ -2106,31 +2303,16 @@ def click_pushButtonSend(self): # We don't actually need the ackdata for acknowledgement since # this is a broadcast message, but we can use it to update the # user interface when the POW is done generating. - streamNumber = decodeAddress(fromAddress)[2] - ackdata = genAckPayload(streamNumber, 0) toAddress = str_broadcast_subscribers - ripe = '' - t = ('', # msgid. We don't know what this will be until the POW is done. - toAddress, - ripe, - fromAddress, - subject, - message, - ackdata, - int(time.time()), # sentTime (this will never change) - int(time.time()), # lastActionTime - 0, # sleepTill time. This will get set when the POW gets done. - 'broadcastqueued', - 0, # retryNumber - 'sent', # folder - encoding, # encoding type - BMConfigParser().getint('bitmessagesettings', 'ttl') - ) - sqlExecute( - '''INSERT INTO sent VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''', *t) + + # msgid. We don't know what this will be until the POW is done. + ackdata = helper_sent.insert( + fromAddress=fromAddress, + subject=subject, message=message, + status='broadcastqueued', encoding=encoding) toLabel = str_broadcast_subscribers - + self.displayNewSentMessage( toAddress, toLabel, fromAddress, subject, message, ackdata) @@ -2159,9 +2341,8 @@ def click_pushButtonLoadFromAddressBook(self): )) def click_pushButtonFetchNamecoinID(self): - nc = namecoinConnection() identities = str(self.ui.lineEditTo.text().toUtf8()).split(";") - err, addr = nc.query(identities[-1].strip()) + err, addr = self.namecoin.query(identities[-1].strip()) if err is not None: self.updateStatusBar( _translate("MainWindow", "Error: %1").arg(err)) @@ -2177,18 +2358,20 @@ def setBroadcastEnablementDependingOnWhetherThisIsAMailingListAddress(self, addr 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(addressInKeysFile, 'mailinglist') + 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 = unicode(BMConfigParser().get(addressInKeysFile, 'label'), 'utf-8', 'ignore').strip() + label = unicode(config.get(addressInKeysFile, 'label'), 'utf-8', 'ignore').strip() if label == "": label = addressInKeysFile self.ui.comboBoxSendFrom.addItem(avatarize(addressInKeysFile), label, addressInKeysFile) @@ -2207,12 +2390,12 @@ def rerenderComboBoxSendFrom(self): 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 = unicode(BMConfigParser().get(addressInKeysFile, 'label'), 'utf-8', 'ignore').strip() + label = unicode(config.get(addressInKeysFile, 'label'), 'utf-8', 'ignore').strip() if label == "": label = addressInKeysFile self.ui.comboBoxSendFromBroadcast.addItem(avatarize(addressInKeysFile), label, addressInKeysFile) @@ -2232,53 +2415,88 @@ def rerenderComboBoxSendFromBroadcast(self): # receives a message to an address that is acting as a # pseudo-mailing-list. The message will be broadcast out. This function # puts the message on the 'Sent' tab. - def displayNewSentMessage(self, toAddress, toLabel, fromAddress, subject, message, ackdata): + def displayNewSentMessage( + self, toAddress, toLabel, fromAddress, subject, + message, ackdata): acct = accountClass(fromAddress) acct.parseMessage(toAddress, fromAddress, subject, message) tab = -1 - for sent in [self.ui.tableWidgetInbox, self.ui.tableWidgetInboxSubscriptions, self.ui.tableWidgetInboxChans]: + for sent in ( + self.ui.tableWidgetInbox, + self.ui.tableWidgetInboxSubscriptions, + self.ui.tableWidgetInboxChans + ): tab += 1 if tab == 1: tab = 2 treeWidget = self.widgetConvert(sent) if self.getCurrentFolder(treeWidget) != "sent": continue - if treeWidget == self.ui.treeWidgetYourIdentities and self.getCurrentAccount(treeWidget) not in (fromAddress, None, False): + if treeWidget == self.ui.treeWidgetYourIdentities \ + and self.getCurrentAccount(treeWidget) not in ( + fromAddress, None, False): continue - elif treeWidget in [self.ui.treeWidgetSubscriptions, self.ui.treeWidgetChans] and self.getCurrentAccount(treeWidget) != toAddress: + elif treeWidget in ( + self.ui.treeWidgetSubscriptions, + self.ui.treeWidgetChans + ) and self.getCurrentAccount(treeWidget) != toAddress: continue - elif not helper_search.check_match(toAddress, fromAddress, subject, message, self.getCurrentSearchOption(tab), self.getCurrentSearchLine(tab)): + elif not helper_search.check_match( + toAddress, fromAddress, subject, message, + self.getCurrentSearchOption(tab), + self.getCurrentSearchLine(tab) + ): continue - - self.addMessageListItemSent(sent, toAddress, fromAddress, subject, "msgqueued", ackdata, time.time()) - self.getAccountTextedit(acct).setPlainText(unicode(message, 'utf-8', 'replace')) + + self.addMessageListItemSent( + sent, toAddress, fromAddress, subject, + "msgqueued", ackdata, time.time()) + self.getAccountTextedit(acct).setPlainText(message) sent.setCurrentCell(0, 0) - def displayNewInboxMessage(self, inventoryHash, toAddress, fromAddress, subject, message): - if toAddress == str_broadcast_subscribers: - acct = accountClass(fromAddress) - else: - acct = accountClass(toAddress) + def displayNewInboxMessage( + self, inventoryHash, toAddress, fromAddress, subject, message): + acct = accountClass( + fromAddress if toAddress == str_broadcast_subscribers + else toAddress + ) inbox = self.getAccountMessagelist(acct) - ret = None + ret = treeWidget = None tab = -1 - for treeWidget in [self.ui.treeWidgetYourIdentities, self.ui.treeWidgetSubscriptions, self.ui.treeWidgetChans]: + for treeWidget in ( + self.ui.treeWidgetYourIdentities, + self.ui.treeWidgetSubscriptions, + self.ui.treeWidgetChans + ): tab += 1 if tab == 1: tab = 2 - tableWidget = self.widgetConvert(treeWidget) - if not helper_search.check_match(toAddress, fromAddress, subject, message, self.getCurrentSearchOption(tab), self.getCurrentSearchLine(tab)): + if not helper_search.check_match( + toAddress, fromAddress, subject, message, + self.getCurrentSearchOption(tab), + self.getCurrentSearchLine(tab) + ): continue - if tableWidget == inbox and self.getCurrentAccount(treeWidget) == acct.address and self.getCurrentFolder(treeWidget) in ["inbox", None]: - ret = self.addMessageListItemInbox(inbox, "inbox", inventoryHash, toAddress, fromAddress, subject, time.time(), 0) - elif treeWidget == self.ui.treeWidgetYourIdentities and self.getCurrentAccount(treeWidget) is None and self.getCurrentFolder(treeWidget) in ["inbox", "new", None]: - ret = self.addMessageListItemInbox(tableWidget, "inbox", inventoryHash, toAddress, fromAddress, subject, time.time(), 0) + tableWidget = self.widgetConvert(treeWidget) + current_account = self.getCurrentAccount(treeWidget) + current_folder = self.getCurrentFolder(treeWidget) + # pylint: disable=too-many-boolean-expressions + if ((tableWidget == inbox + and current_account == acct.address + and current_folder in ("inbox", None)) + or (treeWidget == self.ui.treeWidgetYourIdentities + and current_account is None + and current_folder in ("inbox", "new", None))): + ret = self.addMessageListItemInbox( + tableWidget, toAddress, fromAddress, subject, + inventoryHash, time.time(), False) + if ret is None: acct.parseMessage(toAddress, fromAddress, subject, "") else: acct = ret - self.propagateUnreadCount(acct.address) - if BMConfigParser().getboolean( + self.propagateUnreadCount(widget=treeWidget if ret else None) + if config.safeGetBoolean( 'bitmessagesettings', 'showtraynotifications'): self.notifierShow( _translate("MainWindow", "New Message"), @@ -2286,16 +2504,22 @@ def displayNewInboxMessage(self, inventoryHash, toAddress, fromAddress, subject, unicode(acct.fromLabel, 'utf-8')), sound.SOUND_UNKNOWN ) - if self.getCurrentAccount() is not None and ((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 + if self.getCurrentAccount() is not None and ( + (self.getCurrentFolder(treeWidget) != "inbox" + and self.getCurrentFolder(treeWidget) is not None) + or self.getCurrentAccount(treeWidget) != acct.address): + # Ubuntu should notify of new message irrespective of # whether it's in current message list or not self.indicatorUpdate(True, to_label=acct.toLabel) - # cannot find item to pass here ): - if hasattr(acct, "feedback") \ - and acct.feedback != GatewayAccount.ALL_OK: - if acct.feedback == GatewayAccount.REGISTRATION_DENIED: - dialogs.EmailGatewayDialog( - self, BMConfigParser(), acct).exec_() + + try: + if acct.feedback != GatewayAccount.ALL_OK: + if acct.feedback == GatewayAccount.REGISTRATION_DENIED: + dialogs.EmailGatewayDialog( + self, config, acct).exec_() + # possible other branches? + except AttributeError: + pass def click_pushButtonAddAddressBook(self, dialog=None): if not dialog: @@ -2318,15 +2542,15 @@ def click_pushButtonAddAddressBook(self, dialog=None): )) return - self.addEntryToAddressBook(address, label) - - def addEntryToAddressBook(self, address, label): - if shared.isAddressInMyAddressBook(address): - return - sqlExecute('''INSERT INTO addressbook VALUES (?,?)''', label, address) - self.rerenderMessagelistFromLabels() - self.rerenderMessagelistToLabels() - self.rerenderAddressBook() + if helper_addressbook.insert(label=label, address=address): + self.rerenderMessagelistFromLabels() + self.rerenderMessagelistToLabels() + self.rerenderAddressBook() + else: + self.updateStatusBar(_translate( + "MainWindow", + "Error: You cannot add your own address in the address book." + )) def addSubscription(self, address, label): # This should be handled outside of this function, for error displaying @@ -2374,7 +2598,7 @@ def click_pushButtonAddSubscription(self): )) def click_pushButtonStatusIcon(self): - dialogs.IconGlossaryDialog(self, config=BMConfigParser()).exec_() + dialogs.IconGlossaryDialog(self, config=config).exec_() def click_actionHelp(self): dialogs.HelpDialog(self).exec_() @@ -2386,230 +2610,25 @@ def click_actionAbout(self): dialogs.AboutDialog(self).exec_() def click_actionSettings(self): - self.settingsDialogInstance = settingsDialog(self) - if self._firstrun: - self.settingsDialogInstance.ui.tabWidgetSettings.setCurrentIndex(1) - if self.settingsDialogInstance.exec_(): - if self._firstrun: - BMConfigParser().remove_option( - 'bitmessagesettings', 'dontconnect') - BMConfigParser().set('bitmessagesettings', 'startonlogon', str( - self.settingsDialogInstance.ui.checkBoxStartOnLogon.isChecked())) - BMConfigParser().set('bitmessagesettings', 'minimizetotray', str( - self.settingsDialogInstance.ui.checkBoxMinimizeToTray.isChecked())) - BMConfigParser().set('bitmessagesettings', 'trayonclose', str( - self.settingsDialogInstance.ui.checkBoxTrayOnClose.isChecked())) - BMConfigParser().set('bitmessagesettings', 'hidetrayconnectionnotifications', str( - self.settingsDialogInstance.ui.checkBoxHideTrayConnectionNotifications.isChecked())) - BMConfigParser().set('bitmessagesettings', 'showtraynotifications', str( - self.settingsDialogInstance.ui.checkBoxShowTrayNotifications.isChecked())) - BMConfigParser().set('bitmessagesettings', 'startintray', str( - self.settingsDialogInstance.ui.checkBoxStartInTray.isChecked())) - BMConfigParser().set('bitmessagesettings', 'willinglysendtomobile', str( - self.settingsDialogInstance.ui.checkBoxWillinglySendToMobile.isChecked())) - BMConfigParser().set('bitmessagesettings', 'useidenticons', str( - self.settingsDialogInstance.ui.checkBoxUseIdenticons.isChecked())) - BMConfigParser().set('bitmessagesettings', 'replybelow', str( - self.settingsDialogInstance.ui.checkBoxReplyBelow.isChecked())) - - lang = str(self.settingsDialogInstance.ui.languageComboBox.itemData(self.settingsDialogInstance.ui.languageComboBox.currentIndex()).toString()) - BMConfigParser().set('bitmessagesettings', 'userlocale', lang) - change_translation(l10n.getTranslationLanguage()) - - if int(BMConfigParser().get('bitmessagesettings', 'port')) != int(self.settingsDialogInstance.ui.lineEditTCPPort.text()): - if not BMConfigParser().safeGetBoolean('bitmessagesettings', 'dontconnect'): - QtGui.QMessageBox.about(self, _translate("MainWindow", "Restart"), _translate( - "MainWindow", "You must restart Bitmessage for the port number change to take effect.")) - BMConfigParser().set('bitmessagesettings', 'port', str( - self.settingsDialogInstance.ui.lineEditTCPPort.text())) - if self.settingsDialogInstance.ui.checkBoxUPnP.isChecked() != BMConfigParser().safeGetBoolean('bitmessagesettings', 'upnp'): - BMConfigParser().set('bitmessagesettings', 'upnp', str(self.settingsDialogInstance.ui.checkBoxUPnP.isChecked())) - if self.settingsDialogInstance.ui.checkBoxUPnP.isChecked(): - import upnp - upnpThread = upnp.uPnPThread() - upnpThread.start() - #print 'self.settingsDialogInstance.ui.comboBoxProxyType.currentText()', self.settingsDialogInstance.ui.comboBoxProxyType.currentText() - #print 'self.settingsDialogInstance.ui.comboBoxProxyType.currentText())[0:5]', self.settingsDialogInstance.ui.comboBoxProxyType.currentText()[0:5] - if BMConfigParser().get('bitmessagesettings', 'socksproxytype') == 'none' and self.settingsDialogInstance.ui.comboBoxProxyType.currentText()[0:5] == 'SOCKS': - if shared.statusIconColor != 'red': - QtGui.QMessageBox.about(self, _translate("MainWindow", "Restart"), _translate( - "MainWindow", "Bitmessage will use your proxy from now on but you may want to manually restart Bitmessage now to close existing connections (if any).")) - if BMConfigParser().get('bitmessagesettings', 'socksproxytype')[0:5] == 'SOCKS' and self.settingsDialogInstance.ui.comboBoxProxyType.currentText()[0:5] != 'SOCKS': - self.statusbar.clearMessage() - state.resetNetworkProtocolAvailability() # just in case we changed something in the network connectivity - if self.settingsDialogInstance.ui.comboBoxProxyType.currentText()[0:5] == 'SOCKS': - BMConfigParser().set('bitmessagesettings', 'socksproxytype', str( - self.settingsDialogInstance.ui.comboBoxProxyType.currentText())) - else: - BMConfigParser().set('bitmessagesettings', 'socksproxytype', 'none') - BMConfigParser().set('bitmessagesettings', 'socksauthentication', str( - self.settingsDialogInstance.ui.checkBoxAuthentication.isChecked())) - BMConfigParser().set('bitmessagesettings', 'sockshostname', str( - self.settingsDialogInstance.ui.lineEditSocksHostname.text())) - BMConfigParser().set('bitmessagesettings', 'socksport', str( - self.settingsDialogInstance.ui.lineEditSocksPort.text())) - BMConfigParser().set('bitmessagesettings', 'socksusername', str( - self.settingsDialogInstance.ui.lineEditSocksUsername.text())) - BMConfigParser().set('bitmessagesettings', 'sockspassword', str( - self.settingsDialogInstance.ui.lineEditSocksPassword.text())) - BMConfigParser().set('bitmessagesettings', 'sockslisten', str( - self.settingsDialogInstance.ui.checkBoxSocksListen.isChecked())) - try: - # Rounding to integers just for aesthetics - BMConfigParser().set('bitmessagesettings', 'maxdownloadrate', str( - int(float(self.settingsDialogInstance.ui.lineEditMaxDownloadRate.text())))) - BMConfigParser().set('bitmessagesettings', 'maxuploadrate', str( - int(float(self.settingsDialogInstance.ui.lineEditMaxUploadRate.text())))) - except ValueError: - QtGui.QMessageBox.about(self, _translate("MainWindow", "Number needed"), _translate( - "MainWindow", "Your maximum download and upload rate must be numbers. Ignoring what you typed.")) - else: - set_rates(BMConfigParser().safeGetInt("bitmessagesettings", "maxdownloadrate"), - BMConfigParser().safeGetInt("bitmessagesettings", "maxuploadrate")) - - BMConfigParser().set('bitmessagesettings', 'maxoutboundconnections', str( - int(float(self.settingsDialogInstance.ui.lineEditMaxOutboundConnections.text())))) - - BMConfigParser().set('bitmessagesettings', 'namecoinrpctype', - self.settingsDialogInstance.getNamecoinType()) - BMConfigParser().set('bitmessagesettings', 'namecoinrpchost', str( - self.settingsDialogInstance.ui.lineEditNamecoinHost.text())) - BMConfigParser().set('bitmessagesettings', 'namecoinrpcport', str( - self.settingsDialogInstance.ui.lineEditNamecoinPort.text())) - BMConfigParser().set('bitmessagesettings', 'namecoinrpcuser', str( - self.settingsDialogInstance.ui.lineEditNamecoinUser.text())) - BMConfigParser().set('bitmessagesettings', 'namecoinrpcpassword', str( - self.settingsDialogInstance.ui.lineEditNamecoinPassword.text())) - - # Demanded difficulty tab - if float(self.settingsDialogInstance.ui.lineEditTotalDifficulty.text()) >= 1: - BMConfigParser().set('bitmessagesettings', 'defaultnoncetrialsperbyte', str(int(float( - self.settingsDialogInstance.ui.lineEditTotalDifficulty.text()) * defaults.networkDefaultProofOfWorkNonceTrialsPerByte))) - if float(self.settingsDialogInstance.ui.lineEditSmallMessageDifficulty.text()) >= 1: - BMConfigParser().set('bitmessagesettings', 'defaultpayloadlengthextrabytes', str(int(float( - self.settingsDialogInstance.ui.lineEditSmallMessageDifficulty.text()) * defaults.networkDefaultPayloadLengthExtraBytes))) - - if self.settingsDialogInstance.ui.comboBoxOpenCL.currentText().toUtf8() != BMConfigParser().safeGet("bitmessagesettings", "opencl"): - BMConfigParser().set('bitmessagesettings', 'opencl', str(self.settingsDialogInstance.ui.comboBoxOpenCL.currentText())) - queues.workerQueue.put(('resetPoW', '')) - - acceptableDifficultyChanged = False - - if float(self.settingsDialogInstance.ui.lineEditMaxAcceptableTotalDifficulty.text()) >= 1 or float(self.settingsDialogInstance.ui.lineEditMaxAcceptableTotalDifficulty.text()) == 0: - if BMConfigParser().get('bitmessagesettings','maxacceptablenoncetrialsperbyte') != str(int(float( - self.settingsDialogInstance.ui.lineEditMaxAcceptableTotalDifficulty.text()) * defaults.networkDefaultProofOfWorkNonceTrialsPerByte)): - # the user changed the max acceptable total difficulty - acceptableDifficultyChanged = True - BMConfigParser().set('bitmessagesettings', 'maxacceptablenoncetrialsperbyte', str(int(float( - self.settingsDialogInstance.ui.lineEditMaxAcceptableTotalDifficulty.text()) * defaults.networkDefaultProofOfWorkNonceTrialsPerByte))) - if float(self.settingsDialogInstance.ui.lineEditMaxAcceptableSmallMessageDifficulty.text()) >= 1 or float(self.settingsDialogInstance.ui.lineEditMaxAcceptableSmallMessageDifficulty.text()) == 0: - if BMConfigParser().get('bitmessagesettings','maxacceptablepayloadlengthextrabytes') != str(int(float( - self.settingsDialogInstance.ui.lineEditMaxAcceptableSmallMessageDifficulty.text()) * defaults.networkDefaultPayloadLengthExtraBytes)): - # the user changed the max acceptable small message difficulty - acceptableDifficultyChanged = True - BMConfigParser().set('bitmessagesettings', 'maxacceptablepayloadlengthextrabytes', str(int(float( - self.settingsDialogInstance.ui.lineEditMaxAcceptableSmallMessageDifficulty.text()) * defaults.networkDefaultPayloadLengthExtraBytes))) - if acceptableDifficultyChanged: - # It might now be possible to send msgs which were previously marked as toodifficult. - # Let us change them to 'msgqueued'. The singleWorker will try to send them and will again - # mark them as toodifficult if the receiver's required difficulty is still higher than - # we are willing to do. - sqlExecute('''UPDATE sent SET status='msgqueued' WHERE status='toodifficult' ''') - queues.workerQueue.put(('sendmessage', '')) - - #start:UI setting to stop trying to send messages after X days/months - # I'm open to changing this UI to something else if someone has a better idea. - if ((self.settingsDialogInstance.ui.lineEditDays.text()=='') and (self.settingsDialogInstance.ui.lineEditMonths.text()=='')):#We need to handle this special case. Bitmessage has its default behavior. The input is blank/blank - BMConfigParser().set('bitmessagesettings', 'stopresendingafterxdays', '') - BMConfigParser().set('bitmessagesettings', 'stopresendingafterxmonths', '') - shared.maximumLengthOfTimeToBotherResendingMessages = float('inf') - try: - float(self.settingsDialogInstance.ui.lineEditDays.text()) - lineEditDaysIsValidFloat = True - except: - lineEditDaysIsValidFloat = False - try: - float(self.settingsDialogInstance.ui.lineEditMonths.text()) - lineEditMonthsIsValidFloat = True - except: - lineEditMonthsIsValidFloat = False - if lineEditDaysIsValidFloat and not lineEditMonthsIsValidFloat: - self.settingsDialogInstance.ui.lineEditMonths.setText("0") - if lineEditMonthsIsValidFloat and not lineEditDaysIsValidFloat: - self.settingsDialogInstance.ui.lineEditDays.setText("0") - if lineEditDaysIsValidFloat or lineEditMonthsIsValidFloat: - if (float(self.settingsDialogInstance.ui.lineEditDays.text()) >=0 and float(self.settingsDialogInstance.ui.lineEditMonths.text()) >=0): - shared.maximumLengthOfTimeToBotherResendingMessages = (float(str(self.settingsDialogInstance.ui.lineEditDays.text())) * 24 * 60 * 60) + (float(str(self.settingsDialogInstance.ui.lineEditMonths.text())) * (60 * 60 * 24 *365)/12) - if shared.maximumLengthOfTimeToBotherResendingMessages < 432000: # If the time period is less than 5 hours, we give zero values to all fields. No message will be sent again. - QtGui.QMessageBox.about(self, _translate("MainWindow", "Will not resend ever"), _translate( - "MainWindow", "Note that the time limit you entered is less than the amount of time Bitmessage waits for the first resend attempt therefore your messages will never be resent.")) - BMConfigParser().set('bitmessagesettings', 'stopresendingafterxdays', '0') - BMConfigParser().set('bitmessagesettings', 'stopresendingafterxmonths', '0') - shared.maximumLengthOfTimeToBotherResendingMessages = 0 - else: - BMConfigParser().set('bitmessagesettings', 'stopresendingafterxdays', str(float( - self.settingsDialogInstance.ui.lineEditDays.text()))) - BMConfigParser().set('bitmessagesettings', 'stopresendingafterxmonths', str(float( - self.settingsDialogInstance.ui.lineEditMonths.text()))) - - BMConfigParser().save() - - if 'win32' in sys.platform or 'win64' in sys.platform: - # Auto-startup for Windows - RUN_PATH = "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" - self.settings = QtCore.QSettings(RUN_PATH, QtCore.QSettings.NativeFormat) - if BMConfigParser().getboolean('bitmessagesettings', 'startonlogon'): - self.settings.setValue("PyBitmessage", sys.argv[0]) - else: - self.settings.remove("PyBitmessage") - elif 'darwin' in sys.platform: - # startup for mac - pass - elif 'linux' in sys.platform: - # startup for linux - pass + dialogs.SettingsDialog(self, firstrun=self._firstrun).exec_() - if state.appdata != paths.lookupExeFolder() and self.settingsDialogInstance.ui.checkBoxPortableMode.isChecked(): # If we are NOT using portable mode now but the user selected that we should... - # Write the keys.dat file to disk in the new location - sqlStoredProcedure('movemessagstoprog') - with open(paths.lookupExeFolder() + 'keys.dat', 'wb') as configfile: - BMConfigParser().write(configfile) - # Write the knownnodes.dat file to disk in the new location - knownnodes.saveKnownNodes(paths.lookupExeFolder()) - os.remove(state.appdata + 'keys.dat') - os.remove(state.appdata + 'knownnodes.dat') - previousAppdataLocation = state.appdata - state.appdata = paths.lookupExeFolder() - debug.restartLoggingInUpdatedAppdataLocation() - try: - os.remove(previousAppdataLocation + 'debug.log') - os.remove(previousAppdataLocation + 'debug.log.1') - except: - pass - - if state.appdata == paths.lookupExeFolder() and not self.settingsDialogInstance.ui.checkBoxPortableMode.isChecked(): # If we ARE using portable mode now but the user selected that we shouldn't... - state.appdata = paths.lookupAppdataFolder() - if not os.path.exists(state.appdata): - os.makedirs(state.appdata) - sqlStoredProcedure('movemessagstoappdata') - # Write the keys.dat file to disk in the new location - BMConfigParser().save() - # Write the knownnodes.dat file to disk in the new location - knownnodes.saveKnownNodes(state.appdata) - os.remove(paths.lookupExeFolder() + 'keys.dat') - os.remove(paths.lookupExeFolder() + 'knownnodes.dat') - debug.restartLoggingInUpdatedAppdataLocation() - try: - os.remove(paths.lookupExeFolder() + 'debug.log') - os.remove(paths.lookupExeFolder() + 'debug.log.1') - except: - pass + def on_action_Send(self): + """Send message to current selected address""" + self.click_pushButtonClear() + account_item = self.getCurrentItem() + if not account_item: + return + self.ui.lineEditTo.setText(account_item.accountString()) + self.ui.tabWidget.setCurrentIndex( + self.ui.tabWidget.indexOf(self.ui.send) + ) def on_action_SpecialAddressBehaviorDialog(self): - dialogs.SpecialAddressBehaviorDialog(self, BMConfigParser()) + """Show SpecialAddressBehaviorDialog""" + 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_() try: @@ -2647,45 +2666,48 @@ def on_action_MarkAllRead(self): ), QtGui.QMessageBox.Yes | QtGui.QMessageBox.No ) != QtGui.QMessageBox.Yes: return - # addressAtCurrentRow = self.getCurrentAccount() tableWidget = self.getCurrentMessagelist() idCount = tableWidget.rowCount() if idCount == 0: return - font = QtGui.QFont() - font.setBold(False) - msgids = [] for i in range(0, idCount): - msgids.append(str(tableWidget.item( - i, 3).data(QtCore.Qt.UserRole).toPyObject())) - tableWidget.item(i, 0).setUnread(False) - tableWidget.item(i, 1).setUnread(False) - tableWidget.item(i, 2).setUnread(False) - tableWidget.item(i, 3).setFont(font) + msgids.append(tableWidget.item(i, 3).data()) + for col in xrange(tableWidget.columnCount()): + tableWidget.item(i, col).setUnread(False) markread = sqlExecuteChunked( - "UPDATE %s SET read = 1 WHERE %s IN({0}) AND read=0" % ( - ('sent', 'ackdata') if self.getCurrentFolder() == 'sent' - else ('inbox', 'msgid') - ), idCount, *msgids + "UPDATE inbox SET read = 1 WHERE msgid IN({0}) AND read=0", + idCount, *msgids ) if markread > 0: self.propagateUnreadCount() - # addressAtCurrentRow, self.getCurrentFolder(), None, 0) def click_NewAddressDialog(self): dialogs.NewAddressDialog(self) def network_switch(self): - dontconnect_option = not BMConfigParser().safeGetBoolean( + dontconnect_option = not config.safeGetBoolean( 'bitmessagesettings', 'dontconnect') - BMConfigParser().set( + reply = QtGui.QMessageBox.question( + self, _translate("MainWindow", "Disconnecting") + if dontconnect_option else _translate("MainWindow", "Connecting"), + _translate( + "MainWindow", + "Bitmessage will now drop all connections. Are you sure?" + ) if dontconnect_option else _translate( + "MainWindow", + "Bitmessage will now start connecting to network. Are you sure?" + ), QtGui.QMessageBox.Yes | QtGui.QMessageBox.Cancel, + QtGui.QMessageBox.Cancel) + if reply != QtGui.QMessageBox.Yes: + return + config.set( 'bitmessagesettings', 'dontconnect', str(dontconnect_option)) - BMConfigParser().save() + config.save() self.ui.updateNetworkSwitchMenuLabel(dontconnect_option) self.ui.pushButtonFetchNamecoinID.setHidden( @@ -2694,15 +2716,8 @@ def network_switch(self): # Quit selected from menu or application indicator def quit(self): - '''quit_msg = "Are you sure you want to exit Bitmessage?" - reply = QtGui.QMessageBox.question(self, 'Message', - quit_msg, QtGui.QMessageBox.Yes, QtGui.QMessageBox.No) - - if reply is QtGui.QMessageBox.No: - return - ''' - - if self.quitAccepted: + """Quit the bitmessageqt application""" + if self.quitAccepted and not self.wait: return self.show() @@ -2715,33 +2730,60 @@ def quit(self): # C PoW currently doesn't support interrupting and OpenCL is untested if getPowType() == "python" and (powQueueSize() > 0 or pendingUpload() > 0): - reply = QtGui.QMessageBox.question(self, _translate("MainWindow", "Proof of work pending"), - _translate("MainWindow", "%n object(s) pending proof of work", None, QtCore.QCoreApplication.CodecForTr, powQueueSize()) + ", " + - _translate("MainWindow", "%n object(s) waiting to be distributed", None, QtCore.QCoreApplication.CodecForTr, pendingUpload()) + "\n\n" + - _translate("MainWindow", "Wait until these tasks finish?"), - QtGui.QMessageBox.Yes|QtGui.QMessageBox.No|QtGui.QMessageBox.Cancel, QtGui.QMessageBox.Cancel) + reply = QtGui.QMessageBox.question( + self, _translate("MainWindow", "Proof of work pending"), + _translate( + "MainWindow", + "%n object(s) pending proof of work", None, + QtCore.QCoreApplication.CodecForTr, powQueueSize() + ) + ", " + + _translate( + "MainWindow", + "%n object(s) waiting to be distributed", None, + QtCore.QCoreApplication.CodecForTr, pendingUpload() + ) + "\n\n" + + _translate( + "MainWindow", "Wait until these tasks finish?"), + QtGui.QMessageBox.Yes | QtGui.QMessageBox.No + | QtGui.QMessageBox.Cancel, QtGui.QMessageBox.Cancel) if reply == QtGui.QMessageBox.No: waitForPow = False elif reply == QtGui.QMessageBox.Cancel: return if pendingDownload() > 0: - reply = QtGui.QMessageBox.question(self, _translate("MainWindow", "Synchronisation pending"), - _translate("MainWindow", "Bitmessage hasn't synchronised with the network, %n object(s) to be downloaded. If you quit now, it may cause delivery delays. Wait until the synchronisation finishes?", None, QtCore.QCoreApplication.CodecForTr, pendingDownload()), - QtGui.QMessageBox.Yes|QtGui.QMessageBox.No|QtGui.QMessageBox.Cancel, QtGui.QMessageBox.Cancel) + reply = QtGui.QMessageBox.question( + self, _translate("MainWindow", "Synchronisation pending"), + _translate( + "MainWindow", + "Bitmessage hasn't synchronised with the network," + " %n object(s) to be downloaded. If you quit now," + " it may cause delivery delays. Wait until the" + " synchronisation finishes?", None, + QtCore.QCoreApplication.CodecForTr, pendingDownload() + ), + QtGui.QMessageBox.Yes | QtGui.QMessageBox.No + | QtGui.QMessageBox.Cancel, QtGui.QMessageBox.Cancel) if reply == QtGui.QMessageBox.Yes: - waitForSync = True + self.wait = waitForSync = True elif reply == QtGui.QMessageBox.Cancel: return - if shared.statusIconColor == 'red' and not BMConfigParser().safeGetBoolean( + if state.statusIconColor == 'red' and not config.safeGetBoolean( 'bitmessagesettings', 'dontconnect'): - reply = QtGui.QMessageBox.question(self, _translate("MainWindow", "Not connected"), - _translate("MainWindow", "Bitmessage isn't connected to the network. If you quit now, it may cause delivery delays. Wait until connected and the synchronisation finishes?"), - QtGui.QMessageBox.Yes|QtGui.QMessageBox.No|QtGui.QMessageBox.Cancel, QtGui.QMessageBox.Cancel) + reply = QtGui.QMessageBox.question( + self, _translate("MainWindow", "Not connected"), + _translate( + "MainWindow", + "Bitmessage isn't connected to the network. If you" + " quit now, it may cause delivery delays. Wait until" + " connected and the synchronisation finishes?" + ), + QtGui.QMessageBox.Yes | QtGui.QMessageBox.No + | QtGui.QMessageBox.Cancel, QtGui.QMessageBox.Cancel) if reply == QtGui.QMessageBox.Yes: waitForConnection = True - waitForSync = True + self.wait = waitForSync = True elif reply == QtGui.QMessageBox.Cancel: return @@ -2753,13 +2795,15 @@ def quit(self): if waitForConnection: self.updateStatusBar(_translate( "MainWindow", "Waiting for network connection...")) - while shared.statusIconColor == 'red': + while state.statusIconColor == 'red': time.sleep(0.5) QtCore.QCoreApplication.processEvents( QtCore.QEventLoop.AllEvents, 1000 ) - # this probably will not work correctly, because there is a delay between the status icon turning red and inventory exchange, but it's better than nothing. + # this probably will not work correctly, because there is a delay + # between the status icon turning red and inventory exchange, + # but it's better than nothing. if waitForSync: self.updateStatusBar(_translate( "MainWindow", "Waiting for finishing synchronisation...")) @@ -2781,9 +2825,8 @@ def quit(self): if curWorkerQueue > 0: self.updateStatusBar(_translate( "MainWindow", "Waiting for PoW to finish... %1%" - ).arg(50 * (maxWorkerQueue - curWorkerQueue) - / maxWorkerQueue) - ) + ).arg(50 * (maxWorkerQueue - curWorkerQueue) / + maxWorkerQueue)) time.sleep(0.5) QtCore.QCoreApplication.processEvents( QtCore.QEventLoop.AllEvents, 1000 @@ -2806,13 +2849,12 @@ def quit(self): self.updateStatusBar(_translate( "MainWindow", "Waiting for objects to be sent... %1%").arg(50)) maxPendingUpload = max(1, pendingUpload()) - + while pendingUpload() > 1: self.updateStatusBar(_translate( "MainWindow", "Waiting for objects to be sent... %1%" - ).arg(int(50 + 20 * (pendingUpload()/maxPendingUpload))) - ) + ).arg(int(50 + 20 * (pendingUpload() / maxPendingUpload)))) time.sleep(0.5) QtCore.QCoreApplication.processEvents( QtCore.QEventLoop.AllEvents, 1000 @@ -2845,37 +2887,30 @@ def quit(self): QtCore.QEventLoop.AllEvents, 1000 ) shutdown.doCleanShutdown() + self.updateStatusBar(_translate( "MainWindow", "Stopping notifications... %1%").arg(90)) self.tray.hide() self.updateStatusBar(_translate( "MainWindow", "Shutdown imminent... %1%").arg(100)) - shared.thisapp.cleanup() + logger.info("Shutdown complete") - super(MyForm, myapp).close() - # return - os._exit(0) + self.close() + # FIXME: rewrite loops with timer instead + if self.wait: + self.destroy() + app.quit() - # window close event def closeEvent(self, event): - self.appIndicatorHide() - trayonclose = False - - try: - trayonclose = BMConfigParser().getboolean( - 'bitmessagesettings', 'trayonclose') - except Exception: - pass - - # always ignore, it shuts down by itself - if self.quitAccepted: - event.accept() - return - + """window close event""" event.ignore() - if not trayonclose: - # quit the application + trayonclose = config.safeGetBoolean( + 'bitmessagesettings', 'trayonclose') + if trayonclose: + self.appIndicatorHide() + else: + # custom quit method self.quit() def on_action_InboxMessageForceHtml(self): @@ -2914,8 +2949,7 @@ def on_action_InboxMarkUnread(self): # modified = 0 for row in tableWidget.selectedIndexes(): currentRow = row.row() - msgid = str(tableWidget.item( - currentRow, 3).data(QtCore.Qt.UserRole).toPyObject()) + msgid = tableWidget.item(currentRow, 3).data() msgids.add(msgid) # if not tableWidget.item(currentRow, 0).unread: # modified += 1 @@ -2930,26 +2964,24 @@ def on_action_InboxMarkUnread(self): ) self.propagateUnreadCount() - # if rowcount == 1: - # # performance optimisation - # self.propagateUnreadCount(tableWidget.item(currentRow, 1 if tableWidget.item(currentRow, 1).type == AccountMixin.SUBSCRIPTION else 0).data(QtCore.Qt.UserRole), self.getCurrentFolder()) - # else: - # self.propagateUnreadCount(tableWidget.item(currentRow, 1 if tableWidget.item(currentRow, 1).type == AccountMixin.SUBSCRIPTION else 0).data(QtCore.Qt.UserRole), self.getCurrentFolder(), self.getCurrentTreeWidget(), 0) - # tableWidget.selectRow(currentRow + 1) - # This doesn't de-select the last message if you try to mark it unread, but that doesn't interfere. Might not be necessary. - # We could also select upwards, but then our problem would be with the topmost message. - # tableWidget.clearSelection() manages to mark the message as read again. + # tableWidget.selectRow(currentRow + 1) + # This doesn't de-select the last message if you try to mark it + # unread, but that doesn't interfere. Might not be necessary. + # We could also select upwards, but then our problem would be + # with the topmost message. + # tableWidget.clearSelection() manages to mark the message + # as read again. # Format predefined text on message reply. def quoted_text(self, message): - if not BMConfigParser().safeGetBoolean('bitmessagesettings', 'replybelow'): - return '\n\n------------------------------------------------------\n' + message - - quoteWrapper = textwrap.TextWrapper(replace_whitespace = False, - initial_indent = '> ', - subsequent_indent = '> ', - break_long_words = False, - break_on_hyphens = False) + if not config.safeGetBoolean('bitmessagesettings', 'replybelow'): + return '\n\n------------------------------------------------------\n' + message + + quoteWrapper = textwrap.TextWrapper( + replace_whitespace=False, initial_indent='> ', + subsequent_indent='> ', break_long_words=False, + break_on_hyphens=False) + def quote_line(line): # Do quote empty lines. if line == '' or line.isspace(): @@ -2962,65 +2994,91 @@ def quote_line(line): return quoteWrapper.fill(line) return '\n'.join([quote_line(l) for l in message.splitlines()]) + '\n\n' - def setSendFromComboBox(self, address = None): + def setSendFromComboBox(self, address=None): if address is None: messagelist = self.getCurrentMessagelist() - if messagelist: - currentInboxRow = messagelist.currentRow() - address = messagelist.item( - currentInboxRow, 0).address - for box in [self.ui.comboBoxSendFrom, self.ui.comboBoxSendFromBroadcast]: - listOfAddressesInComboBoxSendFrom = [str(box.itemData(i).toPyObject()) for i in range(box.count())] - if address in listOfAddressesInComboBoxSendFrom: - currentIndex = listOfAddressesInComboBoxSendFrom.index(address) - box.setCurrentIndex(currentIndex) + if not messagelist: + return + currentInboxRow = messagelist.currentRow() + address = messagelist.item(currentInboxRow, 0).address + for box in ( + self.ui.comboBoxSendFrom, self.ui.comboBoxSendFromBroadcast + ): + for i in range(box.count()): + if str(box.itemData(i).toPyObject()) == address: + box.setCurrentIndex(i) + break else: box.setCurrentIndex(0) def on_action_InboxReplyChan(self): self.on_action_InboxReply(self.REPLY_TYPE_CHAN) - - def on_action_InboxReply(self, replyType = None): + + def on_action_SentReply(self): + self.on_action_InboxReply(self.REPLY_TYPE_UPD) + + def on_action_InboxReply(self, reply_type=None): + """Handle any reply action depending on reply_type""" + # pylint: disable=too-many-locals tableWidget = self.getCurrentMessagelist() if not tableWidget: return - - if replyType is None: - replyType = self.REPLY_TYPE_SENDER - + + if reply_type is None: + reply_type = self.REPLY_TYPE_SENDER + # save this to return back after reply is done self.replyFromTab = self.ui.tabWidget.currentIndex() - + + column_to = 1 if reply_type == self.REPLY_TYPE_UPD else 0 + column_from = 0 if reply_type == self.REPLY_TYPE_UPD else 1 + currentInboxRow = tableWidget.currentRow() toAddressAtCurrentInboxRow = tableWidget.item( - currentInboxRow, 0).address + currentInboxRow, column_to).address acct = accountClass(toAddressAtCurrentInboxRow) fromAddressAtCurrentInboxRow = tableWidget.item( - currentInboxRow, 1).address - msgid = str(tableWidget.item( - currentInboxRow, 3).data(QtCore.Qt.UserRole).toPyObject()) + currentInboxRow, column_from).address + msgid = tableWidget.item(currentInboxRow, 3).data() queryreturn = sqlQuery( - '''select message from inbox where msgid=?''', msgid) + "SELECT message FROM inbox WHERE msgid=?", msgid + ) or sqlQuery("SELECT message FROM sent WHERE ackdata=?", msgid) if queryreturn != []: for row in queryreturn: messageAtCurrentInboxRow, = row - acct.parseMessage(toAddressAtCurrentInboxRow, fromAddressAtCurrentInboxRow, tableWidget.item(currentInboxRow, 2).subject, messageAtCurrentInboxRow) + acct.parseMessage( + toAddressAtCurrentInboxRow, fromAddressAtCurrentInboxRow, + tableWidget.item(currentInboxRow, 2).subject, + messageAtCurrentInboxRow) widget = { 'subject': self.ui.lineEditSubject, 'from': self.ui.comboBoxSendFrom, 'message': self.ui.textEditMessage } + if toAddressAtCurrentInboxRow == str_broadcast_subscribers: self.ui.tabWidgetSend.setCurrentIndex( self.ui.tabWidgetSend.indexOf(self.ui.sendDirect) ) # toAddressAtCurrentInboxRow = fromAddressAtCurrentInboxRow - elif not BMConfigParser().has_section(toAddressAtCurrentInboxRow): - QtGui.QMessageBox.information(self, _translate("MainWindow", "Address is gone"), _translate( - "MainWindow", "Bitmessage cannot find your address %1. Perhaps you removed it?").arg(toAddressAtCurrentInboxRow), QtGui.QMessageBox.Ok) - elif not BMConfigParser().getboolean(toAddressAtCurrentInboxRow, 'enabled'): - QtGui.QMessageBox.information(self, _translate("MainWindow", "Address disabled"), _translate( - "MainWindow", "Error: The address from which you are trying to send is disabled. You\'ll have to enable it on the \'Your Identities\' tab before using it."), QtGui.QMessageBox.Ok) + elif not config.has_section(toAddressAtCurrentInboxRow): + QtGui.QMessageBox.information( + self, _translate("MainWindow", "Address is gone"), + _translate( + "MainWindow", + "Bitmessage cannot find your address %1. Perhaps you" + " removed it?" + ).arg(toAddressAtCurrentInboxRow), QtGui.QMessageBox.Ok) + elif not config.getboolean( + toAddressAtCurrentInboxRow, 'enabled'): + QtGui.QMessageBox.information( + self, _translate("MainWindow", "Address disabled"), + _translate( + "MainWindow", + "Error: The address from which you are trying to send" + " is disabled. You\'ll have to enable it on the" + " \'Your Identities\' tab before using it." + ), QtGui.QMessageBox.Ok) else: self.setBroadcastEnablementDependingOnWhetherThisIsAMailingListAddress(toAddressAtCurrentInboxRow) broadcast_tab_index = self.ui.tabWidgetSend.indexOf( @@ -3034,28 +3092,42 @@ def on_action_InboxReply(self, replyType = None): } self.ui.tabWidgetSend.setCurrentIndex(broadcast_tab_index) toAddressAtCurrentInboxRow = fromAddressAtCurrentInboxRow - if fromAddressAtCurrentInboxRow == tableWidget.item(currentInboxRow, 1).label or ( - isinstance(acct, GatewayAccount) and fromAddressAtCurrentInboxRow == acct.relayAddress): + if fromAddressAtCurrentInboxRow == \ + tableWidget.item(currentInboxRow, column_from).label or ( + isinstance(acct, GatewayAccount) and + fromAddressAtCurrentInboxRow == acct.relayAddress): self.ui.lineEditTo.setText(str(acct.fromAddress)) else: - self.ui.lineEditTo.setText(tableWidget.item(currentInboxRow, 1).label + " <" + str(acct.fromAddress) + ">") - - # If the previous message was to a chan then we should send our reply to the chan rather than to the particular person who sent the message. - if acct.type == AccountMixin.CHAN and replyType == self.REPLY_TYPE_CHAN: - logger.debug('original sent to a chan. Setting the to address in the reply to the chan address.') - if toAddressAtCurrentInboxRow == tableWidget.item(currentInboxRow, 0).label: + self.ui.lineEditTo.setText( + tableWidget.item(currentInboxRow, column_from).accountString() + ) + + # If the previous message was to a chan then we should send our + # reply to the chan rather than to the particular person who sent + # the message. + if acct.type == AccountMixin.CHAN and reply_type == self.REPLY_TYPE_CHAN: + logger.debug( + 'Original sent to a chan. Setting the to address in the' + ' reply to the chan address.') + if toAddressAtCurrentInboxRow == \ + tableWidget.item(currentInboxRow, column_to).label: self.ui.lineEditTo.setText(str(toAddressAtCurrentInboxRow)) else: - self.ui.lineEditTo.setText(tableWidget.item(currentInboxRow, 0).label + " <" + str(acct.toAddress) + ">") - + self.ui.lineEditTo.setText( + tableWidget.item(currentInboxRow, column_to).accountString() + ) + self.setSendFromComboBox(toAddressAtCurrentInboxRow) - - quotedText = self.quoted_text(unicode(messageAtCurrentInboxRow, 'utf-8', 'replace')) + + quotedText = self.quoted_text( + unicode(messageAtCurrentInboxRow, 'utf-8', 'replace')) widget['message'].setPlainText(quotedText) - if acct.subject[0:3] in ['Re:', 'RE:']: - widget['subject'].setText(tableWidget.item(currentInboxRow, 2).label) + if acct.subject[0:3] in ('Re:', 'RE:'): + widget['subject'].setText( + tableWidget.item(currentInboxRow, 2).label) else: - widget['subject'].setText('Re: ' + tableWidget.item(currentInboxRow, 2).label) + widget['subject'].setText( + 'Re: ' + tableWidget.item(currentInboxRow, 2).label) self.ui.tabWidget.setCurrentIndex( self.ui.tabWidget.indexOf(self.ui.send) ) @@ -3066,7 +3138,6 @@ def on_action_InboxAddSenderToAddressBook(self): if not tableWidget: return currentInboxRow = tableWidget.currentRow() - # tableWidget.item(currentRow,1).data(Qt.UserRole).toPyObject() addressAtCurrentInboxRow = tableWidget.item( currentInboxRow, 1).data(QtCore.Qt.UserRole) self.ui.tabWidget.setCurrentIndex( @@ -3080,7 +3151,6 @@ def on_action_InboxAddSenderToBlackList(self): if not tableWidget: return currentInboxRow = tableWidget.currentRow() - # tableWidget.item(currentRow,1).data(Qt.UserRole).toPyObject() addressAtCurrentInboxRow = tableWidget.item( currentInboxRow, 1).data(QtCore.Qt.UserRole) recipientAddress = tableWidget.item( @@ -3089,7 +3159,8 @@ def on_action_InboxAddSenderToBlackList(self): queryreturn = sqlQuery('''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) @@ -3104,23 +3175,28 @@ def on_action_InboxAddSenderToBlackList(self): "Error: You cannot add the same address to your blacklist" " twice. Try renaming the existing one if you want.")) - def deleteRowFromMessagelist(self, row = None, inventoryHash = None, ackData = None, messageLists = None): + def deleteRowFromMessagelist( + self, row=None, inventoryHash=None, ackData=None, messageLists=None + ): if messageLists is None: - messageLists = (self.ui.tableWidgetInbox, self.ui.tableWidgetInboxChans, self.ui.tableWidgetInboxSubscriptions) + messageLists = ( + self.ui.tableWidgetInbox, + self.ui.tableWidgetInboxChans, + self.ui.tableWidgetInboxSubscriptions + ) elif type(messageLists) not in (list, tuple): - messageLists = (messageLists) + messageLists = (messageLists,) for messageList in messageLists: if row is not None: - inventoryHash = str(messageList.item(row, 3).data( - QtCore.Qt.UserRole).toPyObject()) + inventoryHash = messageList.item(row, 3).data() messageList.removeRow(row) elif inventoryHash is not None: for i in range(messageList.rowCount() - 1, -1, -1): - if messageList.item(i, 3).data(QtCore.Qt.UserRole).toPyObject() == inventoryHash: + if messageList.item(i, 3).data() == inventoryHash: messageList.removeRow(i) elif ackData is not None: for i in range(messageList.rowCount() - 1, -1, -1): - if messageList.item(i, 3).data(QtCore.Qt.UserRole).toPyObject() == ackData: + if messageList.item(i, 3).data() == ackData: messageList.removeRow(i) # Send item on the Inbox tab to trash @@ -3130,32 +3206,30 @@ def on_action_InboxTrash(self): return currentRow = 0 folder = self.getCurrentFolder() - shifted = QtGui.QApplication.queryKeyboardModifiers() & QtCore.Qt.ShiftModifier - tableWidget.setUpdatesEnabled(False); - inventoryHashesToTrash = [] + shifted = QtGui.QApplication.queryKeyboardModifiers() \ + & QtCore.Qt.ShiftModifier + tableWidget.setUpdatesEnabled(False) + inventoryHashesToTrash = set() # ranges in reversed order - for r in sorted(tableWidget.selectedRanges(), key=lambda r: r.topRow())[::-1]: - for i in range(r.bottomRow()-r.topRow()+1): - inventoryHashToTrash = str(tableWidget.item( - r.topRow()+i, 3).data(QtCore.Qt.UserRole).toPyObject()) - if inventoryHashToTrash in inventoryHashesToTrash: - continue - inventoryHashesToTrash.append(inventoryHashToTrash) + for r in sorted( + tableWidget.selectedRanges(), key=lambda r: r.topRow() + )[::-1]: + for i in range(r.bottomRow() - r.topRow() + 1): + inventoryHashesToTrash.add( + tableWidget.item(r.topRow() + i, 3).data()) currentRow = r.topRow() self.getCurrentMessageTextedit().setText("") - tableWidget.model().removeRows(r.topRow(), r.bottomRow()-r.topRow()+1) + tableWidget.model().removeRows( + r.topRow(), r.bottomRow() - r.topRow() + 1) idCount = len(inventoryHashesToTrash) - if folder == "trash" or shifted: - sqlExecuteChunked('''DELETE FROM inbox WHERE msgid IN ({0})''', - idCount, *inventoryHashesToTrash) - else: - sqlExecuteChunked('''UPDATE inbox SET folder='trash' WHERE msgid IN ({0})''', - idCount, *inventoryHashesToTrash) + sqlExecuteChunked( + ("DELETE FROM inbox" if folder == "trash" or shifted else + "UPDATE inbox SET folder='trash', read=1") + + " WHERE msgid IN ({0})", idCount, *inventoryHashesToTrash) tableWidget.selectRow(0 if currentRow == 0 else currentRow - 1) tableWidget.setUpdatesEnabled(True) - self.propagateUnreadCount(self.getCurrentAccount, folder) - self.updateStatusBar(_translate( - "MainWindow", "Moved items to trash.")) + self.propagateUnreadCount(folder) + self.updateStatusBar(_translate("MainWindow", "Moved items to trash.")) def on_action_TrashUndelete(self): tableWidget = self.getCurrentMessagelist() @@ -3163,28 +3237,26 @@ def on_action_TrashUndelete(self): return currentRow = 0 tableWidget.setUpdatesEnabled(False) - inventoryHashesToTrash = [] + inventoryHashesToTrash = set() # ranges in reversed order - for r in sorted(tableWidget.selectedRanges(), key=lambda r: r.topRow())[::-1]: - for i in range(r.bottomRow()-r.topRow()+1): - inventoryHashToTrash = str(tableWidget.item( - r.topRow()+i, 3).data(QtCore.Qt.UserRole).toPyObject()) - if inventoryHashToTrash in inventoryHashesToTrash: - continue - inventoryHashesToTrash.append(inventoryHashToTrash) + for r in sorted( + tableWidget.selectedRanges(), key=lambda r: r.topRow() + )[::-1]: + for i in range(r.bottomRow() - r.topRow() + 1): + inventoryHashesToTrash.add( + tableWidget.item(r.topRow() + i, 3).data()) currentRow = r.topRow() self.getCurrentMessageTextedit().setText("") - tableWidget.model().removeRows(r.topRow(), r.bottomRow()-r.topRow()+1) - if currentRow == 0: - tableWidget.selectRow(currentRow) - else: - tableWidget.selectRow(currentRow - 1) + tableWidget.model().removeRows( + r.topRow(), r.bottomRow() - r.topRow() + 1) + tableWidget.selectRow(0 if currentRow == 0 else currentRow - 1) idCount = len(inventoryHashesToTrash) - sqlExecuteChunked('''UPDATE inbox SET folder='inbox' WHERE msgid IN({0})''', - idCount, *inventoryHashesToTrash) + sqlExecuteChunked( + "UPDATE inbox SET folder='inbox' WHERE msgid IN({0})", + idCount, *inventoryHashesToTrash) tableWidget.selectRow(0 if currentRow == 0 else currentRow - 1) tableWidget.setUpdatesEnabled(True) - self.propagateUnreadCount(self.getCurrentAccount) + self.propagateUnreadCount() self.updateStatusBar(_translate("MainWindow", "Undeleted item.")) def on_action_InboxSaveMessageAs(self): @@ -3199,8 +3271,7 @@ def on_action_InboxSaveMessageAs(self): subjectAtCurrentInboxRow = '' # Retrieve the message data out of the SQL database - msgid = str(tableWidget.item( - currentInboxRow, 3).data(QtCore.Qt.UserRole).toPyObject()) + msgid = tableWidget.item(currentInboxRow, 3).data() queryreturn = sqlQuery( '''select message from inbox where msgid=?''', msgid) if queryreturn != []: @@ -3208,7 +3279,11 @@ def on_action_InboxSaveMessageAs(self): message, = row defaultFilename = "".join(x for x in subjectAtCurrentInboxRow if x.isalnum()) + '.txt' - filename = QtGui.QFileDialog.getSaveFileName(self, _translate("MainWindow","Save As..."), defaultFilename, "Text files (*.txt);;All files (*.*)") + filename = QtGui.QFileDialog.getSaveFileName( + self, + _translate("MainWindow","Save As..."), + defaultFilename, + "Text files (*.txt);;All files (*.*)") if filename == '': return try: @@ -3221,23 +3296,19 @@ def on_action_InboxSaveMessageAs(self): # Send item on the Sent tab to trash def on_action_SentTrash(self): - currentRow = 0 - unread = False tableWidget = self.getCurrentMessagelist() if not tableWidget: return folder = self.getCurrentFolder() - shifted = (QtGui.QApplication.queryKeyboardModifiers() & QtCore.Qt.ShiftModifier) > 0 + shifted = QtGui.QApplication.queryKeyboardModifiers() & QtCore.Qt.ShiftModifier while tableWidget.selectedIndexes() != []: currentRow = tableWidget.selectedIndexes()[0].row() - ackdataToTrash = str(tableWidget.item( - currentRow, 3).data(QtCore.Qt.UserRole).toPyObject()) - if folder == "trash" or shifted: - sqlExecute('''DELETE FROM sent WHERE ackdata=?''', ackdataToTrash) - else: - sqlExecute('''UPDATE sent SET folder='trash' WHERE ackdata=?''', ackdataToTrash) - if tableWidget.item(currentRow, 0).unread: - self.propagateUnreadCount(tableWidget.item(currentRow, 1 if tableWidget.item(currentRow, 1).type == AccountMixin.SUBSCRIPTION else 0).data(QtCore.Qt.UserRole), folder, self.getCurrentTreeWidget(), -1) + ackdataToTrash = tableWidget.item(currentRow, 3).data() + sqlExecute( + "DELETE FROM sent" if folder == "trash" or shifted else + "UPDATE sent SET folder='trash'" + " WHERE ackdata = ?", ackdataToTrash + ) self.getCurrentMessageTextedit().setPlainText("") tableWidget.removeRow(currentRow) self.updateStatusBar(_translate( @@ -3276,73 +3347,57 @@ def on_action_AddressBookDelete(self): while self.ui.tableWidgetAddressBook.selectedIndexes() != []: currentRow = self.ui.tableWidgetAddressBook.selectedIndexes()[ 0].row() - labelAtCurrentRow = self.ui.tableWidgetAddressBook.item( - currentRow, 0).text().toUtf8() - addressAtCurrentRow = self.ui.tableWidgetAddressBook.item( - currentRow, 1).text() - sqlExecute('''DELETE FROM addressbook WHERE label=? AND address=?''', - str(labelAtCurrentRow), str(addressAtCurrentRow)) + item = self.ui.tableWidgetAddressBook.item(currentRow, 0) + sqlExecute( + 'DELETE FROM addressbook WHERE address=?', item.address) self.ui.tableWidgetAddressBook.removeRow(currentRow) - self.rerenderMessagelistFromLabels() - self.rerenderMessagelistToLabels() + self.rerenderMessagelistFromLabels() + self.rerenderMessagelistToLabels() def on_action_AddressBookClipboard(self): - fullStringOfAddresses = '' - listOfSelectedRows = {} - for i in range(len(self.ui.tableWidgetAddressBook.selectedIndexes())): - listOfSelectedRows[ - self.ui.tableWidgetAddressBook.selectedIndexes()[i].row()] = 0 - for currentRow in listOfSelectedRows: - addressAtCurrentRow = self.ui.tableWidgetAddressBook.item( - currentRow, 1).text() - if fullStringOfAddresses == '': - fullStringOfAddresses = addressAtCurrentRow + addresses_string = '' + for item in self.getAddressbookSelectedItems(): + if addresses_string == '': + addresses_string = item.address else: - fullStringOfAddresses += ', ' + str(addressAtCurrentRow) + addresses_string += ', ' + item.address clipboard = QtGui.QApplication.clipboard() - clipboard.setText(fullStringOfAddresses) + clipboard.setText(addresses_string) def on_action_AddressBookSend(self): - listOfSelectedRows = {} - for i in range(len(self.ui.tableWidgetAddressBook.selectedIndexes())): - listOfSelectedRows[ - self.ui.tableWidgetAddressBook.selectedIndexes()[i].row()] = 0 - for currentRow in listOfSelectedRows: - addressAtCurrentRow = self.ui.tableWidgetAddressBook.item( - currentRow, 0).address - labelAtCurrentRow = self.ui.tableWidgetAddressBook.item( - currentRow, 0).label - stringToAdd = labelAtCurrentRow + " <" + addressAtCurrentRow + ">" - if self.ui.lineEditTo.text() == '': - self.ui.lineEditTo.setText(stringToAdd) - else: - self.ui.lineEditTo.setText(unicode( - self.ui.lineEditTo.text().toUtf8(), encoding="UTF-8") + '; ' + stringToAdd) - if listOfSelectedRows == {}: - self.updateStatusBar(_translate( + selected_items = self.getAddressbookSelectedItems() + + if not selected_items: # FIXME: impossible + return self.updateStatusBar(_translate( "MainWindow", "No addresses selected.")) - else: - self.statusbar.clearMessage() - self.ui.tabWidget.setCurrentIndex( - self.ui.tabWidget.indexOf(self.ui.send) - ) + + addresses_string = unicode( + self.ui.lineEditTo.text().toUtf8(), 'utf-8') + for item in selected_items: + address_string = item.accountString() + if not addresses_string: + addresses_string = address_string + else: + addresses_string += '; ' + address_string + + self.ui.lineEditTo.setText(addresses_string) + self.statusbar.clearMessage() + self.ui.tabWidget.setCurrentIndex( + self.ui.tabWidget.indexOf(self.ui.send) + ) def on_action_AddressBookSubscribe(self): - listOfSelectedRows = {} - for i in range(len(self.ui.tableWidgetAddressBook.selectedIndexes())): - listOfSelectedRows[self.ui.tableWidgetAddressBook.selectedIndexes()[i].row()] = 0 - for currentRow in listOfSelectedRows: - addressAtCurrentRow = str(self.ui.tableWidgetAddressBook.item(currentRow,1).text()) - # Then subscribe to it... provided it's not already in the address book - if shared.isAddressInMySubscriptionsList(addressAtCurrentRow): + for item in self.getAddressbookSelectedItems(): + # Then subscribe to it... + # provided it's not already in the address book + if shared.isAddressInMySubscriptionsList(item.address): self.updateStatusBar(_translate( "MainWindow", "Error: You cannot add the same address to your" " subscriptions twice. Perhaps rename the existing" " one if you want.")) continue - labelAtCurrentRow = self.ui.tableWidgetAddressBook.item(currentRow,0).text().toUtf8() - self.addSubscription(addressAtCurrentRow, labelAtCurrentRow) + self.addSubscription(item.address, item.label) self.ui.tabWidget.setCurrentIndex( self.ui.tabWidget.indexOf(self.ui.subscriptions) ) @@ -3357,15 +3412,19 @@ def on_context_menuAddressBook(self, point): self.popMenuAddressBook.addSeparator() self.popMenuAddressBook.addAction(self.actionAddressBookNew) normal = True - for row in self.ui.tableWidgetAddressBook.selectedIndexes(): - currentRow = row.row() - type = self.ui.tableWidgetAddressBook.item( - currentRow, 0).type - if type != AccountMixin.NORMAL: + selected_items = self.getAddressbookSelectedItems() + for item in selected_items: + if item.type != AccountMixin.NORMAL: normal = False + break if normal: # only if all selected addressbook items are normal, allow delete self.popMenuAddressBook.addAction(self.actionAddressBookDelete) + if len(selected_items) == 1: + self._contact_selected = selected_items.pop() + self.popMenuAddressBook.addSeparator() + for plugin in self.menu_plugins['address']: + self.popMenuAddressBook.addAction(plugin) self.popMenuAddressBook.exec_( self.ui.tableWidgetAddressBook.mapToGlobal(point)) @@ -3435,16 +3494,22 @@ def on_context_menuSubscriptions(self, point): self.popMenuSubscriptions.addAction(self.actionsubscriptionsSetAvatar) self.popMenuSubscriptions.addSeparator() self.popMenuSubscriptions.addAction(self.actionsubscriptionsClipboard) + self.popMenuSubscriptions.addAction(self.actionsubscriptionsSend) self.popMenuSubscriptions.addSeparator() + + self._contact_selected = currentItem # preloaded gui.menu plugins with prefix 'address' for plugin in self.menu_plugins['address']: self.popMenuSubscriptions.addAction(plugin) self.popMenuSubscriptions.addSeparator() - self.popMenuSubscriptions.addAction(self.actionMarkAllRead) + if self.getCurrentFolder() != 'sent': + self.popMenuSubscriptions.addAction(self.actionMarkAllRead) + if self.popMenuSubscriptions.isEmpty(): + return self.popMenuSubscriptions.exec_( self.ui.treeWidgetSubscriptions.mapToGlobal(point)) - def widgetConvert (self, widget): + def widgetConvert(self, widget): if widget == self.ui.tableWidgetInbox: return self.ui.treeWidgetYourIdentities elif widget == self.ui.tableWidgetInboxSubscriptions: @@ -3461,13 +3526,13 @@ def widgetConvert (self, widget): return None def getCurrentTreeWidget(self): - currentIndex = self.ui.tabWidget.currentIndex(); - treeWidgetList = [ + currentIndex = self.ui.tabWidget.currentIndex() + treeWidgetList = ( self.ui.treeWidgetYourIdentities, False, self.ui.treeWidgetSubscriptions, self.ui.treeWidgetChans - ] + ) if currentIndex >= 0 and currentIndex < len(treeWidgetList): return treeWidgetList[currentIndex] else: @@ -3485,18 +3550,16 @@ def getAccountTreeWidget(self, account): return self.ui.treeWidgetYourIdentities def getCurrentMessagelist(self): - currentIndex = self.ui.tabWidget.currentIndex(); - messagelistList = [ + currentIndex = self.ui.tabWidget.currentIndex() + messagelistList = ( self.ui.tableWidgetInbox, False, self.ui.tableWidgetInboxSubscriptions, self.ui.tableWidgetInboxChans, - ] + ) if currentIndex >= 0 and currentIndex < len(messagelistList): return messagelistList[currentIndex] - else: - return False - + def getAccountMessagelist(self, account): try: if account.type == AccountMixin.CHAN: @@ -3513,24 +3576,18 @@ def getCurrentMessageId(self): if messagelist: currentRow = messagelist.currentRow() if currentRow >= 0: - msgid = str(messagelist.item( - currentRow, 3).data(QtCore.Qt.UserRole).toPyObject()) - # data is saved at the 4. column of the table... - return msgid - return False + return messagelist.item(currentRow, 3).data() def getCurrentMessageTextedit(self): currentIndex = self.ui.tabWidget.currentIndex() - messagelistList = [ + messagelistList = ( self.ui.textEditInboxMessage, False, self.ui.textEditInboxMessageSubscriptions, self.ui.textEditInboxMessageChans, - ] + ) if currentIndex >= 0 and currentIndex < len(messagelistList): return messagelistList[currentIndex] - else: - return False def getAccountTextedit(self, account): try: @@ -3546,73 +3603,63 @@ def getAccountTextedit(self, account): def getCurrentSearchLine(self, currentIndex=None, retObj=False): if currentIndex is None: currentIndex = self.ui.tabWidget.currentIndex() - messagelistList = [ + messagelistList = ( self.ui.inboxSearchLineEdit, False, self.ui.inboxSearchLineEditSubscriptions, self.ui.inboxSearchLineEditChans, - ] + ) if currentIndex >= 0 and currentIndex < len(messagelistList): - if retObj: - return messagelistList[currentIndex] - else: - return messagelistList[currentIndex].text().toUtf8().data() - else: - return None + return ( + messagelistList[currentIndex] if retObj + else messagelistList[currentIndex].text().toUtf8().data()) def getCurrentSearchOption(self, currentIndex=None): if currentIndex is None: currentIndex = self.ui.tabWidget.currentIndex() - messagelistList = [ + messagelistList = ( self.ui.inboxSearchOption, False, self.ui.inboxSearchOptionSubscriptions, self.ui.inboxSearchOptionChans, - ] + ) if currentIndex >= 0 and currentIndex < len(messagelistList): - return messagelistList[currentIndex].currentText().toUtf8().data() - else: - return None + return messagelistList[currentIndex].currentText() # Group of functions for the Your Identities dialog box def getCurrentItem(self, treeWidget=None): if treeWidget is None: treeWidget = self.getCurrentTreeWidget() if treeWidget: - currentItem = treeWidget.currentItem() - if currentItem: - return currentItem - return False - + return treeWidget.currentItem() + def getCurrentAccount(self, treeWidget=None): currentItem = self.getCurrentItem(treeWidget) if currentItem: - account = currentItem.address - return account - else: - # TODO need debug msg? - return False + return currentItem.address def getCurrentFolder(self, treeWidget=None): - if treeWidget is None: - treeWidget = self.getCurrentTreeWidget() - #treeWidget = self.ui.treeWidgetYourIdentities - if treeWidget: - currentItem = treeWidget.currentItem() - if currentItem and hasattr(currentItem, 'folderName'): - return currentItem.folderName - else: - return None + currentItem = self.getCurrentItem(treeWidget) + try: + return currentItem.folderName + except AttributeError: + pass def setCurrentItemColor(self, color): - treeWidget = self.getCurrentTreeWidget() - if treeWidget: + currentItem = self.getCurrentItem() + if currentItem: brush = QtGui.QBrush() brush.setStyle(QtCore.Qt.NoBrush) brush.setColor(color) - currentItem = treeWidget.currentItem() currentItem.setForeground(0, brush) + def getAddressbookSelectedItems(self): + return [ + self.ui.tableWidgetAddressBook.item(i.row(), 0) + for i in self.ui.tableWidgetAddressBook.selectedIndexes() + if i.column() == 0 + ] + def on_action_YourIdentitiesNew(self): self.click_NewAddressDialog() @@ -3634,12 +3681,12 @@ def on_action_YourIdentitiesDelete(self): " delete the channel?" ), QtGui.QMessageBox.Yes | QtGui.QMessageBox.No ) == QtGui.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() @@ -3655,8 +3702,8 @@ def on_action_Enable(self): 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() @@ -3667,8 +3714,8 @@ def on_action_Disable(self): 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() @@ -3681,12 +3728,11 @@ def on_action_ClipboardMessagelist(self): tableWidget = self.getCurrentMessagelist() currentColumn = tableWidget.currentColumn() currentRow = tableWidget.currentRow() - if currentColumn not in [0, 1, 2]: # to, from, subject - if self.getCurrentFolder() == "sent": - currentColumn = 0 - else: - currentColumn = 1 - if self.getCurrentFolder() == "sent": + currentFolder = self.getCurrentFolder() + if currentColumn not in (0, 1, 2): # to, from, subject + currentColumn = 0 if currentFolder == "sent" else 1 + + if currentFolder == "sent": myAddress = tableWidget.item(currentRow, 1).data(QtCore.Qt.UserRole) otherAddress = tableWidget.item(currentRow, 0).data(QtCore.Qt.UserRole) else: @@ -3694,23 +3740,23 @@ def on_action_ClipboardMessagelist(self): otherAddress = tableWidget.item(currentRow, 1).data(QtCore.Qt.UserRole) account = accountClass(myAddress) if isinstance(account, GatewayAccount) and otherAddress == account.relayAddress and ( - (currentColumn in [0, 2] and self.getCurrentFolder() == "sent") or - (currentColumn in [1, 2] and self.getCurrentFolder() != "sent")): + (currentColumn in [0, 2] and self.getCurrentFolder() == "sent") or + (currentColumn in [1, 2] and self.getCurrentFolder() != "sent")): text = str(tableWidget.item(currentRow, currentColumn).label) else: text = tableWidget.item(currentRow, currentColumn).data(QtCore.Qt.UserRole) - text = unicode(str(text), 'utf-8', 'ignore') + clipboard = QtGui.QApplication.clipboard() clipboard.setText(text) - #set avatar functions + # set avatar functions def on_action_TreeWidgetSetAvatar(self): address = self.getCurrentAccount() self.setAvatar(address) def on_action_AddressBookSetAvatar(self): self.on_action_SetAvatar(self.ui.tableWidgetAddressBook) - + def on_action_SetAvatar(self, thisTableWidget): currentRow = thisTableWidget.currentRow() addressAtCurrentRow = thisTableWidget.item( @@ -3720,19 +3766,36 @@ def on_action_SetAvatar(self, thisTableWidget): thisTableWidget.item( currentRow, 0).setIcon(avatarize(addressAtCurrentRow)) + # TODO: reuse utils def setAvatar(self, addressAtCurrentRow): if not os.path.exists(state.appdata + 'avatars/'): os.makedirs(state.appdata + 'avatars/') hash = hashlib.md5(addBMIfNotPresent(addressAtCurrentRow)).hexdigest() - extensions = ['PNG', 'GIF', 'JPG', 'JPEG', 'SVG', 'BMP', 'MNG', 'PBM', 'PGM', 'PPM', 'TIFF', 'XBM', 'XPM', 'TGA'] - # http://pyqt.sourceforge.net/Docs/PyQt4/qimagereader.html#supportedImageFormats - names = {'BMP':'Windows Bitmap', 'GIF':'Graphic Interchange Format', 'JPG':'Joint Photographic Experts Group', 'JPEG':'Joint Photographic Experts Group', 'MNG':'Multiple-image Network Graphics', 'PNG':'Portable Network Graphics', 'PBM':'Portable Bitmap', 'PGM':'Portable Graymap', 'PPM':'Portable Pixmap', 'TIFF':'Tagged Image File Format', 'XBM':'X11 Bitmap', 'XPM':'X11 Pixmap', 'SVG':'Scalable Vector Graphics', 'TGA':'Targa Image Format'} + extensions = [ + 'PNG', 'GIF', 'JPG', 'JPEG', 'SVG', 'BMP', 'MNG', 'PBM', + 'PGM', 'PPM', 'TIFF', 'XBM', 'XPM', 'TGA'] + + names = { + 'BMP': 'Windows Bitmap', + 'GIF': 'Graphic Interchange Format', + 'JPG': 'Joint Photographic Experts Group', + 'JPEG': 'Joint Photographic Experts Group', + 'MNG': 'Multiple-image Network Graphics', + 'PNG': 'Portable Network Graphics', + 'PBM': 'Portable Bitmap', + 'PGM': 'Portable Graymap', + 'PPM': 'Portable Pixmap', + 'TIFF': 'Tagged Image File Format', + 'XBM': 'X11 Bitmap', + 'XPM': 'X11 Pixmap', + 'SVG': 'Scalable Vector Graphics', + 'TGA': 'Targa Image Format'} filters = [] all_images_filter = [] current_files = [] for ext in extensions: - filters += [ names[ext] + ' (*.' + ext.lower() + ')' ] - all_images_filter += [ '*.' + ext.lower() ] + filters += [names[ext] + ' (*.' + ext.lower() + ')'] + all_images_filter += ['*.' + ext.lower()] upper = state.appdata + 'avatars/' + hash + '.' + ext.upper() lower = state.appdata + 'avatars/' + hash + '.' + ext.lower() if os.path.isfile(lower): @@ -3743,28 +3806,34 @@ def setAvatar(self, addressAtCurrentRow): filters[1:1] = ['All files (*.*)'] sourcefile = QtGui.QFileDialog.getOpenFileName( self, _translate("MainWindow", "Set avatar..."), - filter = ';;'.join(filters) + filter=';;'.join(filters) ) # determine the correct filename (note that avatars don't use the suffix) destination = state.appdata + 'avatars/' + hash + '.' + sourcefile.split('.')[-1] exists = QtCore.QFile.exists(destination) if sourcefile == '': # ask for removal of avatar - if exists | (len(current_files)>0): - displayMsg = _translate("MainWindow", "Do you really want to remove this avatar?") + if exists | (len(current_files) > 0): + displayMsg = _translate( + "MainWindow", "Do you really want to remove this avatar?") overwrite = QtGui.QMessageBox.question( - self, 'Message', displayMsg, QtGui.QMessageBox.Yes, QtGui.QMessageBox.No) + self, 'Message', displayMsg, + QtGui.QMessageBox.Yes, QtGui.QMessageBox.No) else: overwrite = QtGui.QMessageBox.No else: # ask whether to overwrite old avatar - if exists | (len(current_files)>0): - displayMsg = _translate("MainWindow", "You have already set an avatar for this address. Do you really want to overwrite it?") + if exists | (len(current_files) > 0): + displayMsg = _translate( + "MainWindow", + "You have already set an avatar for this address." + " Do you really want to overwrite it?") overwrite = QtGui.QMessageBox.question( - self, 'Message', displayMsg, QtGui.QMessageBox.Yes, QtGui.QMessageBox.No) + self, 'Message', displayMsg, + QtGui.QMessageBox.Yes, QtGui.QMessageBox.No) else: overwrite = QtGui.QMessageBox.No - + # copy the image file to the appdata folder if (not exists) | (overwrite == QtGui.QMessageBox.Yes): if overwrite == QtGui.QMessageBox.Yes: @@ -3851,12 +3920,15 @@ def on_context_menuYourIdentities(self, point): self.popMenuYourIdentities.addAction(self.actionEmailGateway) self.popMenuYourIdentities.addSeparator() if currentItem.type != AccountMixin.ALL: + self._contact_selected = currentItem # preloaded gui.menu plugins with prefix 'address' for plugin in self.menu_plugins['address']: self.popMenuYourIdentities.addAction(plugin) self.popMenuYourIdentities.addSeparator() - self.popMenuYourIdentities.addAction(self.actionMarkAllRead) - + if self.getCurrentFolder() != 'sent': + self.popMenuYourIdentities.addAction(self.actionMarkAllRead) + if self.popMenuYourIdentities.isEmpty(): + return self.popMenuYourIdentities.exec_( self.ui.treeWidgetYourIdentities.mapToGlobal(point)) @@ -3868,69 +3940,86 @@ def on_context_menuChan(self, point): self.popMenu.addAction(self.actionNew) self.popMenu.addAction(self.actionDelete) self.popMenu.addSeparator() - self.popMenu.addAction(self.actionClipboard) - self.popMenu.addSeparator() if currentItem.isEnabled: self.popMenu.addAction(self.actionDisable) else: self.popMenu.addAction(self.actionEnable) self.popMenu.addAction(self.actionSetAvatar) self.popMenu.addSeparator() + self.popMenu.addAction(self.actionClipboard) + self.popMenu.addAction(self.actionSend) + self.popMenu.addSeparator() + self._contact_selected = currentItem # preloaded gui.menu plugins with prefix 'address' for plugin in self.menu_plugins['address']: self.popMenu.addAction(plugin) self.popMenu.addSeparator() - self.popMenu.addAction(self.actionMarkAllRead) + if self.getCurrentFolder() != 'sent': + self.popMenu.addAction(self.actionMarkAllRead) + if self.popMenu.isEmpty(): + return self.popMenu.exec_( self.ui.treeWidgetChans.mapToGlobal(point)) def on_context_menuInbox(self, point): tableWidget = self.getCurrentMessagelist() - if tableWidget: - currentFolder = self.getCurrentFolder() - if currentFolder is None: - pass - if currentFolder == 'sent': - self.on_context_menuSent(point) - else: - self.popMenuInbox = QtGui.QMenu(self) - self.popMenuInbox.addAction(self.actionForceHtml) - self.popMenuInbox.addAction(self.actionMarkUnread) - self.popMenuInbox.addSeparator() - address = tableWidget.item( - tableWidget.currentRow(), 0).data(QtCore.Qt.UserRole) - account = accountClass(address) - if account.type == AccountMixin.CHAN: - self.popMenuInbox.addAction(self.actionReplyChan) - self.popMenuInbox.addAction(self.actionReply) - self.popMenuInbox.addAction(self.actionAddSenderToAddressBook) - self.actionClipboardMessagelist = self.ui.inboxContextMenuToolbar.addAction( - _translate("MainWindow", - "Copy subject to clipboard" if tableWidget.currentColumn() == 2 else "Copy address to clipboard" - ), - self.on_action_ClipboardMessagelist) - self.popMenuInbox.addAction(self.actionClipboardMessagelist) - self.popMenuInbox.addSeparator() - self.popMenuInbox.addAction(self.actionAddSenderToBlackList) - self.popMenuInbox.addSeparator() - self.popMenuInbox.addAction(self.actionSaveMessageAs) - if currentFolder == "trash": - self.popMenuInbox.addAction(self.actionUndeleteTrashedMessage) - else: - self.popMenuInbox.addAction(self.actionTrashInboxMessage) - self.popMenuInbox.exec_(tableWidget.mapToGlobal(point)) + if not tableWidget: + return + + currentFolder = self.getCurrentFolder() + if currentFolder == 'sent': + self.on_context_menuSent(point) + return + + self.popMenuInbox = QtGui.QMenu(self) + self.popMenuInbox.addAction(self.actionForceHtml) + self.popMenuInbox.addAction(self.actionMarkUnread) + self.popMenuInbox.addSeparator() + currentRow = tableWidget.currentRow() + account = accountClass( + tableWidget.item(currentRow, 0).data(QtCore.Qt.UserRole)) + + if account.type == AccountMixin.CHAN: + self.popMenuInbox.addAction(self.actionReplyChan) + self.popMenuInbox.addAction(self.actionReply) + self.popMenuInbox.addAction(self.actionAddSenderToAddressBook) + self.actionClipboardMessagelist = self.ui.inboxContextMenuToolbar.addAction( + _translate("MainWindow", "Copy subject to clipboard") + if tableWidget.currentColumn() == 2 else + _translate("MainWindow", "Copy address to clipboard"), + self.on_action_ClipboardMessagelist) + self.popMenuInbox.addAction(self.actionClipboardMessagelist) + # pylint: disable=no-member + self._contact_selected = tableWidget.item(currentRow, 1) + # preloaded gui.menu plugins with prefix 'address' + for plugin in self.menu_plugins['address']: + self.popMenuInbox.addAction(plugin) + self.popMenuInbox.addSeparator() + self.popMenuInbox.addAction(self.actionAddSenderToBlackList) + self.popMenuInbox.addSeparator() + self.popMenuInbox.addAction(self.actionSaveMessageAs) + if currentFolder == "trash": + self.popMenuInbox.addAction(self.actionUndeleteTrashedMessage) + else: + self.popMenuInbox.addAction(self.actionTrashInboxMessage) + self.popMenuInbox.exec_(tableWidget.mapToGlobal(point)) def on_context_menuSent(self, point): + currentRow = self.ui.tableWidgetInbox.currentRow() self.popMenuSent = QtGui.QMenu(self) self.popMenuSent.addAction(self.actionSentClipboard) + self._contact_selected = self.ui.tableWidgetInbox.item(currentRow, 0) + # preloaded gui.menu plugins with prefix 'address' + for plugin in self.menu_plugins['address']: + self.popMenuSent.addAction(plugin) + self.popMenuSent.addSeparator() self.popMenuSent.addAction(self.actionTrashSentMessage) + self.popMenuSent.addAction(self.actionSentReply) # Check to see if this item is toodifficult and display an additional # menu option (Force Send) if it is. - currentRow = self.ui.tableWidgetInbox.currentRow() if currentRow >= 0: - ackData = str(self.ui.tableWidgetInbox.item( - currentRow, 3).data(QtCore.Qt.UserRole).toPyObject()) + ackData = self.ui.tableWidgetInbox.item(currentRow, 3).data() queryreturn = sqlQuery('''SELECT status FROM sent where ackdata=?''', ackData) for row in queryreturn: status, = row @@ -3941,51 +4030,55 @@ def on_context_menuSent(self, point): def inboxSearchLineEditUpdated(self, text): # dynamic search for too short text is slow - if len(str(text)) < 3: + text = text.toUtf8() + if 0 < len(text) < 3: return messagelist = self.getCurrentMessagelist() - searchOption = self.getCurrentSearchOption() if messagelist: + searchOption = self.getCurrentSearchOption() account = self.getCurrentAccount() folder = self.getCurrentFolder() - self.loadMessagelist(messagelist, account, folder, searchOption, str(text)) + self.loadMessagelist( + messagelist, account, folder, searchOption, text) def inboxSearchLineEditReturnPressed(self): logger.debug("Search return pressed") searchLine = self.getCurrentSearchLine() messagelist = self.getCurrentMessagelist() - if len(str(searchLine)) < 3: + if messagelist and len(str(searchLine)) < 3: searchOption = self.getCurrentSearchOption() account = self.getCurrentAccount() folder = self.getCurrentFolder() - self.loadMessagelist(messagelist, account, folder, searchOption, searchLine) - if messagelist: + self.loadMessagelist( + messagelist, account, folder, searchOption, searchLine) messagelist.setFocus() def treeWidgetItemClicked(self): - searchLine = self.getCurrentSearchLine() - searchOption = self.getCurrentSearchOption() + messagelist = self.getCurrentMessagelist() + if not messagelist: + return messageTextedit = self.getCurrentMessageTextedit() if messageTextedit: - messageTextedit.setPlainText(QtCore.QString("")) - messagelist = self.getCurrentMessagelist() - if messagelist: - account = self.getCurrentAccount() - folder = self.getCurrentFolder() - treeWidget = self.getCurrentTreeWidget() - # refresh count indicator - self.propagateUnreadCount(account.address if hasattr(account, 'address') else None, folder, treeWidget, 0) - self.loadMessagelist(messagelist, account, folder, searchOption, searchLine) + messageTextedit.setPlainText("") + account = self.getCurrentAccount() + folder = self.getCurrentFolder() + # refresh count indicator + self.propagateUnreadCount(folder) + self.loadMessagelist( + messagelist, account, folder, + self.getCurrentSearchOption(), self.getCurrentSearchLine()) def treeWidgetItemChanged(self, item, column): # only for manual edits. automatic edits (setText) are ignored 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)): + if (not self.getCurrentItem()) or (not isinstance(self.getCurrentItem(), Ui_AddressWidget)): return # only currently selected item if item.address != self.getCurrentAccount(): @@ -3993,7 +4086,7 @@ def treeWidgetItemChanged(self, item, column): # "All accounts" can't be renamed if item.type == AccountMixin.ALL: return - + newLabel = unicode(item.text(0), 'utf-8', 'ignore') oldLabel = item.defaultLabel() @@ -4018,12 +4111,12 @@ def treeWidgetItemChanged(self, item, column): self.recurDepth -= 1 def tableWidgetInboxItemClicked(self): - folder = self.getCurrentFolder() messageTextedit = self.getCurrentMessageTextedit() if not messageTextedit: return msgid = self.getCurrentMessageId() + folder = self.getCurrentFolder() if msgid: queryreturn = sqlQuery( '''SELECT message FROM %s WHERE %s=?''' % ( @@ -4084,12 +4177,15 @@ def writeNewAddressToTable(self, label, address, streamNumber): self.rerenderAddressBook() def updateStatusBar(self, data): - if type(data) is tuple or type(data) is list: - option = data[1] - message = data[0] - else: + try: + message, option = data + except ValueError: option = 0 message = data + except TypeError: + logger.debug( + 'Invalid argument for updateStatusBar!', exc_info=True) + if message != "": logger.info('Status bar: ' + message) @@ -4098,10 +4194,23 @@ def updateStatusBar(self, data): else: self.statusbar.showMessage(message, 10000) + def resetNamecoinConnection(self): + namecoin.ensureNamecoinOptions() + self.namecoin = namecoin.namecoinConnection() + + # Check to see whether we can connect to namecoin. + # Hide the 'Fetch Namecoin ID' button if we can't. + if config.safeGetBoolean( + 'bitmessagesettings', 'dontconnect' + ) or self.namecoin.test()[0] == 'failed': + logger.warning( + 'There was a problem testing for a Namecoin daemon.' + ' Hiding the Fetch Namecoin ID button') + self.ui.pushButtonFetchNamecoinID.hide() + else: + self.ui.pushButtonFetchNamecoinID.show() + def initSettings(self): - QtCore.QCoreApplication.setOrganizationName("PyBitmessage") - QtCore.QCoreApplication.setOrganizationDomain("bitmessage.org") - QtCore.QCoreApplication.setApplicationName("pybitmessageqt") self.loadSettings() for attr, obj in self.ui.__dict__.iteritems(): if hasattr(obj, "__class__") and \ @@ -4111,253 +4220,11 @@ def initSettings(self): obj.loadSettings() -class settingsDialog(QtGui.QDialog): - - def __init__(self, parent): - QtGui.QWidget.__init__(self, parent) - self.ui = Ui_settingsDialog() - self.ui.setupUi(self) - self.parent = parent - self.ui.checkBoxStartOnLogon.setChecked( - BMConfigParser().getboolean('bitmessagesettings', 'startonlogon')) - self.ui.checkBoxMinimizeToTray.setChecked( - BMConfigParser().getboolean('bitmessagesettings', 'minimizetotray')) - self.ui.checkBoxTrayOnClose.setChecked( - BMConfigParser().safeGetBoolean('bitmessagesettings', 'trayonclose')) - self.ui.checkBoxHideTrayConnectionNotifications.setChecked( - BMConfigParser().getboolean("bitmessagesettings", "hidetrayconnectionnotifications")) - self.ui.checkBoxShowTrayNotifications.setChecked( - BMConfigParser().getboolean('bitmessagesettings', 'showtraynotifications')) - self.ui.checkBoxStartInTray.setChecked( - BMConfigParser().getboolean('bitmessagesettings', 'startintray')) - self.ui.checkBoxWillinglySendToMobile.setChecked( - BMConfigParser().safeGetBoolean('bitmessagesettings', 'willinglysendtomobile')) - self.ui.checkBoxUseIdenticons.setChecked( - BMConfigParser().safeGetBoolean('bitmessagesettings', 'useidenticons')) - self.ui.checkBoxReplyBelow.setChecked( - BMConfigParser().safeGetBoolean('bitmessagesettings', 'replybelow')) - - if state.appdata == paths.lookupExeFolder(): - self.ui.checkBoxPortableMode.setChecked(True) - else: - try: - import tempfile - tempfile.NamedTemporaryFile( - dir=paths.lookupExeFolder(), delete=True - ).close() # should autodelete - except: - self.ui.checkBoxPortableMode.setDisabled(True) - - if 'darwin' in sys.platform: - self.ui.checkBoxStartOnLogon.setDisabled(True) - self.ui.checkBoxStartOnLogon.setText(_translate( - "MainWindow", "Start-on-login not yet supported on your OS.")) - self.ui.checkBoxMinimizeToTray.setDisabled(True) - self.ui.checkBoxMinimizeToTray.setText(_translate( - "MainWindow", "Minimize-to-tray not yet supported on your OS.")) - self.ui.checkBoxShowTrayNotifications.setDisabled(True) - self.ui.checkBoxShowTrayNotifications.setText(_translate( - "MainWindow", "Tray notifications not yet supported on your OS.")) - elif 'linux' in sys.platform: - self.ui.checkBoxStartOnLogon.setDisabled(True) - self.ui.checkBoxStartOnLogon.setText(_translate( - "MainWindow", "Start-on-login not yet supported on your OS.")) - # On the Network settings tab: - self.ui.lineEditTCPPort.setText(str( - BMConfigParser().get('bitmessagesettings', 'port'))) - self.ui.checkBoxUPnP.setChecked( - BMConfigParser().safeGetBoolean('bitmessagesettings', 'upnp')) - self.ui.checkBoxAuthentication.setChecked(BMConfigParser().getboolean( - 'bitmessagesettings', 'socksauthentication')) - self.ui.checkBoxSocksListen.setChecked(BMConfigParser().getboolean( - 'bitmessagesettings', 'sockslisten')) - if str(BMConfigParser().get('bitmessagesettings', 'socksproxytype')) == 'none': - self.ui.comboBoxProxyType.setCurrentIndex(0) - self.ui.lineEditSocksHostname.setEnabled(False) - self.ui.lineEditSocksPort.setEnabled(False) - self.ui.lineEditSocksUsername.setEnabled(False) - self.ui.lineEditSocksPassword.setEnabled(False) - self.ui.checkBoxAuthentication.setEnabled(False) - self.ui.checkBoxSocksListen.setEnabled(False) - elif str(BMConfigParser().get('bitmessagesettings', 'socksproxytype')) == 'SOCKS4a': - self.ui.comboBoxProxyType.setCurrentIndex(1) - elif str(BMConfigParser().get('bitmessagesettings', 'socksproxytype')) == 'SOCKS5': - self.ui.comboBoxProxyType.setCurrentIndex(2) - - self.ui.lineEditSocksHostname.setText(str( - BMConfigParser().get('bitmessagesettings', 'sockshostname'))) - self.ui.lineEditSocksPort.setText(str( - BMConfigParser().get('bitmessagesettings', 'socksport'))) - self.ui.lineEditSocksUsername.setText(str( - BMConfigParser().get('bitmessagesettings', 'socksusername'))) - self.ui.lineEditSocksPassword.setText(str( - BMConfigParser().get('bitmessagesettings', 'sockspassword'))) - QtCore.QObject.connect(self.ui.comboBoxProxyType, QtCore.SIGNAL( - "currentIndexChanged(int)"), self.comboBoxProxyTypeChanged) - self.ui.lineEditMaxDownloadRate.setText(str( - BMConfigParser().get('bitmessagesettings', 'maxdownloadrate'))) - self.ui.lineEditMaxUploadRate.setText(str( - BMConfigParser().get('bitmessagesettings', 'maxuploadrate'))) - self.ui.lineEditMaxOutboundConnections.setText(str( - BMConfigParser().get('bitmessagesettings', 'maxoutboundconnections'))) - - # Demanded difficulty tab - self.ui.lineEditTotalDifficulty.setText(str((float(BMConfigParser().getint( - 'bitmessagesettings', 'defaultnoncetrialsperbyte')) / defaults.networkDefaultProofOfWorkNonceTrialsPerByte))) - self.ui.lineEditSmallMessageDifficulty.setText(str((float(BMConfigParser().getint( - 'bitmessagesettings', 'defaultpayloadlengthextrabytes')) / defaults.networkDefaultPayloadLengthExtraBytes))) - - # Max acceptable difficulty tab - self.ui.lineEditMaxAcceptableTotalDifficulty.setText(str((float(BMConfigParser().getint( - 'bitmessagesettings', 'maxacceptablenoncetrialsperbyte')) / defaults.networkDefaultProofOfWorkNonceTrialsPerByte))) - self.ui.lineEditMaxAcceptableSmallMessageDifficulty.setText(str((float(BMConfigParser().getint( - 'bitmessagesettings', 'maxacceptablepayloadlengthextrabytes')) / defaults.networkDefaultPayloadLengthExtraBytes))) - - # OpenCL - if openclpow.openclAvailable(): - self.ui.comboBoxOpenCL.setEnabled(True) - else: - self.ui.comboBoxOpenCL.setEnabled(False) - self.ui.comboBoxOpenCL.clear() - self.ui.comboBoxOpenCL.addItem("None") - self.ui.comboBoxOpenCL.addItems(openclpow.vendors) - self.ui.comboBoxOpenCL.setCurrentIndex(0) - for i in range(self.ui.comboBoxOpenCL.count()): - if self.ui.comboBoxOpenCL.itemText(i) == BMConfigParser().safeGet('bitmessagesettings', 'opencl'): - self.ui.comboBoxOpenCL.setCurrentIndex(i) - break - - # Namecoin integration tab - nmctype = BMConfigParser().get('bitmessagesettings', 'namecoinrpctype') - self.ui.lineEditNamecoinHost.setText(str( - BMConfigParser().get('bitmessagesettings', 'namecoinrpchost'))) - self.ui.lineEditNamecoinPort.setText(str( - BMConfigParser().get('bitmessagesettings', 'namecoinrpcport'))) - self.ui.lineEditNamecoinUser.setText(str( - BMConfigParser().get('bitmessagesettings', 'namecoinrpcuser'))) - self.ui.lineEditNamecoinPassword.setText(str( - BMConfigParser().get('bitmessagesettings', 'namecoinrpcpassword'))) - - if nmctype == "namecoind": - self.ui.radioButtonNamecoinNamecoind.setChecked(True) - elif nmctype == "nmcontrol": - self.ui.radioButtonNamecoinNmcontrol.setChecked(True) - self.ui.lineEditNamecoinUser.setEnabled(False) - self.ui.labelNamecoinUser.setEnabled(False) - self.ui.lineEditNamecoinPassword.setEnabled(False) - self.ui.labelNamecoinPassword.setEnabled(False) - else: - assert False - - QtCore.QObject.connect(self.ui.radioButtonNamecoinNamecoind, QtCore.SIGNAL( - "toggled(bool)"), self.namecoinTypeChanged) - QtCore.QObject.connect(self.ui.radioButtonNamecoinNmcontrol, QtCore.SIGNAL( - "toggled(bool)"), self.namecoinTypeChanged) - QtCore.QObject.connect(self.ui.pushButtonNamecoinTest, QtCore.SIGNAL( - "clicked()"), self.click_pushButtonNamecoinTest) - - #Message Resend tab - self.ui.lineEditDays.setText(str( - BMConfigParser().get('bitmessagesettings', 'stopresendingafterxdays'))) - self.ui.lineEditMonths.setText(str( - BMConfigParser().get('bitmessagesettings', 'stopresendingafterxmonths'))) - - - #'System' tab removed for now. - """try: - maxCores = BMConfigParser().getint('bitmessagesettings', 'maxcores') - except: - maxCores = 99999 - if maxCores <= 1: - self.ui.comboBoxMaxCores.setCurrentIndex(0) - elif maxCores == 2: - self.ui.comboBoxMaxCores.setCurrentIndex(1) - elif maxCores <= 4: - self.ui.comboBoxMaxCores.setCurrentIndex(2) - elif maxCores <= 8: - self.ui.comboBoxMaxCores.setCurrentIndex(3) - elif maxCores <= 16: - self.ui.comboBoxMaxCores.setCurrentIndex(4) - else: - self.ui.comboBoxMaxCores.setCurrentIndex(5)""" - - QtGui.QWidget.resize(self, QtGui.QWidget.sizeHint(self)) - - def comboBoxProxyTypeChanged(self, comboBoxIndex): - if comboBoxIndex == 0: - self.ui.lineEditSocksHostname.setEnabled(False) - self.ui.lineEditSocksPort.setEnabled(False) - self.ui.lineEditSocksUsername.setEnabled(False) - self.ui.lineEditSocksPassword.setEnabled(False) - self.ui.checkBoxAuthentication.setEnabled(False) - self.ui.checkBoxSocksListen.setEnabled(False) - elif comboBoxIndex == 1 or comboBoxIndex == 2: - self.ui.lineEditSocksHostname.setEnabled(True) - self.ui.lineEditSocksPort.setEnabled(True) - self.ui.checkBoxAuthentication.setEnabled(True) - self.ui.checkBoxSocksListen.setEnabled(True) - if self.ui.checkBoxAuthentication.isChecked(): - self.ui.lineEditSocksUsername.setEnabled(True) - self.ui.lineEditSocksPassword.setEnabled(True) - - # Check status of namecoin integration radio buttons and translate - # it to a string as in the options. - def getNamecoinType(self): - if self.ui.radioButtonNamecoinNamecoind.isChecked(): - return "namecoind" - if self.ui.radioButtonNamecoinNmcontrol.isChecked(): - return "nmcontrol" - assert False - - # Namecoin connection type was changed. - def namecoinTypeChanged(self, checked): - nmctype = self.getNamecoinType() - assert nmctype == "namecoind" or nmctype == "nmcontrol" - - isNamecoind = (nmctype == "namecoind") - self.ui.lineEditNamecoinUser.setEnabled(isNamecoind) - self.ui.labelNamecoinUser.setEnabled(isNamecoind) - self.ui.lineEditNamecoinPassword.setEnabled(isNamecoind) - self.ui.labelNamecoinPassword.setEnabled(isNamecoind) - - if isNamecoind: - self.ui.lineEditNamecoinPort.setText(defaults.namecoinDefaultRpcPort) - else: - self.ui.lineEditNamecoinPort.setText("9000") - - # Test the namecoin settings specified in the settings dialog. - def click_pushButtonNamecoinTest(self): - self.ui.labelNamecoinTestResult.setText(_translate( - "MainWindow", "Testing...")) - options = {} - options["type"] = self.getNamecoinType() - options["host"] = str(self.ui.lineEditNamecoinHost.text().toUtf8()) - options["port"] = str(self.ui.lineEditNamecoinPort.text().toUtf8()) - options["user"] = str(self.ui.lineEditNamecoinUser.text().toUtf8()) - options["password"] = str(self.ui.lineEditNamecoinPassword.text().toUtf8()) - nc = namecoinConnection(options) - response = nc.test() - responseStatus = response[0] - responseText = response[1] - self.ui.labelNamecoinTestResult.setText(responseText) - if responseStatus== 'success': - self.parent.ui.pushButtonFetchNamecoinID.show() - - -# In order for the time columns on the Inbox and Sent tabs to be sorted -# correctly (rather than alphabetically), we need to overload the < -# operator and use this class instead of QTableWidgetItem. -class myTableWidgetItem(QtGui.QTableWidgetItem): - - def __lt__(self, other): - return int(self.data(33).toPyObject()) < int(other.data(33).toPyObject()) - - app = None myapp = None -class MySingleApplication(QtGui.QApplication): +class BitmessageQtApplication(QtGui.QApplication): """ Listener to allow our Qt form to get focus when another instance of the application is open. @@ -4369,9 +4236,29 @@ class MySingleApplication(QtGui.QApplication): # Unique identifier for this application uuid = '6ec0149b-96e1-4be1-93ab-1465fb3ebf7c' + @staticmethod + def get_windowstyle(): + """Get window style set in config or default""" + return config.safeGet( + 'bitmessagesettings', 'windowstyle', + 'Windows' if is_windows else 'GTK+' + ) + def __init__(self, *argv): - super(MySingleApplication, self).__init__(*argv) - id = MySingleApplication.uuid + super(BitmessageQtApplication, self).__init__(*argv) + id = BitmessageQtApplication.uuid + + QtCore.QCoreApplication.setOrganizationName("PyBitmessage") + QtCore.QCoreApplication.setOrganizationDomain("bitmessage.org") + QtCore.QCoreApplication.setApplicationName("pybitmessageqt") + + self.setStyle(self.get_windowstyle()) + + font = config.safeGet('bitmessagesettings', 'font') + if font: + # family, size, weight = font.split(',') + family, size = font.split(',') + self.setFont(QtGui.QFont(family, int(size))) self.server = None self.is_running = False @@ -4399,6 +4286,8 @@ def __init__(self, *argv): self.server.listen(id) self.server.newConnection.connect(self.on_new_connection) + self.setStyleSheet("QStatusBar::item { border: 0px solid black }") + def __del__(self): if self.server: self.server.close() @@ -4411,35 +4300,30 @@ def on_new_connection(self): def init(): global app if not app: - app = MySingleApplication(sys.argv) + app = BitmessageQtApplication(sys.argv) return app def run(): global myapp app = init() - change_translation(l10n.getTranslationLanguage()) - app.setStyleSheet("QStatusBar::item { border: 0px solid black }") myapp = MyForm() - myapp.sqlInit() myapp.appIndicatorInit(app) - myapp.indicatorInit() - myapp.notifierInit() - myapp._firstrun = BMConfigParser().safeGetBoolean( - 'bitmessagesettings', 'dontconnect') + if myapp._firstrun: myapp.showConnectDialog() # ask the user if we may connect - myapp.ui.updateNetworkSwitchMenuLabel() # try: -# if BMConfigParser().get('bitmessagesettings', 'mailchuck') < 1: -# myapp.showMigrationWizard(BMConfigParser().get('bitmessagesettings', 'mailchuck')) +# 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)) - sys.exit(app.exec_()) + app.exec_() diff --git a/src/bitmessageqt/about.ui b/src/bitmessageqt/about.ui index 8ec7159c3c..49bd4ecacb 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 © 2013-2017 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 @@ -66,7 +66,7 @@ - <html><head/><body><p>Distributed under the MIT/X11 software license; see <a href="http://www.opensource.org/licenses/mit-license.php"><span style=" text-decoration: underline; color:#0000ff;">http://www.opensource.org/licenses/mit-license.php</span></a></p></body></html> + <html><head/><body><p>Distributed under the MIT/X11 software license; see <a href="https://www.opensource.org/licenses/mit-license.php"><span style=" text-decoration: underline; color:#0000ff;">https://www.opensource.org/licenses/mit-license.php</span></a></p></body></html> true diff --git a/src/bitmessageqt/account.py b/src/bitmessageqt/account.py index 92d497f8f7..8c82c6f64e 100644 --- a/src/bitmessageqt/account.py +++ b/src/bitmessageqt/account.py @@ -1,26 +1,39 @@ -from PyQt4 import QtCore, QtGui +# pylint: disable=too-many-instance-attributes,attribute-defined-outside-init +""" +account.py +========== -import queues +Account related functions. + +""" + +from __future__ import absolute_import + +import inspect import re import sys -import inspect -from helper_sql import * -from helper_ackPayload import genAckPayload -from addresses import decodeAddress -from bmconfigparser import BMConfigParser -from foldertree import AccountMixin -from pyelliptic.openssl import OpenSSL -from utils import str_broadcast_subscribers import time -def getSortedAccounts(): - configSections = BMConfigParser().addresses() - configSections.sort(cmp = - lambda x,y: cmp(unicode(BMConfigParser().get(x, 'label'), 'utf-8').lower(), unicode(BMConfigParser().get(y, 'label'), 'utf-8').lower()) - ) - return configSections +from PyQt4 import QtGui + +import queues +from addresses import decodeAddress +from bmconfigparser import config +from helper_ackPayload import genAckPayload +from helper_sql import sqlQuery, sqlExecute +from .foldertree import AccountMixin +from .utils import str_broadcast_subscribers + -def getSortedSubscriptions(count = False): +def getSortedSubscriptions(count=False): + """ + Actually return a grouped dictionary rather than a sorted list + + :param count: Whether to count messages for each fromaddress in the inbox + :type count: bool, default False + :retuns: dict keys are addresses, values are dicts containing settings + :rtype: dict, default {} + """ queryreturn = sqlQuery('SELECT label, address, enabled FROM subscriptions ORDER BY label COLLATE NOCASE ASC') ret = {} for row in queryreturn: @@ -37,7 +50,7 @@ def getSortedSubscriptions(count = False): GROUP BY inbox.fromaddress, folder''', str_broadcast_subscribers) for row in queryreturn: address, folder, cnt = row - if not folder in ret[address]: + if folder not in ret[address]: ret[address][folder] = { 'label': ret[address]['inbox']['label'], 'enabled': ret[address]['inbox']['enabled'] @@ -45,9 +58,11 @@ def getSortedSubscriptions(count = False): ret[address][folder]['count'] = cnt return ret + def accountClass(address): - if not BMConfigParser().has_section(address): - # FIXME: This BROADCAST section makes no sense + """Return a BMAccount for the address""" + if not config.has_section(address): + # .. todo:: This BROADCAST section makes no sense if address == str_broadcast_subscribers: subscription = BroadcastAccount(address) if subscription.type != AccountMixin.BROADCAST: @@ -59,9 +74,8 @@ def accountClass(address): return NoAccount(address) return subscription try: - gateway = BMConfigParser().get(address, "gateway") - for name, cls in inspect.getmembers(sys.modules[__name__], inspect.isclass): -# obj = g(address) + gateway = config.get(address, "gateway") + for _, cls in inspect.getmembers(sys.modules[__name__], inspect.isclass): if issubclass(cls, GatewayAccount) and cls.gatewayName == gateway: return cls(address) # general gateway @@ -70,35 +84,40 @@ def accountClass(address): pass # no gateway return BMAccount(address) - -class AccountColor(AccountMixin): - def __init__(self, address, type = None): + + +class AccountColor(AccountMixin): # pylint: disable=too-few-public-methods + """Set the type of account""" + + def __init__(self, address, address_type=None): self.isEnabled = True self.address = address - if type is None: + 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=?''', self.address): + '''select label from subscriptions where address=?''', self.address): self.type = AccountMixin.SUBSCRIPTION else: self.type = AccountMixin.NORMAL else: - self.type = type + self.type = address_type + - class BMAccount(object): - def __init__(self, address = None): + """Encapsulate a Bitmessage account""" + + def __init__(self, address=None): self.address = address self.type = AccountMixin.NORMAL - 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 @@ -108,12 +127,11 @@ def __init__(self, address = None): if queryreturn: self.type = AccountMixin.SUBSCRIPTION - def getLabel(self, address = None): + def getLabel(self, address=None): + """Get a label for this bitmessage account""" if address is None: address = self.address - label = address - if BMConfigParser().has_section(address): - label = BMConfigParser().get(address, 'label') + label = config.safeGet(address, 'label', address) queryreturn = sqlQuery( '''select label from addressbook where address=?''', address) if queryreturn != []: @@ -126,8 +144,10 @@ def getLabel(self, address = None): for row in queryreturn: label, = row return label - + def parseMessage(self, toAddress, fromAddress, subject, message): + """Set metadata and address labels on self""" + self.toAddress = toAddress self.fromAddress = fromAddress if isinstance(subject, unicode): @@ -140,36 +160,45 @@ def parseMessage(self, toAddress, fromAddress, subject, message): class NoAccount(BMAccount): - def __init__(self, address = None): + """Override the __init__ method on a BMAccount""" + + def __init__(self, address=None): # pylint: disable=super-init-not-called self.address = address self.type = AccountMixin.NORMAL - def getLabel(self, address = None): + def getLabel(self, address=None): if address is None: address = self.address return address - + class SubscriptionAccount(BMAccount): + """Encapsulate a subscription account""" pass - + class BroadcastAccount(BMAccount): + """Encapsulate a broadcast account""" pass - - + + class GatewayAccount(BMAccount): + """Encapsulate a gateway account""" + gatewayName = None ALL_OK = 0 REGISTRATION_DENIED = 1 + def __init__(self, address): super(GatewayAccount, self).__init__(address) - + def send(self): + """Override the send method for gateway accounts""" + + # pylint: disable=unused-variable status, addressVersionNumber, streamNumber, ripe = decodeAddress(self.toAddress) - stealthLevel = BMConfigParser().safeGetInt('bitmessagesettings', 'ackstealthlevel') + stealthLevel = config.safeGetInt('bitmessagesettings', 'ackstealthlevel') ackdata = genAckPayload(streamNumber, stealthLevel) - t = () sqlExecute( '''INSERT INTO sent VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''', '', @@ -179,47 +208,52 @@ def send(self): self.subject, self.message, ackdata, - int(time.time()), # sentTime (this will never change) - int(time.time()), # lastActionTime - 0, # sleepTill time. This will get set when the POW gets done. + int(time.time()), # sentTime (this will never change) + int(time.time()), # lastActionTime + 0, # sleepTill time. This will get set when the POW gets done. 'msgqueued', - 0, # retryNumber - 'sent', # folder - 2, # encodingtype - min(BMConfigParser().getint('bitmessagesettings', 'ttl'), 86400 * 2) # not necessary to have a TTL higher than 2 days + 0, # retryNumber + 'sent', # folder + 2, # encodingtype + # not necessary to have a TTL higher than 2 days + min(config.getint('bitmessagesettings', 'ttl'), 86400 * 2) ) queues.workerQueue.put(('sendmessage', self.toAddress)) - - def parseMessage(self, toAddress, fromAddress, subject, message): - super(GatewayAccount, self).parseMessage(toAddress, fromAddress, subject, message) + class MailchuckAccount(GatewayAccount): + """Encapsulate a particular kind of gateway account""" + # set "gateway" in keys.dat to this gatewayName = "mailchuck" registrationAddress = "BM-2cVYYrhaY5Gbi3KqrX9Eae2NRNrkfrhCSA" unregistrationAddress = "BM-2cVMAHTRjZHCTPMue75XBK5Tco175DtJ9J" relayAddress = "BM-2cWim8aZwUNqxzjMxstnUMtVEUQJeezstf" - regExpIncoming = re.compile("(.*)MAILCHUCK-FROM::(\S+) \| (.*)") - regExpOutgoing = re.compile("(\S+) (.*)") + regExpIncoming = re.compile(r"(.*)MAILCHUCK-FROM::(\S+) \| (.*)") + regExpOutgoing = re.compile(r"(\S+) (.*)") + def __init__(self, address): super(MailchuckAccount, self).__init__(address) self.feedback = self.ALL_OK - + def createMessage(self, toAddress, fromAddress, subject, message): + """createMessage specific to a MailchuckAccount""" self.subject = toAddress + " " + subject self.toAddress = self.relayAddress self.fromAddress = fromAddress self.message = message - + def register(self, email): + """register specific to a MailchuckAccount""" self.toAddress = self.registrationAddress self.subject = email self.message = "" self.fromAddress = self.address self.send() - + def unregister(self): + """unregister specific to a MailchuckAccount""" self.toAddress = self.unregistrationAddress self.subject = "" self.message = "" @@ -227,6 +261,7 @@ def unregister(self): self.send() def status(self): + """status specific to a MailchuckAccount""" self.toAddress = self.registrationAddress self.subject = "status" self.message = "" @@ -234,12 +269,16 @@ def status(self): self.send() def settings(self): + """settings specific to a MailchuckAccount""" + self.toAddress = self.registrationAddress self.subject = "config" - self.message = QtGui.QApplication.translate("Mailchuck", """# You can use this to configure your email gateway account + self.message = QtGui.QApplication.translate( + "Mailchuck", + """# You can use this to configure your email gateway account # Uncomment the setting you want to use # Here are the options: -# +# # pgp: server # The email gateway will create and maintain PGP keys for you and sign, verify, # encrypt and decrypt on your behalf. When you want to use PGP but are lazy, @@ -255,7 +294,7 @@ def settings(self): # # attachments: no # Attachments will be ignored. -# +# # archive: yes # Your incoming emails will be archived on the server. Use this if you need # help with debugging problems or you need a third party proof of emails. This @@ -279,10 +318,12 @@ def settings(self): self.fromAddress = self.address def parseMessage(self, toAddress, fromAddress, subject, message): + """parseMessage specific to a MailchuckAccount""" + super(MailchuckAccount, self).parseMessage(toAddress, fromAddress, subject, message) if fromAddress == self.relayAddress: matches = self.regExpIncoming.search(subject) - if not matches is None: + if matches is not None: self.subject = "" if not matches.group(1) is None: self.subject += matches.group(1) @@ -293,7 +334,7 @@ def parseMessage(self, toAddress, fromAddress, subject, message): self.fromAddress = matches.group(2) if toAddress == self.relayAddress: matches = self.regExpOutgoing.search(subject) - if not matches is None: + if matches is not None: if not matches.group(2) is None: self.subject = matches.group(2) if not matches.group(1) is None: diff --git a/src/bitmessageqt/address_dialogs.py b/src/bitmessageqt/address_dialogs.py index 2ea5cef818..bf571041a6 100644 --- a/src/bitmessageqt/address_dialogs.py +++ b/src/bitmessageqt/address_dialogs.py @@ -1,29 +1,38 @@ +""" +Dialogs that work with BM address. +""" +# pylint: disable=attribute-defined-outside-init,too-few-public-methods,relative-import + +import hashlib + from PyQt4 import QtCore, QtGui -from addresses import decodeAddress, encodeVarint, addBMIfNotPresent -from account import ( - GatewayAccount, MailchuckAccount, AccountMixin, accountClass, - getSortedAccounts -) -from tr import _translate -from retranslateui import RetranslateMixin -import widgets import queues -import hashlib -from inventory import Inventory +import widgets +import state +from account import AccountMixin, GatewayAccount, MailchuckAccount, accountClass +from addresses import addBMIfNotPresent, decodeAddress, encodeVarint +from bmconfigparser import config as global_config +from tr import _translate class AddressCheckMixin(object): + """Base address validation class for QT UI""" def __init__(self): self.valid = False - QtCore.QObject.connect(self.lineEditAddress, QtCore.SIGNAL( - "textChanged(QString)"), self.addressChanged) + QtCore.QObject.connect( # pylint: disable=no-member + self.lineEditAddress, + QtCore.SIGNAL("textChanged(QString)"), + self.addressChanged) def _onSuccess(self, addressVersion, streamNumber, ripe): pass def addressChanged(self, QString): + """ + Address validation callback, performs validation and gives feedback + """ status, addressVersion, streamNumber, ripe = decodeAddress( str(QString)) self.valid = status == 'success' @@ -71,11 +80,14 @@ def addressChanged(self, QString): class AddressDataDialog(QtGui.QDialog, AddressCheckMixin): + """QDialog with Bitmessage address validation""" + def __init__(self, parent): super(AddressDataDialog, self).__init__(parent) self.parent = parent def accept(self): + """Callback for QDIalog accepting value""" if self.valid: self.data = ( addBMIfNotPresent(str(self.lineEditAddress.text())), @@ -89,7 +101,8 @@ def accept(self): super(AddressDataDialog, self).accept() -class AddAddressDialog(AddressDataDialog, RetranslateMixin): +class AddAddressDialog(AddressDataDialog): + """QDialog for adding a new address""" def __init__(self, parent=None, address=None): super(AddAddressDialog, self).__init__(parent) @@ -99,7 +112,8 @@ def __init__(self, parent=None, address=None): self.lineEditAddress.setText(address) -class NewAddressDialog(QtGui.QDialog, RetranslateMixin): +class NewAddressDialog(QtGui.QDialog): + """QDialog for generating a new address""" def __init__(self, parent=None): super(NewAddressDialog, self).__init__(parent) @@ -107,7 +121,7 @@ def __init__(self, parent=None): # 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) @@ -115,6 +129,7 @@ def __init__(self, parent=None): self.show() def accept(self): + """accept callback""" self.hide() # self.buttonBox.enabled = False if self.radioButtonRandomAddress.isChecked(): @@ -159,7 +174,8 @@ def accept(self): )) -class NewSubscriptionDialog(AddressDataDialog, RetranslateMixin): +class NewSubscriptionDialog(AddressDataDialog): + """QDialog for subscribing to an address""" def __init__(self, parent=None): super(NewSubscriptionDialog, self).__init__(parent) @@ -174,13 +190,13 @@ def _onSuccess(self, addressVersion, streamNumber, ripe): " broadcasts." )) else: - Inventory().flush() + state.Inventory.flush() doubleHashOfAddressData = hashlib.sha512(hashlib.sha512( - encodeVarint(addressVersion) + - encodeVarint(streamNumber) + ripe + 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( @@ -201,7 +217,8 @@ def _onSuccess(self, addressVersion, streamNumber, ripe): )) -class RegenerateAddressesDialog(QtGui.QDialog, RetranslateMixin): +class RegenerateAddressesDialog(QtGui.QDialog): + """QDialog for regenerating deterministic addresses""" def __init__(self, parent=None): super(RegenerateAddressesDialog, self).__init__(parent) widgets.load('regenerateaddresses.ui', self) @@ -209,9 +226,12 @@ def __init__(self, parent=None): QtGui.QWidget.resize(self, QtGui.QWidget.sizeHint(self)) -class SpecialAddressBehaviorDialog(QtGui.QDialog, RetranslateMixin): +class SpecialAddressBehaviorDialog(QtGui.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() @@ -237,11 +257,7 @@ def __init__(self, parent=None, config=None): self.radioButtonBehaviorMailingList.click() else: self.radioButtonBehaveNormalAddress.click() - try: - mailingListName = config.get( - self.address, 'mailinglistname') - except: - mailingListName = '' + mailingListName = config.safeGet(self.address, 'mailinglistname', '') self.lineEditMailingListName.setText( unicode(mailingListName, 'utf-8') ) @@ -250,6 +266,7 @@ def __init__(self, parent=None, config=None): self.show() def accept(self): + """Accept callback""" self.hide() if self.address_is_chan: return @@ -267,15 +284,16 @@ def accept(self): self.config.set(str(self.address), 'mailinglistname', str( self.lineEditMailingListName.text().toUtf8())) self.parent.setCurrentItemColor( - QtGui.QColor(137, 04, 177)) # magenta + QtGui.QColor(137, 4, 177)) # magenta self.parent.rerenderComboBoxSendFrom() self.parent.rerenderComboBoxSendFromBroadcast() self.config.save() self.parent.rerenderMessagelistToLabels() -class EmailGatewayDialog(QtGui.QDialog, RetranslateMixin): - def __init__(self, parent, config=None, account=None): +class EmailGatewayDialog(QtGui.QDialog): + """QDialog for email gateway control""" + def __init__(self, parent, config=global_config, account=None): super(EmailGatewayDialog, self).__init__(parent) widgets.load('emailgateway.ui', self) self.parent = parent @@ -315,6 +333,7 @@ def __init__(self, parent, config=None, account=None): QtGui.QWidget.resize(self, QtGui.QWidget.sizeHint(self)) def accept(self): + """Accept callback""" self.hide() # no chans / mailinglists if self.acct.type != AccountMixin.NORMAL: diff --git a/src/bitmessageqt/addressvalidator.py b/src/bitmessageqt/addressvalidator.py index f9de70a214..dc61b41cde 100644 --- a/src/bitmessageqt/addressvalidator.py +++ b/src/bitmessageqt/addressvalidator.py @@ -1,14 +1,30 @@ -from PyQt4 import QtGui +""" +Address validator module. +""" +# pylint: disable=too-many-branches,too-many-arguments + from Queue import Empty +from PyQt4 import QtGui + from addresses import decodeAddress, addBMIfNotPresent -from account import getSortedAccounts +from bmconfigparser import config from queues import apiAddressGeneratorReturnQueue, addressGeneratorQueue from tr import _translate from utils import str_chan -class AddressPassPhraseValidatorMixin(): - def setParams(self, passPhraseObject=None, addressObject=None, feedBackObject=None, buttonBox=None, addressMandatory=True): + +class AddressPassPhraseValidatorMixin(object): + """Bitmessage address or passphrase validator class for Qt UI""" + def setParams( + self, + passPhraseObject=None, + addressObject=None, + feedBackObject=None, + buttonBox=None, + addressMandatory=True, + ): + """Initialisation""" self.addressObject = addressObject self.passPhraseObject = passPhraseObject self.feedBackObject = feedBackObject @@ -19,6 +35,7 @@ def setParams(self, passPhraseObject=None, addressObject=None, feedBackObject=No self.okButtonLabel = self.buttonBox.button(QtGui.QDialogButtonBox.Ok).text() def setError(self, string): + """Indicate that the validation is pending or failed""" if string is not None and self.feedBackObject is not None: font = QtGui.QFont() font.setBold(True) @@ -29,11 +46,14 @@ def setError(self, string): if self.buttonBox: self.buttonBox.button(QtGui.QDialogButtonBox.Ok).setEnabled(False) if string is not None and self.feedBackObject is not None: - self.buttonBox.button(QtGui.QDialogButtonBox.Ok).setText(_translate("AddressValidator", "Invalid")) + self.buttonBox.button(QtGui.QDialogButtonBox.Ok).setText( + _translate("AddressValidator", "Invalid")) else: - self.buttonBox.button(QtGui.QDialogButtonBox.Ok).setText(_translate("AddressValidator", "Validating...")) + self.buttonBox.button(QtGui.QDialogButtonBox.Ok).setText( + _translate("AddressValidator", "Validating...")) def setOK(self, string): + """Indicate that the validation succeeded""" if string is not None and self.feedBackObject is not None: font = QtGui.QFont() font.setBold(False) @@ -46,12 +66,13 @@ def setOK(self, string): self.buttonBox.button(QtGui.QDialogButtonBox.Ok).setText(self.okButtonLabel) def checkQueue(self): + """Validator queue loop""" gotOne = False # wait until processing is done if not addressGeneratorQueue.empty(): self.setError(None) - return + return None while True: try: @@ -60,25 +81,30 @@ def checkQueue(self): if gotOne: break else: - return + return None else: gotOne = True - if len(addressGeneratorReturnValue) == 0: + if not addressGeneratorReturnValue: self.setError(_translate("AddressValidator", "Address already present as one of your identities.")) return (QtGui.QValidator.Intermediate, 0) if addressGeneratorReturnValue[0] == 'chan name does not match address': - self.setError(_translate("AddressValidator", "Although the Bitmessage address you entered was valid, it doesn\'t match the chan name.")) + self.setError( + _translate( + "AddressValidator", + "Although the Bitmessage address you " + "entered was valid, it doesn't match the chan name.")) return (QtGui.QValidator.Intermediate, 0) self.setOK(_translate("MainWindow", "Passphrase and address appear to be valid.")) def returnValid(self): + """Return the value of whether the validation was successful""" if self.isValid: return QtGui.QValidator.Acceptable - else: - return QtGui.QValidator.Intermediate + return QtGui.QValidator.Intermediate def validate(self, s, pos): + """Top level validator method""" if self.addressObject is None: address = None else: @@ -99,15 +125,21 @@ def validate(self, s, pos): if self.addressMandatory or address is not None: # check if address already exists: - if address in getSortedAccounts(): + if address in config.addresses(): self.setError(_translate("AddressValidator", "Address already present as one of your identities.")) return (QtGui.QValidator.Intermediate, pos) # version too high if decodeAddress(address)[0] == 'versiontoohigh': - self.setError(_translate("AddressValidator", "Address too new. Although that Bitmessage address might be valid, its version number is too new for us to handle. Perhaps you need to upgrade Bitmessage.")) + self.setError( + _translate( + "AddressValidator", + "Address too new. Although that Bitmessage" + " address might be valid, its version number" + " is too new for us to handle. Perhaps you need" + " to upgrade Bitmessage.")) return (QtGui.QValidator.Intermediate, pos) - + # invalid if decodeAddress(address)[0] != 'success': self.setError(_translate("AddressValidator", "The Bitmessage address is not valid.")) @@ -122,23 +154,28 @@ def validate(self, s, pos): if address is None: addressGeneratorQueue.put(('createChan', 4, 1, str_chan + ' ' + str(passPhrase), passPhrase, False)) else: - addressGeneratorQueue.put(('joinChan', addBMIfNotPresent(address), str_chan + ' ' + str(passPhrase), passPhrase, False)) + addressGeneratorQueue.put( + ('joinChan', addBMIfNotPresent(address), + "{} {}".format(str_chan, passPhrase), passPhrase, False)) if self.buttonBox.button(QtGui.QDialogButtonBox.Ok).hasFocus(): return (self.returnValid(), pos) - else: - return (QtGui.QValidator.Intermediate, pos) + return (QtGui.QValidator.Intermediate, pos) def checkData(self): + """Validator Qt signal interface""" return self.validate("", 0) + class AddressValidator(QtGui.QValidator, AddressPassPhraseValidatorMixin): + """AddressValidator class for Qt UI""" def __init__(self, parent=None, passPhraseObject=None, feedBackObject=None, buttonBox=None, addressMandatory=True): super(AddressValidator, self).__init__(parent) self.setParams(passPhraseObject, parent, feedBackObject, buttonBox, addressMandatory) class PassPhraseValidator(QtGui.QValidator, AddressPassPhraseValidatorMixin): + """PassPhraseValidator class for Qt UI""" def __init__(self, parent=None, addressObject=None, feedBackObject=None, buttonBox=None, addressMandatory=False): super(PassPhraseValidator, self).__init__(parent) self.setParams(parent, addressObject, feedBackObject, buttonBox, addressMandatory) diff --git a/src/bitmessageqt/bitmessageui.py b/src/bitmessageqt/bitmessageui.py index cb3578c08f..bee8fd571a 100644 --- a/src/bitmessageqt/bitmessageui.py +++ b/src/bitmessageqt/bitmessageui.py @@ -8,7 +8,7 @@ # WARNING! All changes made in this file will be lost! from PyQt4 import QtCore, QtGui -from bmconfigparser import BMConfigParser +from bmconfigparser import config from foldertree import AddressBookCompleter from messageview import MessageView from messagecompose import MessageCompose @@ -24,24 +24,28 @@ def _fromUtf8(s): try: _encoding = QtGui.QApplication.UnicodeUTF8 - def _translate(context, text, disambig, encoding = QtCore.QCoreApplication.CodecForTr, n = None): + + def _translate(context, text, disambig, encoding=QtCore.QCoreApplication.CodecForTr, n=None): if n is None: return QtGui.QApplication.translate(context, text, disambig, _encoding) else: return QtGui.QApplication.translate(context, text, disambig, _encoding, n) except AttributeError: - def _translate(context, text, disambig, encoding = QtCore.QCoreApplication.CodecForTr, n = None): + def _translate(context, text, disambig, encoding=QtCore.QCoreApplication.CodecForTr, n=None): if n is None: return QtGui.QApplication.translate(context, text, disambig) else: return QtGui.QApplication.translate(context, text, disambig, QtCore.QCoreApplication.CodecForTr, n) + class Ui_MainWindow(object): def setupUi(self, MainWindow): MainWindow.setObjectName(_fromUtf8("MainWindow")) MainWindow.resize(885, 580) icon = QtGui.QIcon() - icon.addPixmap(QtGui.QPixmap(_fromUtf8(":/newPrefix/images/can-icon-24px.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off) + icon.addPixmap( + QtGui.QPixmap(_fromUtf8(":/newPrefix/images/can-icon-24px.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off + ) MainWindow.setWindowIcon(icon) MainWindow.setTabShape(QtGui.QTabWidget.Rounded) self.centralwidget = QtGui.QWidget(MainWindow) @@ -57,7 +61,8 @@ def setupUi(self, MainWindow): self.tabWidget.setMinimumSize(QtCore.QSize(0, 0)) self.tabWidget.setBaseSize(QtCore.QSize(0, 0)) font = QtGui.QFont() - font.setPointSize(9) + base_size = QtGui.QApplication.instance().font().pointSize() + font.setPointSize(int(base_size * 0.75)) self.tabWidget.setFont(font) self.tabWidget.setTabPosition(QtGui.QTabWidget.North) self.tabWidget.setTabShape(QtGui.QTabWidget.Rounded) @@ -75,7 +80,9 @@ def setupUi(self, MainWindow): self.treeWidgetYourIdentities.setObjectName(_fromUtf8("treeWidgetYourIdentities")) self.treeWidgetYourIdentities.resize(200, self.treeWidgetYourIdentities.height()) icon1 = QtGui.QIcon() - icon1.addPixmap(QtGui.QPixmap(_fromUtf8(":/newPrefix/images/identities.png")), QtGui.QIcon.Selected, QtGui.QIcon.Off) + icon1.addPixmap( + QtGui.QPixmap(_fromUtf8(":/newPrefix/images/identities.png")), QtGui.QIcon.Selected, QtGui.QIcon.Off + ) self.treeWidgetYourIdentities.headerItem().setIcon(0, icon1) self.verticalSplitter_12.addWidget(self.treeWidgetYourIdentities) self.pushButtonNewAddress = QtGui.QPushButton(self.inbox) @@ -104,6 +111,7 @@ def setupUi(self, MainWindow): self.inboxSearchOption.addItem(_fromUtf8("")) self.inboxSearchOption.addItem(_fromUtf8("")) self.inboxSearchOption.setSizeAdjustPolicy(QtGui.QComboBox.AdjustToContents) + self.inboxSearchOption.setCurrentIndex(3) self.horizontalSplitterSearch.addWidget(self.inboxSearchOption) self.horizontalSplitterSearch.handle(1).setEnabled(False) self.horizontalSplitterSearch.setStretchFactor(0, 1) @@ -175,7 +183,9 @@ def setupUi(self, MainWindow): self.tableWidgetAddressBook.resize(200, self.tableWidgetAddressBook.height()) item = QtGui.QTableWidgetItem() icon3 = QtGui.QIcon() - icon3.addPixmap(QtGui.QPixmap(_fromUtf8(":/newPrefix/images/addressbook.png")), QtGui.QIcon.Selected, QtGui.QIcon.Off) + icon3.addPixmap( + QtGui.QPixmap(_fromUtf8(":/newPrefix/images/addressbook.png")), QtGui.QIcon.Selected, QtGui.QIcon.Off + ) item.setIcon(icon3) self.tableWidgetAddressBook.setHorizontalHeaderItem(0, item) item = QtGui.QTableWidgetItem() @@ -376,7 +386,9 @@ def setupUi(self, MainWindow): self.treeWidgetSubscriptions.setObjectName(_fromUtf8("treeWidgetSubscriptions")) self.treeWidgetSubscriptions.resize(200, self.treeWidgetSubscriptions.height()) icon5 = QtGui.QIcon() - icon5.addPixmap(QtGui.QPixmap(_fromUtf8(":/newPrefix/images/subscriptions.png")), QtGui.QIcon.Selected, QtGui.QIcon.Off) + icon5.addPixmap( + QtGui.QPixmap(_fromUtf8(":/newPrefix/images/subscriptions.png")), QtGui.QIcon.Selected, QtGui.QIcon.Off + ) self.treeWidgetSubscriptions.headerItem().setIcon(0, icon5) self.verticalSplitter_3.addWidget(self.treeWidgetSubscriptions) self.pushButtonAddSubscription = QtGui.QPushButton(self.subscriptions) @@ -403,8 +415,8 @@ def setupUi(self, MainWindow): self.inboxSearchOptionSubscriptions.addItem(_fromUtf8("")) self.inboxSearchOptionSubscriptions.addItem(_fromUtf8("")) self.inboxSearchOptionSubscriptions.addItem(_fromUtf8("")) - self.inboxSearchOptionSubscriptions.addItem(_fromUtf8("")) self.inboxSearchOptionSubscriptions.setSizeAdjustPolicy(QtGui.QComboBox.AdjustToContents) + self.inboxSearchOptionSubscriptions.setCurrentIndex(2) self.horizontalSplitter_2.addWidget(self.inboxSearchOptionSubscriptions) self.horizontalSplitter_2.handle(1).setEnabled(False) self.horizontalSplitter_2.setStretchFactor(0, 1) @@ -455,7 +467,9 @@ def setupUi(self, MainWindow): self.horizontalSplitter_4.setCollapsible(1, False) self.gridLayout_3.addWidget(self.horizontalSplitter_4, 0, 0, 1, 1) icon6 = QtGui.QIcon() - icon6.addPixmap(QtGui.QPixmap(_fromUtf8(":/newPrefix/images/subscriptions.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off) + icon6.addPixmap( + QtGui.QPixmap(_fromUtf8(":/newPrefix/images/subscriptions.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off + ) self.tabWidget.addTab(self.subscriptions, icon6, _fromUtf8("")) self.chans = QtGui.QWidget() self.chans.setObjectName(_fromUtf8("chans")) @@ -475,7 +489,9 @@ def setupUi(self, MainWindow): self.treeWidgetChans.setObjectName(_fromUtf8("treeWidgetChans")) self.treeWidgetChans.resize(200, self.treeWidgetChans.height()) icon7 = QtGui.QIcon() - icon7.addPixmap(QtGui.QPixmap(_fromUtf8(":/newPrefix/images/can-icon-16px.png")), QtGui.QIcon.Selected, QtGui.QIcon.Off) + icon7.addPixmap( + QtGui.QPixmap(_fromUtf8(":/newPrefix/images/can-icon-16px.png")), QtGui.QIcon.Selected, QtGui.QIcon.Off + ) self.treeWidgetChans.headerItem().setIcon(0, icon7) self.verticalSplitter_17.addWidget(self.treeWidgetChans) self.pushButtonAddChan = QtGui.QPushButton(self.chans) @@ -504,6 +520,7 @@ def setupUi(self, MainWindow): self.inboxSearchOptionChans.addItem(_fromUtf8("")) self.inboxSearchOptionChans.addItem(_fromUtf8("")) self.inboxSearchOptionChans.setSizeAdjustPolicy(QtGui.QComboBox.AdjustToContents) + self.inboxSearchOptionChans.setCurrentIndex(3) self.horizontalSplitter_6.addWidget(self.inboxSearchOptionChans) self.horizontalSplitter_6.handle(1).setEnabled(False) self.horizontalSplitter_6.setStretchFactor(0, 1) @@ -554,12 +571,14 @@ def setupUi(self, MainWindow): self.horizontalSplitter_7.setCollapsible(1, False) self.gridLayout_4.addWidget(self.horizontalSplitter_7, 0, 0, 1, 1) icon8 = QtGui.QIcon() - icon8.addPixmap(QtGui.QPixmap(_fromUtf8(":/newPrefix/images/can-icon-16px.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off) + icon8.addPixmap( + QtGui.QPixmap(_fromUtf8(":/newPrefix/images/can-icon-16px.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off + ) self.tabWidget.addTab(self.chans, icon8, _fromUtf8("")) 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() @@ -651,9 +670,17 @@ def setupUi(self, MainWindow): MainWindow.setTabOrder(self.lineEditSubject, self.textEditMessage) MainWindow.setTabOrder(self.textEditMessage, self.pushButtonAddSubscription) + # Popup menu actions container for the Sent page + # pylint: disable=attribute-defined-outside-init + self.sentContextMenuToolbar = QtGui.QToolBar() + # Popup menu actions container for chans tree + self.addressContextMenuToolbar = QtGui.QToolBar() + # Popup menu actions container for subscriptions tree + self.subscriptionsContextMenuToolbar = QtGui.QToolBar() + 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) @@ -691,19 +718,24 @@ def retranslateUi(self, MainWindow): 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, QtCore.QCoreApplication.CodecForTr, hours)) + self.labelHumanFriendlyTTLDescription.setText( + _translate("MainWindow", "%n hour(s)", None, QtCore.QCoreApplication.CodecForTr, hours) + ) self.pushButtonClear.setText(_translate("MainWindow", "Clear", None)) self.pushButtonSend.setText(_translate("MainWindow", "Send", None)) self.tabWidget.setTabText(self.tabWidget.indexOf(self.send), _translate("MainWindow", "Send", None)) @@ -711,10 +743,9 @@ def retranslateUi(self, MainWindow): self.pushButtonAddSubscription.setText(_translate("MainWindow", "Add new Subscription", None)) self.inboxSearchLineEditSubscriptions.setPlaceholderText(_translate("MainWindow", "Search", None)) self.inboxSearchOptionSubscriptions.setItemText(0, _translate("MainWindow", "All", None)) - self.inboxSearchOptionSubscriptions.setItemText(1, _translate("MainWindow", "To", None)) - self.inboxSearchOptionSubscriptions.setItemText(2, _translate("MainWindow", "From", None)) - self.inboxSearchOptionSubscriptions.setItemText(3, _translate("MainWindow", "Subject", None)) - self.inboxSearchOptionSubscriptions.setItemText(4, _translate("MainWindow", "Message", None)) + self.inboxSearchOptionSubscriptions.setItemText(1, _translate("MainWindow", "From", None)) + self.inboxSearchOptionSubscriptions.setItemText(2, _translate("MainWindow", "Subject", None)) + self.inboxSearchOptionSubscriptions.setItemText(3, _translate("MainWindow", "Message", None)) self.tableWidgetInboxSubscriptions.setSortingEnabled(True) item = self.tableWidgetInboxSubscriptions.horizontalHeaderItem(0) item.setText(_translate("MainWindow", "To", None)) @@ -724,7 +755,10 @@ def retranslateUi(self, MainWindow): 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)) @@ -744,9 +778,15 @@ def retranslateUi(self, MainWindow): 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)) @@ -759,19 +799,20 @@ def retranslateUi(self, MainWindow): 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() -import bitmessage_icons_rc if __name__ == "__main__": import sys - + app = QtGui.QApplication(sys.argv) MainWindow = settingsmixin.SMainWindow() ui = Ui_MainWindow() ui.setupUi(MainWindow) MainWindow.show() sys.exit(app.exec_()) - diff --git a/src/bitmessageqt/blacklist.py b/src/bitmessageqt/blacklist.py index 64413ebb75..093f23d866 100644 --- a/src/bitmessageqt/blacklist.py +++ b/src/bitmessageqt/blacklist.py @@ -1,14 +1,15 @@ from PyQt4 import QtCore, QtGui -from tr import _translate -import l10n + 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 from retranslateui import RetranslateMixin -from utils import avatarize +from tr import _translate from uisignaler import UISignaler +from utils import avatarize class Blacklist(QtGui.QWidget, RetranslateMixin): @@ -38,17 +39,17 @@ def __init__(self, parent=None): "rerenderBlackWhiteList()"), 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() @@ -64,7 +65,7 @@ def click_pushButtonAddBlacklist(self): # 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=?''' @@ -82,17 +83,27 @@ def click_pushButtonAddBlacklist(self): self.tableWidgetBlacklist.setItem(0, 1, newItem) self.tableWidgetBlacklist.setSortingEnabled(True) t = (str(self.NewBlacklistDialogInstance.lineEditLabel.text().toUtf8()), 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 (?,?,?)''' sqlExecute(sql, *t) else: - self.statusBar().showMessage(_translate( - "MainWindow", "Error: You cannot add the same address to your list twice. Perhaps rename the existing one if you want.")) + UISignalQueue.put(( + 'updateStatusBar', + _translate( + "MainWindow", + "Error: You cannot add the same address to your" + " list twice. Perhaps rename the existing one" + " if you want.") + )) else: - self.statusBar().showMessage(_translate( - "MainWindow", "The address you entered was invalid. Ignoring it.")) + UISignalQueue.put(( + 'updateStatusBar', + _translate( + "MainWindow", + "The address you entered was invalid. Ignoring it.") + )) def tableWidgetBlacklistItemChanged(self, item): if item.column() == 0: @@ -147,12 +158,12 @@ def init_blacklist_popup_menu(self, connectSignal=True): 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: @@ -184,7 +195,7 @@ def on_action_BlacklistDelete(self): currentRow, 0).text().toUtf8() 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=?''', str(labelAtCurrentRow), str(addressAtCurrentRow)) @@ -213,7 +224,7 @@ def on_action_BlacklistEnable(self): currentRow, 0).setTextColor(QtGui.QApplication.palette().text().color()) self.tableWidgetBlacklist.item( currentRow, 1).setTextColor(QtGui.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)) @@ -230,7 +241,7 @@ def on_action_BlacklistDisable(self): currentRow, 0).setTextColor(QtGui.QColor(128, 128, 128)) self.tableWidgetBlacklist.item( currentRow, 1).setTextColor(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: @@ -239,4 +250,3 @@ def on_action_BlacklistDisable(self): def on_action_BlacklistSetAvatar(self): self.window().on_action_SetAvatar(self.tableWidgetBlacklist) - diff --git a/src/bitmessageqt/dialogs.py b/src/bitmessageqt/dialogs.py index cb82f3486b..dc31e26697 100644 --- a/src/bitmessageqt/dialogs.py +++ b/src/bitmessageqt/dialogs.py @@ -1,26 +1,32 @@ +""" +Custom dialog classes +""" +# pylint: disable=too-few-public-methods from PyQt4 import QtGui -from tr import _translate -from retranslateui import RetranslateMixin -import widgets -from newchandialog import NewChanDialog +import paths +import widgets from address_dialogs import ( - AddAddressDialog, NewAddressDialog, NewSubscriptionDialog, - RegenerateAddressesDialog, SpecialAddressBehaviorDialog, EmailGatewayDialog + AddAddressDialog, EmailGatewayDialog, NewAddressDialog, + NewSubscriptionDialog, RegenerateAddressesDialog, + SpecialAddressBehaviorDialog ) - -import paths +from newchandialog import NewChanDialog +from settings import SettingsDialog +from tr import _translate from version import softwareVersion __all__ = [ "NewChanDialog", "AddAddressDialog", "NewAddressDialog", "NewSubscriptionDialog", "RegenerateAddressesDialog", - "SpecialAddressBehaviorDialog", "EmailGatewayDialog" + "SpecialAddressBehaviorDialog", "EmailGatewayDialog", + "SettingsDialog" ] -class AboutDialog(QtGui.QDialog, RetranslateMixin): +class AboutDialog(QtGui.QDialog): + """The `About` dialog""" def __init__(self, parent=None): super(AboutDialog, self).__init__(parent) widgets.load('about.ui', self) @@ -32,14 +38,14 @@ def __init__(self, parent=None): self.labelVersion.setText( self.labelVersion.text().replace( ':version:', version - ).replace(':branch:', commit or 'v%s' % version) + ).replace(':branch:', commit or 'v%s' % version) ) self.labelVersion.setOpenExternalLinks(True) try: self.label_2.setText( self.label_2.text().replace( - '2017', str(last_commit.get('time').year) + '2022', str(last_commit.get('time').year) )) except AttributeError: pass @@ -47,29 +53,32 @@ def __init__(self, parent=None): self.setFixedSize(QtGui.QWidget.sizeHint(self)) -class IconGlossaryDialog(QtGui.QDialog, RetranslateMixin): +class IconGlossaryDialog(QtGui.QDialog): + """The `Icon Glossary` dialog, explaining the status icon colors""" def __init__(self, parent=None, config=None): super(IconGlossaryDialog, self).__init__(parent) widgets.load('iconglossary.ui', self) - # FIXME: check the window title visibility here + # .. todo:: FIXME: check the window title visibility here self.groupBox.setTitle('') self.labelPortNumber.setText(_translate( "iconGlossaryDialog", "You are using TCP port %1. (This can be changed in the settings)." - ).arg(config.getint('bitmessagesettings', 'port'))) + ).arg(config.getint('bitmessagesettings', 'port'))) self.setFixedSize(QtGui.QWidget.sizeHint(self)) -class HelpDialog(QtGui.QDialog, RetranslateMixin): +class HelpDialog(QtGui.QDialog): + """The `Help` dialog""" def __init__(self, parent=None): super(HelpDialog, self).__init__(parent) widgets.load('help.ui', self) self.setFixedSize(QtGui.QWidget.sizeHint(self)) -class ConnectDialog(QtGui.QDialog, RetranslateMixin): +class ConnectDialog(QtGui.QDialog): + """The `Connect` dialog""" def __init__(self, parent=None): super(ConnectDialog, self).__init__(parent) widgets.load('connect.ui', self) diff --git a/src/bitmessageqt/foldertree.py b/src/bitmessageqt/foldertree.py index 3cdf4ab020..c50b7d3d47 100644 --- a/src/bitmessageqt/foldertree.py +++ b/src/bitmessageqt/foldertree.py @@ -1,10 +1,18 @@ +""" +Folder tree and messagelist widgets definitions. +""" +# pylint: disable=too-many-arguments,bad-super-call +# pylint: disable=attribute-defined-outside-init + +from cgi import escape + from PyQt4 import QtCore, QtGui +from bmconfigparser import config +from helper_sql import sqlExecute, sqlQuery +from settingsmixin import SettingsMixin from tr import _translate -from bmconfigparser import BMConfigParser -from helper_sql import * from utils import avatarize -from settingsmixin import SettingsMixin # for pylupdate _translate("MainWindow", "inbox") @@ -12,8 +20,11 @@ _translate("MainWindow", "sent") _translate("MainWindow", "trash") +TimestampRole = QtCore.Qt.UserRole + 1 + class AccountMixin(object): + """UI-related functionality for accounts""" ALL = 0 NORMAL = 1 CHAN = 2 @@ -22,38 +33,50 @@ class AccountMixin(object): BROADCAST = 5 def accountColor(self): + """QT UI color for an account""" if not self.isEnabled: return QtGui.QColor(128, 128, 128) elif self.type == self.CHAN: return QtGui.QColor(216, 119, 0) elif self.type in [self.MAILINGLIST, self.SUBSCRIPTION]: - return QtGui.QColor(137, 04, 177) - else: - return QtGui.QApplication.palette().text().color() + return QtGui.QColor(137, 4, 177) + return QtGui.QApplication.palette().text().color() def folderColor(self): + """QT UI color for a folder""" if not self.parent().isEnabled: return QtGui.QColor(128, 128, 128) - else: - return QtGui.QApplication.palette().text().color() + return QtGui.QApplication.palette().text().color() def accountBrush(self): + """Account brush (for QT UI)""" brush = QtGui.QBrush(self.accountColor()) brush.setStyle(QtCore.Qt.NoBrush) return brush def folderBrush(self): + """Folder brush (for QT UI)""" brush = QtGui.QBrush(self.folderColor()) brush.setStyle(QtCore.Qt.NoBrush) return brush + def accountString(self): + """Account string suitable for use in To: field: label
""" + label = self._getLabel() + return ( + self.address if label == self.address + else '%s <%s>' % (label, self.address) + ) + def setAddress(self, address): + """Set bitmessage address of the object""" if address is None: self.address = None else: self.address = str(address) def setUnreadCount(self, cnt): + """Set number of unread messages""" try: if self.unreadCount == int(cnt): return @@ -64,6 +87,7 @@ def setUnreadCount(self, cnt): self.emitDataChanged() def setEnabled(self, enabled): + """Set account enabled (QT UI)""" self.isEnabled = enabled try: self.setExpanded(enabled) @@ -77,21 +101,23 @@ def setEnabled(self, enabled): self.emitDataChanged() def setType(self): + """Set account type (QT UI)""" self.setFlags(self.flags() | QtCore.Qt.ItemIsEditable) 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=?''', self.address): + '''select label from subscriptions where address=?''', self.address): self.type = AccountMixin.SUBSCRIPTION else: self.type = self.NORMAL def defaultLabel(self): + """Default label (in case no label is set manually)""" queryreturn = None retval = None if self.type in ( @@ -99,8 +125,8 @@ def defaultLabel(self): AccountMixin.CHAN, AccountMixin.MAILINGLIST): try: retval = unicode( - BMConfigParser().get(self.address, 'label'), 'utf-8') - except Exception as e: + config.get(self.address, 'label'), 'utf-8') + except Exception: queryreturn = sqlQuery( '''select label from addressbook where address=?''', self.address) elif self.type == AccountMixin.SUBSCRIPTION: @@ -120,6 +146,7 @@ def defaultLabel(self): class BMTreeWidgetItem(QtGui.QTreeWidgetItem, AccountMixin): """A common abstract class for Tree widget item""" + def __init__(self, parent, pos, address, unreadCount): super(QtGui.QTreeWidgetItem, self).__init__() self.setAddress(address) @@ -127,9 +154,10 @@ def __init__(self, parent, pos, address, unreadCount): self._setup(parent, pos) def _getAddressBracket(self, unreadCount=False): - return (" (" + str(self.unreadCount) + ")") if unreadCount else "" + return " (" + str(self.unreadCount) + ")" if unreadCount else "" def data(self, column, role): + """Override internal QT method for returning object data""" if column == 0: if role == QtCore.Qt.DisplayRole: return self._getLabel() + self._getAddressBracket( @@ -146,6 +174,7 @@ def data(self, column, role): class Ui_FolderWidget(BMTreeWidgetItem): + """Item in the account/folder tree representing a folder""" folderWeight = {"inbox": 1, "new": 2, "sent": 3, "trash": 4} def __init__( @@ -161,9 +190,11 @@ def _getLabel(self): return _translate("MainWindow", self.folderName) def setFolderName(self, fname): + """Set folder name (for QT UI)""" self.folderName = str(fname) def data(self, column, role): + """Override internal QT method for returning object data""" if column == 0 and role == QtCore.Qt.ForegroundRole: return self.folderBrush() return super(Ui_FolderWidget, self).data(column, role) @@ -183,15 +214,14 @@ def __lt__(self, other): self.treeWidget().header().sortIndicatorOrder() if x == y: return self.folderName < other.folderName - else: - return (x >= y if reverse else x < y) + return x >= y if reverse else x < y return super(QtGui.QTreeWidgetItem, self).__lt__(other) class Ui_AddressWidget(BMTreeWidgetItem, SettingsMixin): - def __init__( - self, parent, pos=0, address=None, unreadCount=0, enabled=True): + """Item in the account/folder tree representing an account""" + def __init__(self, parent, pos=0, address=None, unreadCount=0, enabled=True): super(Ui_AddressWidget, self).__init__( parent, pos, address, unreadCount) self.setEnabled(enabled) @@ -207,7 +237,7 @@ def _getLabel(self): else: try: return unicode( - BMConfigParser().get(self.address, 'label'), + config.get(self.address, 'label'), 'utf-8', 'ignore') except: return unicode(self.address, 'utf-8') @@ -220,6 +250,7 @@ def _getAddressBracket(self, unreadCount=False): return ret def data(self, column, role): + """Override internal QT method for returning object data""" if column == 0: if role == QtCore.Qt.DecorationRole: return avatarize( @@ -229,18 +260,20 @@ def data(self, column, role): return super(Ui_AddressWidget, self).data(column, role) def setData(self, column, role, value): + """Save account label (if you edit in the the UI, this will be triggered and will save it to keys.dat)""" if role == QtCore.Qt.EditRole \ and self.type != AccountMixin.SUBSCRIPTION: - BMConfigParser().set( + config.set( str(self.address), 'label', str(value.toString().toUtf8()) if isinstance(value, QtCore.QVariant) else value.encode('utf-8') ) - BMConfigParser().save() + config.save() return super(Ui_AddressWidget, self).setData(column, role, value) def setAddress(self, address): + """Set address to object (for QT UI)""" super(Ui_AddressWidget, self).setAddress(address) self.setData(0, QtCore.Qt.UserRole, self.address) @@ -249,6 +282,7 @@ def _getSortRank(self): # label (or address) alphabetically, disabled at the end def __lt__(self, other): + # pylint: disable=protected-access if isinstance(other, Ui_AddressWidget): reverse = QtCore.Qt.DescendingOrder == \ self.treeWidget().header().sortIndicatorOrder() @@ -265,9 +299,9 @@ def __lt__(self, other): class Ui_SubscriptionWidget(Ui_AddressWidget): - def __init__( - self, parent, pos=0, address="", unreadCount=0, label="", - enabled=True): + """Special treating of subscription addresses""" + # pylint: disable=unused-argument + def __init__(self, parent, pos=0, address="", unreadCount=0, label="", enabled=True): super(Ui_SubscriptionWidget, self).__init__( parent, pos, address, unreadCount, enabled) @@ -281,10 +315,12 @@ def _getLabel(self): return unicode(self.address, 'utf-8') def setType(self): + """Set account type""" super(Ui_SubscriptionWidget, self).setType() # sets it editable self.type = AccountMixin.SUBSCRIPTION # overrides type def setData(self, column, role, value): + """Save subscription label to database""" if role == QtCore.Qt.EditRole: if isinstance(value, QtCore.QVariant): label = str( @@ -299,21 +335,26 @@ def setData(self, column, role, value): class BMTableWidgetItem(QtGui.QTableWidgetItem, SettingsMixin): """A common abstract class for Table widget item""" - def __init__(self, parent=None, label=None, unread=False): + + def __init__(self, label=None, unread=False): super(QtGui.QTableWidgetItem, self).__init__() self.setLabel(label) self.setUnread(unread) self._setup() - if parent is not None: - parent.append(self) + + def _setup(self): + self.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) def setLabel(self, label): + """Set object label""" self.label = label def setUnread(self, unread): + """Set/unset read state of an item""" self.unread = unread def data(self, role): + """Return object data (QT UI)""" if role in ( QtCore.Qt.DisplayRole, QtCore.Qt.EditRole, QtCore.Qt.ToolTipRole ): @@ -327,14 +368,21 @@ def data(self, role): class BMAddressWidget(BMTableWidgetItem, AccountMixin): """A common class for Table widget item with account""" + def _setup(self): + super(BMAddressWidget, self)._setup() self.setEnabled(True) + self.setType() + + def _getLabel(self): + return self.label def data(self, role): + """Return object data (QT UI)""" 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: @@ -343,16 +391,13 @@ def data(self, role): class MessageList_AddressWidget(BMAddressWidget): - def __init__(self, parent, address=None, label=None, unread=False): + """Address item in a messagelist""" + def __init__(self, address=None, label=None, unread=False): self.setAddress(address) - super(MessageList_AddressWidget, self).__init__(parent, label, unread) - - def _setup(self): - self.isEnabled = True - self.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) - self.setType() + super(MessageList_AddressWidget, self).__init__(label, unread) def setLabel(self, label=None): + """Set label""" super(MessageList_AddressWidget, self).setLabel(label) if label is not None: return @@ -363,7 +408,7 @@ def setLabel(self, label=None): AccountMixin.CHAN, AccountMixin.MAILINGLIST): try: newLabel = unicode( - BMConfigParser().get(self.address, 'label'), + config.get(self.address, 'label'), 'utf-8', 'ignore') except: queryreturn = sqlQuery( @@ -378,11 +423,13 @@ def setLabel(self, label=None): self.label = newLabel def data(self, role): + """Return object data (QT UI)""" if role == QtCore.Qt.UserRole: return self.address return super(MessageList_AddressWidget, self).data(role) def setData(self, role, value): + """Set object data""" if role == QtCore.Qt.EditRole: self.setLabel() return super(MessageList_AddressWidget, self).setData(role, value) @@ -395,19 +442,21 @@ def __lt__(self, other): class MessageList_SubjectWidget(BMTableWidgetItem): - def __init__(self, parent, subject=None, label=None, unread=False): + """Message list subject item""" + def __init__(self, subject=None, label=None, unread=False): self.setSubject(subject) - super(MessageList_SubjectWidget, self).__init__(parent, label, unread) - - def _setup(self): - self.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) + super(MessageList_SubjectWidget, self).__init__(label, unread) def setSubject(self, subject): + """Set subject""" self.subject = subject def data(self, role): + """Return object data (QT UI)""" if role == QtCore.Qt.UserRole: return self.subject + if role == QtCore.Qt.ToolTipRole: + return escape(unicode(self.subject, 'utf-8')) return super(MessageList_SubjectWidget, self).data(role) # label (or address) alphabetically, disabled at the end @@ -417,17 +466,52 @@ def __lt__(self, other): return super(QtGui.QTableWidgetItem, self).__lt__(other) +# In order for the time columns on the Inbox and Sent tabs to be sorted +# correctly (rather than alphabetically), we need to overload the < +# operator and use this class instead of QTableWidgetItem. +class MessageList_TimeWidget(BMTableWidgetItem): + """ + A subclass of QTableWidgetItem for received (lastactiontime) field. + '<' operator is overloaded to sort by TimestampRole == 33 + msgid is available by QtCore.Qt.UserRole + """ + + def __init__(self, label=None, unread=False, timestamp=None, msgid=''): + super(MessageList_TimeWidget, self).__init__(label, unread) + self.setData(QtCore.Qt.UserRole, QtCore.QByteArray(msgid)) + self.setData(TimestampRole, int(timestamp)) + + def __lt__(self, other): + return self.data(TimestampRole) < other.data(TimestampRole) + + def data(self, role=QtCore.Qt.UserRole): + """ + Returns expected python types for QtCore.Qt.UserRole and TimestampRole + custom roles and super for any Qt role + """ + data = super(MessageList_TimeWidget, self).data(role) + if role == TimestampRole: + return int(data.toPyObject()) + if role == QtCore.Qt.UserRole: + return str(data.toPyObject()) + return data + + class Ui_AddressBookWidgetItem(BMAddressWidget): - def __init__(self, label=None, type=AccountMixin.NORMAL): - self.type = type + """Addressbook item""" + # pylint: disable=unused-argument + def __init__(self, label=None, acc_type=AccountMixin.NORMAL): + self.type = acc_type super(Ui_AddressBookWidgetItem, self).__init__(label=label) def data(self, role): + """Return object data""" if role == QtCore.Qt.UserRole: return self.type return super(Ui_AddressBookWidgetItem, self).data(role) def setData(self, role, value): + """Set data""" if role == QtCore.Qt.EditRole: self.label = str( value.toString().toUtf8() @@ -437,9 +521,9 @@ def setData(self, role, value): 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=?''', self.label, self.address) elif self.type == AccountMixin.SUBSCRIPTION: @@ -455,49 +539,56 @@ def __lt__(self, other): if self.type == other.type: return self.label.lower() < other.label.lower() - else: - return (not reverse if self.type < other.type else reverse) + return not reverse if self.type < other.type else reverse return super(QtGui.QTableWidgetItem, self).__lt__(other) class Ui_AddressBookWidgetItemLabel(Ui_AddressBookWidgetItem): - def __init__(self, address, label, type): - super(Ui_AddressBookWidgetItemLabel, self).__init__(label, type) + """Addressbook label item""" + def __init__(self, address, label, acc_type): self.address = address + super(Ui_AddressBookWidgetItemLabel, self).__init__(label, acc_type) def data(self, role): + """Return object data""" self.label = self.defaultLabel() return super(Ui_AddressBookWidgetItemLabel, self).data(role) class Ui_AddressBookWidgetItemAddress(Ui_AddressBookWidgetItem): - def __init__(self, address, label, type): - super(Ui_AddressBookWidgetItemAddress, self).__init__(address, type) + """Addressbook address item""" + def __init__(self, address, label, acc_type): self.address = address - self.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) + super(Ui_AddressBookWidgetItemAddress, self).__init__(address, acc_type) def data(self, role): + """Return object data""" if role == QtCore.Qt.ToolTipRole: return self.address if role == QtCore.Qt.DecorationRole: - return + return None return super(Ui_AddressBookWidgetItemAddress, self).data(role) class AddressBookCompleter(QtGui.QCompleter): + """Addressbook completer""" + def __init__(self): super(AddressBookCompleter, self).__init__() self.cursorPos = -1 - def onCursorPositionChanged(self, oldPos, newPos): + def onCursorPositionChanged(self, oldPos, newPos): # pylint: disable=unused-argument + """Callback for cursor position change""" if oldPos != self.cursorPos: self.cursorPos = -1 def splitPath(self, path): + """Split on semicolon""" text = unicode(path.toUtf8(), 'utf-8') return [text[:self.widget().cursorPosition()].split(';')[-1].strip()] def pathFromIndex(self, index): + """Perform autocompletion (reimplemented QCompleter method)""" autoString = unicode( index.data(QtCore.Qt.EditRole).toString().toUtf8(), 'utf-8') text = unicode(self.widget().text().toUtf8(), 'utf-8') diff --git a/src/bitmessageqt/languagebox.py b/src/bitmessageqt/languagebox.py index 552e035044..34f96b02a7 100644 --- a/src/bitmessageqt/languagebox.py +++ b/src/bitmessageqt/languagebox.py @@ -1,37 +1,48 @@ +"""Language Box Module for Locale Settings""" +# pylint: disable=too-few-public-methods,bad-continuation import glob import os + from PyQt4 import QtCore, QtGui -from bmconfigparser import BMConfigParser import paths +from bmconfigparser import config + class LanguageBox(QtGui.QComboBox): - languageName = {"system": "System Settings", "eo": "Esperanto", "en_pirate": "Pirate English"} - def __init__(self, parent = None): + """LanguageBox class for Qt UI""" + languageName = { + "system": "System Settings", "eo": "Esperanto", + "en_pirate": "Pirate English" + } + + def __init__(self, parent=None): super(QtGui.QComboBox, self).__init__(parent) self.populate() def populate(self): - self.languages = [] + """Populates drop down list with all available languages.""" self.clear() - localesPath = os.path.join (paths.codePath(), 'translations') - configuredLocale = "system" - try: - configuredLocale = BMConfigParser().get('bitmessagesettings', 'userlocale', "system") - except: - pass - self.addItem(QtGui.QApplication.translate("settingsDialog", "System Settings", "system"), "system") + localesPath = os.path.join(paths.codePath(), 'translations') + self.addItem(QtGui.QApplication.translate( + "settingsDialog", "System Settings", "system"), "system") self.setCurrentIndex(0) self.setInsertPolicy(QtGui.QComboBox.InsertAlphabetically) - for translationFile in sorted(glob.glob(os.path.join(localesPath, "bitmessage_*.qm"))): - localeShort = os.path.split(translationFile)[1].split("_", 1)[1][:-3] - locale = QtCore.QLocale(QtCore.QString(localeShort)) + for translationFile in sorted( + glob.glob(os.path.join(localesPath, "bitmessage_*.qm")) + ): + localeShort = \ + os.path.split(translationFile)[1].split("_", 1)[1][:-3] if localeShort in LanguageBox.languageName: - self.addItem(LanguageBox.languageName[localeShort], localeShort) - elif locale.nativeLanguageName() == "": - self.addItem(localeShort, localeShort) + self.addItem( + LanguageBox.languageName[localeShort], localeShort) else: - self.addItem(locale.nativeLanguageName(), localeShort) + locale = QtCore.QLocale(localeShort) + self.addItem( + locale.nativeLanguageName() or localeShort, localeShort) + + configuredLocale = config.safeGet( + 'bitmessagesettings', 'userlocale', "system") for i in range(self.count()): if self.itemData(i) == configuredLocale: self.setCurrentIndex(i) diff --git a/src/bitmessageqt/messagecompose.py b/src/bitmessageqt/messagecompose.py index f7d5dac3fc..c51282f8aa 100644 --- a/src/bitmessageqt/messagecompose.py +++ b/src/bitmessageqt/messagecompose.py @@ -1,23 +1,37 @@ +""" +Message editor with a wheel zoom functionality +""" +# pylint: disable=bad-continuation + from PyQt4 import QtCore, QtGui + class MessageCompose(QtGui.QTextEdit): - - def __init__(self, parent = 0): + """Editor class with wheel zoom functionality""" + def __init__(self, parent=0): super(MessageCompose, self).__init__(parent) - self.setAcceptRichText(False) # we'll deal with this later when we have a new message format + self.setAcceptRichText(False) self.defaultFontPointSize = self.currentFont().pointSize() - + def wheelEvent(self, event): - if (QtGui.QApplication.queryKeyboardModifiers() & QtCore.Qt.ControlModifier) == QtCore.Qt.ControlModifier and event.orientation() == QtCore.Qt.Vertical: + """Mouse wheel scroll event handler""" + if ( + QtGui.QApplication.queryKeyboardModifiers() & QtCore.Qt.ControlModifier + ) == QtCore.Qt.ControlModifier and event.orientation() == QtCore.Qt.Vertical: if event.delta() > 0: self.zoomIn(1) else: self.zoomOut(1) zoom = self.currentFont().pointSize() * 100 / self.defaultFontPointSize - QtGui.QApplication.activeWindow().statusBar().showMessage(QtGui.QApplication.translate("MainWindow", "Zoom level %1%").arg(str(zoom))) + QtGui.QApplication.activeWindow().statusBar().showMessage( + QtGui.QApplication.translate("MainWindow", "Zoom level %1%").arg( + str(zoom) + ) + ) else: # in QTextEdit, super does not zoom, only scroll super(MessageCompose, self).wheelEvent(event) def reset(self): + """Clear the edit content""" self.setText('') diff --git a/src/bitmessageqt/messageview.py b/src/bitmessageqt/messageview.py index 4d2e768d83..13ea16f97b 100644 --- a/src/bitmessageqt/messageview.py +++ b/src/bitmessageqt/messageview.py @@ -1,17 +1,24 @@ +""" +Custom message viewer with support for switching between HTML and plain +text rendering, HTML sanitization, lazy rendering (as you scroll down), +zoom and URL click warning popup + +""" + from PyQt4 import QtCore, QtGui -import multiprocessing -import Queue -from urlparse import urlparse -from safehtmlparser import * +from safehtmlparser import SafeHTMLParser +from tr import _translate + class MessageView(QtGui.QTextBrowser): + """Message content viewer class, can switch between plaintext and HTML""" MODE_PLAIN = 0 MODE_HTML = 1 - - def __init__(self, parent = 0): + + def __init__(self, parent=0): super(MessageView, self).__init__(parent) - self.mode = MessageView.MODE_PLAIN + self.mode = MessageView.MODE_PLAIN self.html = None self.setOpenExternalLinks(False) self.setOpenLinks(False) @@ -25,12 +32,14 @@ def __init__(self, parent = 0): self.setWrappingWidth() def resizeEvent(self, event): + """View resize event handler""" super(MessageView, self).resizeEvent(event) self.setWrappingWidth(event.size().width()) - + def mousePressEvent(self, event): - #text = textCursor.block().text() - if event.button() == QtCore.Qt.LeftButton and self.html and self.html.has_html and self.cursorForPosition(event.pos()).block().blockNumber() == 0: + """Mouse press button event handler""" + if event.button() == QtCore.Qt.LeftButton and self.html and self.html.has_html and self.cursorForPosition( + event.pos()).block().blockNumber() == 0: if self.mode == MessageView.MODE_PLAIN: self.showHTML() else: @@ -39,19 +48,25 @@ def mousePressEvent(self, event): super(MessageView, self).mousePressEvent(event) def wheelEvent(self, event): + """Mouse wheel scroll event handler""" # super will actually automatically take care of zooming super(MessageView, self).wheelEvent(event) - if (QtGui.QApplication.queryKeyboardModifiers() & QtCore.Qt.ControlModifier) == QtCore.Qt.ControlModifier and event.orientation() == QtCore.Qt.Vertical: + if ( + QtGui.QApplication.queryKeyboardModifiers() & QtCore.Qt.ControlModifier + ) == QtCore.Qt.ControlModifier and event.orientation() == QtCore.Qt.Vertical: zoom = self.currentFont().pointSize() * 100 / self.defaultFontPointSize - QtGui.QApplication.activeWindow().statusBar().showMessage(QtGui.QApplication.translate("MainWindow", "Zoom level %1%").arg(str(zoom))) + QtGui.QApplication.activeWindow().statusBar().showMessage(_translate( + "MainWindow", "Zoom level %1%").arg(str(zoom))) def setWrappingWidth(self, width=None): + """Set word-wrapping width""" self.setLineWrapMode(QtGui.QTextEdit.FixedPixelWidth) if width is None: width = self.width() self.setLineWrapColumnOrWidth(width) def confirmURL(self, link): + """Show a dialog requesting URL opening confirmation""" if link.scheme() == "mailto": window = QtGui.QApplication.activeWindow() window.ui.lineEditTo.setText(link.path()) @@ -68,35 +83,39 @@ def confirmURL(self, link): ) window.ui.textEditMessage.setFocus() return - reply = QtGui.QMessageBox.warning(self, - QtGui.QApplication.translate("MessageView", "Follow external link"), - QtGui.QApplication.translate("MessageView", "The link \"%1\" will open in a browser. It may be a security risk, it could de-anonymise you or download malicious data. Are you sure?").arg(unicode(link.toString())), - QtGui.QMessageBox.Yes, QtGui.QMessageBox.No) + reply = QtGui.QMessageBox.warning( + self, + QtGui.QApplication.translate( + "MessageView", + "Follow external link"), + QtGui.QApplication.translate( + "MessageView", + "The link \"%1\" will open in a browser. It may be a security risk, it could de-anonymise you" + " or download malicious data. Are you sure?").arg(unicode(link.toString())), + QtGui.QMessageBox.Yes, + QtGui.QMessageBox.No) if reply == QtGui.QMessageBox.Yes: QtGui.QDesktopServices.openUrl(link) - def loadResource (self, restype, name): - if restype == QtGui.QTextDocument.ImageResource and name.scheme() == "bmmsg": - pass -# QImage correctImage; -# lookup the correct QImage from a cache -# return QVariant::fromValue(correctImage); -# elif restype == QtGui.QTextDocument.HtmlResource: -# elif restype == QtGui.QTextDocument.ImageResource: -# elif restype == QtGui.QTextDocument.StyleSheetResource: -# elif restype == QtGui.QTextDocument.UserResource: - else: - pass -# by default, this will interpret it as a local file -# QtGui.QTextBrowser.loadResource(restype, name) + def loadResource(self, restype, name): + """ + Callback for loading referenced objects, such as an image. For security reasons at the moment doesn't do + anything) + """ + pass def lazyRender(self): + """ + Partially render a message. This is to avoid UI freezing when loading huge messages. It continues loading as + you scroll down. + """ if self.rendering: return self.rendering = True position = self.verticalScrollBar().value() cursor = QtGui.QTextCursor(self.document()) - while self.outpos < len(self.out) and self.verticalScrollBar().value() >= self.document().size().height() - 2 * self.size().height(): + while self.outpos < len(self.out) and self.verticalScrollBar().value( + ) >= self.document().size().height() - 2 * self.size().height(): startpos = self.outpos self.outpos += 10240 # find next end of tag @@ -108,27 +127,33 @@ def lazyRender(self): cursor.insertHtml(QtCore.QString(self.out[startpos:self.outpos])) self.verticalScrollBar().setValue(position) self.rendering = False - + def showPlain(self): + """Render message as plain text.""" self.mode = MessageView.MODE_PLAIN out = self.html.raw if self.html.has_html: - out = "
" + unicode(QtGui.QApplication.translate("MessageView", "HTML detected, click here to display")) + "

" + out + out = "
" + unicode( + QtGui.QApplication.translate( + "MessageView", "HTML detected, click here to display")) + "

" + out self.out = out self.outpos = 0 self.setHtml("") self.lazyRender() def showHTML(self): + """Render message as HTML""" self.mode = MessageView.MODE_HTML out = self.html.sanitised - out = "
" + unicode(QtGui.QApplication.translate("MessageView", "Click here to disable HTML")) + "

" + out + out = "
" + unicode( + QtGui.QApplication.translate("MessageView", "Click here to disable HTML")) + "

" + out self.out = out self.outpos = 0 self.setHtml("") self.lazyRender() def setContent(self, data): + """Set message content from argument""" self.html = SafeHTMLParser() self.html.reset() self.html.reset_safe() diff --git a/src/bitmessageqt/networkstatus.py b/src/bitmessageqt/networkstatus.py index 3691d5b35c..79ea415cff 100644 --- a/src/bitmessageqt/networkstatus.py +++ b/src/bitmessageqt/networkstatus.py @@ -1,20 +1,23 @@ -from PyQt4 import QtCore, QtGui +""" +Network status tab widget definition. +""" + import time -import shared -from tr import _translate -from inventory import Inventory -import knownnodes +from PyQt4 import QtCore, QtGui + import l10n import network.stats +import state +import widgets +from network import connectionpool, knownnodes from retranslateui import RetranslateMixin +from tr import _translate from uisignaler import UISignaler -import widgets - -from network.connectionpool import BMConnectionPool class NetworkStatus(QtGui.QWidget, RetranslateMixin): + """Network status tab""" def __init__(self, parent=None): super(NetworkStatus, self).__init__(parent) widgets.load('networkstatus.ui', self) @@ -27,10 +30,9 @@ def __init__(self, parent=None): header.setSortIndicator(0, QtCore.Qt.AscendingOrder) self.startup = time.localtime() - self.labelStartupTime.setText(_translate("networkstatus", "Since startup on %1").arg( - l10n.formatTimestamp(self.startup))) - + self.UISignalThread = UISignaler.get() + # pylint: disable=no-member QtCore.QObject.connect(self.UISignalThread, QtCore.SIGNAL( "updateNumberOfMessagesProcessed()"), self.updateNumberOfMessagesProcessed) QtCore.QObject.connect(self.UISignalThread, QtCore.SIGNAL( @@ -42,131 +44,206 @@ def __init__(self, parent=None): self.timer = QtCore.QTimer() - QtCore.QObject.connect( - self.timer, QtCore.SIGNAL("timeout()"), self.runEveryTwoSeconds) + QtCore.QObject.connect(self.timer, QtCore.SIGNAL("timeout()"), self.runEveryTwoSeconds) + # pylint: enable=no-member def startUpdate(self): - Inventory().numberOfInventoryLookupsPerformed = 0 + """Start a timer to update counters every 2 seconds""" + state.Inventory.numberOfInventoryLookupsPerformed = 0 self.runEveryTwoSeconds() self.timer.start(2000) # milliseconds def stopUpdate(self): + """Stop counter update timer""" self.timer.stop() def formatBytes(self, num): - for x in [_translate("networkstatus", "byte(s)", None, QtCore.QCoreApplication.CodecForTr, num), "kB", "MB", "GB"]: + """Format bytes nicely (SI prefixes)""" + # pylint: disable=no-self-use + for x in [ + _translate( + "networkstatus", + "byte(s)", + None, + QtCore.QCoreApplication.CodecForTr, + num), + "kB", + "MB", + "GB", + ]: if num < 1000.0: return "%3.0f %s" % (num, x) num /= 1000.0 return "%3.0f %s" % (num, 'TB') def formatByteRate(self, num): + """Format transfer speed in kB/s""" + # pylint: disable=no-self-use num /= 1000 return "%4.0f kB" % num - + def updateNumberOfObjectsToBeSynced(self): - self.labelSyncStatus.setText(_translate("networkstatus", "Object(s) to be synced: %n", None, QtCore.QCoreApplication.CodecForTr, network.stats.pendingDownload() + network.stats.pendingUpload())) + """Update the counter for number of objects to be synced""" + self.labelSyncStatus.setText( + _translate( + "networkstatus", + "Object(s) to be synced: %n", + None, + QtCore.QCoreApplication.CodecForTr, + network.stats.pendingDownload() + + network.stats.pendingUpload())) def updateNumberOfMessagesProcessed(self): + """Update the counter for number of processed messages""" self.updateNumberOfObjectsToBeSynced() - self.labelMessageCount.setText(_translate( - "networkstatus", "Processed %n person-to-person message(s).", None, QtCore.QCoreApplication.CodecForTr, shared.numberOfMessagesProcessed)) + self.labelMessageCount.setText( + _translate( + "networkstatus", + "Processed %n person-to-person message(s).", + None, + QtCore.QCoreApplication.CodecForTr, + state.numberOfMessagesProcessed)) def updateNumberOfBroadcastsProcessed(self): + """Update the counter for the number of processed broadcasts""" self.updateNumberOfObjectsToBeSynced() - self.labelBroadcastCount.setText(_translate( - "networkstatus", "Processed %n broadcast message(s).", None, QtCore.QCoreApplication.CodecForTr, shared.numberOfBroadcastsProcessed)) + self.labelBroadcastCount.setText( + _translate( + "networkstatus", + "Processed %n broadcast message(s).", + None, + QtCore.QCoreApplication.CodecForTr, + state.numberOfBroadcastsProcessed)) def updateNumberOfPubkeysProcessed(self): + """Update the counter for the number of processed pubkeys""" self.updateNumberOfObjectsToBeSynced() - self.labelPubkeyCount.setText(_translate( - "networkstatus", "Processed %n public key(s).", None, QtCore.QCoreApplication.CodecForTr, shared.numberOfPubkeysProcessed)) + self.labelPubkeyCount.setText( + _translate( + "networkstatus", + "Processed %n public key(s).", + None, + QtCore.QCoreApplication.CodecForTr, + state.numberOfPubkeysProcessed)) def updateNumberOfBytes(self): """ This function is run every two seconds, so we divide the rate of bytes sent and received by 2. """ - self.labelBytesRecvCount.setText(_translate( - "networkstatus", "Down: %1/s Total: %2").arg(self.formatByteRate(network.stats.downloadSpeed()), self.formatBytes(network.stats.receivedBytes()))) - self.labelBytesSentCount.setText(_translate( - "networkstatus", "Up: %1/s Total: %2").arg(self.formatByteRate(network.stats.uploadSpeed()), self.formatBytes(network.stats.sentBytes()))) + self.labelBytesRecvCount.setText( + _translate( + "networkstatus", + "Down: %1/s Total: %2").arg( + self.formatByteRate(network.stats.downloadSpeed()), + self.formatBytes(network.stats.receivedBytes()))) + self.labelBytesSentCount.setText( + _translate( + "networkstatus", "Up: %1/s Total: %2").arg( + self.formatByteRate(network.stats.uploadSpeed()), + self.formatBytes(network.stats.sentBytes()))) def updateNetworkStatusTab(self, outbound, add, destination): + """Add or remove an entry to the list of connected peers""" + # 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 self.tableWidgetConnectionCount.setUpdatesEnabled(False) self.tableWidgetConnectionCount.setSortingEnabled(False) + if add: self.tableWidgetConnectionCount.insertRow(0) - self.tableWidgetConnectionCount.setItem(0, 0, + self.tableWidgetConnectionCount.setItem( + 0, 0, QtGui.QTableWidgetItem("%s:%i" % (destination.host, destination.port)) - ) - self.tableWidgetConnectionCount.setItem(0, 2, + ) + self.tableWidgetConnectionCount.setItem( + 0, 2, QtGui.QTableWidgetItem("%s" % (c.userAgent)) - ) - self.tableWidgetConnectionCount.setItem(0, 3, + ) + self.tableWidgetConnectionCount.setItem( + 0, 3, QtGui.QTableWidgetItem("%s" % (c.tlsVersion)) - ) - self.tableWidgetConnectionCount.setItem(0, 4, - QtGui.QTableWidgetItem("%s" % (",".join(map(str,c.streams)))) - ) + ) + self.tableWidgetConnectionCount.setItem( + 0, 4, + QtGui.QTableWidgetItem("%s" % (",".join(map(str, c.streams)))) + ) try: - # FIXME hard coded stream no + # .. todo:: FIXME: hard coded stream no rating = "%.1f" % (knownnodes.knownNodes[1][destination]['rating']) except KeyError: rating = "-" - self.tableWidgetConnectionCount.setItem(0, 1, + self.tableWidgetConnectionCount.setItem( + 0, 1, QtGui.QTableWidgetItem("%s" % (rating)) - ) - if outbound: - brush = QtGui.QBrush(QtGui.QColor("yellow"), QtCore.Qt.SolidPattern) - else: - brush = QtGui.QBrush(QtGui.QColor("green"), QtCore.Qt.SolidPattern) - for j in (range(1)): + ) + brush = QtGui.QBrush( + QtGui.QColor("yellow" if outbound else "green"), + QtCore.Qt.SolidPattern) + for j in range(1): self.tableWidgetConnectionCount.item(0, j).setBackground(brush) + self.tableWidgetConnectionCount.item(0, j).setForeground( + QtGui.QBrush(QtGui.QColor("black"), QtCore.Qt.SolidPattern)) self.tableWidgetConnectionCount.item(0, 0).setData(QtCore.Qt.UserRole, destination) self.tableWidgetConnectionCount.item(0, 1).setData(QtCore.Qt.UserRole, outbound) else: + if not connectionpool.pool.inboundConnections: + self.window().setStatusIcon('yellow') for i in range(self.tableWidgetConnectionCount.rowCount()): if self.tableWidgetConnectionCount.item(i, 0).data(QtCore.Qt.UserRole).toPyObject() != destination: continue if self.tableWidgetConnectionCount.item(i, 1).data(QtCore.Qt.UserRole).toPyObject() == outbound: self.tableWidgetConnectionCount.removeRow(i) break + self.tableWidgetConnectionCount.setUpdatesEnabled(True) self.tableWidgetConnectionCount.setSortingEnabled(True) - self.labelTotalConnections.setText(_translate( - "networkstatus", "Total Connections: %1").arg(str(self.tableWidgetConnectionCount.rowCount()))) - # FYI: The 'singlelistener' thread sets the icon color to green when it receives an incoming connection, meaning that the user's firewall is configured correctly. - if self.tableWidgetConnectionCount.rowCount() and shared.statusIconColor == 'red': + self.labelTotalConnections.setText( + _translate( + "networkstatus", "Total Connections: %1").arg( + str(self.tableWidgetConnectionCount.rowCount()))) + # FYI: The 'singlelistener' thread sets the icon color to green when it + # receives an incoming connection, meaning that the user's firewall is + # configured correctly. + if self.tableWidgetConnectionCount.rowCount() and state.statusIconColor == 'red': self.window().setStatusIcon('yellow') - elif self.tableWidgetConnectionCount.rowCount() == 0 and shared.statusIconColor != "red": + elif self.tableWidgetConnectionCount.rowCount() == 0 and state.statusIconColor != "red": self.window().setStatusIcon('red') # timer driven def runEveryTwoSeconds(self): - self.labelLookupsPerSecond.setText(_translate( - "networkstatus", "Inventory lookups per second: %1").arg(str(Inventory().numberOfInventoryLookupsPerformed/2))) - Inventory().numberOfInventoryLookupsPerformed = 0 + """Updates counters, runs every 2 seconds if the timer is running""" + self.labelLookupsPerSecond.setText(_translate("networkstatus", "Inventory lookups per second: %1").arg( + str(state.Inventory.numberOfInventoryLookupsPerformed / 2))) + state.Inventory.numberOfInventoryLookupsPerformed = 0 self.updateNumberOfBytes() self.updateNumberOfObjectsToBeSynced() def retranslateUi(self): + """Conventional Qt Designer method for dynamic l10n""" super(NetworkStatus, self).retranslateUi() - self.labelStartupTime.setText(_translate("networkstatus", "Since startup on %1").arg( - l10n.formatTimestamp(self.startup))) + self.labelTotalConnections.setText( + _translate( + "networkstatus", "Total Connections: %1").arg( + str(self.tableWidgetConnectionCount.rowCount()))) + self.labelStartupTime.setText(_translate( + "networkstatus", "Since startup on %1" + ).arg(l10n.formatTimestamp(self.startup))) + self.updateNumberOfMessagesProcessed() + self.updateNumberOfBroadcastsProcessed() + self.updateNumberOfPubkeysProcessed() diff --git a/src/bitmessageqt/networkstatus.ui b/src/bitmessageqt/networkstatus.ui index e0c01b570a..121d60c5d2 100644 --- a/src/bitmessageqt/networkstatus.ui +++ b/src/bitmessageqt/networkstatus.ui @@ -16,6 +16,13 @@ 0 + + +STableWidget { + background: palette(Button); +} + + @@ -42,42 +49,8 @@ - - - - - - - 212 - 208 - 200 - - - - - - - - - 212 - 208 - 200 - - - - - - - - - 212 - 208 - 200 - - - - - + + Qt::NoFocus QFrame::Box diff --git a/src/bitmessageqt/newaddressdialog.ui b/src/bitmessageqt/newaddressdialog.ui index a9eda5c330..8b5276cc33 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/newaddresswizard.py b/src/bitmessageqt/newaddresswizard.py deleted file mode 100644 index 2311239c81..0000000000 --- a/src/bitmessageqt/newaddresswizard.py +++ /dev/null @@ -1,354 +0,0 @@ -#!/usr/bin/env python2.7 -from PyQt4 import QtCore, QtGui - -class NewAddressWizardIntroPage(QtGui.QWizardPage): - def __init__(self): - super(QtGui.QWizardPage, self).__init__() - self.setTitle("Creating a new address") - - label = QtGui.QLabel("This wizard will help you create as many addresses as you like. Indeed, creating and abandoning addresses is encouraged.\n\n" - "What type of address would you like? Would you like to send emails or not?\n" - "You can still change your mind later, and register/unregister with an email service provider.\n\n") - label.setWordWrap(True) - - self.emailAsWell = QtGui.QRadioButton("Combined email and bitmessage address") - self.onlyBM = QtGui.QRadioButton("Bitmessage-only address (no email)") - self.emailAsWell.setChecked(True) - self.registerField("emailAsWell", self.emailAsWell) - self.registerField("onlyBM", self.onlyBM) - - layout = QtGui.QVBoxLayout() - layout.addWidget(label) - layout.addWidget(self.emailAsWell) - layout.addWidget(self.onlyBM) - self.setLayout(layout) - - def nextId(self): - if self.emailAsWell.isChecked(): - return 4 - else: - return 1 - - -class NewAddressWizardRngPassphrasePage(QtGui.QWizardPage): - def __init__(self): - super(QtGui.QWizardPage, self).__init__() - self.setTitle("Random or Passphrase") - - label = QtGui.QLabel("

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:

" - "" - "" - "
Pros:Cons:
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.You must remember (or write down) your passphrase if you expect to be able " - "to recreate your keys if they are lost. " -# "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." - "

") - label.setWordWrap(True) - - self.randomAddress = QtGui.QRadioButton("Use a random number generator to make an address") - self.deterministicAddress = QtGui.QRadioButton("Use a passphrase to make an address") - self.randomAddress.setChecked(True) - - layout = QtGui.QVBoxLayout() - layout.addWidget(label) - layout.addWidget(self.randomAddress) - layout.addWidget(self.deterministicAddress) - self.setLayout(layout) - - def nextId(self): - if self.randomAddress.isChecked(): - return 2 - else: - return 3 - -class NewAddressWizardRandomPage(QtGui.QWizardPage): - def __init__(self, addresses): - super(QtGui.QWizardPage, self).__init__() - self.setTitle("Random") - - label = QtGui.QLabel("Random address.") - label.setWordWrap(True) - - labelLabel = QtGui.QLabel("Label (not shown to anyone except you):") - self.labelLineEdit = QtGui.QLineEdit() - - self.radioButtonMostAvailable = QtGui.QRadioButton("Use the most available stream\n" - "(best if this is the first of many addresses you will create)") - self.radioButtonExisting = QtGui.QRadioButton("Use the same stream as an existing address\n" - "(saves you some bandwidth and processing power)") - self.radioButtonMostAvailable.setChecked(True) - self.comboBoxExisting = QtGui.QComboBox() - self.comboBoxExisting.setEnabled(False) - self.comboBoxExisting.setEditable(True) - - for address in addresses: - self.comboBoxExisting.addItem(address) - -# self.comboBoxExisting.setObjectName(_fromUtf8("comboBoxExisting")) - self.checkBoxEighteenByteRipe = QtGui.QCheckBox("Spend several minutes of extra computing time to make the address(es) 1 or 2 characters shorter") - - layout = QtGui.QGridLayout() - layout.addWidget(label, 0, 0) - layout.addWidget(labelLabel, 1, 0) - layout.addWidget(self.labelLineEdit, 2, 0) - layout.addWidget(self.radioButtonMostAvailable, 3, 0) - layout.addWidget(self.radioButtonExisting, 4, 0) - layout.addWidget(self.comboBoxExisting, 5, 0) - layout.addWidget(self.checkBoxEighteenByteRipe, 6, 0) - self.setLayout(layout) - - QtCore.QObject.connect(self.radioButtonExisting, QtCore.SIGNAL("toggled(bool)"), self.comboBoxExisting.setEnabled) - - self.registerField("label", self.labelLineEdit) - self.registerField("radioButtonMostAvailable", self.radioButtonMostAvailable) - self.registerField("radioButtonExisting", self.radioButtonExisting) - self.registerField("comboBoxExisting", self.comboBoxExisting) - -# self.emailAsWell = QtGui.QRadioButton("Combined email and bitmessage account") -# self.onlyBM = QtGui.QRadioButton("Bitmessage-only account (no email)") -# self.emailAsWell.setChecked(True) - - def nextId(self): - return 6 - - -class NewAddressWizardPassphrasePage(QtGui.QWizardPage): - def __init__(self): - super(QtGui.QWizardPage, self).__init__() - self.setTitle("Passphrase") - - label = QtGui.QLabel("Deterministric address.") - label.setWordWrap(True) - - passphraseLabel = QtGui.QLabel("Passphrase") - self.lineEditPassphrase = QtGui.QLineEdit() - self.lineEditPassphrase.setEchoMode(QtGui.QLineEdit.Password) - self.lineEditPassphrase.setInputMethodHints(QtCore.Qt.ImhHiddenText|QtCore.Qt.ImhNoAutoUppercase|QtCore.Qt.ImhNoPredictiveText) - retypePassphraseLabel = QtGui.QLabel("Retype passphrase") - self.lineEditPassphraseAgain = QtGui.QLineEdit() - self.lineEditPassphraseAgain.setEchoMode(QtGui.QLineEdit.Password) - - numberLabel = QtGui.QLabel("Number of addresses to make based on your passphrase:") - self.spinBoxNumberOfAddressesToMake = QtGui.QSpinBox() - self.spinBoxNumberOfAddressesToMake.setMinimum(1) - self.spinBoxNumberOfAddressesToMake.setProperty("value", 8) -# self.spinBoxNumberOfAddressesToMake.setObjectName(_fromUtf8("spinBoxNumberOfAddressesToMake")) - label2 = QtGui.QLabel("In addition to your passphrase, you must remember these numbers:") - label3 = QtGui.QLabel("Address version number: 4") - label4 = QtGui.QLabel("Stream number: 1") - - layout = QtGui.QGridLayout() - layout.addWidget(label, 0, 0, 1, 4) - layout.addWidget(passphraseLabel, 1, 0, 1, 4) - layout.addWidget(self.lineEditPassphrase, 2, 0, 1, 4) - layout.addWidget(retypePassphraseLabel, 3, 0, 1, 4) - layout.addWidget(self.lineEditPassphraseAgain, 4, 0, 1, 4) - layout.addWidget(numberLabel, 5, 0, 1, 3) - layout.addWidget(self.spinBoxNumberOfAddressesToMake, 5, 3) - layout.setColumnMinimumWidth(3, 1) - layout.addWidget(label2, 6, 0, 1, 4) - layout.addWidget(label3, 7, 0, 1, 2) - layout.addWidget(label4, 7, 2, 1, 2) - self.setLayout(layout) - - def nextId(self): - return 6 - - -class NewAddressWizardEmailProviderPage(QtGui.QWizardPage): - def __init__(self): - super(QtGui.QWizardPage, self).__init__() - self.setTitle("Choose email provider") - - label = QtGui.QLabel("Currently only Mailchuck email gateway is available " - "(@mailchuck.com email address). In the future, maybe other gateways will be available. " - "Press Next.") - label.setWordWrap(True) - -# self.mailchuck = QtGui.QRadioButton("Mailchuck email gateway (@mailchuck.com)") -# self.mailchuck.setChecked(True) - - layout = QtGui.QVBoxLayout() - layout.addWidget(label) -# layout.addWidget(self.mailchuck) - self.setLayout(layout) - - def nextId(self): - return 5 - - -class NewAddressWizardEmailAddressPage(QtGui.QWizardPage): - def __init__(self): - super(QtGui.QWizardPage, self).__init__() - self.setTitle("Email address") - - label = QtGui.QLabel("Choosing an email address. Address must end with @mailchuck.com") - label.setWordWrap(True) - - self.specificEmail = QtGui.QRadioButton("Pick your own email address:") - self.specificEmail.setChecked(True) - self.emailLineEdit = QtGui.QLineEdit() - self.randomEmail = QtGui.QRadioButton("Generate a random email address") - - QtCore.QObject.connect(self.specificEmail, QtCore.SIGNAL("toggled(bool)"), self.emailLineEdit.setEnabled) - - layout = QtGui.QVBoxLayout() - layout.addWidget(label) - layout.addWidget(self.specificEmail) - layout.addWidget(self.emailLineEdit) - layout.addWidget(self.randomEmail) - self.setLayout(layout) - - def nextId(self): - return 6 - - -class NewAddressWizardWaitPage(QtGui.QWizardPage): - def __init__(self): - super(QtGui.QWizardPage, self).__init__() - self.setTitle("Wait") - - self.label = QtGui.QLabel("Wait!") - self.label.setWordWrap(True) - self.progressBar = QtGui.QProgressBar() - self.progressBar.setMinimum(0) - self.progressBar.setMaximum(100) - self.progressBar.setValue(0) - -# self.emailAsWell = QtGui.QRadioButton("Combined email and bitmessage account") -# self.onlyBM = QtGui.QRadioButton("Bitmessage-only account (no email)") -# self.emailAsWell.setChecked(True) - - layout = QtGui.QVBoxLayout() - layout.addWidget(self.label) - layout.addWidget(self.progressBar) -# layout.addWidget(self.emailAsWell) -# layout.addWidget(self.onlyBM) - self.setLayout(layout) - - def update(self, i): - if i == 101 and self.wizard().currentId() == 6: - self.wizard().button(QtGui.QWizard.NextButton).click() - return - elif i == 101: - print "haha" - return - self.progressBar.setValue(i) - if i == 50: - self.emit(QtCore.SIGNAL('completeChanged()')) - - def isComplete(self): -# print "val = " + str(self.progressBar.value()) - if self.progressBar.value() >= 50: - return True - else: - return False - - def initializePage(self): - if self.field("emailAsWell").toBool(): - val = "yes/" - else: - val = "no/" - if self.field("onlyBM").toBool(): - val += "yes" - else: - val += "no" - - self.label.setText("Wait! " + val) -# self.wizard().button(QtGui.QWizard.NextButton).setEnabled(False) - self.progressBar.setValue(0) - self.thread = NewAddressThread() - self.connect(self.thread, self.thread.signal, self.update) - self.thread.start() - - def nextId(self): - return 10 - - -class NewAddressWizardConclusionPage(QtGui.QWizardPage): - def __init__(self): - super(QtGui.QWizardPage, self).__init__() - self.setTitle("All done!") - - label = QtGui.QLabel("You successfully created a new address.") - label.setWordWrap(True) - - layout = QtGui.QVBoxLayout() - layout.addWidget(label) - self.setLayout(layout) - -class Ui_NewAddressWizard(QtGui.QWizard): - def __init__(self, addresses): - super(QtGui.QWizard, self).__init__() - - self.pages = {} - - page = NewAddressWizardIntroPage() - self.setPage(0, page) - self.setStartId(0) - page = NewAddressWizardRngPassphrasePage() - self.setPage(1, page) - page = NewAddressWizardRandomPage(addresses) - self.setPage(2, page) - page = NewAddressWizardPassphrasePage() - self.setPage(3, page) - page = NewAddressWizardEmailProviderPage() - self.setPage(4, page) - page = NewAddressWizardEmailAddressPage() - self.setPage(5, page) - page = NewAddressWizardWaitPage() - self.setPage(6, page) - page = NewAddressWizardConclusionPage() - self.setPage(10, page) - - self.setWindowTitle("New address wizard") - self.adjustSize() - self.show() - -class NewAddressThread(QtCore.QThread): - def __init__(self): - QtCore.QThread.__init__(self) - self.signal = QtCore.SIGNAL("signal") - - def __del__(self): - self.wait() - - def createDeterministic(self): - pass - - def createPassphrase(self): - pass - - def broadcastAddress(self): - pass - - def registerMailchuck(self): - pass - - def waitRegistration(self): - pass - - def run(self): - import time - for i in range(1, 101): - time.sleep(0.1) # artificial time delay - self.emit(self.signal, i) - self.emit(self.signal, 101) -# self.terminate() - -if __name__ == '__main__': - - import sys - - app = QtGui.QApplication(sys.argv) - - wizard = Ui_NewAddressWizard(["a", "b", "c", "d"]) - if (wizard.exec_()): - print "Email: " + ("yes" if wizard.field("emailAsWell").toBool() else "no") - print "BM: " + ("yes" if wizard.field("onlyBM").toBool() else "no") - else: - print "Wizard cancelled" - sys.exit() diff --git a/src/bitmessageqt/newchandialog.py b/src/bitmessageqt/newchandialog.py index ed683b1307..c0629cd797 100644 --- a/src/bitmessageqt/newchandialog.py +++ b/src/bitmessageqt/newchandialog.py @@ -1,41 +1,72 @@ +""" +src/bitmessageqt/newchandialog.py +================================= + +""" + from PyQt4 import QtCore, QtGui +import widgets from addresses import addBMIfNotPresent from addressvalidator import AddressValidator, PassPhraseValidator -from queues import apiAddressGeneratorReturnQueue, addressGeneratorQueue, UISignalQueue -from retranslateui import RetranslateMixin +from queues import ( + addressGeneratorQueue, apiAddressGeneratorReturnQueue, UISignalQueue) from tr import _translate from utils import str_chan -import widgets -class NewChanDialog(QtGui.QDialog, RetranslateMixin): + +class NewChanDialog(QtGui.QDialog): + """The `New Chan` dialog""" def __init__(self, parent=None): super(NewChanDialog, self).__init__(parent) widgets.load('newchandialog.ui', self) self.parent = parent - self.chanAddress.setValidator(AddressValidator(self.chanAddress, self.chanPassPhrase, self.validatorFeedback, self.buttonBox, False)) - self.chanPassPhrase.setValidator(PassPhraseValidator(self.chanPassPhrase, self.chanAddress, self.validatorFeedback, self.buttonBox, False)) + self.chanAddress.setValidator( + AddressValidator( + self.chanAddress, + self.chanPassPhrase, + self.validatorFeedback, + self.buttonBox, + False)) + self.chanPassPhrase.setValidator( + PassPhraseValidator( + self.chanPassPhrase, + self.chanAddress, + self.validatorFeedback, + self.buttonBox, + False)) self.timer = QtCore.QTimer() - QtCore.QObject.connect(self.timer, QtCore.SIGNAL("timeout()"), self.delayedUpdateStatus) - self.timer.start(500) # milliseconds + QtCore.QObject.connect( # pylint: disable=no-member + self.timer, QtCore.SIGNAL("timeout()"), self.delayedUpdateStatus) + self.timer.start(500) # milliseconds self.setAttribute(QtCore.Qt.WA_DeleteOnClose) self.show() def delayedUpdateStatus(self): + """Related to updating the UI for the chan passphrase validity""" self.chanPassPhrase.validator().checkQueue() def accept(self): + """Proceed in joining the chan""" self.timer.stop() self.hide() apiAddressGeneratorReturnQueue.queue.clear() if self.chanAddress.text().toUtf8() == "": - addressGeneratorQueue.put(('createChan', 4, 1, str_chan + ' ' + str(self.chanPassPhrase.text().toUtf8()), self.chanPassPhrase.text().toUtf8(), True)) + addressGeneratorQueue.put( + ('createChan', 4, 1, str_chan + ' ' + str(self.chanPassPhrase.text().toUtf8()), + self.chanPassPhrase.text().toUtf8(), + True)) else: - addressGeneratorQueue.put(('joinChan', addBMIfNotPresent(self.chanAddress.text().toUtf8()), str_chan + ' ' + str(self.chanPassPhrase.text().toUtf8()), self.chanPassPhrase.text().toUtf8(), True)) + addressGeneratorQueue.put( + ('joinChan', addBMIfNotPresent(self.chanAddress.text().toUtf8()), + str_chan + ' ' + str(self.chanPassPhrase.text().toUtf8()), + self.chanPassPhrase.text().toUtf8(), + True)) addressGeneratorReturnValue = apiAddressGeneratorReturnQueue.get(True) - if len(addressGeneratorReturnValue) > 0 and addressGeneratorReturnValue[0] != 'chan name does not match address': - UISignalQueue.put(('updateStatusBar', _translate("newchandialog", "Successfully created / joined chan %1").arg(unicode(self.chanPassPhrase.text())))) + if addressGeneratorReturnValue and addressGeneratorReturnValue[0] != 'chan name does not match address': + UISignalQueue.put(('updateStatusBar', _translate( + "newchandialog", "Successfully created / joined chan %1").arg(unicode(self.chanPassPhrase.text())))) self.parent.ui.tabWidget.setCurrentIndex( self.parent.ui.tabWidget.indexOf(self.parent.ui.chans) ) @@ -45,6 +76,7 @@ def accept(self): self.done(QtGui.QDialog.Rejected) def reject(self): + """Cancel joining the chan""" self.timer.stop() self.hide() UISignalQueue.put(('updateStatusBar', _translate("newchandialog", "Chan creation / joining cancelled"))) diff --git a/src/bitmessageqt/retranslateui.py b/src/bitmessageqt/retranslateui.py index e9d5bb3a84..c7676f770d 100644 --- a/src/bitmessageqt/retranslateui.py +++ b/src/bitmessageqt/retranslateui.py @@ -13,6 +13,8 @@ def retranslateUi(self): getattr(self, attr).setText(getattr(defaults, attr).text()) elif isinstance(value, QtGui.QTableWidget): for i in range (value.columnCount()): - getattr(self, attr).horizontalHeaderItem(i).setText(getattr(defaults, attr).horizontalHeaderItem(i).text()) + getattr(self, attr).horizontalHeaderItem(i).setText( + getattr(defaults, attr).horizontalHeaderItem(i).text()) for i in range (value.rowCount()): - getattr(self, attr).verticalHeaderItem(i).setText(getattr(defaults, attr).verticalHeaderItem(i).text()) + getattr(self, attr).verticalHeaderItem(i).setText( + getattr(defaults, attr).verticalHeaderItem(i).text()) diff --git a/src/bitmessageqt/safehtmlparser.py b/src/bitmessageqt/safehtmlparser.py index d1d7910c71..d408d2c701 100644 --- a/src/bitmessageqt/safehtmlparser.py +++ b/src/bitmessageqt/safehtmlparser.py @@ -1,51 +1,75 @@ -from HTMLParser import HTMLParser +"""Subclass of HTMLParser.HTMLParser for MessageView widget""" + import inspect import re -from urllib import quote, quote_plus +from HTMLParser import HTMLParser + +from urllib import quote_plus from urlparse import urlparse + class SafeHTMLParser(HTMLParser): + """HTML parser with sanitisation""" # from html5lib.sanitiser - acceptable_elements = ['a', 'abbr', 'acronym', 'address', 'area', - 'article', 'aside', 'audio', 'b', 'big', 'blockquote', 'br', 'button', - 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', - 'command', 'datagrid', 'datalist', 'dd', 'del', 'details', 'dfn', - 'dialog', 'dir', 'div', 'dl', 'dt', 'em', 'event-source', 'fieldset', - 'figcaption', 'figure', 'footer', 'font', 'header', 'h1', - 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'ins', - 'keygen', 'kbd', 'label', 'legend', 'li', 'm', 'map', 'menu', 'meter', - 'multicol', 'nav', 'nextid', 'ol', 'output', 'optgroup', 'option', - 'p', 'pre', 'progress', 'q', 's', 'samp', 'section', 'select', - 'small', 'sound', 'source', 'spacer', 'span', 'strike', 'strong', - 'sub', 'sup', 'table', 'tbody', 'td', 'textarea', 'time', 'tfoot', - 'th', 'thead', 'tr', 'tt', 'u', 'ul', 'var', 'video'] - replaces_pre = [["&", "&"], ["\"", """], ["<", "<"], [">", ">"]] - replaces_post = [["\n", "
"], ["\t", "    "], [" ", "  "], [" ", "  "], ["
", "
 "]] - src_schemes = [ "data" ] - #uriregex1 = re.compile(r'(?i)\b((?:(https?|ftp|bitcoin):(?:/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:\'".,<>?]))') - uriregex1 = re.compile(r'((https?|ftp|bitcoin):(?:/{1,3}|[a-z0-9%])(?:[a-zA-Z]|[0-9]|[$-_@.&+#]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)') + acceptable_elements = ( + 'a', 'abbr', 'acronym', 'address', 'area', + 'article', 'aside', 'audio', 'b', 'big', 'blockquote', 'br', 'button', + 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', + 'command', 'datagrid', 'datalist', 'dd', 'del', 'details', 'dfn', + 'dialog', 'dir', 'div', 'dl', 'dt', 'em', 'event-source', 'fieldset', + 'figcaption', 'figure', 'footer', 'font', 'header', 'h1', + 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'ins', + 'keygen', 'kbd', 'label', 'legend', 'li', 'm', 'map', 'menu', 'meter', + 'multicol', 'nav', 'nextid', 'ol', 'output', 'optgroup', 'option', + 'p', 'pre', 'progress', 'q', 's', 'samp', 'section', 'select', + 'small', 'sound', 'source', 'spacer', 'span', 'strike', 'strong', + 'sub', 'sup', 'table', 'tbody', 'td', 'textarea', 'time', 'tfoot', + 'th', 'thead', 'tr', 'tt', 'u', 'ul', 'var', 'video' + ) + replaces_pre = ( + ("&", "&"), ("\"", """), ("<", "<"), (">", ">")) + replaces_post = ( + ("\n", "
"), ("\t", "    "), + (" ", "  "), (" ", "  "), ("
", "
 ")) + src_schemes = ["data"] + # uriregex1 = re.compile( + # r'(?i)\b((?:(https?|ftp|bitcoin):(?:/{1,3}|[a-z0-9%])' + # r'|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)' + # r'(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))' + # r'+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:\'".,<>?]))') + uriregex1 = re.compile( + r'((https?|ftp|bitcoin):(?:/{1,3}|[a-z0-9%])' + r'(?:[a-zA-Z]|[0-9]|[$-_@.&+#]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)' + ) uriregex2 = re.compile(r' 1 and text[0] == " ": text = " " + text[1:] return text def __init__(self, *args, **kwargs): HTMLParser.__init__(self, *args, **kwargs) + self.reset() self.reset_safe() - + self.has_html = None + self.allow_picture = None + def reset_safe(self): + """Reset runtime variables specific to this class""" self.elements = set() self.raw = u"" self.sanitised = u"" @@ -53,8 +77,9 @@ def reset_safe(self): self.allow_picture = False self.allow_external_src = False - def add_if_acceptable(self, tag, attrs = None): - if tag not in SafeHTMLParser.acceptable_elements: + def add_if_acceptable(self, tag, attrs=None): + """Add tag if it passes sanitisation""" + if tag not in self.acceptable_elements: return self.sanitised += "<" if inspect.stack()[1][3] == "handle_endtag": @@ -66,34 +91,34 @@ def add_if_acceptable(self, tag, attrs = None): val = "" elif attr == "src" and not self.allow_external_src: url = urlparse(val) - if url.scheme not in SafeHTMLParser.src_schemes: + if url.scheme not in self.src_schemes: val = "" self.sanitised += " " + quote_plus(attr) - if not (val is None): + if val is not None: self.sanitised += "=\"" + val + "\"" if inspect.stack()[1][3] == "handle_startendtag": self.sanitised += "/" self.sanitised += ">" - + def handle_starttag(self, tag, attrs): - if tag in SafeHTMLParser.acceptable_elements: + if tag in self.acceptable_elements: self.has_html = True self.add_if_acceptable(tag, attrs) def handle_endtag(self, tag): self.add_if_acceptable(tag) - + def handle_startendtag(self, tag, attrs): - if tag in SafeHTMLParser.acceptable_elements: + if tag in self.acceptable_elements: self.has_html = True self.add_if_acceptable(tag, attrs) - + def handle_data(self, data): self.sanitised += data - + def handle_charref(self, name): self.sanitised += "&#" + name + ";" - + def handle_entityref(self, name): self.sanitised += "&" + name + ";" @@ -104,15 +129,14 @@ def feed(self, data): data = unicode(data, 'utf-8', errors='replace') HTMLParser.feed(self, data) tmp = SafeHTMLParser.replace_pre(data) - tmp = SafeHTMLParser.uriregex1.sub( - r'\1', - tmp) - tmp = SafeHTMLParser.uriregex2.sub(r'\1', tmp) + tmp = self.uriregex1.sub(r'\1', tmp) + tmp = self.uriregex2.sub(r'\1', tmp) tmp = SafeHTMLParser.replace_post(tmp) self.raw += tmp - def is_html(self, text = None, allow_picture = False): + def is_html(self, text=None, allow_picture=False): + """Detect if string contains HTML tags""" if text: self.reset() self.reset_safe() diff --git a/src/bitmessageqt/settings.py b/src/bitmessageqt/settings.py index 4342fd090d..eeb507c75e 100644 --- a/src/bitmessageqt/settings.py +++ b/src/bitmessageqt/settings.py @@ -1,516 +1,675 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'settings.ui' -# -# Created: Thu Dec 25 23:21:20 2014 -# by: PyQt4 UI code generator 4.10.3 -# -# WARNING! All changes made in this file will be lost! +""" +This module setting file is for settings +""" +import ConfigParser +import os +import sys +import tempfile +import six from PyQt4 import QtCore, QtGui -from languagebox import LanguageBox -from sys import platform - -try: - _fromUtf8 = QtCore.QString.fromUtf8 -except AttributeError: - def _fromUtf8(s): - return s - -try: - _encoding = QtGui.QApplication.UnicodeUTF8 - def _translate(context, text, disambig): - return QtGui.QApplication.translate(context, text, disambig, _encoding) -except AttributeError: - def _translate(context, text, disambig): - return QtGui.QApplication.translate(context, text, disambig) - -class Ui_settingsDialog(object): - def setupUi(self, settingsDialog): - settingsDialog.setObjectName(_fromUtf8("settingsDialog")) - settingsDialog.resize(521, 413) - self.gridLayout = QtGui.QGridLayout(settingsDialog) - self.gridLayout.setObjectName(_fromUtf8("gridLayout")) - self.buttonBox = QtGui.QDialogButtonBox(settingsDialog) - self.buttonBox.setOrientation(QtCore.Qt.Horizontal) - self.buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Cancel|QtGui.QDialogButtonBox.Ok) - self.buttonBox.setObjectName(_fromUtf8("buttonBox")) - self.gridLayout.addWidget(self.buttonBox, 1, 0, 1, 1) - self.tabWidgetSettings = QtGui.QTabWidget(settingsDialog) - self.tabWidgetSettings.setObjectName(_fromUtf8("tabWidgetSettings")) - self.tabUserInterface = QtGui.QWidget() - self.tabUserInterface.setEnabled(True) - self.tabUserInterface.setObjectName(_fromUtf8("tabUserInterface")) - self.formLayout = QtGui.QFormLayout(self.tabUserInterface) - self.formLayout.setObjectName(_fromUtf8("formLayout")) - self.checkBoxStartOnLogon = QtGui.QCheckBox(self.tabUserInterface) - self.checkBoxStartOnLogon.setObjectName(_fromUtf8("checkBoxStartOnLogon")) - self.formLayout.setWidget(0, QtGui.QFormLayout.LabelRole, self.checkBoxStartOnLogon) - self.groupBoxTray = QtGui.QGroupBox(self.tabUserInterface) - self.groupBoxTray.setObjectName(_fromUtf8("groupBoxTray")) - self.formLayoutTray = QtGui.QFormLayout(self.groupBoxTray) - self.formLayoutTray.setObjectName(_fromUtf8("formLayoutTray")) - self.checkBoxStartInTray = QtGui.QCheckBox(self.groupBoxTray) - self.checkBoxStartInTray.setObjectName(_fromUtf8("checkBoxStartInTray")) - self.formLayoutTray.setWidget(0, QtGui.QFormLayout.SpanningRole, self.checkBoxStartInTray) - self.checkBoxMinimizeToTray = QtGui.QCheckBox(self.groupBoxTray) - self.checkBoxMinimizeToTray.setChecked(True) - self.checkBoxMinimizeToTray.setObjectName(_fromUtf8("checkBoxMinimizeToTray")) - self.formLayoutTray.setWidget(1, QtGui.QFormLayout.LabelRole, self.checkBoxMinimizeToTray) - self.checkBoxTrayOnClose = QtGui.QCheckBox(self.groupBoxTray) - self.checkBoxTrayOnClose.setChecked(True) - self.checkBoxTrayOnClose.setObjectName(_fromUtf8("checkBoxTrayOnClose")) - self.formLayoutTray.setWidget(2, QtGui.QFormLayout.LabelRole, self.checkBoxTrayOnClose) - self.formLayout.setWidget(1, QtGui.QFormLayout.SpanningRole, self.groupBoxTray) - self.checkBoxHideTrayConnectionNotifications = QtGui.QCheckBox(self.tabUserInterface) - self.checkBoxHideTrayConnectionNotifications.setChecked(False) - self.checkBoxHideTrayConnectionNotifications.setObjectName(_fromUtf8("checkBoxHideTrayConnectionNotifications")) - self.formLayout.setWidget(2, QtGui.QFormLayout.LabelRole, self.checkBoxHideTrayConnectionNotifications) - self.checkBoxShowTrayNotifications = QtGui.QCheckBox(self.tabUserInterface) - self.checkBoxShowTrayNotifications.setObjectName(_fromUtf8("checkBoxShowTrayNotifications")) - self.formLayout.setWidget(3, QtGui.QFormLayout.LabelRole, self.checkBoxShowTrayNotifications) - self.checkBoxPortableMode = QtGui.QCheckBox(self.tabUserInterface) - self.checkBoxPortableMode.setObjectName(_fromUtf8("checkBoxPortableMode")) - self.formLayout.setWidget(4, QtGui.QFormLayout.LabelRole, self.checkBoxPortableMode) - self.PortableModeDescription = QtGui.QLabel(self.tabUserInterface) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.PortableModeDescription.sizePolicy().hasHeightForWidth()) - self.PortableModeDescription.setSizePolicy(sizePolicy) - self.PortableModeDescription.setWordWrap(True) - self.PortableModeDescription.setObjectName(_fromUtf8("PortableModeDescription")) - self.formLayout.setWidget(5, QtGui.QFormLayout.SpanningRole, self.PortableModeDescription) - self.checkBoxWillinglySendToMobile = QtGui.QCheckBox(self.tabUserInterface) - self.checkBoxWillinglySendToMobile.setObjectName(_fromUtf8("checkBoxWillinglySendToMobile")) - self.formLayout.setWidget(6, QtGui.QFormLayout.SpanningRole, self.checkBoxWillinglySendToMobile) - self.checkBoxUseIdenticons = QtGui.QCheckBox(self.tabUserInterface) - self.checkBoxUseIdenticons.setObjectName(_fromUtf8("checkBoxUseIdenticons")) - self.formLayout.setWidget(7, QtGui.QFormLayout.LabelRole, self.checkBoxUseIdenticons) - self.checkBoxReplyBelow = QtGui.QCheckBox(self.tabUserInterface) - self.checkBoxReplyBelow.setObjectName(_fromUtf8("checkBoxReplyBelow")) - self.formLayout.setWidget(8, QtGui.QFormLayout.LabelRole, self.checkBoxReplyBelow) - self.groupBox = QtGui.QGroupBox(self.tabUserInterface) - self.groupBox.setObjectName(_fromUtf8("groupBox")) - self.formLayout_2 = QtGui.QFormLayout(self.groupBox) - self.formLayout_2.setObjectName(_fromUtf8("formLayout_2")) - self.languageComboBox = LanguageBox(self.groupBox) - self.languageComboBox.setMinimumSize(QtCore.QSize(100, 0)) - self.languageComboBox.setObjectName(_fromUtf8("languageComboBox")) - self.formLayout_2.setWidget(0, QtGui.QFormLayout.LabelRole, self.languageComboBox) - self.formLayout.setWidget(9, QtGui.QFormLayout.FieldRole, self.groupBox) - self.tabWidgetSettings.addTab(self.tabUserInterface, _fromUtf8("")) - self.tabNetworkSettings = QtGui.QWidget() - self.tabNetworkSettings.setObjectName(_fromUtf8("tabNetworkSettings")) - self.gridLayout_4 = QtGui.QGridLayout(self.tabNetworkSettings) - self.gridLayout_4.setObjectName(_fromUtf8("gridLayout_4")) - self.groupBox1 = QtGui.QGroupBox(self.tabNetworkSettings) - self.groupBox1.setObjectName(_fromUtf8("groupBox1")) - self.gridLayout_3 = QtGui.QGridLayout(self.groupBox1) - self.gridLayout_3.setObjectName(_fromUtf8("gridLayout_3")) - #spacerItem = QtGui.QSpacerItem(125, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) - #self.gridLayout_3.addItem(spacerItem, 0, 0, 1, 1) - self.label = QtGui.QLabel(self.groupBox1) - self.label.setObjectName(_fromUtf8("label")) - self.gridLayout_3.addWidget(self.label, 0, 0, 1, 1, QtCore.Qt.AlignRight) - self.lineEditTCPPort = QtGui.QLineEdit(self.groupBox1) - self.lineEditTCPPort.setMaximumSize(QtCore.QSize(70, 16777215)) - self.lineEditTCPPort.setObjectName(_fromUtf8("lineEditTCPPort")) - self.gridLayout_3.addWidget(self.lineEditTCPPort, 0, 1, 1, 1, QtCore.Qt.AlignLeft) - self.labelUPnP = QtGui.QLabel(self.groupBox1) - self.labelUPnP.setObjectName(_fromUtf8("labelUPnP")) - self.gridLayout_3.addWidget(self.labelUPnP, 0, 2, 1, 1, QtCore.Qt.AlignRight) - self.checkBoxUPnP = QtGui.QCheckBox(self.groupBox1) - self.checkBoxUPnP.setObjectName(_fromUtf8("checkBoxUPnP")) - self.gridLayout_3.addWidget(self.checkBoxUPnP, 0, 3, 1, 1, QtCore.Qt.AlignLeft) - self.gridLayout_4.addWidget(self.groupBox1, 0, 0, 1, 1) - self.groupBox_3 = QtGui.QGroupBox(self.tabNetworkSettings) - self.groupBox_3.setObjectName(_fromUtf8("groupBox_3")) - self.gridLayout_9 = QtGui.QGridLayout(self.groupBox_3) - self.gridLayout_9.setObjectName(_fromUtf8("gridLayout_9")) - spacerItem1 = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) - self.gridLayout_9.addItem(spacerItem1, 0, 0, 2, 1) - self.label_24 = QtGui.QLabel(self.groupBox_3) - self.label_24.setObjectName(_fromUtf8("label_24")) - self.gridLayout_9.addWidget(self.label_24, 0, 1, 1, 1) - self.lineEditMaxDownloadRate = QtGui.QLineEdit(self.groupBox_3) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.lineEditMaxDownloadRate.sizePolicy().hasHeightForWidth()) - self.lineEditMaxDownloadRate.setSizePolicy(sizePolicy) - self.lineEditMaxDownloadRate.setMaximumSize(QtCore.QSize(60, 16777215)) - self.lineEditMaxDownloadRate.setObjectName(_fromUtf8("lineEditMaxDownloadRate")) - self.gridLayout_9.addWidget(self.lineEditMaxDownloadRate, 0, 2, 1, 1) - self.label_25 = QtGui.QLabel(self.groupBox_3) - self.label_25.setObjectName(_fromUtf8("label_25")) - self.gridLayout_9.addWidget(self.label_25, 1, 1, 1, 1) - self.lineEditMaxUploadRate = QtGui.QLineEdit(self.groupBox_3) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.lineEditMaxUploadRate.sizePolicy().hasHeightForWidth()) - self.lineEditMaxUploadRate.setSizePolicy(sizePolicy) - self.lineEditMaxUploadRate.setMaximumSize(QtCore.QSize(60, 16777215)) - self.lineEditMaxUploadRate.setObjectName(_fromUtf8("lineEditMaxUploadRate")) - self.gridLayout_9.addWidget(self.lineEditMaxUploadRate, 1, 2, 1, 1) - self.label_26 = QtGui.QLabel(self.groupBox_3) - self.label_26.setObjectName(_fromUtf8("label_26")) - self.gridLayout_9.addWidget(self.label_26, 2, 1, 1, 1) - self.lineEditMaxOutboundConnections = QtGui.QLineEdit(self.groupBox_3) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.lineEditMaxOutboundConnections.sizePolicy().hasHeightForWidth()) - self.lineEditMaxOutboundConnections.setSizePolicy(sizePolicy) - self.lineEditMaxOutboundConnections.setMaximumSize(QtCore.QSize(60, 16777215)) - self.lineEditMaxOutboundConnections.setObjectName(_fromUtf8("lineEditMaxOutboundConnections")) - self.lineEditMaxOutboundConnections.setValidator(QtGui.QIntValidator(0, 8, self.lineEditMaxOutboundConnections)) - self.gridLayout_9.addWidget(self.lineEditMaxOutboundConnections, 2, 2, 1, 1) - self.gridLayout_4.addWidget(self.groupBox_3, 2, 0, 1, 1) - self.groupBox_2 = QtGui.QGroupBox(self.tabNetworkSettings) - self.groupBox_2.setObjectName(_fromUtf8("groupBox_2")) - self.gridLayout_2 = QtGui.QGridLayout(self.groupBox_2) - self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2")) - self.label_2 = QtGui.QLabel(self.groupBox_2) - self.label_2.setObjectName(_fromUtf8("label_2")) - self.gridLayout_2.addWidget(self.label_2, 0, 0, 1, 1) - self.label_3 = QtGui.QLabel(self.groupBox_2) - self.label_3.setObjectName(_fromUtf8("label_3")) - self.gridLayout_2.addWidget(self.label_3, 1, 1, 1, 1) - self.lineEditSocksHostname = QtGui.QLineEdit(self.groupBox_2) - self.lineEditSocksHostname.setObjectName(_fromUtf8("lineEditSocksHostname")) - self.lineEditSocksHostname.setPlaceholderText(_fromUtf8("127.0.0.1")) - self.gridLayout_2.addWidget(self.lineEditSocksHostname, 1, 2, 1, 2) - self.label_4 = QtGui.QLabel(self.groupBox_2) - self.label_4.setObjectName(_fromUtf8("label_4")) - self.gridLayout_2.addWidget(self.label_4, 1, 4, 1, 1) - self.lineEditSocksPort = QtGui.QLineEdit(self.groupBox_2) - self.lineEditSocksPort.setObjectName(_fromUtf8("lineEditSocksPort")) - if platform in ['darwin', 'win32', 'win64']: - self.lineEditSocksPort.setPlaceholderText(_fromUtf8("9150")) + +import debug +import defaults +import namecoin +import openclpow +import paths +import queues +import state +import widgets +from bmconfigparser import config as config_obj +from helper_sql import sqlExecute, sqlStoredProcedure +from helper_startup import start_proxyconfig +from network import connectionpool, knownnodes +from network.announcethread import AnnounceThread +from network.asyncore_pollchoose import set_rates +from tr import _translate + + +def getSOCKSProxyType(config): + """Get user socksproxytype setting from *config*""" + try: + result = ConfigParser.SafeConfigParser.get( + config, 'bitmessagesettings', 'socksproxytype') + except (ConfigParser.NoSectionError, ConfigParser.NoOptionError): + return None + else: + if result.lower() in ('', 'none', 'false'): + result = None + return result + + +class SettingsDialog(QtGui.QDialog): + """The "Settings" dialog""" + # pylint: disable=too-many-instance-attributes + def __init__(self, parent=None, firstrun=False): + super(SettingsDialog, self).__init__(parent) + widgets.load('settings.ui', self) + + self.app = QtGui.QApplication.instance() + self.parent = parent + self.firstrun = firstrun + self.config = config_obj + self.net_restart_needed = False + self.font_setting = None + self.timer = QtCore.QTimer() + + if self.config.safeGetBoolean('bitmessagesettings', 'dontconnect'): + self.firstrun = False + try: + import pkg_resources + except ImportError: + pass + else: + # Append proxy types defined in plugins + # FIXME: this should be a function in mod:`plugin` + for ep in pkg_resources.iter_entry_points( + 'bitmessage.proxyconfig'): + try: + ep.load() + except Exception: # it should add only functional plugins + # many possible exceptions, which are don't matter + pass + else: + self.comboBoxProxyType.addItem(ep.name) + + self.lineEditMaxOutboundConnections.setValidator( + QtGui.QIntValidator(0, 8, self.lineEditMaxOutboundConnections)) + + self.adjust_from_config(self.config) + if firstrun: + # switch to "Network Settings" tab if user selected + # "Let me configure special network settings first" on first run + self.tabWidgetSettings.setCurrentIndex( + self.tabWidgetSettings.indexOf(self.tabNetworkSettings) + ) + QtGui.QWidget.resize(self, QtGui.QWidget.sizeHint(self)) + + def adjust_from_config(self, config): + """Adjust all widgets state according to config settings""" + # pylint: disable=too-many-branches,too-many-statements + + current_style = self.app.get_windowstyle() + for i, sk in enumerate(QtGui.QStyleFactory.keys()): + self.comboBoxStyle.addItem(sk) + if sk == current_style: + self.comboBoxStyle.setCurrentIndex(i) + + self.save_font_setting(self.app.font()) + + if not self.parent.tray.isSystemTrayAvailable(): + self.groupBoxTray.setEnabled(False) + self.groupBoxTray.setTitle(_translate( + "MainWindow", "Tray (not available in your system)")) + for setting in ( + 'minimizetotray', 'trayonclose', 'startintray'): + config.set('bitmessagesettings', setting, 'false') else: - self.lineEditSocksPort.setPlaceholderText(_fromUtf8("9050")) - self.gridLayout_2.addWidget(self.lineEditSocksPort, 1, 5, 1, 1) - self.checkBoxAuthentication = QtGui.QCheckBox(self.groupBox_2) - self.checkBoxAuthentication.setObjectName(_fromUtf8("checkBoxAuthentication")) - self.gridLayout_2.addWidget(self.checkBoxAuthentication, 2, 1, 1, 1) - self.label_5 = QtGui.QLabel(self.groupBox_2) - self.label_5.setObjectName(_fromUtf8("label_5")) - self.gridLayout_2.addWidget(self.label_5, 2, 2, 1, 1) - self.lineEditSocksUsername = QtGui.QLineEdit(self.groupBox_2) - self.lineEditSocksUsername.setEnabled(False) - self.lineEditSocksUsername.setObjectName(_fromUtf8("lineEditSocksUsername")) - self.gridLayout_2.addWidget(self.lineEditSocksUsername, 2, 3, 1, 1) - self.label_6 = QtGui.QLabel(self.groupBox_2) - self.label_6.setObjectName(_fromUtf8("label_6")) - self.gridLayout_2.addWidget(self.label_6, 2, 4, 1, 1) - self.lineEditSocksPassword = QtGui.QLineEdit(self.groupBox_2) - self.lineEditSocksPassword.setEnabled(False) - self.lineEditSocksPassword.setInputMethodHints(QtCore.Qt.ImhHiddenText|QtCore.Qt.ImhNoAutoUppercase|QtCore.Qt.ImhNoPredictiveText) - self.lineEditSocksPassword.setEchoMode(QtGui.QLineEdit.Password) - self.lineEditSocksPassword.setObjectName(_fromUtf8("lineEditSocksPassword")) - self.gridLayout_2.addWidget(self.lineEditSocksPassword, 2, 5, 1, 1) - self.checkBoxSocksListen = QtGui.QCheckBox(self.groupBox_2) - self.checkBoxSocksListen.setObjectName(_fromUtf8("checkBoxSocksListen")) - self.gridLayout_2.addWidget(self.checkBoxSocksListen, 3, 1, 1, 4) - self.comboBoxProxyType = QtGui.QComboBox(self.groupBox_2) - self.comboBoxProxyType.setObjectName(_fromUtf8("comboBoxProxyType")) - self.comboBoxProxyType.addItem(_fromUtf8("")) - self.comboBoxProxyType.addItem(_fromUtf8("")) - self.comboBoxProxyType.addItem(_fromUtf8("")) - self.gridLayout_2.addWidget(self.comboBoxProxyType, 0, 1, 1, 1) - self.gridLayout_4.addWidget(self.groupBox_2, 1, 0, 1, 1) - spacerItem2 = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) - self.gridLayout_4.addItem(spacerItem2, 3, 0, 1, 1) - self.tabWidgetSettings.addTab(self.tabNetworkSettings, _fromUtf8("")) - self.tabDemandedDifficulty = QtGui.QWidget() - self.tabDemandedDifficulty.setObjectName(_fromUtf8("tabDemandedDifficulty")) - self.gridLayout_6 = QtGui.QGridLayout(self.tabDemandedDifficulty) - self.gridLayout_6.setObjectName(_fromUtf8("gridLayout_6")) - self.label_9 = QtGui.QLabel(self.tabDemandedDifficulty) - self.label_9.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.label_9.setObjectName(_fromUtf8("label_9")) - self.gridLayout_6.addWidget(self.label_9, 1, 1, 1, 1) - self.label_10 = QtGui.QLabel(self.tabDemandedDifficulty) - self.label_10.setWordWrap(True) - self.label_10.setObjectName(_fromUtf8("label_10")) - self.gridLayout_6.addWidget(self.label_10, 2, 0, 1, 3) - self.label_11 = QtGui.QLabel(self.tabDemandedDifficulty) - self.label_11.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.label_11.setObjectName(_fromUtf8("label_11")) - self.gridLayout_6.addWidget(self.label_11, 3, 1, 1, 1) - self.label_8 = QtGui.QLabel(self.tabDemandedDifficulty) - self.label_8.setWordWrap(True) - self.label_8.setObjectName(_fromUtf8("label_8")) - self.gridLayout_6.addWidget(self.label_8, 0, 0, 1, 3) - spacerItem3 = QtGui.QSpacerItem(203, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) - self.gridLayout_6.addItem(spacerItem3, 1, 0, 1, 1) - self.label_12 = QtGui.QLabel(self.tabDemandedDifficulty) - self.label_12.setWordWrap(True) - self.label_12.setObjectName(_fromUtf8("label_12")) - self.gridLayout_6.addWidget(self.label_12, 4, 0, 1, 3) - self.lineEditSmallMessageDifficulty = QtGui.QLineEdit(self.tabDemandedDifficulty) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.lineEditSmallMessageDifficulty.sizePolicy().hasHeightForWidth()) - self.lineEditSmallMessageDifficulty.setSizePolicy(sizePolicy) - self.lineEditSmallMessageDifficulty.setMaximumSize(QtCore.QSize(70, 16777215)) - self.lineEditSmallMessageDifficulty.setObjectName(_fromUtf8("lineEditSmallMessageDifficulty")) - self.gridLayout_6.addWidget(self.lineEditSmallMessageDifficulty, 3, 2, 1, 1) - self.lineEditTotalDifficulty = QtGui.QLineEdit(self.tabDemandedDifficulty) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.lineEditTotalDifficulty.sizePolicy().hasHeightForWidth()) - self.lineEditTotalDifficulty.setSizePolicy(sizePolicy) - self.lineEditTotalDifficulty.setMaximumSize(QtCore.QSize(70, 16777215)) - self.lineEditTotalDifficulty.setObjectName(_fromUtf8("lineEditTotalDifficulty")) - self.gridLayout_6.addWidget(self.lineEditTotalDifficulty, 1, 2, 1, 1) - spacerItem4 = QtGui.QSpacerItem(203, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) - self.gridLayout_6.addItem(spacerItem4, 3, 0, 1, 1) - spacerItem5 = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) - self.gridLayout_6.addItem(spacerItem5, 5, 0, 1, 1) - self.tabWidgetSettings.addTab(self.tabDemandedDifficulty, _fromUtf8("")) - self.tabMaxAcceptableDifficulty = QtGui.QWidget() - self.tabMaxAcceptableDifficulty.setObjectName(_fromUtf8("tabMaxAcceptableDifficulty")) - self.gridLayout_7 = QtGui.QGridLayout(self.tabMaxAcceptableDifficulty) - self.gridLayout_7.setObjectName(_fromUtf8("gridLayout_7")) - self.label_15 = QtGui.QLabel(self.tabMaxAcceptableDifficulty) - self.label_15.setWordWrap(True) - self.label_15.setObjectName(_fromUtf8("label_15")) - self.gridLayout_7.addWidget(self.label_15, 0, 0, 1, 3) - spacerItem6 = QtGui.QSpacerItem(102, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) - self.gridLayout_7.addItem(spacerItem6, 1, 0, 1, 1) - self.label_13 = QtGui.QLabel(self.tabMaxAcceptableDifficulty) - self.label_13.setLayoutDirection(QtCore.Qt.LeftToRight) - self.label_13.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.label_13.setObjectName(_fromUtf8("label_13")) - self.gridLayout_7.addWidget(self.label_13, 1, 1, 1, 1) - self.lineEditMaxAcceptableTotalDifficulty = QtGui.QLineEdit(self.tabMaxAcceptableDifficulty) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.lineEditMaxAcceptableTotalDifficulty.sizePolicy().hasHeightForWidth()) - self.lineEditMaxAcceptableTotalDifficulty.setSizePolicy(sizePolicy) - self.lineEditMaxAcceptableTotalDifficulty.setMaximumSize(QtCore.QSize(70, 16777215)) - self.lineEditMaxAcceptableTotalDifficulty.setObjectName(_fromUtf8("lineEditMaxAcceptableTotalDifficulty")) - self.gridLayout_7.addWidget(self.lineEditMaxAcceptableTotalDifficulty, 1, 2, 1, 1) - spacerItem7 = QtGui.QSpacerItem(102, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) - self.gridLayout_7.addItem(spacerItem7, 2, 0, 1, 1) - self.label_14 = QtGui.QLabel(self.tabMaxAcceptableDifficulty) - self.label_14.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.label_14.setObjectName(_fromUtf8("label_14")) - self.gridLayout_7.addWidget(self.label_14, 2, 1, 1, 1) - self.lineEditMaxAcceptableSmallMessageDifficulty = QtGui.QLineEdit(self.tabMaxAcceptableDifficulty) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.lineEditMaxAcceptableSmallMessageDifficulty.sizePolicy().hasHeightForWidth()) - self.lineEditMaxAcceptableSmallMessageDifficulty.setSizePolicy(sizePolicy) - self.lineEditMaxAcceptableSmallMessageDifficulty.setMaximumSize(QtCore.QSize(70, 16777215)) - self.lineEditMaxAcceptableSmallMessageDifficulty.setObjectName(_fromUtf8("lineEditMaxAcceptableSmallMessageDifficulty")) - self.gridLayout_7.addWidget(self.lineEditMaxAcceptableSmallMessageDifficulty, 2, 2, 1, 1) - spacerItem8 = QtGui.QSpacerItem(20, 147, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) - self.gridLayout_7.addItem(spacerItem8, 3, 1, 1, 1) - self.labelOpenCL = QtGui.QLabel(self.tabMaxAcceptableDifficulty) - self.labelOpenCL.setObjectName(_fromUtf8("labelOpenCL")) - self.gridLayout_7.addWidget(self.labelOpenCL, 4, 0, 1, 1) - self.comboBoxOpenCL = QtGui.QComboBox(self.tabMaxAcceptableDifficulty) - self.comboBoxOpenCL.setObjectName = (_fromUtf8("comboBoxOpenCL")) - self.gridLayout_7.addWidget(self.comboBoxOpenCL, 4, 1, 1, 1) - self.tabWidgetSettings.addTab(self.tabMaxAcceptableDifficulty, _fromUtf8("")) - self.tabNamecoin = QtGui.QWidget() - self.tabNamecoin.setObjectName(_fromUtf8("tabNamecoin")) - self.gridLayout_8 = QtGui.QGridLayout(self.tabNamecoin) - self.gridLayout_8.setObjectName(_fromUtf8("gridLayout_8")) - spacerItem9 = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) - self.gridLayout_8.addItem(spacerItem9, 2, 0, 1, 1) - self.label_16 = QtGui.QLabel(self.tabNamecoin) - self.label_16.setWordWrap(True) - self.label_16.setObjectName(_fromUtf8("label_16")) - self.gridLayout_8.addWidget(self.label_16, 0, 0, 1, 3) - self.label_17 = QtGui.QLabel(self.tabNamecoin) - self.label_17.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.label_17.setObjectName(_fromUtf8("label_17")) - self.gridLayout_8.addWidget(self.label_17, 2, 1, 1, 1) - self.lineEditNamecoinHost = QtGui.QLineEdit(self.tabNamecoin) - self.lineEditNamecoinHost.setObjectName(_fromUtf8("lineEditNamecoinHost")) - self.gridLayout_8.addWidget(self.lineEditNamecoinHost, 2, 2, 1, 1) - spacerItem10 = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) - self.gridLayout_8.addItem(spacerItem10, 3, 0, 1, 1) - spacerItem11 = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) - self.gridLayout_8.addItem(spacerItem11, 4, 0, 1, 1) - self.label_18 = QtGui.QLabel(self.tabNamecoin) - self.label_18.setEnabled(True) - self.label_18.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.label_18.setObjectName(_fromUtf8("label_18")) - self.gridLayout_8.addWidget(self.label_18, 3, 1, 1, 1) - self.lineEditNamecoinPort = QtGui.QLineEdit(self.tabNamecoin) - self.lineEditNamecoinPort.setObjectName(_fromUtf8("lineEditNamecoinPort")) - self.gridLayout_8.addWidget(self.lineEditNamecoinPort, 3, 2, 1, 1) - spacerItem12 = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) - self.gridLayout_8.addItem(spacerItem12, 8, 1, 1, 1) - self.labelNamecoinUser = QtGui.QLabel(self.tabNamecoin) - self.labelNamecoinUser.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.labelNamecoinUser.setObjectName(_fromUtf8("labelNamecoinUser")) - self.gridLayout_8.addWidget(self.labelNamecoinUser, 4, 1, 1, 1) - self.lineEditNamecoinUser = QtGui.QLineEdit(self.tabNamecoin) - self.lineEditNamecoinUser.setObjectName(_fromUtf8("lineEditNamecoinUser")) - self.gridLayout_8.addWidget(self.lineEditNamecoinUser, 4, 2, 1, 1) - spacerItem13 = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) - self.gridLayout_8.addItem(spacerItem13, 5, 0, 1, 1) - self.labelNamecoinPassword = QtGui.QLabel(self.tabNamecoin) - self.labelNamecoinPassword.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.labelNamecoinPassword.setObjectName(_fromUtf8("labelNamecoinPassword")) - self.gridLayout_8.addWidget(self.labelNamecoinPassword, 5, 1, 1, 1) - self.lineEditNamecoinPassword = QtGui.QLineEdit(self.tabNamecoin) - self.lineEditNamecoinPassword.setInputMethodHints(QtCore.Qt.ImhHiddenText|QtCore.Qt.ImhNoAutoUppercase|QtCore.Qt.ImhNoPredictiveText) - self.lineEditNamecoinPassword.setEchoMode(QtGui.QLineEdit.Password) - self.lineEditNamecoinPassword.setObjectName(_fromUtf8("lineEditNamecoinPassword")) - self.gridLayout_8.addWidget(self.lineEditNamecoinPassword, 5, 2, 1, 1) - self.labelNamecoinTestResult = QtGui.QLabel(self.tabNamecoin) - self.labelNamecoinTestResult.setText(_fromUtf8("")) - self.labelNamecoinTestResult.setObjectName(_fromUtf8("labelNamecoinTestResult")) - self.gridLayout_8.addWidget(self.labelNamecoinTestResult, 7, 0, 1, 2) - self.pushButtonNamecoinTest = QtGui.QPushButton(self.tabNamecoin) - self.pushButtonNamecoinTest.setObjectName(_fromUtf8("pushButtonNamecoinTest")) - self.gridLayout_8.addWidget(self.pushButtonNamecoinTest, 7, 2, 1, 1) - self.horizontalLayout = QtGui.QHBoxLayout() - self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout")) - self.label_21 = QtGui.QLabel(self.tabNamecoin) - self.label_21.setObjectName(_fromUtf8("label_21")) - self.horizontalLayout.addWidget(self.label_21) - self.radioButtonNamecoinNamecoind = QtGui.QRadioButton(self.tabNamecoin) - self.radioButtonNamecoinNamecoind.setObjectName(_fromUtf8("radioButtonNamecoinNamecoind")) - self.horizontalLayout.addWidget(self.radioButtonNamecoinNamecoind) - self.radioButtonNamecoinNmcontrol = QtGui.QRadioButton(self.tabNamecoin) - self.radioButtonNamecoinNmcontrol.setObjectName(_fromUtf8("radioButtonNamecoinNmcontrol")) - self.horizontalLayout.addWidget(self.radioButtonNamecoinNmcontrol) - self.gridLayout_8.addLayout(self.horizontalLayout, 1, 0, 1, 3) - self.tabWidgetSettings.addTab(self.tabNamecoin, _fromUtf8("")) - self.tabResendsExpire = QtGui.QWidget() - self.tabResendsExpire.setObjectName(_fromUtf8("tabResendsExpire")) - self.gridLayout_5 = QtGui.QGridLayout(self.tabResendsExpire) - self.gridLayout_5.setObjectName(_fromUtf8("gridLayout_5")) - self.label_7 = QtGui.QLabel(self.tabResendsExpire) - self.label_7.setWordWrap(True) - self.label_7.setObjectName(_fromUtf8("label_7")) - self.gridLayout_5.addWidget(self.label_7, 0, 0, 1, 3) - spacerItem14 = QtGui.QSpacerItem(212, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) - self.gridLayout_5.addItem(spacerItem14, 1, 0, 1, 1) - self.widget = QtGui.QWidget(self.tabResendsExpire) - self.widget.setMinimumSize(QtCore.QSize(231, 75)) - self.widget.setObjectName(_fromUtf8("widget")) - self.label_19 = QtGui.QLabel(self.widget) - self.label_19.setGeometry(QtCore.QRect(10, 20, 101, 20)) - self.label_19.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.label_19.setObjectName(_fromUtf8("label_19")) - self.label_20 = QtGui.QLabel(self.widget) - self.label_20.setGeometry(QtCore.QRect(30, 40, 80, 16)) - self.label_20.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.label_20.setObjectName(_fromUtf8("label_20")) - self.lineEditDays = QtGui.QLineEdit(self.widget) - self.lineEditDays.setGeometry(QtCore.QRect(113, 20, 51, 20)) - self.lineEditDays.setObjectName(_fromUtf8("lineEditDays")) - self.lineEditMonths = QtGui.QLineEdit(self.widget) - self.lineEditMonths.setGeometry(QtCore.QRect(113, 40, 51, 20)) - self.lineEditMonths.setObjectName(_fromUtf8("lineEditMonths")) - self.label_22 = QtGui.QLabel(self.widget) - self.label_22.setGeometry(QtCore.QRect(169, 23, 61, 16)) - self.label_22.setObjectName(_fromUtf8("label_22")) - self.label_23 = QtGui.QLabel(self.widget) - self.label_23.setGeometry(QtCore.QRect(170, 41, 71, 16)) - self.label_23.setObjectName(_fromUtf8("label_23")) - self.gridLayout_5.addWidget(self.widget, 1, 2, 1, 1) - spacerItem15 = QtGui.QSpacerItem(20, 129, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) - self.gridLayout_5.addItem(spacerItem15, 2, 1, 1, 1) - self.tabWidgetSettings.addTab(self.tabResendsExpire, _fromUtf8("")) - self.gridLayout.addWidget(self.tabWidgetSettings, 0, 0, 1, 1) - - self.retranslateUi(settingsDialog) - self.tabWidgetSettings.setCurrentIndex(0) - QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL(_fromUtf8("accepted()")), settingsDialog.accept) - QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL(_fromUtf8("rejected()")), settingsDialog.reject) - QtCore.QObject.connect(self.checkBoxAuthentication, QtCore.SIGNAL(_fromUtf8("toggled(bool)")), self.lineEditSocksUsername.setEnabled) - QtCore.QObject.connect(self.checkBoxAuthentication, QtCore.SIGNAL(_fromUtf8("toggled(bool)")), self.lineEditSocksPassword.setEnabled) - QtCore.QMetaObject.connectSlotsByName(settingsDialog) - settingsDialog.setTabOrder(self.tabWidgetSettings, self.checkBoxStartOnLogon) - settingsDialog.setTabOrder(self.checkBoxStartOnLogon, self.checkBoxStartInTray) - settingsDialog.setTabOrder(self.checkBoxStartInTray, self.checkBoxMinimizeToTray) - settingsDialog.setTabOrder(self.checkBoxMinimizeToTray, self.lineEditTCPPort) - settingsDialog.setTabOrder(self.lineEditTCPPort, self.comboBoxProxyType) - settingsDialog.setTabOrder(self.comboBoxProxyType, self.lineEditSocksHostname) - settingsDialog.setTabOrder(self.lineEditSocksHostname, self.lineEditSocksPort) - settingsDialog.setTabOrder(self.lineEditSocksPort, self.checkBoxAuthentication) - settingsDialog.setTabOrder(self.checkBoxAuthentication, self.lineEditSocksUsername) - settingsDialog.setTabOrder(self.lineEditSocksUsername, self.lineEditSocksPassword) - settingsDialog.setTabOrder(self.lineEditSocksPassword, self.checkBoxSocksListen) - settingsDialog.setTabOrder(self.checkBoxSocksListen, self.buttonBox) - - def retranslateUi(self, settingsDialog): - settingsDialog.setWindowTitle(_translate("settingsDialog", "Settings", None)) - self.checkBoxStartOnLogon.setText(_translate("settingsDialog", "Start Bitmessage on user login", None)) - self.groupBoxTray.setTitle(_translate("settingsDialog", "Tray", None)) - self.checkBoxStartInTray.setText(_translate("settingsDialog", "Start Bitmessage in the tray (don\'t show main window)", None)) - self.checkBoxMinimizeToTray.setText(_translate("settingsDialog", "Minimize to tray", None)) - self.checkBoxTrayOnClose.setText(_translate("settingsDialog", "Close to tray", None)) - self.checkBoxHideTrayConnectionNotifications.setText(_translate("settingsDialog", "Hide connection notifications", None)) - self.checkBoxShowTrayNotifications.setText(_translate("settingsDialog", "Show notification when message received", None)) - self.checkBoxPortableMode.setText(_translate("settingsDialog", "Run in Portable Mode", None)) - self.PortableModeDescription.setText(_translate("settingsDialog", "In Portable Mode, messages and config files are stored in the same directory as the program rather than the normal application-data folder. This makes it convenient to run Bitmessage from a USB thumb drive.", None)) - self.checkBoxWillinglySendToMobile.setText(_translate("settingsDialog", "Willingly include unencrypted destination address when sending to a mobile device", None)) - self.checkBoxUseIdenticons.setText(_translate("settingsDialog", "Use Identicons", None)) - self.checkBoxReplyBelow.setText(_translate("settingsDialog", "Reply below Quote", None)) - self.groupBox.setTitle(_translate("settingsDialog", "Interface Language", None)) - self.languageComboBox.setItemText(0, _translate("settingsDialog", "System Settings", "system")) - self.tabWidgetSettings.setTabText(self.tabWidgetSettings.indexOf(self.tabUserInterface), _translate("settingsDialog", "User Interface", None)) - self.groupBox1.setTitle(_translate("settingsDialog", "Listening port", None)) - self.label.setText(_translate("settingsDialog", "Listen for connections on port:", None)) - self.labelUPnP.setText(_translate("settingsDialog", "UPnP:", None)) - self.groupBox_3.setTitle(_translate("settingsDialog", "Bandwidth limit", None)) - self.label_24.setText(_translate("settingsDialog", "Maximum download rate (kB/s): [0: unlimited]", None)) - self.label_25.setText(_translate("settingsDialog", "Maximum upload rate (kB/s): [0: unlimited]", None)) - self.label_26.setText(_translate("settingsDialog", "Maximum outbound connections: [0: none]", None)) - self.groupBox_2.setTitle(_translate("settingsDialog", "Proxy server / Tor", None)) - self.label_2.setText(_translate("settingsDialog", "Type:", None)) - self.label_3.setText(_translate("settingsDialog", "Server hostname:", None)) - self.label_4.setText(_translate("settingsDialog", "Port:", None)) - self.checkBoxAuthentication.setText(_translate("settingsDialog", "Authentication", None)) - self.label_5.setText(_translate("settingsDialog", "Username:", None)) - self.label_6.setText(_translate("settingsDialog", "Pass:", None)) - self.checkBoxSocksListen.setText(_translate("settingsDialog", "Listen for incoming connections when using proxy", None)) - self.comboBoxProxyType.setItemText(0, _translate("settingsDialog", "none", None)) - self.comboBoxProxyType.setItemText(1, _translate("settingsDialog", "SOCKS4a", None)) - self.comboBoxProxyType.setItemText(2, _translate("settingsDialog", "SOCKS5", None)) - self.tabWidgetSettings.setTabText(self.tabWidgetSettings.indexOf(self.tabNetworkSettings), _translate("settingsDialog", "Network Settings", None)) - self.label_9.setText(_translate("settingsDialog", "Total difficulty:", None)) - self.label_10.setText(_translate("settingsDialog", "The \'Total difficulty\' affects the absolute amount of work the sender must complete. Doubling this value doubles the amount of work.", None)) - self.label_11.setText(_translate("settingsDialog", "Small message difficulty:", None)) - self.label_8.setText(_translate("settingsDialog", "When 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. ", None)) - self.label_12.setText(_translate("settingsDialog", "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.", None)) - self.tabWidgetSettings.setTabText(self.tabWidgetSettings.indexOf(self.tabDemandedDifficulty), _translate("settingsDialog", "Demanded difficulty", None)) - self.label_15.setText(_translate("settingsDialog", "Here 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.", None)) - self.label_13.setText(_translate("settingsDialog", "Maximum acceptable total difficulty:", None)) - self.label_14.setText(_translate("settingsDialog", "Maximum acceptable small message difficulty:", None)) - self.tabWidgetSettings.setTabText(self.tabWidgetSettings.indexOf(self.tabMaxAcceptableDifficulty), _translate("settingsDialog", "Max acceptable difficulty", None)) - self.labelOpenCL.setText(_translate("settingsDialog", "Hardware GPU acceleration (OpenCL):", None)) - self.label_16.setText(_translate("settingsDialog", "

Bitmessage 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.

(Getting your own Bitmessage address into Namecoin is still rather difficult).

Bitmessage can use either namecoind directly or a running nmcontrol instance.

", None)) - self.label_17.setText(_translate("settingsDialog", "Host:", None)) - self.label_18.setText(_translate("settingsDialog", "Port:", None)) - self.labelNamecoinUser.setText(_translate("settingsDialog", "Username:", None)) - self.labelNamecoinPassword.setText(_translate("settingsDialog", "Password:", None)) - self.pushButtonNamecoinTest.setText(_translate("settingsDialog", "Test", None)) - self.label_21.setText(_translate("settingsDialog", "Connect to:", None)) - self.radioButtonNamecoinNamecoind.setText(_translate("settingsDialog", "Namecoind", None)) - self.radioButtonNamecoinNmcontrol.setText(_translate("settingsDialog", "NMControl", None)) - self.tabWidgetSettings.setTabText(self.tabWidgetSettings.indexOf(self.tabNamecoin), _translate("settingsDialog", "Namecoin integration", None)) - self.label_7.setText(_translate("settingsDialog", "

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.

Leave these input fields blank for the default behavior.

", None)) - self.label_19.setText(_translate("settingsDialog", "Give up after", None)) - self.label_20.setText(_translate("settingsDialog", "and", None)) - self.label_22.setText(_translate("settingsDialog", "days", None)) - self.label_23.setText(_translate("settingsDialog", "months.", None)) - self.tabWidgetSettings.setTabText(self.tabWidgetSettings.indexOf(self.tabResendsExpire), _translate("settingsDialog", "Resends Expire", None)) - -import bitmessage_icons_rc + self.checkBoxMinimizeToTray.setChecked( + config.getboolean('bitmessagesettings', 'minimizetotray')) + self.checkBoxTrayOnClose.setChecked( + config.safeGetBoolean('bitmessagesettings', 'trayonclose')) + self.checkBoxStartInTray.setChecked( + config.getboolean('bitmessagesettings', 'startintray')) + + self.checkBoxHideTrayConnectionNotifications.setChecked( + config.getboolean( + 'bitmessagesettings', 'hidetrayconnectionnotifications')) + self.checkBoxShowTrayNotifications.setChecked( + config.getboolean('bitmessagesettings', 'showtraynotifications')) + + self.checkBoxStartOnLogon.setChecked( + config.getboolean('bitmessagesettings', 'startonlogon')) + + self.checkBoxWillinglySendToMobile.setChecked( + config.safeGetBoolean( + 'bitmessagesettings', 'willinglysendtomobile')) + self.checkBoxUseIdenticons.setChecked( + config.safeGetBoolean('bitmessagesettings', 'useidenticons')) + self.checkBoxReplyBelow.setChecked( + config.safeGetBoolean('bitmessagesettings', 'replybelow')) + + if state.appdata == paths.lookupExeFolder(): + self.checkBoxPortableMode.setChecked(True) + else: + try: + tempfile.NamedTemporaryFile( + dir=paths.lookupExeFolder(), delete=True + ).close() # should autodelete + except Exception: + self.checkBoxPortableMode.setDisabled(True) + + if 'darwin' in sys.platform: + self.checkBoxMinimizeToTray.setDisabled(True) + self.checkBoxMinimizeToTray.setText(_translate( + "MainWindow", + "Minimize-to-tray not yet supported on your OS.")) + self.checkBoxShowTrayNotifications.setDisabled(True) + self.checkBoxShowTrayNotifications.setText(_translate( + "MainWindow", + "Tray notifications not yet supported on your OS.")) + + if not sys.platform.startswith('win') and not self.parent.desktop: + self.checkBoxStartOnLogon.setDisabled(True) + self.checkBoxStartOnLogon.setText(_translate( + "MainWindow", "Start-on-login not yet supported on your OS.")) + + # On the Network settings tab: + self.lineEditTCPPort.setText(str( + config.get('bitmessagesettings', 'port'))) + self.checkBoxUPnP.setChecked( + config.safeGetBoolean('bitmessagesettings', 'upnp')) + self.checkBoxUDP.setChecked( + config.safeGetBoolean('bitmessagesettings', 'udp')) + self.checkBoxAuthentication.setChecked( + config.getboolean('bitmessagesettings', 'socksauthentication')) + self.checkBoxSocksListen.setChecked( + config.getboolean('bitmessagesettings', 'sockslisten')) + self.checkBoxOnionOnly.setChecked( + config.safeGetBoolean('bitmessagesettings', 'onionservicesonly')) + + self._proxy_type = getSOCKSProxyType(config) + self.comboBoxProxyType.setCurrentIndex( + 0 if not self._proxy_type + 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( + config.get('bitmessagesettings', 'socksport'))) + self.lineEditSocksUsername.setText( + config.get('bitmessagesettings', 'socksusername')) + self.lineEditSocksPassword.setText( + config.get('bitmessagesettings', 'sockspassword')) + + self.lineEditMaxDownloadRate.setText(str( + config.get('bitmessagesettings', 'maxdownloadrate'))) + self.lineEditMaxUploadRate.setText(str( + config.get('bitmessagesettings', 'maxuploadrate'))) + self.lineEditMaxOutboundConnections.setText(str( + config.get('bitmessagesettings', 'maxoutboundconnections'))) + + # Demanded difficulty tab + self.lineEditTotalDifficulty.setText(str((float( + config.getint( + 'bitmessagesettings', 'defaultnoncetrialsperbyte') + ) / defaults.networkDefaultProofOfWorkNonceTrialsPerByte))) + self.lineEditSmallMessageDifficulty.setText(str((float( + config.getint( + 'bitmessagesettings', 'defaultpayloadlengthextrabytes') + ) / defaults.networkDefaultPayloadLengthExtraBytes))) + + # Max acceptable difficulty tab + self.lineEditMaxAcceptableTotalDifficulty.setText(str((float( + config.getint( + 'bitmessagesettings', 'maxacceptablenoncetrialsperbyte') + ) / defaults.networkDefaultProofOfWorkNonceTrialsPerByte))) + self.lineEditMaxAcceptableSmallMessageDifficulty.setText(str((float( + config.getint( + 'bitmessagesettings', 'maxacceptablepayloadlengthextrabytes') + ) / defaults.networkDefaultPayloadLengthExtraBytes))) + + # OpenCL + self.comboBoxOpenCL.setEnabled(openclpow.openclAvailable()) + self.comboBoxOpenCL.clear() + self.comboBoxOpenCL.addItem("None") + self.comboBoxOpenCL.addItems(openclpow.vendors) + self.comboBoxOpenCL.setCurrentIndex(0) + for i in range(self.comboBoxOpenCL.count()): + if self.comboBoxOpenCL.itemText(i) == config.safeGet( + 'bitmessagesettings', 'opencl'): + self.comboBoxOpenCL.setCurrentIndex(i) + break + + # Namecoin integration tab + nmctype = config.get('bitmessagesettings', 'namecoinrpctype') + self.lineEditNamecoinHost.setText( + config.get('bitmessagesettings', 'namecoinrpchost')) + self.lineEditNamecoinPort.setText(str( + config.get('bitmessagesettings', 'namecoinrpcport'))) + self.lineEditNamecoinUser.setText( + config.get('bitmessagesettings', 'namecoinrpcuser')) + self.lineEditNamecoinPassword.setText( + config.get('bitmessagesettings', 'namecoinrpcpassword')) + + if nmctype == "namecoind": + self.radioButtonNamecoinNamecoind.setChecked(True) + elif nmctype == "nmcontrol": + self.radioButtonNamecoinNmcontrol.setChecked(True) + self.lineEditNamecoinUser.setEnabled(False) + self.labelNamecoinUser.setEnabled(False) + self.lineEditNamecoinPassword.setEnabled(False) + self.labelNamecoinPassword.setEnabled(False) + else: + assert False + + # Message Resend tab + self.lineEditDays.setText(str( + config.get('bitmessagesettings', 'stopresendingafterxdays'))) + self.lineEditMonths.setText(str( + config.get('bitmessagesettings', 'stopresendingafterxmonths'))) + + def comboBoxProxyTypeChanged(self, comboBoxIndex): + """A callback for currentIndexChanged event of comboBoxProxyType""" + if comboBoxIndex == 0: + self.lineEditSocksHostname.setEnabled(False) + self.lineEditSocksPort.setEnabled(False) + self.lineEditSocksUsername.setEnabled(False) + self.lineEditSocksPassword.setEnabled(False) + self.checkBoxAuthentication.setEnabled(False) + self.checkBoxSocksListen.setEnabled(False) + self.checkBoxOnionOnly.setEnabled(False) + else: + self.lineEditSocksHostname.setEnabled(True) + self.lineEditSocksPort.setEnabled(True) + self.checkBoxAuthentication.setEnabled(True) + self.checkBoxSocksListen.setEnabled(True) + self.checkBoxOnionOnly.setEnabled(True) + if self.checkBoxAuthentication.isChecked(): + self.lineEditSocksUsername.setEnabled(True) + self.lineEditSocksPassword.setEnabled(True) + + def getNamecoinType(self): + """ + Check status of namecoin integration radio buttons + and translate it to a string as in the options. + """ + if self.radioButtonNamecoinNamecoind.isChecked(): + return "namecoind" + if self.radioButtonNamecoinNmcontrol.isChecked(): + return "nmcontrol" + assert False + + # Namecoin connection type was changed. + def namecoinTypeChanged(self, checked): # pylint: disable=unused-argument + """A callback for toggled event of radioButtonNamecoinNamecoind""" + nmctype = self.getNamecoinType() + assert nmctype == "namecoind" or nmctype == "nmcontrol" + + isNamecoind = (nmctype == "namecoind") + self.lineEditNamecoinUser.setEnabled(isNamecoind) + self.labelNamecoinUser.setEnabled(isNamecoind) + self.lineEditNamecoinPassword.setEnabled(isNamecoind) + self.labelNamecoinPassword.setEnabled(isNamecoind) + + if isNamecoind: + self.lineEditNamecoinPort.setText(defaults.namecoinDefaultRpcPort) + else: + self.lineEditNamecoinPort.setText("9000") + + def click_pushButtonNamecoinTest(self): + """Test the namecoin settings specified in the settings dialog.""" + self.labelNamecoinTestResult.setText( + _translate("MainWindow", "Testing...")) + nc = namecoin.namecoinConnection({ + 'type': self.getNamecoinType(), + 'host': str(self.lineEditNamecoinHost.text().toUtf8()), + 'port': str(self.lineEditNamecoinPort.text().toUtf8()), + 'user': str(self.lineEditNamecoinUser.text().toUtf8()), + 'password': str(self.lineEditNamecoinPassword.text().toUtf8()) + }) + status, text = nc.test() + self.labelNamecoinTestResult.setText(text) + if status == 'success': + self.parent.namecoin = nc + + def save_font_setting(self, font): + """Save user font setting and set the buttonFont text""" + font_setting = (font.family(), font.pointSize()) + self.buttonFont.setText('{} {}'.format(*font_setting)) + self.font_setting = '{},{}'.format(*font_setting) + + def choose_font(self): + """Show the font selection dialog""" + font, valid = QtGui.QFontDialog.getFont() + if valid: + self.save_font_setting(font) + + def accept(self): + """A callback for accepted event of buttonBox (OK button pressed)""" + # pylint: disable=too-many-branches,too-many-statements + super(SettingsDialog, self).accept() + if self.firstrun: + self.config.remove_option('bitmessagesettings', 'dontconnect') + self.config.set('bitmessagesettings', 'startonlogon', str( + self.checkBoxStartOnLogon.isChecked())) + self.config.set('bitmessagesettings', 'minimizetotray', str( + self.checkBoxMinimizeToTray.isChecked())) + self.config.set('bitmessagesettings', 'trayonclose', str( + self.checkBoxTrayOnClose.isChecked())) + self.config.set( + 'bitmessagesettings', 'hidetrayconnectionnotifications', + str(self.checkBoxHideTrayConnectionNotifications.isChecked())) + self.config.set('bitmessagesettings', 'showtraynotifications', str( + self.checkBoxShowTrayNotifications.isChecked())) + self.config.set('bitmessagesettings', 'startintray', str( + self.checkBoxStartInTray.isChecked())) + self.config.set('bitmessagesettings', 'willinglysendtomobile', str( + self.checkBoxWillinglySendToMobile.isChecked())) + self.config.set('bitmessagesettings', 'useidenticons', str( + self.checkBoxUseIdenticons.isChecked())) + self.config.set('bitmessagesettings', 'replybelow', str( + self.checkBoxReplyBelow.isChecked())) + + window_style = str(self.comboBoxStyle.currentText()) + if self.app.get_windowstyle() != window_style or self.config.safeGet( + 'bitmessagesettings', 'font' + ) != self.font_setting: + self.config.set('bitmessagesettings', 'windowstyle', window_style) + self.config.set('bitmessagesettings', 'font', self.font_setting) + queues.UISignalQueue.put(( + 'updateStatusBar', ( + _translate( + "MainWindow", + "You need to restart the application to apply" + " the window style or default font."), 1) + )) + + lang = str(self.languageComboBox.itemData( + self.languageComboBox.currentIndex()).toString()) + self.config.set('bitmessagesettings', 'userlocale', lang) + self.parent.change_translation() + + if int(self.config.get('bitmessagesettings', 'port')) != int( + self.lineEditTCPPort.text()): + self.config.set( + 'bitmessagesettings', 'port', str(self.lineEditTCPPort.text())) + if not self.config.safeGetBoolean( + 'bitmessagesettings', 'dontconnect'): + self.net_restart_needed = True + + if self.checkBoxUPnP.isChecked() != self.config.safeGetBoolean( + 'bitmessagesettings', 'upnp'): + self.config.set( + 'bitmessagesettings', 'upnp', + str(self.checkBoxUPnP.isChecked())) + if self.checkBoxUPnP.isChecked(): + import upnp + upnpThread = upnp.uPnPThread() + upnpThread.start() + + udp_enabled = self.checkBoxUDP.isChecked() + if udp_enabled != self.config.safeGetBoolean( + 'bitmessagesettings', 'udp'): + self.config.set('bitmessagesettings', 'udp', str(udp_enabled)) + if udp_enabled: + announceThread = AnnounceThread() + announceThread.daemon = True + announceThread.start() + else: + try: + state.announceThread.stopThread() + except AttributeError: + pass + + proxytype_index = self.comboBoxProxyType.currentIndex() + if proxytype_index == 0: + if self._proxy_type and state.statusIconColor != 'red': + self.net_restart_needed = True + elif state.statusIconColor == 'red' and self.config.safeGetBoolean( + 'bitmessagesettings', 'dontconnect'): + self.net_restart_needed = False + elif self.comboBoxProxyType.currentText() != self._proxy_type: + self.net_restart_needed = True + self.parent.statusbar.clearMessage() + + self.config.set( + 'bitmessagesettings', 'socksproxytype', + 'none' if self.comboBoxProxyType.currentIndex() == 0 + else str(self.comboBoxProxyType.currentText()) + ) + if proxytype_index > 2: # last literal proxytype in ui + start_proxyconfig() + + self.config.set('bitmessagesettings', 'socksauthentication', str( + self.checkBoxAuthentication.isChecked())) + self.config.set('bitmessagesettings', 'sockshostname', str( + self.lineEditSocksHostname.text())) + self.config.set('bitmessagesettings', 'socksport', str( + self.lineEditSocksPort.text())) + self.config.set('bitmessagesettings', 'socksusername', str( + self.lineEditSocksUsername.text())) + self.config.set('bitmessagesettings', 'sockspassword', str( + self.lineEditSocksPassword.text())) + self.config.set('bitmessagesettings', 'sockslisten', str( + self.checkBoxSocksListen.isChecked())) + if ( + self.checkBoxOnionOnly.isChecked() + and not self.config.safeGetBoolean( + 'bitmessagesettings', 'onionservicesonly') + ): + self.net_restart_needed = True + self.config.set('bitmessagesettings', 'onionservicesonly', str( + self.checkBoxOnionOnly.isChecked())) + try: + # Rounding to integers just for aesthetics + self.config.set('bitmessagesettings', 'maxdownloadrate', str( + int(float(self.lineEditMaxDownloadRate.text())))) + self.config.set('bitmessagesettings', 'maxuploadrate', str( + int(float(self.lineEditMaxUploadRate.text())))) + except ValueError: + QtGui.QMessageBox.about( + self, _translate("MainWindow", "Number needed"), + _translate( + "MainWindow", + "Your maximum download and upload rate must be numbers." + " Ignoring what you typed.") + ) + else: + set_rates( + self.config.safeGetInt('bitmessagesettings', 'maxdownloadrate'), + self.config.safeGetInt('bitmessagesettings', 'maxuploadrate')) + + self.config.set('bitmessagesettings', 'maxoutboundconnections', str( + int(float(self.lineEditMaxOutboundConnections.text())))) + + self.config.set( + 'bitmessagesettings', 'namecoinrpctype', self.getNamecoinType()) + self.config.set('bitmessagesettings', 'namecoinrpchost', str( + self.lineEditNamecoinHost.text())) + self.config.set('bitmessagesettings', 'namecoinrpcport', str( + self.lineEditNamecoinPort.text())) + self.config.set('bitmessagesettings', 'namecoinrpcuser', str( + self.lineEditNamecoinUser.text())) + self.config.set('bitmessagesettings', 'namecoinrpcpassword', str( + self.lineEditNamecoinPassword.text())) + self.parent.resetNamecoinConnection() + + # Demanded difficulty tab + if float(self.lineEditTotalDifficulty.text()) >= 1: + self.config.set( + 'bitmessagesettings', 'defaultnoncetrialsperbyte', + str(int( + float(self.lineEditTotalDifficulty.text()) + * defaults.networkDefaultProofOfWorkNonceTrialsPerByte))) + if float(self.lineEditSmallMessageDifficulty.text()) >= 1: + self.config.set( + 'bitmessagesettings', 'defaultpayloadlengthextrabytes', + str(int( + float(self.lineEditSmallMessageDifficulty.text()) + * defaults.networkDefaultPayloadLengthExtraBytes))) + + if self.comboBoxOpenCL.currentText().toUtf8() != self.config.safeGet( + 'bitmessagesettings', 'opencl'): + self.config.set( + 'bitmessagesettings', 'opencl', + str(self.comboBoxOpenCL.currentText())) + queues.workerQueue.put(('resetPoW', '')) + + acceptableDifficultyChanged = False + + if ( + float(self.lineEditMaxAcceptableTotalDifficulty.text()) >= 1 + or float(self.lineEditMaxAcceptableTotalDifficulty.text()) == 0 + ): + if self.config.get( + 'bitmessagesettings', 'maxacceptablenoncetrialsperbyte' + ) != str(int( + float(self.lineEditMaxAcceptableTotalDifficulty.text()) + * defaults.networkDefaultProofOfWorkNonceTrialsPerByte)): + # the user changed the max acceptable total difficulty + acceptableDifficultyChanged = True + self.config.set( + 'bitmessagesettings', 'maxacceptablenoncetrialsperbyte', + str(int( + float(self.lineEditMaxAcceptableTotalDifficulty.text()) + * defaults.networkDefaultProofOfWorkNonceTrialsPerByte)) + ) + if ( + float(self.lineEditMaxAcceptableSmallMessageDifficulty.text()) >= 1 + or float(self.lineEditMaxAcceptableSmallMessageDifficulty.text()) == 0 + ): + if self.config.get( + 'bitmessagesettings', 'maxacceptablepayloadlengthextrabytes' + ) != str(int( + float(self.lineEditMaxAcceptableSmallMessageDifficulty.text()) + * defaults.networkDefaultPayloadLengthExtraBytes)): + # the user changed the max acceptable small message difficulty + acceptableDifficultyChanged = True + self.config.set( + 'bitmessagesettings', 'maxacceptablepayloadlengthextrabytes', + str(int( + float(self.lineEditMaxAcceptableSmallMessageDifficulty.text()) + * defaults.networkDefaultPayloadLengthExtraBytes)) + ) + if acceptableDifficultyChanged: + # It might now be possible to send msgs which were previously + # marked as toodifficult. Let us change them to 'msgqueued'. + # The singleWorker will try to send them and will again mark + # them as toodifficult if the receiver's required difficulty + # is still higher than we are willing to do. + sqlExecute( + "UPDATE sent SET status='msgqueued'" + " WHERE status='toodifficult'") + queues.workerQueue.put(('sendmessage', '')) + + stopResendingDefaults = False + + # UI setting to stop trying to send messages after X days/months + # I'm open to changing this UI to something else if someone has a better idea. + if self.lineEditDays.text() == '' and self.lineEditMonths.text() == '': + # We need to handle this special case. Bitmessage has its + # default behavior. The input is blank/blank + self.config.set('bitmessagesettings', 'stopresendingafterxdays', '') + self.config.set('bitmessagesettings', 'stopresendingafterxmonths', '') + state.maximumLengthOfTimeToBotherResendingMessages = float('inf') + stopResendingDefaults = True + + try: + days = float(self.lineEditDays.text()) + except ValueError: + self.lineEditDays.setText("0") + days = 0.0 + try: + months = float(self.lineEditMonths.text()) + except ValueError: + self.lineEditMonths.setText("0") + months = 0.0 + + if days >= 0 and months >= 0 and not stopResendingDefaults: + state.maximumLengthOfTimeToBotherResendingMessages = \ + days * 24 * 60 * 60 + months * 60 * 60 * 24 * 365 / 12 + if state.maximumLengthOfTimeToBotherResendingMessages < 432000: + # If the time period is less than 5 hours, we give + # zero values to all fields. No message will be sent again. + QtGui.QMessageBox.about( + self, + _translate("MainWindow", "Will not resend ever"), + _translate( + "MainWindow", + "Note that the time limit you entered is less" + " than the amount of time Bitmessage waits for" + " the first resend attempt therefore your" + " messages will never be resent.") + ) + self.config.set( + 'bitmessagesettings', 'stopresendingafterxdays', '0') + self.config.set( + 'bitmessagesettings', 'stopresendingafterxmonths', '0') + state.maximumLengthOfTimeToBotherResendingMessages = 0.0 + else: + self.config.set( + 'bitmessagesettings', 'stopresendingafterxdays', str(days)) + self.config.set( + 'bitmessagesettings', 'stopresendingafterxmonths', + str(months)) + + self.config.save() + + if self.net_restart_needed: + self.net_restart_needed = False + self.config.setTemp('bitmessagesettings', 'dontconnect', 'true') + self.timer.singleShot( + 5000, lambda: + self.config.setTemp( + 'bitmessagesettings', 'dontconnect', 'false') + ) + + self.parent.updateStartOnLogon() + + if ( + state.appdata != paths.lookupExeFolder() + and self.checkBoxPortableMode.isChecked() + ): + # If we are NOT using portable mode now but the user selected + # that we should... + # Write the keys.dat file to disk in the new location + sqlStoredProcedure('movemessagstoprog') + with open(paths.lookupExeFolder() + 'keys.dat', 'wb') as configfile: + self.config.write(configfile) + # Write the knownnodes.dat file to disk in the new location + knownnodes.saveKnownNodes(paths.lookupExeFolder()) + os.remove(state.appdata + 'keys.dat') + os.remove(state.appdata + 'knownnodes.dat') + previousAppdataLocation = state.appdata + state.appdata = paths.lookupExeFolder() + debug.resetLogging() + try: + os.remove(previousAppdataLocation + 'debug.log') + os.remove(previousAppdataLocation + 'debug.log.1') + except Exception: + pass + + if ( + state.appdata == paths.lookupExeFolder() + and not self.checkBoxPortableMode.isChecked() + ): + # If we ARE using portable mode now but the user selected + # that we shouldn't... + state.appdata = paths.lookupAppdataFolder() + if not os.path.exists(state.appdata): + os.makedirs(state.appdata) + sqlStoredProcedure('movemessagstoappdata') + # Write the keys.dat file to disk in the new location + self.config.save() + # Write the knownnodes.dat file to disk in the new location + knownnodes.saveKnownNodes(state.appdata) + os.remove(paths.lookupExeFolder() + 'keys.dat') + os.remove(paths.lookupExeFolder() + 'knownnodes.dat') + debug.resetLogging() + try: + os.remove(paths.lookupExeFolder() + 'debug.log') + os.remove(paths.lookupExeFolder() + 'debug.log.1') + except Exception: + pass diff --git a/src/bitmessageqt/settings.ui b/src/bitmessageqt/settings.ui index 4aeba3cef7..e7ce1d7106 100644 --- a/src/bitmessageqt/settings.ui +++ b/src/bitmessageqt/settings.ui @@ -37,6 +37,18 @@ User Interface + + 8 + + + 8 + + + 8 + + + 8 + @@ -44,20 +56,43 @@ - - - - Start Bitmessage in the tray (don't show main window) + + + + Tray + + + + + Start Bitmessage in the tray (don't show main window) + + + + + + + Minimize to tray + + + false + + + + + + + Close to tray + + + + - + - Minimize to tray - - - true + Hide connection notifications @@ -112,95 +147,46 @@ + + + + Custom Style + + + + + + + 100 + 0 + + + + + + + + Font + + + + + + Interface Language - - - + + + 100 0 - - - System Settings - - - - - English - - - - - Esperanto - - - - - Français - - - - - Deutsch - - - - - Español - - - - - русский - - - - - Norsk - - - - - العربية - - - - - 简体中文 - - - - - 日本語 - - - - - Nederlands - - - - - Česky - - - - - Pirate English - - - - - Other (set in keys.dat) - - @@ -213,6 +199,18 @@ Network Settings + + 8 + + + 8 + + + 8 + + + 8 + @@ -220,26 +218,13 @@ - - - Qt::Horizontal - - - - 125 - 20 - - - - - Listen for connections on port: - + @@ -249,10 +234,30 @@ + + + + UPnP + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + - + Bandwidth limit @@ -343,7 +348,7 @@ - + Proxy server / Tor @@ -424,6 +429,13 @@ + + + + Only connect to onion services (*.onion) + + + @@ -433,12 +445,12 @@ - SOCKS4a + SOCKS4a - SOCKS5 + SOCKS5 @@ -446,7 +458,14 @@ - + + + + Announce self by UDP + + + + Qt::Vertical @@ -466,6 +485,18 @@ Demanded difficulty + + 8 + + + 8 + + + 8 + + + 8 + @@ -594,6 +625,18 @@ Max acceptable difficulty + + 8 + + + 8 + + + 8 + + + 8 + @@ -698,6 +741,33 @@ + + + + + + Hardware GPU acceleration (OpenCL): + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + @@ -705,6 +775,18 @@ Namecoin integration + + 8 + + + 8 + + + 8 + + + 8 + @@ -888,6 +970,18 @@ Resends Expire + + 8 + + + 8 + + + 8 + + + 8 + @@ -912,91 +1006,69 @@ - + 231 75 + + - - - 10 - 20 - 101 - 20 - - Give up after - + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - + + + - - - 30 - 40 - 80 - 16 - - and - + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - 113 - 20 - 51 - 20 - - + + + + + + 55 + 100 + + - - - - 113 - 40 - 51 - 20 - - + + + + + + 55 + 100 + + - - - - 169 - 23 - 61 - 16 - - + + + days - - - - 170 - 41 - 71 - 16 - - + + + months. + + @@ -1017,7 +1089,14 @@ - + + + + LanguageBox + QComboBox +
bitmessageqt.languagebox
+
+
tabWidgetSettings checkBoxStartOnLogon @@ -1101,5 +1180,59 @@ + + comboBoxProxyType + currentIndexChanged(int) + settingsDialog + comboBoxProxyTypeChanged + + + 20 + 20 + + + 20 + 20 + + + + + radioButtonNamecoinNamecoind + toggled(bool) + settingsDialog + namecoinTypeChanged + + + 20 + 20 + + + 20 + 20 + + + + + pushButtonNamecoinTest + clicked() + settingsDialog + click_pushButtonNamecoinTest + + + 20 + 20 + + + 20 + 20 + + + + + buttonFont + clicked() + settingsDialog + choose_font + diff --git a/src/bitmessageqt/settingsmixin.py b/src/bitmessageqt/settingsmixin.py index c534d1b589..3d5999e203 100644 --- a/src/bitmessageqt/settingsmixin.py +++ b/src/bitmessageqt/settingsmixin.py @@ -1,79 +1,108 @@ #!/usr/bin/python2.7 +""" +src/settingsmixin.py +==================== + +""" from PyQt4 import QtCore, QtGui + class SettingsMixin(object): + """Mixin for adding geometry and state saving between restarts.""" def warnIfNoObjectName(self): + """ + Handle objects which don't have a name. Currently it ignores them. Objects without a name can't have their + state/geometry saved as they don't have an identifier. + """ if self.objectName() == "": - # TODO: logger + # .. todo:: logger pass - + def writeState(self, source): + """Save object state (e.g. relative position of a splitter)""" self.warnIfNoObjectName() settings = QtCore.QSettings() settings.beginGroup(self.objectName()) settings.setValue("state", source.saveState()) settings.endGroup() - + def writeGeometry(self, source): + """Save object geometry (e.g. window size and position)""" self.warnIfNoObjectName() settings = QtCore.QSettings() settings.beginGroup(self.objectName()) settings.setValue("geometry", source.saveGeometry()) settings.endGroup() - + def readGeometry(self, target): + """Load object geometry""" self.warnIfNoObjectName() settings = QtCore.QSettings() try: geom = settings.value("/".join([str(self.objectName()), "geometry"])) target.restoreGeometry(geom.toByteArray() if hasattr(geom, 'toByteArray') else geom) - except Exception as e: + except Exception: pass def readState(self, target): + """Load object state""" self.warnIfNoObjectName() settings = QtCore.QSettings() try: state = settings.value("/".join([str(self.objectName()), "state"])) target.restoreState(state.toByteArray() if hasattr(state, 'toByteArray') else state) - except Exception as e: + except Exception: pass - + class SMainWindow(QtGui.QMainWindow, SettingsMixin): + """Main window with Settings functionality.""" def loadSettings(self): + """Load main window settings.""" self.readGeometry(self) self.readState(self) - + def saveSettings(self): + """Save main window settings""" self.writeState(self) self.writeGeometry(self) class STableWidget(QtGui.QTableWidget, SettingsMixin): + """Table widget with Settings functionality""" + # pylint: disable=too-many-ancestors def loadSettings(self): + """Load table settings.""" self.readState(self.horizontalHeader()) def saveSettings(self): + """Save table settings.""" self.writeState(self.horizontalHeader()) - + class SSplitter(QtGui.QSplitter, SettingsMixin): + """Splitter with Settings functionality.""" def loadSettings(self): + """Load splitter settings""" self.readState(self) def saveSettings(self): + """Save splitter settings.""" self.writeState(self) - + class STreeWidget(QtGui.QTreeWidget, SettingsMixin): + """Tree widget with settings functionality.""" + # pylint: disable=too-many-ancestors def loadSettings(self): - #recurse children - #self.readState(self) + """Load tree settings.""" + # recurse children + # self.readState(self) pass def saveSettings(self): - #recurse children - #self.writeState(self) + """Save tree settings""" + # recurse children + # self.writeState(self) pass diff --git a/src/bitmessageqt/sound.py b/src/bitmessageqt/sound.py index 9c86a9a4c4..33b4c50082 100644 --- a/src/bitmessageqt/sound.py +++ b/src/bitmessageqt/sound.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +"""Sound Module""" # sound type constants SOUND_NONE = 0 @@ -12,10 +13,12 @@ # returns true if the given sound category is a connection sound # rather than a received message sound def is_connection_sound(category): + """Check if sound type is related to connectivity""" return category in ( SOUND_CONNECTED, SOUND_DISCONNECTED, SOUND_CONNECTION_GREEN ) + extensions = ('wav', 'mp3', 'oga') diff --git a/src/bitmessageqt/statusbar.py b/src/bitmessageqt/statusbar.py index 65a5acfb1d..2add604d2c 100644 --- a/src/bitmessageqt/statusbar.py +++ b/src/bitmessageqt/statusbar.py @@ -1,8 +1,12 @@ -from PyQt4 import QtCore, QtGui -from Queue import Queue +# pylint: disable=unused-argument +"""Status bar Module""" + from time import time +from PyQt4 import QtGui + class BMStatusBar(QtGui.QStatusBar): + """Status bar with queue and priorities""" duration = 10000 deleteAfter = 60 @@ -13,6 +17,9 @@ def __init__(self, parent=None): self.iterator = 0 def timerEvent(self, event): + """an event handler which allows to queue and prioritise messages to + show in the status bar, for example if many messages come very quickly + after one another, it adds delays and so on""" while len(self.important) > 0: self.iterator += 1 try: @@ -30,9 +37,3 @@ def addImportant(self, message): self.important.append([message, time()]) self.iterator = len(self.important) - 2 self.timerEvent(None) - - def showMessage(self, message, timeout=0): - super(BMStatusBar, self).showMessage(message, timeout) - - def clearMessage(self): - super(BMStatusBar, self).clearMessage() diff --git a/src/bitmessageqt/support.py b/src/bitmessageqt/support.py index 25c6113d15..a84affa46c 100644 --- a/src/bitmessageqt/support.py +++ b/src/bitmessageqt/support.py @@ -1,32 +1,43 @@ +"""Composing support request message functions.""" +# pylint: disable=no-member + import ctypes -from PyQt4 import QtCore, QtGui import ssl import sys import time +from PyQt4 import QtCore + import account -from bmconfigparser import BMConfigParser -from debug import logger import defaults -from foldertree import AccountMixin -from helper_sql import * -from l10n import getTranslationLanguage -from openclpow import openclAvailable, openclEnabled +import network.stats import paths import proofofwork -from pyelliptic.openssl import OpenSSL import queues -import network.stats import state +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 version import softwareVersion +from tr import _translate + # this is BM support address going to Peter Surda OLD_SUPPORT_ADDRESS = 'BM-2cTkCtMYkrSPwFTpgcBrMrf5d8oZwvMZWK' SUPPORT_ADDRESS = 'BM-2cUdgkDDAahwPAU6oD2A7DnjqZz3hgY832' -SUPPORT_LABEL = 'PyBitmessage support' -SUPPORT_MY_LABEL = 'My new address' +SUPPORT_LABEL = _translate("Support", "PyBitmessage support") +SUPPORT_MY_LABEL = _translate("Support", "My new address") SUPPORT_SUBJECT = 'Support request' -SUPPORT_MESSAGE = '''You can use this message to send a report to one of the PyBitmessage core developers regarding PyBitmessage or the mailchuck.com email service. If you are using PyBitmessage involuntarily, for example because your computer was infected with ransomware, this is not an appropriate venue for resolving such issues. +SUPPORT_MESSAGE = _translate("Support", ''' +You can use this message to send a report to one of the PyBitmessage core \ +developers regarding PyBitmessage or the mailchuck.com email service. \ +If you are using PyBitmessage involuntarily, for example because \ +your computer was infected with ransomware, this is not an appropriate venue \ +for resolving such issues. Please describe what you are trying to do: @@ -36,7 +47,8 @@ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Please write above this line and if possible, keep the information about your environment below intact. +Please write above this line and if possible, keep the information about your \ +environment below intact. PyBitmessage version: {} Operating system: {} @@ -51,39 +63,53 @@ SOCKS: {} UPnP: {} Connected hosts: {} -''' +''') + def checkAddressBook(myapp): - sqlExecute('''DELETE from addressbook WHERE address=?''', OLD_SUPPORT_ADDRESS) - queryreturn = sqlQuery('''SELECT * FROM addressbook WHERE address=?''', SUPPORT_ADDRESS) + sqlExecute('DELETE from addressbook WHERE address=?', OLD_SUPPORT_ADDRESS) + queryreturn = sqlQuery('SELECT * FROM addressbook WHERE address=?', SUPPORT_ADDRESS) if queryreturn == []: - sqlExecute('''INSERT INTO addressbook VALUES (?,?)''', str(QtGui.QApplication.translate("Support", SUPPORT_LABEL)), SUPPORT_ADDRESS) + sqlExecute( + 'INSERT INTO addressbook VALUES (?,?)', + SUPPORT_LABEL.toUtf8(), SUPPORT_ADDRESS) myapp.rerenderAddressBook() + def checkHasNormalAddress(): - for address in account.getSortedAccounts(): + for address in config.addresses(): acct = account.accountClass(address) - if acct.type == AccountMixin.NORMAL and BMConfigParser().safeGetBoolean(address, 'enabled'): + if acct.type == AccountMixin.NORMAL and config.safeGetBoolean(address, 'enabled'): return address return False + def createAddressIfNeeded(myapp): if not checkHasNormalAddress(): - queues.addressGeneratorQueue.put(('createRandomAddress', 4, 1, str(QtGui.QApplication.translate("Support", SUPPORT_MY_LABEL)), 1, "", False, defaults.networkDefaultProofOfWorkNonceTrialsPerByte, defaults.networkDefaultPayloadLengthExtraBytes)) + queues.addressGeneratorQueue.put(( + 'createRandomAddress', 4, 1, + str(SUPPORT_MY_LABEL.toUtf8()), + 1, "", False, + defaults.networkDefaultProofOfWorkNonceTrialsPerByte, + defaults.networkDefaultPayloadLengthExtraBytes + )) while state.shutdown == 0 and not checkHasNormalAddress(): time.sleep(.2) myapp.rerenderComboBoxSendFrom() return checkHasNormalAddress() + def createSupportMessage(myapp): checkAddressBook(myapp) address = createAddressIfNeeded(myapp) if state.shutdown: return - myapp.ui.lineEditSubject.setText(str(QtGui.QApplication.translate("Support", SUPPORT_SUBJECT))) - addrIndex = myapp.ui.comboBoxSendFrom.findData(address, QtCore.Qt.UserRole, QtCore.Qt.MatchFixedString | QtCore.Qt.MatchCaseSensitive) - if addrIndex == -1: # something is very wrong + myapp.ui.lineEditSubject.setText(SUPPORT_SUBJECT) + addrIndex = myapp.ui.comboBoxSendFrom.findData( + address, QtCore.Qt.UserRole, + QtCore.Qt.MatchFixedString | QtCore.Qt.MatchCaseSensitive) + if addrIndex == -1: # something is very wrong return myapp.ui.comboBoxSendFrom.setCurrentIndex(addrIndex) myapp.ui.lineEditTo.setText(SUPPORT_ADDRESS) @@ -106,29 +132,26 @@ def createSupportMessage(myapp): pass architecture = "32" if ctypes.sizeof(ctypes.c_voidp) == 4 else "64" pythonversion = sys.version - - opensslversion = "%s (Python internal), %s (external for PyElliptic)" % (ssl.OPENSSL_VERSION, OpenSSL._version) + + opensslversion = "%s (Python internal), %s (external for PyElliptic)" % ( + ssl.OPENSSL_VERSION, OpenSSL._version) frozen = "N/A" if paths.frozen: frozen = paths.frozen portablemode = "True" if state.appdata == paths.lookupExeFolder() else "False" cpow = "True" if proofofwork.bmpow else "False" - #cpow = QtGui.QApplication.translate("Support", cpow) - openclpow = str(BMConfigParser().safeGet('bitmessagesettings', 'opencl')) if openclEnabled() else "None" - #openclpow = QtGui.QApplication.translate("Support", openclpow) + openclpow = str( + config.safeGet('bitmessagesettings', 'opencl') + ) if openclEnabled() else "None" locale = getTranslationLanguage() - try: - socks = BMConfigParser().get('bitmessagesettings', 'socksproxytype') - except: - socks = "N/A" - try: - upnp = BMConfigParser().get('bitmessagesettings', 'upnp') - except: - 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(str(QtGui.QApplication.translate("Support", SUPPORT_MESSAGE)).format(version, os, architecture, pythonversion, opensslversion, frozen, portablemode, cpow, openclpow, locale, socks, upnp, connectedhosts)) + myapp.ui.textEditMessage.setText(unicode(SUPPORT_MESSAGE, 'utf-8').format( + version, os, architecture, pythonversion, opensslversion, frozen, + portablemode, cpow, openclpow, locale, socks, upnp, connectedhosts)) # single msg tab myapp.ui.tabWidgetSend.setCurrentIndex( diff --git a/src/bitmessageqt/tests/__init__.py b/src/bitmessageqt/tests/__init__.py new file mode 100644 index 0000000000..a81ddb0475 --- /dev/null +++ b/src/bitmessageqt/tests/__init__.py @@ -0,0 +1,11 @@ +"""bitmessageqt tests""" + +from .addressbook import TestAddressbook +from .main import TestMain, TestUISignaler +from .settings import TestSettings +from .support import TestSupport + +__all__ = [ + "TestAddressbook", "TestMain", "TestSettings", "TestSupport", + "TestUISignaler" +] diff --git a/src/bitmessageqt/tests/addressbook.py b/src/bitmessageqt/tests/addressbook.py new file mode 100644 index 0000000000..cd86c5d665 --- /dev/null +++ b/src/bitmessageqt/tests/addressbook.py @@ -0,0 +1,17 @@ +import helper_addressbook +from bitmessageqt.support import createAddressIfNeeded + +from main import TestBase + + +class TestAddressbook(TestBase): + """Test case for addressbook""" + + def test_add_own_address_to_addressbook(self): + """Checking own address adding in addressbook""" + try: + address = createAddressIfNeeded(self.window) + self.assertFalse( + helper_addressbook.insert(label='test', address=address)) + except IndexError: + self.fail("Can't generate addresses") diff --git a/src/bitmessageqt/tests/main.py b/src/bitmessageqt/tests/main.py new file mode 100644 index 0000000000..d3fda8aa71 --- /dev/null +++ b/src/bitmessageqt/tests/main.py @@ -0,0 +1,71 @@ +"""Common definitions for bitmessageqt tests""" + +import sys +import unittest + +from PyQt4 import QtCore, QtGui +from six.moves import queue + +import bitmessageqt +from bitmessageqt import _translate, config, queues + + +class TestBase(unittest.TestCase): + """Base class for bitmessageqt test case""" + + @classmethod + def setUpClass(cls): + """Provide the UI test cases with common settings""" + cls.config = config + + def setUp(self): + self.app = ( + QtGui.QApplication.instance() + or bitmessageqt.BitmessageQtApplication(sys.argv)) + self.window = self.app.activeWindow() + if not self.window: + self.window = bitmessageqt.MyForm() + self.window.appIndicatorInit(self.app) + + def tearDown(self): + """Search for exceptions in closures called by timer and fail if any""" + # self.app.deleteLater() + concerning = [] + while True: + try: + thread, exc = queues.excQueue.get(block=False) + except queue.Empty: + break + if thread == 'tests': + concerning.append(exc) + if concerning: + self.fail( + 'Exceptions found in the main thread:\n%s' % '\n'.join(( + str(e) for e in concerning + ))) + + +class TestMain(unittest.TestCase): + """Test case for main window - basic features""" + + def test_translate(self): + """Check the results of _translate() with various args""" + self.assertIsInstance( + _translate("MainWindow", "Test"), + QtCore.QString + ) + + +class TestUISignaler(TestBase): + """Test case for UISignalQueue""" + + def test_updateStatusBar(self): + """Check arguments order of updateStatusBar command""" + queues.UISignalQueue.put(( + 'updateStatusBar', ( + _translate("test", "Testing updateStatusBar..."), 1) + )) + + QtCore.QTimer.singleShot(60, self.app.quit) + self.app.exec_() + # self.app.processEvents(QtCore.QEventLoop.AllEvents, 60) diff --git a/src/bitmessageqt/tests/settings.py b/src/bitmessageqt/tests/settings.py new file mode 100644 index 0000000000..bad28ed736 --- /dev/null +++ b/src/bitmessageqt/tests/settings.py @@ -0,0 +1,78 @@ +"""Tests for PyBitmessage settings""" +import threading +import time + +from PyQt4 import QtCore, QtGui, QtTest + +from bmconfigparser import config +from bitmessageqt import settings + +from .main import TestBase + + +class TestSettings(TestBase): + """A test case for the "Settings" dialog""" + def setUp(self): + super(TestSettings, self).setUp() + self.dialog = settings.SettingsDialog(self.window) + + def test_udp(self): + """Test the effect of checkBoxUDP""" + 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, + config.safeGetBoolean('bitmessagesettings', 'udp')) + time.sleep(5) + for thread in threading.enumerate(): + if thread.name == 'Announcer': # find Announcer thread + if udp_setting: + self.fail( + 'Announcer thread is running while udp set to False') + break + else: + if not udp_setting: + self.fail('No Announcer thread found while udp set to True') + + def test_styling(self): + """Test custom windows style and font""" + style_setting = config.safeGet('bitmessagesettings', 'windowstyle') + font_setting = config.safeGet('bitmessagesettings', 'font') + self.assertIs(style_setting, None) + self.assertIs(font_setting, None) + style_control = self.dialog.comboBoxStyle + self.assertEqual( + style_control.currentText(), self.app.get_windowstyle()) + + def call_font_dialog(): + """A function to get the open font dialog and accept it""" + font_dialog = QtGui.QApplication.activeModalWidget() + self.assertTrue(isinstance(font_dialog, QtGui.QFontDialog)) + selected_font = font_dialog.currentFont() + self.assertEqual( + config.safeGet('bitmessagesettings', 'font'), '{},{}'.format( + selected_font.family(), selected_font.pointSize())) + + font_dialog.accept() + self.dialog.accept() + self.assertEqual( + config.safeGet('bitmessagesettings', 'windowstyle'), + style_control.currentText()) + + def click_font_button(): + """Use QtTest to click the button""" + QtTest.QTest.mouseClick( + self.dialog.buttonFont, QtCore.Qt.LeftButton) + + style_count = style_control.count() + self.assertGreater(style_count, 1) + for i in range(style_count): + if i != style_control.currentIndex(): + style_control.setCurrentIndex(i) + break + + QtCore.QTimer.singleShot(30, click_font_button) + QtCore.QTimer.singleShot(60, call_font_dialog) + time.sleep(2) diff --git a/src/bitmessageqt/tests/support.py b/src/bitmessageqt/tests/support.py new file mode 100644 index 0000000000..ba28b73a1b --- /dev/null +++ b/src/bitmessageqt/tests/support.py @@ -0,0 +1,33 @@ +# from PyQt4 import QtTest + +import sys + +from shared import isAddressInMyAddressBook + +from main import TestBase + + +class TestSupport(TestBase): + """A test case for support module""" + SUPPORT_ADDRESS = 'BM-2cUdgkDDAahwPAU6oD2A7DnjqZz3hgY832' + SUPPORT_SUBJECT = 'Support request' + + def test(self): + """trigger menu action "Contact Support" and check the result""" + ui = self.window.ui + self.assertEqual(ui.lineEditTo.text(), '') + self.assertEqual(ui.lineEditSubject.text(), '') + + ui.actionSupport.trigger() + + self.assertTrue( + isAddressInMyAddressBook(self.SUPPORT_ADDRESS)) + + self.assertEqual( + ui.tabWidget.currentIndex(), ui.tabWidget.indexOf(ui.send)) + self.assertEqual( + ui.lineEditTo.text(), self.SUPPORT_ADDRESS) + self.assertEqual( + ui.lineEditSubject.text(), self.SUPPORT_SUBJECT) + self.assertIn( + sys.version, ui.textEditMessage.toPlainText()) diff --git a/src/bitmessageqt/uisignaler.py b/src/bitmessageqt/uisignaler.py index 055f9097e0..c23ec3bc42 100644 --- a/src/bitmessageqt/uisignaler.py +++ b/src/bitmessageqt/uisignaler.py @@ -22,8 +22,11 @@ def run(self): command, data = queues.UISignalQueue.get() if command == 'writeNewAddressToTable': label, address, streamNumber = data - self.emit(SIGNAL( - "writeNewAddressToTable(PyQt_PyObject,PyQt_PyObject,PyQt_PyObject)"), label, address, str(streamNumber)) + self.emit( + SIGNAL("writeNewAddressToTable(PyQt_PyObject,PyQt_PyObject,PyQt_PyObject)"), + label, + address, + str(streamNumber)) elif command == 'updateStatusBar': self.emit(SIGNAL("updateStatusBar(PyQt_PyObject)"), data) elif command == 'updateSentItemStatusByToAddress': @@ -46,7 +49,11 @@ def run(self): toAddress, fromLabel, fromAddress, subject, message, ackdata) elif command == 'updateNetworkStatusTab': outbound, add, destination = data - self.emit(SIGNAL("updateNetworkStatusTab(PyQt_PyObject,PyQt_PyObject,PyQt_PyObject)"), outbound, add, destination) + self.emit( + SIGNAL("updateNetworkStatusTab(PyQt_PyObject,PyQt_PyObject,PyQt_PyObject)"), + outbound, + add, + destination) elif command == 'updateNumberOfMessagesProcessed': self.emit(SIGNAL("updateNumberOfMessagesProcessed()")) elif command == 'updateNumberOfPubkeysProcessed': @@ -73,7 +80,11 @@ def run(self): self.emit(SIGNAL("newVersionAvailable(PyQt_PyObject)"), data) elif command == 'alert': title, text, exitAfterUserClicksOk = data - self.emit(SIGNAL("displayAlert(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)"), title, text, exitAfterUserClicksOk) + self.emit( + SIGNAL("displayAlert(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)"), + title, + text, + exitAfterUserClicksOk) else: sys.stderr.write( 'Command sent to UISignaler not recognized: %s\n' % command) diff --git a/src/bitmessageqt/utils.py b/src/bitmessageqt/utils.py index 369d05ef6b..9f849b3bb0 100644 --- a/src/bitmessageqt/utils.py +++ b/src/bitmessageqt/utils.py @@ -1,61 +1,70 @@ -from PyQt4 import QtGui import hashlib import os -from addresses import addBMIfNotPresent -from bmconfigparser import BMConfigParser + +from PyQt4 import QtGui + import state +from addresses import addBMIfNotPresent +from bmconfigparser import config str_broadcast_subscribers = '[Broadcast subscribers]' str_chan = '[chan]' + def identiconize(address): size = 48 - - # If you include another identicon library, please generate an + + 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 - - try: - identicon_lib = BMConfigParser().get('bitmessagesettings', 'identiconlib') - except: - # default to qidenticon_two_x - identicon_lib = 'qidenticon_two_x' - # As an 'identiconsuffix' you could put "@bitmessge.ch" or "@bm.addr" to make it compatible with other identicon generators. (Note however, that E-Mail programs might convert the BM-address to lowercase first.) - # 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. - identiconsuffix = BMConfigParser().get('bitmessagesettings', 'identiconsuffix') - - if not BMConfigParser().getboolean('bitmessagesettings', 'useidenticons'): - idcon = QtGui.QIcon() - return idcon - - if (identicon_lib[:len('qidenticon')] == 'qidenticon'): - # print identicon_lib + identicon_lib = config.safeGet( + 'bitmessagesettings', 'identiconlib', 'qidenticon_two_x') + + # As an 'identiconsuffix' you could put "@bitmessge.ch" or "@bm.addr" + # to make it compatible with other identicon generators. (Note however, + # that E-Mail programs might convert the BM-address to lowercase first.) + # 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. + identiconsuffix = config.get('bitmessagesettings', 'identiconsuffix') + if identicon_lib[:len('qidenticon')] == 'qidenticon': # originally by: # :Author:Shin Adachi # Licesensed under FreeBSD License. # stripped from PIL and uses QT instead (by sendiulo, same license) import qidenticon - hash = hashlib.md5(addBMIfNotPresent(address)+identiconsuffix).hexdigest() - use_two_colors = (identicon_lib[:len('qidenticon_two')] == 'qidenticon_two') - opacity = int(not((identicon_lib == 'qidenticon_x') | (identicon_lib == 'qidenticon_two_x') | (identicon_lib == 'qidenticon_b') | (identicon_lib == 'qidenticon_two_b')))*255 + icon_hash = hashlib.md5( + addBMIfNotPresent(address) + identiconsuffix).hexdigest() + use_two_colors = identicon_lib[:len('qidenticon_two')] == 'qidenticon_two' + opacity = int( + identicon_lib not in ( + 'qidenticon_x', 'qidenticon_two_x', + 'qidenticon_b', 'qidenticon_two_b' + )) * 255 penwidth = 0 - image = qidenticon.render_identicon(int(hash, 16), size, use_two_colors, opacity, penwidth) + image = qidenticon.render_identicon( + int(icon_hash, 16), size, use_two_colors, opacity, penwidth) # filename = './images/identicons/'+hash+'.png' # image.save(filename) idcon = QtGui.QIcon() idcon.addPixmap(image, QtGui.QIcon.Normal, QtGui.QIcon.Off) return idcon elif identicon_lib == 'pydenticon': - # print identicon_lib - # Here you could load pydenticon.py (just put it in the "src" folder of your Bitmessage source) + # Here you could load pydenticon.py + # (just put it in the "src" folder of your Bitmessage source) from pydenticon import Pydenticon # It is not included in the source, because it is licensed under GPLv3 # GPLv3 is a copyleft license that would influence our licensing - # Find the source here: http://boottunes.googlecode.com/svn-history/r302/trunk/src/pydenticon.py - # note that it requires PIL to be installed: http://www.pythonware.com/products/pil/ - idcon_render = Pydenticon(addBMIfNotPresent(address)+identiconsuffix, size*3) + # Find the source here: + # https://github.com/azaghal/pydenticon + # note that it requires pillow (or PIL) to be installed: + # https://python-pillow.org/ + idcon_render = Pydenticon( + addBMIfNotPresent(address) + identiconsuffix, size * 3) rendering = idcon_render._render() data = rendering.convert("RGBA").tostring("raw", "RGBA") qim = QtGui.QImage(data, size, size, QtGui.QImage.Format_ARGB32) @@ -64,32 +73,31 @@ def identiconize(address): idcon.addPixmap(pix, QtGui.QIcon.Normal, QtGui.QIcon.Off) return idcon + def avatarize(address): """ - loads a supported image for the given address' hash form 'avatars' folder - falls back to default avatar if 'default.*' file exists - falls back to identiconize(address) + Loads a supported image for the given address' hash form 'avatars' folder + falls back to default avatar if 'default.*' file exists + falls back to identiconize(address) """ idcon = QtGui.QIcon() - hash = hashlib.md5(addBMIfNotPresent(address)).hexdigest() - str_broadcast_subscribers = '[Broadcast subscribers]' + icon_hash = hashlib.md5(addBMIfNotPresent(address)).hexdigest() if address == str_broadcast_subscribers: # don't hash [Broadcast subscribers] - hash = address - # http://pyqt.sourceforge.net/Docs/PyQt4/qimagereader.html#supportedImageFormats - # print QImageReader.supportedImageFormats () + icon_hash = address + # https://www.riverbankcomputing.com/static/Docs/PyQt4/qimagereader.html#supportedImageFormats # QImageReader.supportedImageFormats () - extensions = ['PNG', 'GIF', 'JPG', 'JPEG', 'SVG', 'BMP', 'MNG', 'PBM', 'PGM', 'PPM', 'TIFF', 'XBM', 'XPM', 'TGA'] + extensions = [ + 'PNG', 'GIF', 'JPG', 'JPEG', 'SVG', 'BMP', 'MNG', 'PBM', 'PGM', 'PPM', + 'TIFF', 'XBM', 'XPM', 'TGA'] # try to find a specific avatar for ext in extensions: - lower_hash = state.appdata + 'avatars/' + hash + '.' + ext.lower() - upper_hash = state.appdata + 'avatars/' + hash + '.' + ext.upper() + lower_hash = state.appdata + 'avatars/' + icon_hash + '.' + ext.lower() + upper_hash = state.appdata + 'avatars/' + icon_hash + '.' + ext.upper() if os.path.isfile(lower_hash): - # print 'found avatar of ', address idcon.addFile(lower_hash) return idcon elif os.path.isfile(upper_hash): - # print 'found avatar of ', address idcon.addFile(upper_hash) return idcon # if we haven't found any, try to find a default avatar diff --git a/src/bitmsghash/Makefile b/src/bitmsghash/Makefile index c4fb4ab528..7a494c39b1 100644 --- a/src/bitmsghash/Makefile +++ b/src/bitmsghash/Makefile @@ -1,14 +1,14 @@ UNAME_S := $(shell uname -s) ifeq ($(UNAME_S),Darwin) - CCFLAGS += -I/usr/local/Cellar/openssl/1.0.2d_1/include - LDFLAGS += -L/usr/local/Cellar/openssl/1.0.2d_1/lib + CCFLAGS += -I/usr/local/opt/openssl/include + LDFLAGS += -L/usr/local/opt/openssl/lib else ifeq ($(UNAME_S),MINGW32_NT-6.1) CCFLAGS += -IC:\OpenSSL-1.0.2j-mingw\include -D_WIN32 -march=native LDFLAGS += -static-libgcc -LC:\OpenSSL-1.0.2j-mingw\lib -lwsock32 -o bitmsghash32.dll -Wl,--out-implib,bitmsghash.a else LDFLAGS += -lpthread -o bitmsghash.so endif - + all: bitmsghash.so powtest: @@ -22,4 +22,3 @@ bitmsghash.o: clean: rm -f bitmsghash.o bitmsghash.so bitmsghash*.dll - diff --git a/src/bitmsghash/bitmsghash.cpp b/src/bitmsghash/bitmsghash.cpp index 2d0d4b50ab..24775475de 100644 --- a/src/bitmsghash/bitmsghash.cpp +++ b/src/bitmsghash/bitmsghash.cpp @@ -1,7 +1,7 @@ // bitmessage cracker, build with g++ or MSVS to a shared library, use included python code for usage under bitmessage #ifdef _WIN32 -#include "Winsock.h" -#include "Windows.h" +#include "winsock.h" +#include "windows.h" #define uint64_t unsigned __int64 #else #include diff --git a/src/bmconfigparser.py b/src/bmconfigparser.py index 2e60b5a738..f0e24c25ca 100644 --- a/src/bmconfigparser.py +++ b/src/bmconfigparser.py @@ -1,127 +1,154 @@ -import ConfigParser -import datetime -import shutil +""" +BMConfigParser class definition and default configuration settings +""" + import os +import shutil +from datetime import datetime +from threading import Event + +from six import string_types +from six.moves import configparser + +try: + import state +except ImportError: + from pybitmessage import state + +SafeConfigParser = configparser.SafeConfigParser +config_ready = Event() + + +class BMConfigParser(SafeConfigParser): + """ + Singleton class inherited from :class:`configparser.SafeConfigParser` + with additional methods specific to bitmessage config. + """ + # pylint: disable=too-many-ancestors + _temp = {} -from singleton import Singleton -import state - -BMConfigDefaults = { - "bitmessagesettings": { - "maxaddrperstreamsend": 500, - "maxbootstrapconnections": 20, - "maxdownloadrate": 0, - "maxoutboundconnections": 8, - "maxtotalconnections": 200, - "maxuploadrate": 0, - "apiinterface": "127.0.0.1", - "apiport": 8442 - }, - "threads": { - "receive": 3, - }, - "network": { - "bind": '', - "dandelion": 90, - }, - "inventory": { - "storage": "sqlite", - "acceptmismatch": False, - }, - "knownnodes": { - "maxnodes": 20000, - }, - "zlib": { - 'maxsize': 1048576 - } -} - -@Singleton -class BMConfigParser(ConfigParser.SafeConfigParser): def set(self, section, option, value=None): if self._optcre is self.OPTCRE or value: - if not isinstance(value, basestring): + if not isinstance(value, string_types): raise TypeError("option values must be strings") if not self.validate(section, option, value): - raise ValueError("Invalid value %s" % str(value)) - return ConfigParser.ConfigParser.set(self, section, option, value) + raise ValueError("Invalid value %s" % value) + return SafeConfigParser.set(self, section, option, value) + + 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 get(self, section, option, raw=False, variables=None): + def setTemp(self, section, option, value=None): + """Temporary set option to value, not saving.""" try: - if section == "bitmessagesettings" and option == "timeformat": - return ConfigParser.ConfigParser.get(self, section, option, raw, variables) - return ConfigParser.ConfigParser.get(self, section, option, True, variables) - except ConfigParser.InterpolationError: - return ConfigParser.ConfigParser.get(self, section, option, True, variables) - except (ConfigParser.NoSectionError, ConfigParser.NoOptionError) as e: - try: - return BMConfigDefaults[section][option] - except (KeyError, ValueError, AttributeError): - raise e - - def safeGetBoolean(self, section, field): + self._temp[section][option] = value + except KeyError: + self._temp[section] = {option: value} + + def safeGetBoolean(self, section, option): + """Return value as boolean, False on exceptions""" try: - return self.getboolean(section, field) - except (ConfigParser.NoSectionError, ConfigParser.NoOptionError, ValueError, AttributeError): + 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: + return int(self.get(section, option)) + except (configparser.NoSectionError, configparser.NoOptionError, + ValueError, AttributeError): + return default + + def safeGetFloat(self, section, option, default=0.0): + """Return value as float, default on exceptions, + 0.0 if default missing""" try: - return self.getint(section, field) - except (ConfigParser.NoSectionError, ConfigParser.NoOptionError, ValueError, AttributeError): + return self.getfloat(section, option) + except (configparser.NoSectionError, configparser.NoOptionError, + ValueError, AttributeError): return default - def safeGet(self, section, option, default = None): + def safeGet(self, section, option, default=None): + """ + Return value as is, default on exceptions, None if default missing + """ try: return self.get(section, option) - except (ConfigParser.NoSectionError, ConfigParser.NoOptionError, ValueError, AttributeError): + except (configparser.NoSectionError, configparser.NoOptionError, + ValueError, AttributeError): return default def items(self, section, raw=False, variables=None): - return ConfigParser.ConfigParser.items(self, section, True, variables) - - def addresses(self): - return filter(lambda x: x.startswith('BM-'), BMConfigParser().sections()) - - def read(self, filenames): - ConfigParser.ConfigParser.read(self, filenames) - for section in self.sections(): - for option in self.options(section): - try: - if not self.validate(section, option, ConfigParser.ConfigParser.get(self, section, option)): - try: - newVal = BMConfigDefaults[section][option] - except KeyError: - continue - ConfigParser.ConfigParser.set(self, section, option, newVal) - except ConfigParser.InterpolationError: - continue + # pylint: disable=signature-differs + """Return section variables as parent, + but override the "raw" argument to always True""" + 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. + """ + self._temp = {} + sections = self.sections() + for x in sections: + self.remove_section(x) + + 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) + + 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').lower()) + return sections def save(self): + """Save the runtime config onto the filesystem""" fileName = os.path.join(state.appdata, 'keys.dat') - fileNameBak = fileName + "." + datetime.datetime.now().strftime("%Y%j%H%M%S%f") + '.bak' - # create a backup copy to prevent the accidental loss due to the disk write failure + fileNameBak = '.'.join([ + fileName, datetime.now().strftime("%Y%j%H%M%S%f"), 'bak']) + # create a backup copy to prevent the accidental loss due to + # the disk write failure try: shutil.copyfile(fileName, fileNameBak) # The backup succeeded. fileNameExisted = True - except (IOError, Exception): - # The backup failed. This can happen if the file didn't exist before. + except(IOError, Exception): + # The backup failed. This can happen if the file + # didn't exist before. fileNameExisted = False - # write the file - with open(fileName, 'wb') as configfile: + + with open(fileName, 'w') as configfile: self.write(configfile) # delete the backup if fileNameExisted: os.remove(fileNameBak) def validate(self, section, option, value): + """Input validator interface (using factory pattern)""" try: - return getattr(self, "validate_%s_%s" % (section, option))(value) + return getattr(self, 'validate_%s_%s' % (section, option))(value) except AttributeError: return True - def validate_bitmessagesettings_maxoutboundconnections(self, value): + @staticmethod + def validate_bitmessagesettings_maxoutboundconnections(value): + """Reject maxoutboundconnections that are too high or too low""" try: value = int(value) except ValueError: @@ -129,3 +156,24 @@ def validate_bitmessagesettings_maxoutboundconnections(self, value): 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 1d8f470eb8..d83e9b9bc9 100644 --- a/src/build_osx.py +++ b/src/build_osx.py @@ -1,5 +1,6 @@ -from glob import glob +"""Building osx.""" import os +from glob import glob from PyQt4 import QtCore from setuptools import setup @@ -8,24 +9,31 @@ 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')), - ('translations', glob(str(QtCore.QLibraryInfo.location(QtCore.QLibraryInfo.TranslationsPath)) + '/qt_??.qm')), - ('translations', glob(str(QtCore.QLibraryInfo.location(QtCore.QLibraryInfo.TranslationsPath)) + '/qt_??_??.qm')), + ( + 'translations', + glob(os.path.join(str(QtCore.QLibraryInfo.location( + QtCore.QLibraryInfo.TranslationsPath)), 'qt_??.qm'))), + ( + 'translations', + glob(os.path.join(str(QtCore.QLibraryInfo.location( + QtCore.QLibraryInfo.TranslationsPath)), 'qt_??_??.qm'))), ] setup( - name = name, - version = version, - app = mainscript, - data_files = DATA_FILES, - setup_requires = ["py2app"], - options = dict( - py2app = dict( - includes = ['sip', 'PyQt4._qt'], - iconfile = "images/bitmessage.icns" + name=name, + version=version, + app=mainscript, + data_files=DATA_FILES, + setup_requires=["py2app"], + options=dict( + py2app=dict( + includes=['sip', 'PyQt4._qt'], + iconfile="images/bitmessage.icns" ) ) ) diff --git a/src/class_addressGenerator.py b/src/class_addressGenerator.py index 2b99e39dec..79b76e7300 100644 --- a/src/class_addressGenerator.py +++ b/src/class_addressGenerator.py @@ -1,38 +1,98 @@ +""" +A thread for creating addresses +""" import time -import threading -import hashlib from binascii import hexlify -from pyelliptic import arithmetic -from pyelliptic.openssl import OpenSSL -import tr -import queues -import state -import shared +from six.moves import configparser, queue +# pylint: disable=import-error import defaults import highlevelcrypto -from bmconfigparser import BMConfigParser -from debug import logger +import queues +import shared +import state from addresses import decodeAddress, encodeAddress, encodeVarint -from helper_threading import StoppableThread +from bmconfigparser import config +from network import StoppableThread +from tr import _translate -class addressGenerator(threading.Thread, StoppableThread): +class addressGenerator(StoppableThread): + """A thread for creating addresses""" - def __init__(self): - # QThread.__init__(self, parent) - threading.Thread.__init__(self, name="addressGenerator") - self.initStop() + 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: - pass + except queue.Full: + self.logger.error('addressGeneratorQueue is Full') + super(addressGenerator, self).stopThread() + def save_address( + # pylint: disable=too-many-arguments,too-many-positional-arguments + self, version, stream, ripe, label, signing_key, encryption_key, + nonceTrialsPerByte, payloadLengthExtraBytes + ): + """Write essential address config values and reload cryptors""" + address = encodeAddress(version, stream, ripe) + try: + config.add_section(address) + except configparser.DuplicateSectionError: + self.logger.info( + '%s already exists. Not adding it again.', address) + queues.UISignalQueue.put(( + 'updateStatusBar', + _translate( + "MainWindow", "%1 is already in 'Your Identities'." + " Not adding it again.").arg(address) + )) + return False + + self.logger.debug('label: %s', label) + signingKeyWIF = highlevelcrypto.encodeWalletImportFormat(signing_key) + encryptionKeyWIF = highlevelcrypto.encodeWalletImportFormat( + encryption_key) + config.set(address, 'label', label) + config.set(address, 'enabled', 'true') + config.set(address, 'decoy', 'false') + config.set(address, 'privsigningkey', signingKeyWIF.decode()) + config.set(address, 'privencryptionkey', encryptionKeyWIF.decode()) + config.set(address, 'noncetrialsperbyte', str(nonceTrialsPerByte)) + config.set( + address, 'payloadlengthextrabytes', str(payloadLengthExtraBytes)) + config.save() + + queues.UISignalQueue.put(( + 'writeNewAddressToTable', (label, address, stream))) + + shared.myECCryptorObjects[ripe] = highlevelcrypto.makeCryptor( + hexlify(encryption_key)) + shared.myAddressesByHash[ripe] = address + tag = highlevelcrypto.double_sha512( + encodeVarint(version) + encodeVarint(stream) + ripe)[32:] + shared.myAddressesByTag[tag] = address + + if version == 3: + # If this is a chan address, the worker thread won't send out + # the pubkey over the network. + queues.workerQueue.put(('sendOutOrStoreMyV3Pubkey', ripe)) + elif version == 4: + queues.workerQueue.put(('sendOutOrStoreMyV4Pubkey', address)) + + return address + def run(self): + """ + Process the requests for addresses generation + from `.queues.addressGeneratorQueue` + """ + # pylint: disable=too-many-locals,too-many-branches,too-many-statements + # pylint: disable=too-many-nested-blocks + while state.shutdown == 0: queueValue = queues.addressGeneratorQueue.get() nonceTrialsPerByte = 0 @@ -56,66 +116,56 @@ def run(self): command, addressVersionNumber, streamNumber, label, \ numberOfAddressesToMake, deterministicPassphrase, \ eighteenByteRipe = queueValue - try: - numberOfNullBytesDemandedOnFrontOfRipeHash = \ - BMConfigParser().getint( - 'bitmessagesettings', - 'numberofnullbytesonaddress' - ) - except: - if eighteenByteRipe: - numberOfNullBytesDemandedOnFrontOfRipeHash = 2 - else: - # the default - numberOfNullBytesDemandedOnFrontOfRipeHash = 1 + + numberOfNullBytesDemandedOnFrontOfRipeHash = \ + config.safeGetInt( + 'bitmessagesettings', + 'numberofnullbytesonaddress', + 2 if eighteenByteRipe else 1 + ) elif len(queueValue) == 9: command, addressVersionNumber, streamNumber, label, \ numberOfAddressesToMake, deterministicPassphrase, \ eighteenByteRipe, nonceTrialsPerByte, \ payloadLengthExtraBytes = queueValue - try: - numberOfNullBytesDemandedOnFrontOfRipeHash = \ - BMConfigParser().getint( - 'bitmessagesettings', - 'numberofnullbytesonaddress' - ) - except: - if eighteenByteRipe: - numberOfNullBytesDemandedOnFrontOfRipeHash = 2 - else: - # the default - numberOfNullBytesDemandedOnFrontOfRipeHash = 1 + + numberOfNullBytesDemandedOnFrontOfRipeHash = \ + config.safeGetInt( + 'bitmessagesettings', + 'numberofnullbytesonaddress', + 2 if eighteenByteRipe else 1 + ) elif queueValue[0] == 'stopThread': break else: - logger.error( + self.logger.error( 'Programming error: A structure with the wrong number' ' of values was passed into the addressGeneratorQueue.' ' Here is the queueValue: %r\n', queueValue) + continue if addressVersionNumber < 3 or addressVersionNumber > 4: - logger.error( + self.logger.error( 'Program error: For some reason the address generator' ' queue has been given a request to create at least' ' one version %s address which it cannot do.\n', addressVersionNumber) + continue if nonceTrialsPerByte == 0: - nonceTrialsPerByte = BMConfigParser().getint( + nonceTrialsPerByte = config.getint( 'bitmessagesettings', 'defaultnoncetrialsperbyte') - if nonceTrialsPerByte < \ - defaults.networkDefaultProofOfWorkNonceTrialsPerByte: - nonceTrialsPerByte = \ - defaults.networkDefaultProofOfWorkNonceTrialsPerByte + nonceTrialsPerByte = max( + nonceTrialsPerByte, + defaults.networkDefaultProofOfWorkNonceTrialsPerByte) if payloadLengthExtraBytes == 0: - payloadLengthExtraBytes = BMConfigParser().getint( + payloadLengthExtraBytes = config.getint( 'bitmessagesettings', 'defaultpayloadlengthextrabytes') - if payloadLengthExtraBytes < \ - defaults.networkDefaultPayloadLengthExtraBytes: - payloadLengthExtraBytes = \ - defaults.networkDefaultPayloadLengthExtraBytes + payloadLengthExtraBytes = max( + payloadLengthExtraBytes, + defaults.networkDefaultPayloadLengthExtraBytes) 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 @@ -125,26 +175,22 @@ def run(self): # 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) - ripe = hashlib.new('ripemd160') - sha = hashlib.new('sha512') - sha.update( - potentialPubSigningKey + potentialPubEncryptionKey) - ripe.update(sha.digest()) - if ripe.digest()[:numberOfNullBytesDemandedOnFrontOfRipeHash] == '\x00' * numberOfNullBytesDemandedOnFrontOfRipeHash: + potentialPrivEncryptionKey, potentialPubEncryptionKey = \ + highlevelcrypto.random_keys() + ripe = highlevelcrypto.to_ripe( + pubSigningKey, potentialPubEncryptionKey) + if ( + ripe[:numberOfNullBytesDemandedOnFrontOfRipeHash] + == b'\x00' * numberOfNullBytesDemandedOnFrontOfRipeHash + ): break - logger.info( - 'Generated address with ripe digest: %s', - hexlify(ripe.digest())) + self.logger.info( + 'Generated address with ripe digest: %s', hexlify(ripe)) try: - logger.info( + self.logger.info( 'Address generator calculated %s addresses at %s' ' addresses per second before finding one with' ' the correct ripe-prefix.', @@ -155,37 +201,12 @@ def run(self): # The user must have a pretty fast computer. # time.time() - startTime equaled zero. pass - address = encodeAddress( - addressVersionNumber, streamNumber, ripe.digest()) - - # 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 = '\x80' + potentialPrivSigningKey - checksum = hashlib.sha256(hashlib.sha256( - privSigningKey).digest()).digest()[0:4] - privSigningKeyWIF = arithmetic.changebase( - privSigningKey + checksum, 256, 58) - privEncryptionKey = '\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( - nonceTrialsPerByte)) - BMConfigParser().set(address, 'payloadlengthextrabytes', str( - payloadLengthExtraBytes)) - BMConfigParser().set( - address, 'privsigningkey', privSigningKeyWIF) - BMConfigParser().set( - address, 'privencryptionkey', privEncryptionKeyWIF) - BMConfigParser().save() + address = self.save_address( + addressVersionNumber, streamNumber, ripe, label, + privSigningKey, potentialPrivEncryptionKey, + nonceTrialsPerByte, payloadLengthExtraBytes + ) # The API and the join and create Chan functionality # both need information back from the address generator. @@ -193,33 +214,25 @@ def run(self): queues.UISignalQueue.put(( 'updateStatusBar', - tr._translate( + _translate( "MainWindow", "Done generating address. Doing work necessary" " to broadcast it...") )) - queues.UISignalQueue.put(('writeNewAddressToTable', ( - label, address, streamNumber))) - shared.reloadMyAddressHashes() - if addressVersionNumber == 3: - queues.workerQueue.put(( - 'sendOutOrStoreMyV3Pubkey', ripe.digest())) - elif addressVersionNumber == 4: - queues.workerQueue.put(( - 'sendOutOrStoreMyV4Pubkey', address)) - elif command == 'createDeterministicAddresses' \ - or command == 'getDeterministicAddress' \ - or command == 'createChan' or command == 'joinChan': - if len(deterministicPassphrase) == 0: - logger.warning( + elif command in ( + 'createDeterministicAddresses', 'createChan', + 'getDeterministicAddress', 'joinChan' + ): + if not deterministicPassphrase: + self.logger.warning( 'You are creating deterministic' ' address(es) using a blank passphrase.' ' Bitmessage will do it but it is rather stupid.') if command == 'createDeterministicAddresses': queues.UISignalQueue.put(( 'updateStatusBar', - tr._translate( + _translate( "MainWindow", "Generating %1 new addresses." ).arg(str(numberOfAddressesToMake)) @@ -230,7 +243,7 @@ def run(self): # need it if we end up passing the info to the API. listOfNewAddressesToSendOutThroughTheAPI = [] - for i in range(numberOfAddressesToMake): + for _ in range(numberOfAddressesToMake): # This next section is a little bit strange. We're # going to generate keys over and over until we find # one that has a RIPEMD hash that starts with either @@ -241,46 +254,42 @@ def run(self): 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 - ripe = hashlib.new('ripemd160') - sha = hashlib.new('sha512') - sha.update( - potentialPubSigningKey + potentialPubEncryptionKey) - ripe.update(sha.digest()) - if ripe.digest()[:numberOfNullBytesDemandedOnFrontOfRipeHash] == '\x00' * numberOfNullBytesDemandedOnFrontOfRipeHash: + ripe = highlevelcrypto.to_ripe( + potentialPubSigningKey, potentialPubEncryptionKey) + if ( + ripe[:numberOfNullBytesDemandedOnFrontOfRipeHash] + == b'\x00' * numberOfNullBytesDemandedOnFrontOfRipeHash + ): break - logger.info( - 'Generated address with ripe digest: %s', - hexlify(ripe.digest())) + self.logger.info( + 'Generated address with ripe digest: %s', hexlify(ripe)) try: - logger.info( + self.logger.info( 'Address generator calculated %s addresses' ' at %s addresses per second before finding' ' one with the correct ripe-prefix.', numberOfAddressesWeHadToMakeBeforeWeFoundOneWithTheCorrectRipePrefix, - numberOfAddressesWeHadToMakeBeforeWeFoundOneWithTheCorrectRipePrefix / - (time.time() - startTime) + numberOfAddressesWeHadToMakeBeforeWeFoundOneWithTheCorrectRipePrefix + / (time.time() - startTime) ) except ZeroDivisionError: # The user must have a pretty fast computer. # time.time() - startTime equaled zero. pass address = encodeAddress( - addressVersionNumber, streamNumber, ripe.digest()) + addressVersionNumber, streamNumber, ripe) saveAddressToDisk = True # If we are joining an existing chan, let us check @@ -293,107 +302,38 @@ def run(self): if command == 'getDeterministicAddress': 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 = '\x80' + potentialPrivSigningKey - checksum = hashlib.sha256(hashlib.sha256( - privSigningKey).digest()).digest()[0:4] - privSigningKeyWIF = arithmetic.changebase( - privSigningKey + checksum, 256, 58) - - privEncryptionKey = '\x80' + \ - potentialPrivEncryptionKey - checksum = hashlib.sha256(hashlib.sha256( - privEncryptionKey).digest()).digest()[0:4] - privEncryptionKeyWIF = arithmetic.changebase( - privEncryptionKey + checksum, 256, 58) - - try: - BMConfigParser().add_section(address) - addressAlreadyExists = False - except: - addressAlreadyExists = True + if saveAddressToDisk and live and self.save_address( + addressVersionNumber, streamNumber, ripe, label, + potentialPrivSigningKey, potentialPrivEncryptionKey, + nonceTrialsPerByte, payloadLengthExtraBytes + ): + if command in ('createChan', 'joinChan'): + config.set(address, 'chan', 'true') + config.save() - if addressAlreadyExists: - logger.info( - '%s already exists. Not adding it again.', - address - ) - queues.UISignalQueue.put(( - 'updateStatusBar', - tr._translate( - "MainWindow", - "%1 is already in 'Your Identities'." - " Not adding it again." - ).arg(address) - )) - else: - logger.debug('label: %s', label) - BMConfigParser().set(address, 'label', label) - BMConfigParser().set(address, 'enabled', 'true') - BMConfigParser().set(address, 'decoy', 'false') - if command == 'joinChan' \ - or command == 'createChan': - BMConfigParser().set(address, 'chan', 'true') - BMConfigParser().set( - address, 'noncetrialsperbyte', - str(nonceTrialsPerByte)) - BMConfigParser().set( - address, 'payloadlengthextrabytes', - str(payloadLengthExtraBytes)) - BMConfigParser().set( - address, 'privSigningKey', - privSigningKeyWIF) - BMConfigParser().set( - address, 'privEncryptionKey', - privEncryptionKeyWIF) - BMConfigParser().save() + listOfNewAddressesToSendOutThroughTheAPI.append( + address) - queues.UISignalQueue.put(( - 'writeNewAddressToTable', - (label, address, str(streamNumber)) - )) - listOfNewAddressesToSendOutThroughTheAPI.append( - address) - shared.myECCryptorObjects[ripe.digest()] = \ - highlevelcrypto.makeCryptor( - hexlify(potentialPrivEncryptionKey)) - shared.myAddressesByHash[ripe.digest()] = address - tag = hashlib.sha512(hashlib.sha512( - encodeVarint(addressVersionNumber) + - encodeVarint(streamNumber) + ripe.digest() - ).digest()).digest()[32:] - shared.myAddressesByTag[tag] = address - if addressVersionNumber == 3: - # If this is a chan address, - # the worker thread won't send out - # the pubkey over the network. - queues.workerQueue.put(( - 'sendOutOrStoreMyV3Pubkey', ripe.digest())) - elif addressVersionNumber == 4: - queues.workerQueue.put(( - 'sendOutOrStoreMyV4Pubkey', address)) - queues.UISignalQueue.put(( - 'updateStatusBar', - tr._translate( - "MainWindow", "Done generating address") - )) + queues.UISignalQueue.put(( + 'updateStatusBar', + _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 == 'createDeterministicAddresses' \ - or command == 'joinChan' or command == 'createChan': + if command in ( + 'createDeterministicAddresses', 'createChan', 'joinChan' + ): queues.apiAddressGeneratorReturnQueue.put( listOfNewAddressesToSendOutThroughTheAPI) elif command == 'getDeterministicAddress': queues.apiAddressGeneratorReturnQueue.put(address) else: - raise Exception( - "Error in the addressGenerator thread. Thread was" + - " given a command it could not understand: " + command) + raise RuntimeError( + "Error in the addressGenerator thread. Thread was" + + " given a command it could not understand: " + command) queues.addressGeneratorQueue.task_done() diff --git a/src/class_objectProcessor.py b/src/class_objectProcessor.py index 181ce30efe..974631cbb7 100644 --- a/src/class_objectProcessor.py +++ b/src/class_objectProcessor.py @@ -1,33 +1,41 @@ -import time -import threading -import shared +""" +The objectProcessor thread, of which there is only one, +processes the network objects +""" +# pylint: disable=too-many-locals,too-many-return-statements +# pylint: disable=too-many-branches,too-many-statements import hashlib +import logging +import os import random -from struct import unpack, pack -import sys -import string -from subprocess import call # used when the API must execute an outside program -import traceback +import subprocess # nosec B404 +import threading +import time from binascii import hexlify -from pyelliptic.openssl import OpenSSL -import highlevelcrypto -from addresses import * -from bmconfigparser import BMConfigParser -import helper_generic -from helper_generic import addDataPadding import helper_bitcoin import helper_inbox import helper_msgcoding import helper_sent -from helper_sql import * -from helper_ackPayload import genAckPayload +import highlevelcrypto +import l10n import protocol import queues +import shared import state -import tr -from debug import logger -import l10n +from addresses import ( + decodeAddress, decodeVarint, + encodeAddress, encodeVarint, varintDecodeError +) +from bmconfigparser import config +from helper_sql import ( + sql_ready, sql_timeout, SqlBulkExecute, sqlExecute, sqlQuery) +from network import knownnodes, invQueue +from network.node import Peer +from tr import _translate + +logger = logging.getLogger('default') + class objectProcessor(threading.Thread): """ @@ -36,65 +44,95 @@ class objectProcessor(threading.Thread): """ def __init__(self): threading.Thread.__init__(self, name="objectProcessor") - """ - 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. - """ + 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. queryreturn = sqlQuery( - '''SELECT objecttype, data FROM objectprocessorqueue''') - for row in queryreturn: - objectType, data = row - queues.objectProcessorQueue.put((objectType,data)) - sqlExecute('''DELETE FROM objectprocessorqueue''') - logger.debug('Loaded %s objects from disk into the objectProcessorQueue.' % str(len(queryreturn))) - + 'SELECT objecttype, data FROM objectprocessorqueue') + for objectType, data in queryreturn: + queues.objectProcessorQueue.put((objectType, data)) + sqlExecute('DELETE FROM objectprocessorqueue') + logger.debug( + 'Loaded %s objects from disk into the objectProcessorQueue.', + len(queryreturn)) + self.successfullyDecryptMessageTimings = [] def run(self): + """Process the objects from `.queues.objectProcessorQueue`""" while True: objectType, data = queues.objectProcessorQueue.get() self.checkackdata(data) try: - if objectType == 0: # getpubkey + if objectType == protocol.OBJECT_GETPUBKEY: self.processgetpubkey(data) - elif objectType == 1: #pubkey + elif objectType == protocol.OBJECT_PUBKEY: self.processpubkey(data) - elif objectType == 2: #msg + elif objectType == protocol.OBJECT_MSG: self.processmsg(data) - elif objectType == 3: #broadcast + elif objectType == protocol.OBJECT_BROADCAST: self.processbroadcast(data) - elif objectType == 'checkShutdownVariable': # is more of a command, not an object type. Is used to get this thread past the queue.get() so that it will check the shutdown variable. + elif objectType == protocol.OBJECT_ONIONPEER: + self.processonion(data) + # is more of a command, not an object type. Is used to get + # this thread past the queue.get() so that it will check + # the shutdown variable. + elif objectType == 'checkShutdownVariable': pass else: if isinstance(objectType, int): - logger.info('Don\'t know how to handle object type 0x%08X', objectType) + logger.info( + 'Don\'t know how to handle object type 0x%08X', + objectType) else: - logger.info('Don\'t know how to handle object type %s', objectType) + logger.info( + 'Don\'t know how to handle object type %s', + objectType) except helper_msgcoding.DecompressionSizeException as e: - logger.error("The object is too big after decompression (stopped decompressing at %ib, your configured limit %ib). Ignoring", e.size, BMConfigParser().safeGetInt("zlib", "maxsize")) + logger.error( + 'The object is too big after decompression (stopped' + ' decompressing at %ib, your configured limit %ib).' + ' Ignoring', + e.size, config.safeGetInt('zlib', 'maxsize')) except varintDecodeError as e: - logger.debug("There was a problem with a varint while processing an object. Some details: %s" % e) - except Exception as e: - logger.critical("Critical error within objectProcessorThread: \n%s" % traceback.format_exc()) + logger.debug( + 'There was a problem with a varint while processing an' + ' object. Some details: %s', e) + except Exception: + logger.critical( + 'Critical error within objectProcessorThread: \n', + exc_info=True) if state.shutdown: - time.sleep(.5) # Wait just a moment for most of the connections to close + # Wait just a moment for most of the connections to close + time.sleep(.5) numberOfObjectsThatWereInTheObjectProcessorQueue = 0 with SqlBulkExecute() as sql: while queues.objectProcessorQueue.curSize > 0: objectType, data = queues.objectProcessorQueue.get() - sql.execute('''INSERT INTO objectprocessorqueue VALUES (?,?)''', - objectType,data) + sql.execute( + 'INSERT INTO objectprocessorqueue VALUES (?,?)', + objectType, data) numberOfObjectsThatWereInTheObjectProcessorQueue += 1 - logger.debug('Saved %s objects from the objectProcessorQueue to disk. objectProcessorThread exiting.' % str(numberOfObjectsThatWereInTheObjectProcessorQueue)) + logger.debug( + 'Saved %s objects from the objectProcessorQueue to' + ' disk. objectProcessorThread exiting.', + numberOfObjectsThatWereInTheObjectProcessorQueue) state.shutdown = 2 break - def checkackdata(self, data): + @staticmethod + def checkackdata(data): + """Checking Acknowledgement of message received or not?""" # Let's check whether this is a message acknowledgement bound for us. if len(data) < 32: return @@ -102,22 +140,50 @@ def checkackdata(self, data): # bypass nonce and time, retain object type/version/stream + body readPosition = 16 - if data[readPosition:] in shared.ackdataForWhichImWatching: + if data[readPosition:] in state.ackdataForWhichImWatching: logger.info('This object is an acknowledgement bound for me.') - del shared.ackdataForWhichImWatching[data[readPosition:]] - sqlExecute('UPDATE sent SET status=?, lastactiontime=? WHERE ackdata=?', - 'ackreceived', - int(time.time()), - data[readPosition:]) - queues.UISignalQueue.put(('updateSentItemStatusByAckdata', (data[readPosition:], tr._translate("MainWindow",'Acknowledgement of the message received %1').arg(l10n.formatTimestamp())))) + del state.ackdataForWhichImWatching[data[readPosition:]] + sqlExecute( + "UPDATE sent SET status='ackreceived', lastactiontime=?" + " WHERE ackdata=?", int(time.time()), data[readPosition:]) + queues.UISignalQueue.put(( + 'updateSentItemStatusByAckdata', ( + data[readPosition:], + _translate( + "MainWindow", + "Acknowledgement of the message received %1" + ).arg(l10n.formatTimestamp())) + )) else: logger.debug('This object is not an acknowledgement bound for me.') - - def processgetpubkey(self, data): - if len(data) > 200: - logger.info('getpubkey is abnormally long. Sanity check failed. Ignoring object.') + @staticmethod + def processonion(data): + """Process onionpeer object""" + readPosition = 20 # bypass the nonce, time, and object type + length = decodeVarint(data[readPosition:readPosition + 10])[1] + readPosition += length + stream, length = decodeVarint(data[readPosition:readPosition + 10]) + readPosition += length + # it seems that stream is checked in network.bmproto + port, length = decodeVarint(data[readPosition:readPosition + 10]) + host = protocol.checkIPAddress(data[readPosition + length:]) + + if not host: return + peer = Peer(host, port) + with knownnodes.knownNodesLock: + # FIXME: adjust expirestime + knownnodes.addKnownNode( + stream, peer, is_self=state.ownAddresses.get(peer)) + + @staticmethod + def processgetpubkey(data): + """Process getpubkey object""" + if len(data) > 200: + return logger.info( + 'getpubkey is abnormally long. Sanity check failed.' + ' Ignoring object.') readPosition = 20 # bypass the nonce, time, and object type requestedAddressVersionNumber, addressVersionLength = decodeVarint( data[readPosition:readPosition + 10]) @@ -127,30 +193,40 @@ def processgetpubkey(self, data): readPosition += streamNumberLength if requestedAddressVersionNumber == 0: - logger.debug('The requestedAddressVersionNumber of the pubkey request is zero. That doesn\'t make any sense. Ignoring it.') - return - elif requestedAddressVersionNumber == 1: - logger.debug('The requestedAddressVersionNumber of the pubkey request is 1 which isn\'t supported anymore. Ignoring it.') - return - elif requestedAddressVersionNumber > 4: - logger.debug('The requestedAddressVersionNumber of the pubkey request is too high. Can\'t understand. Ignoring it.') - return + return logger.debug( + 'The requestedAddressVersionNumber of the pubkey request' + ' is zero. That doesn\'t make any sense. Ignoring it.') + if requestedAddressVersionNumber == 1: + return logger.debug( + 'The requestedAddressVersionNumber of the pubkey request' + ' is 1 which isn\'t supported anymore. Ignoring it.') + if requestedAddressVersionNumber > 4: + return logger.debug( + 'The requestedAddressVersionNumber of the pubkey request' + ' is too high. Can\'t understand. Ignoring it.') myAddress = '' - if requestedAddressVersionNumber <= 3 : + if requestedAddressVersionNumber <= 3: requestedHash = data[readPosition:readPosition + 20] if len(requestedHash) != 20: - logger.debug('The length of the requested hash is not 20 bytes. Something is wrong. Ignoring.') - return - logger.info('the hash requested in this getpubkey request is: %s' % hexlify(requestedHash)) - if requestedHash in shared.myAddressesByHash: # if this address hash is one of mine + return logger.debug( + 'The length of the requested hash is not 20 bytes.' + ' Something is wrong. Ignoring.') + logger.info( + 'the hash requested in this getpubkey request is: %s', + hexlify(requestedHash)) + # if this address hash is one of mine + if requestedHash in shared.myAddressesByHash: myAddress = shared.myAddressesByHash[requestedHash] elif requestedAddressVersionNumber >= 4: requestedTag = data[readPosition:readPosition + 32] if len(requestedTag) != 32: - logger.debug('The length of the requested tag is not 32 bytes. Something is wrong. Ignoring.') - return - logger.debug('the tag requested in this getpubkey request is: %s' % hexlify(requestedTag)) + return logger.debug( + 'The length of the requested tag is not 32 bytes.' + ' Something is wrong. Ignoring.') + logger.debug( + 'the tag requested in this getpubkey request is: %s', + hexlify(requestedTag)) if requestedTag in shared.myAddressesByTag: myAddress = shared.myAddressesByTag[requestedTag] @@ -159,39 +235,48 @@ def processgetpubkey(self, data): return if decodeAddress(myAddress)[1] != requestedAddressVersionNumber: - logger.warning('(Within the processgetpubkey function) Someone requested one of my pubkeys but the requestedAddressVersionNumber doesn\'t match my actual address version number. Ignoring.') - return + return logger.warning( + '(Within the processgetpubkey function) Someone requested' + ' one of my pubkeys but the requestedAddressVersionNumber' + ' doesn\'t match my actual address version number.' + ' Ignoring.') if decodeAddress(myAddress)[2] != streamNumber: - logger.warning('(Within the processgetpubkey function) Someone requested one of my pubkeys but the stream number on which we heard this getpubkey object doesn\'t match this address\' stream number. Ignoring.') - return - if BMConfigParser().safeGetBoolean(myAddress, 'chan'): - logger.info('Ignoring getpubkey request because it is for one of my chan addresses. The other party should already have the pubkey.') - return - try: - lastPubkeySendTime = int(BMConfigParser().get( - myAddress, 'lastpubkeysendtime')) - except: - lastPubkeySendTime = 0 - if lastPubkeySendTime > time.time() - 2419200: # If the last time we sent our pubkey was more recent than 28 days ago... - logger.info('Found getpubkey-requested-item in my list of EC hashes BUT we already sent it recently. Ignoring request. The lastPubkeySendTime is: %s' % lastPubkeySendTime) - return - logger.info('Found getpubkey-requested-hash in my list of EC hashes. Telling Worker thread to do the POW for a pubkey message and send it out.') + return logger.warning( + '(Within the processgetpubkey function) Someone requested' + ' one of my pubkeys but the stream number on which we' + ' heard this getpubkey object doesn\'t match this' + ' address\' stream number. Ignoring.') + 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 = config.safeGetInt( + myAddress, 'lastpubkeysendtime') + # If the last time we sent our pubkey was more recent than + # 28 days ago... + if lastPubkeySendTime > time.time() - 2419200: + return logger.info( + 'Found getpubkey-requested-item in my list of EC hashes' + ' BUT we already sent it recently. Ignoring request.' + ' The lastPubkeySendTime is: %s', lastPubkeySendTime) + logger.info( + 'Found getpubkey-requested-hash in my list of EC hashes.' + ' Telling Worker thread to do the POW for a pubkey message' + ' and send it out.') if requestedAddressVersionNumber == 2: - queues.workerQueue.put(( - 'doPOWForMyV2Pubkey', requestedHash)) + queues.workerQueue.put(('doPOWForMyV2Pubkey', requestedHash)) elif requestedAddressVersionNumber == 3: - queues.workerQueue.put(( - 'sendOutOrStoreMyV3Pubkey', requestedHash)) + queues.workerQueue.put(('sendOutOrStoreMyV3Pubkey', requestedHash)) elif requestedAddressVersionNumber == 4: - queues.workerQueue.put(( - 'sendOutOrStoreMyV4Pubkey', myAddress)) + queues.workerQueue.put(('sendOutOrStoreMyV4Pubkey', myAddress)) def processpubkey(self, data): + """Process a pubkey object""" pubkeyProcessingStartTime = time.time() - shared.numberOfPubkeysProcessed += 1 + state.numberOfPubkeysProcessed += 1 queues.UISignalQueue.put(( 'updateNumberOfPubkeysProcessed', 'no data')) - embeddedTime, = unpack('>Q', data[8:16]) readPosition = 20 # bypass the nonce, time, and object type addressVersion, varintLength = decodeVarint( data[readPosition:readPosition + 10]) @@ -200,225 +285,258 @@ def processpubkey(self, data): data[readPosition:readPosition + 10]) readPosition += varintLength if addressVersion == 0: - logger.debug('(Within processpubkey) addressVersion of 0 doesn\'t make sense.') - return + return logger.debug( + '(Within processpubkey) addressVersion of 0 doesn\'t' + ' make sense.') if addressVersion > 4 or addressVersion == 1: - logger.info('This version of Bitmessage cannot handle version %s addresses.' % addressVersion) - return + return logger.info( + 'This version of Bitmessage cannot handle version %s' + ' addresses.', addressVersion) if addressVersion == 2: - if len(data) < 146: # sanity check. This is the minimum possible length. - logger.debug('(within processpubkey) payloadLength less than 146. Sanity check failed.') - return - bitfieldBehaviors = data[readPosition:readPosition + 4] + # sanity check. This is the minimum possible length. + if len(data) < 146: + return logger.debug( + '(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: - logger.debug('publicEncryptionKey length less than 64. Sanity check failed.') - return + pubEncryptionKey = '\x04' + data[readPosition:readPosition + 64] + if len(pubEncryptionKey) < 65: + return logger.debug( + 'publicEncryptionKey length less than 64. Sanity check' + ' failed.') readPosition += 64 - dataToStore = data[20:readPosition] # The data we'll store in the pubkeys table. - sha = hashlib.new('sha512') - sha.update( - '\x04' + publicSigningKey + '\x04' + publicEncryptionKey) - ripeHasher = hashlib.new('ripemd160') - ripeHasher.update(sha.digest()) - ripe = ripeHasher.digest() - - - logger.debug('within recpubkey, addressVersion: %s, streamNumber: %s \n\ - ripe %s\n\ - publicSigningKey in hex: %s\n\ - publicEncryptionKey in hex: %s' % (addressVersion, - streamNumber, - hexlify(ripe), - hexlify(publicSigningKey), - hexlify(publicEncryptionKey) - ) - ) - - + # The data we'll store in the pubkeys table. + dataToStore = data[20:readPosition] + ripe = highlevelcrypto.to_ripe(pubSigningKey, pubEncryptionKey) + + if logger.isEnabledFor(logging.DEBUG): + logger.debug( + 'within recpubkey, addressVersion: %s, streamNumber: %s' + '\nripe %s\npublicSigningKey in hex: %s' + '\npublicEncryptionKey in hex: %s', + addressVersion, streamNumber, hexlify(ripe), + hexlify(pubSigningKey), hexlify(pubEncryptionKey) + ) + address = encodeAddress(addressVersion, streamNumber, ripe) - + queryreturn = sqlQuery( - '''SELECT usedpersonally FROM pubkeys WHERE address=? AND usedpersonally='yes' ''', address) - if queryreturn != []: # if this pubkey is already in our database and if we have used it personally: - logger.info('We HAVE used this pubkey personally. Updating time.') - t = (address, addressVersion, dataToStore, int(time.time()), 'yes') + "SELECT usedpersonally FROM pubkeys WHERE address=?" + " AND usedpersonally='yes'", address) + # if this pubkey is already in our database and if we have + # used it personally: + if queryreturn != []: + logger.info( + 'We HAVE used this pubkey personally. Updating time.') + t = (address, addressVersion, dataToStore, + int(time.time()), 'yes') else: - logger.info('We have NOT used this pubkey personally. Inserting in database.') - t = (address, addressVersion, dataToStore, int(time.time()), 'no') + logger.info( + 'We have NOT used this pubkey personally. Inserting' + ' in database.') + t = (address, addressVersion, dataToStore, + int(time.time()), 'no') sqlExecute('''INSERT INTO pubkeys VALUES (?,?,?,?,?)''', *t) self.possibleNewPubkey(address) if addressVersion == 3: if len(data) < 170: # sanity check. - logger.warning('(within processpubkey) payloadLength less than 170. Sanity check failed.') + logger.warning( + '(within processpubkey) payloadLength less than 170.' + ' Sanity check failed.') return - bitfieldBehaviors = data[readPosition:readPosition + 4] 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 - specifiedNonceTrialsPerByte, specifiedNonceTrialsPerByteLength = decodeVarint( - data[readPosition:readPosition + 10]) + specifiedNonceTrialsPerByteLength = decodeVarint( + data[readPosition:readPosition + 10])[1] readPosition += specifiedNonceTrialsPerByteLength - specifiedPayloadLengthExtraBytes, specifiedPayloadLengthExtraBytesLength = decodeVarint( - data[readPosition:readPosition + 10]) + specifiedPayloadLengthExtraBytesLength = decodeVarint( + data[readPosition:readPosition + 10])[1] readPosition += specifiedPayloadLengthExtraBytesLength endOfSignedDataPosition = readPosition - dataToStore = data[20:readPosition] # The data we'll store in the pubkeys table. + # The data we'll store in the pubkeys table. + dataToStore = data[20:readPosition] signatureLength, signatureLengthLength = decodeVarint( data[readPosition:readPosition + 10]) readPosition += signatureLengthLength signature = data[readPosition:readPosition + signatureLength] - if highlevelcrypto.verify(data[8:endOfSignedDataPosition], signature, hexlify(publicSigningKey)): + if highlevelcrypto.verify( + data[8:endOfSignedDataPosition], + 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) - ripeHasher = hashlib.new('ripemd160') - ripeHasher.update(sha.digest()) - ripe = ripeHasher.digest() - - - logger.debug('within recpubkey, addressVersion: %s, streamNumber: %s \n\ - ripe %s\n\ - publicSigningKey in hex: %s\n\ - publicEncryptionKey in hex: %s' % (addressVersion, - streamNumber, - hexlify(ripe), - hexlify(publicSigningKey), - hexlify(publicEncryptionKey) - ) - ) + ripe = highlevelcrypto.to_ripe(pubSigningKey, pubEncryptionKey) + + if logger.isEnabledFor(logging.DEBUG): + logger.debug( + 'within recpubkey, addressVersion: %s, streamNumber: %s' + '\nripe %s\npublicSigningKey in hex: %s' + '\npublicEncryptionKey in hex: %s', + addressVersion, streamNumber, hexlify(ripe), + hexlify(pubSigningKey), hexlify(pubEncryptionKey) + ) address = encodeAddress(addressVersion, streamNumber, ripe) - queryreturn = sqlQuery('''SELECT usedpersonally FROM pubkeys WHERE address=? AND usedpersonally='yes' ''', address) - if queryreturn != []: # if this pubkey is already in our database and if we have used it personally: - logger.info('We HAVE used this pubkey personally. Updating time.') - t = (address, addressVersion, dataToStore, int(time.time()), 'yes') + queryreturn = sqlQuery( + "SELECT usedpersonally FROM pubkeys WHERE address=?" + " AND usedpersonally='yes'", address) + # if this pubkey is already in our database and if we have + # used it personally: + if queryreturn != []: + logger.info( + 'We HAVE used this pubkey personally. Updating time.') + t = (address, addressVersion, dataToStore, + int(time.time()), 'yes') else: - logger.info('We have NOT used this pubkey personally. Inserting in database.') - t = (address, addressVersion, dataToStore, int(time.time()), 'no') + logger.info( + 'We have NOT used this pubkey personally. Inserting' + ' in database.') + t = (address, addressVersion, dataToStore, + int(time.time()), 'no') sqlExecute('''INSERT INTO pubkeys VALUES (?,?,?,?,?)''', *t) self.possibleNewPubkey(address) if addressVersion == 4: if len(data) < 350: # sanity check. - logger.debug('(within processpubkey) payloadLength less than 350. Sanity check failed.') - return + return logger.debug( + '(within processpubkey) payloadLength less than 350.' + ' Sanity check failed.') tag = data[readPosition:readPosition + 32] if tag not in state.neededPubkeys: - logger.info('We don\'t need this v4 pubkey. We didn\'t ask for it.') - return - + return logger.info( + 'We don\'t need this v4 pubkey. We didn\'t ask for it.') + # Let us try to decrypt the pubkey - toAddress, cryptorObject = state.neededPubkeys[tag] - if shared.decryptAndCheckPubkeyPayload(data, toAddress) == 'successful': - # At this point we know that we have been waiting on this pubkey. - # This function will command the workerThread to start work on - # the messages that require it. + toAddress = state.neededPubkeys[tag][0] + if protocol.decryptAndCheckPubkeyPayload(data, toAddress) == \ + 'successful': + # At this point we know that we have been waiting on this + # pubkey. This function will command the workerThread + # to start work on the messages that require it. self.possibleNewPubkey(toAddress) # Display timing data - timeRequiredToProcessPubkey = time.time( - ) - pubkeyProcessingStartTime - logger.debug('Time required to process this pubkey: %s' % timeRequiredToProcessPubkey) - + logger.debug( + 'Time required to process this pubkey: %s', + time.time() - pubkeyProcessingStartTime) def processmsg(self, data): + """Process a message object""" messageProcessingStartTime = time.time() - shared.numberOfMessagesProcessed += 1 + state.numberOfMessagesProcessed += 1 queues.UISignalQueue.put(( 'updateNumberOfMessagesProcessed', 'no data')) - readPosition = 20 # bypass the nonce, time, and object type - msgVersion, msgVersionLength = decodeVarint(data[readPosition:readPosition + 9]) + readPosition = 20 # bypass the nonce, time, and object type + msgVersion, msgVersionLength = decodeVarint( + data[readPosition:readPosition + 9]) if msgVersion != 1: - logger.info('Cannot understand message versions other than one. Ignoring message.') - return + return logger.info( + 'Cannot understand message versions other than one.' + ' Ignoring message.') readPosition += msgVersionLength - - streamNumberAsClaimedByMsg, streamNumberAsClaimedByMsgLength = decodeVarint( - data[readPosition:readPosition + 9]) + + 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 # bound for me by trying to decrypt it with my private keys. - - for key, cryptorObject in sorted(shared.myECCryptorObjects.items(), key=lambda x: random.random()): + + for key, cryptorObject in sorted( + shared.myECCryptorObjects.items(), + key=lambda x: random.random()): # nosec B311 try: - if initialDecryptionSuccessful: # continue decryption attempts to avoid timing attacks + # continue decryption attempts to avoid timing attacks + if initialDecryptionSuccessful: cryptorObject.decrypt(data[readPosition:]) else: decryptedData = cryptorObject.decrypt(data[readPosition:]) - toRipe = key # This is the RIPE hash of my pubkeys. We need this below to compare to the destination_ripe included in the encrypted data. + # This is the RIPE hash of my pubkeys. We need this + # below to compare to the destination_ripe included + # in the encrypted data. + toRipe = key initialDecryptionSuccessful = True - logger.info('EC decryption successful using key associated with ripe hash: %s.' % hexlify(key)) - except Exception as err: + logger.info( + 'EC decryption successful using key associated' + ' with ripe hash: %s.', hexlify(key)) + except Exception: # nosec B110 pass if not initialDecryptionSuccessful: # This is not a message bound for me. - logger.info('Length of time program spent failing to decrypt this message: %s seconds.' % (time.time() - messageProcessingStartTime,)) - return + return logger.info( + 'Length of time program spent failing to decrypt this' + ' message: %s seconds.', + time.time() - messageProcessingStartTime) # This is a message bound for me. - toAddress = shared.myAddressesByHash[ - toRipe] # Look up my address based on the RIPE hash. + # Look up my address based on the RIPE hash. + toAddress = shared.myAddressesByHash[toRipe] readPosition = 0 - sendersAddressVersionNumber, sendersAddressVersionNumberLength = decodeVarint( - decryptedData[readPosition:readPosition + 10]) + sendersAddressVersionNumber, sendersAddressVersionNumberLength = \ + decodeVarint(decryptedData[readPosition:readPosition + 10]) readPosition += sendersAddressVersionNumberLength if sendersAddressVersionNumber == 0: - logger.info('Cannot understand sendersAddressVersionNumber = 0. Ignoring message.') - return + return logger.info( + 'Cannot understand sendersAddressVersionNumber = 0.' + ' Ignoring message.') if sendersAddressVersionNumber > 4: - logger.info('Sender\'s address version number %s not yet supported. Ignoring message.' % sendersAddressVersionNumber) - return + return logger.info( + 'Sender\'s address version number %s not yet supported.' + ' Ignoring message.', sendersAddressVersionNumber) if len(decryptedData) < 170: - logger.info('Length of the unencrypted data is unreasonably short. Sanity check failed. Ignoring message.') - return + return logger.info( + 'Length of the unencrypted data is unreasonably short.' + ' Sanity check failed. Ignoring message.') sendersStreamNumber, sendersStreamNumberLength = decodeVarint( decryptedData[readPosition:readPosition + 10]) if sendersStreamNumber == 0: logger.info('sender\'s stream number is 0. Ignoring message.') return readPosition += sendersStreamNumberLength - behaviorBitfield = decryptedData[readPosition:readPosition + 4] readPosition += 4 - pubSigningKey = '\x04' + decryptedData[ - readPosition:readPosition + 64] + pubSigningKey = '\x04' + decryptedData[readPosition:readPosition + 64] readPosition += 64 - pubEncryptionKey = '\x04' + decryptedData[ - readPosition:readPosition + 64] + pubEncryptionKey = '\x04' + decryptedData[readPosition:readPosition + 64] readPosition += 64 if sendersAddressVersionNumber >= 3: - requiredAverageProofOfWorkNonceTrialsPerByte, varintLength = decodeVarint( - decryptedData[readPosition:readPosition + 10]) + requiredAverageProofOfWorkNonceTrialsPerByte, varintLength = \ + decodeVarint(decryptedData[readPosition:readPosition + 10]) readPosition += varintLength - logger.info('sender\'s requiredAverageProofOfWorkNonceTrialsPerByte is %s' % requiredAverageProofOfWorkNonceTrialsPerByte) + logger.info( + 'sender\'s requiredAverageProofOfWorkNonceTrialsPerByte is %s', + requiredAverageProofOfWorkNonceTrialsPerByte) requiredPayloadLengthExtraBytes, varintLength = decodeVarint( decryptedData[readPosition:readPosition + 10]) readPosition += varintLength - logger.info('sender\'s requiredPayloadLengthExtraBytes is %s' % requiredPayloadLengthExtraBytes) - endOfThePublicKeyPosition = readPosition # needed for when we store the pubkey in our database of pubkeys for later use. + logger.info( + 'sender\'s requiredPayloadLengthExtraBytes is %s', + requiredPayloadLengthExtraBytes) + # needed for when we store the pubkey in our database of pubkeys + # for later use. + endOfThePublicKeyPosition = readPosition if toRipe != decryptedData[readPosition:readPosition + 20]: - logger.info('The original sender of this message did not send it to you. Someone is attempting a Surreptitious Forwarding Attack.\n\ - See: http://world.std.com/~dtd/sign_encrypt/sign_encrypt7.html \n\ - your toRipe: %s\n\ - embedded destination toRipe: %s' % (hexlify(toRipe), hexlify(decryptedData[readPosition:readPosition + 20])) - ) - return + return logger.info( + 'The original sender of this message did not send it to' + ' you. Someone is attempting a Surreptitious Forwarding' + ' Attack.\nSee: ' + 'http://world.std.com/~dtd/sign_encrypt/sign_encrypt7.html' + '\nyour toRipe: %s\nembedded destination toRipe: %s', + hexlify(toRipe), + hexlify(decryptedData[readPosition:readPosition + 20]) + ) readPosition += 20 messageEncodingType, messageEncodingTypeLength = decodeVarint( decryptedData[readPosition:readPosition + 10]) @@ -427,38 +545,47 @@ def processmsg(self, data): decryptedData[readPosition:readPosition + 10]) readPosition += messageLengthLength message = decryptedData[readPosition:readPosition + messageLength] - # print 'First 150 characters of message:', repr(message[:150]) readPosition += messageLength ackLength, ackLengthLength = decodeVarint( decryptedData[readPosition:readPosition + 10]) readPosition += ackLengthLength ackData = decryptedData[readPosition:readPosition + ackLength] readPosition += ackLength - positionOfBottomOfAckData = readPosition # needed to mark the end of what is covered by the signature + # needed to mark the end of what is covered by the signature + positionOfBottomOfAckData = readPosition signatureLength, signatureLengthLength = decodeVarint( decryptedData[readPosition:readPosition + 10]) readPosition += signatureLengthLength signature = decryptedData[ readPosition:readPosition + signatureLength] - signedData = data[8:20] + encodeVarint(1) + encodeVarint(streamNumberAsClaimedByMsg) + decryptedData[:positionOfBottomOfAckData] - - if not highlevelcrypto.verify(signedData, signature, hexlify(pubSigningKey)): - logger.debug('ECDSA verify failed') - return + signedData = data[8:20] + encodeVarint(1) + encodeVarint( + streamNumberAsClaimedByMsg + ) + decryptedData[:positionOfBottomOfAckData] + + if not highlevelcrypto.verify( + signedData, signature, hexlify(pubSigningKey)): + return logger.debug('ECDSA verify failed') logger.debug('ECDSA verify passed') - logger.debug('As a matter of intellectual curiosity, here is the Bitcoin address associated with the keys owned by the other person: %s ..and here is the testnet address: %s. The other person must take their private signing key from Bitmessage and import it into Bitcoin (or a service like Blockchain.info) for it to be of any use. Do not use this unless you know what you are doing.' % - (helper_bitcoin.calculateBitcoinAddressFromPubkey(pubSigningKey), helper_bitcoin.calculateTestnetAddressFromPubkey(pubSigningKey)) - ) - sigHash = hashlib.sha512(hashlib.sha512(signature).digest()).digest()[32:] # Used to detect and ignore duplicate messages in our inbox + if logger.isEnabledFor(logging.DEBUG): + logger.debug( + 'As a matter of intellectual curiosity, here is the Bitcoin' + ' address associated with the keys owned by the other person:' + ' %s ..and here is the testnet address: %s. The other person' + ' must take their private signing key from Bitmessage and' + ' import it into Bitcoin (or a service like Blockchain.info)' + ' for it to be of any use. Do not use this unless you know' + ' what you are doing.', + helper_bitcoin.calculateBitcoinAddressFromPubkey(pubSigningKey), + helper_bitcoin.calculateTestnetAddressFromPubkey(pubSigningKey) + ) + # Used to detect and ignore duplicate messages in our inbox + sigHash = highlevelcrypto.double_sha512(signature)[32:] # calculate the fromRipe. - sha = hashlib.new('sha512') - sha.update(pubSigningKey + pubEncryptionKey) - ripe = hashlib.new('ripemd160') - ripe.update(sha.digest()) + ripe = highlevelcrypto.to_ripe(pubSigningKey, pubEncryptionKey) fromAddress = encodeAddress( - sendersAddressVersionNumber, sendersStreamNumber, ripe.digest()) - + sendersAddressVersionNumber, sendersStreamNumber, ripe) + # Let's store the public key in case we want to reply to this # person. sqlExecute( @@ -468,30 +595,42 @@ def processmsg(self, data): decryptedData[:endOfThePublicKeyPosition], int(time.time()), 'yes') - + # Check to see whether we happen to be awaiting this # pubkey in order to send a message. If we are, it will do the POW # and send it. self.possibleNewPubkey(fromAddress) - + # If this message is bound for one of my version 3 addresses (or # higher), then we must check to make sure it meets our demanded # proof of work requirement. If this is bound for one of my chan # addresses then we skip this check; the minimum network POW is # fine. - if decodeAddress(toAddress)[1] >= 3 and not BMConfigParser().safeGetBoolean(toAddress, 'chan'): # If the toAddress version number is 3 or higher and not one of my chan addresses: - if not shared.isAddressInMyAddressBookSubscriptionsListOrWhitelist(fromAddress): # If I'm not friendly with this person: - requiredNonceTrialsPerByte = BMConfigParser().getint( + # If the toAddress version number is 3 or higher and not one of + # my chan addresses: + if decodeAddress(toAddress)[1] >= 3 \ + and not config.safeGetBoolean(toAddress, 'chan'): + # If I'm not friendly with this person: + if not shared.isAddressInMyAddressBookSubscriptionsListOrWhitelist( + fromAddress): + requiredNonceTrialsPerByte = config.getint( toAddress, 'noncetrialsperbyte') - requiredPayloadLengthExtraBytes = BMConfigParser().getint( + requiredPayloadLengthExtraBytes = config.getint( toAddress, 'payloadlengthextrabytes') - if not protocol.isProofOfWorkSufficient(data, requiredNonceTrialsPerByte, requiredPayloadLengthExtraBytes): - logger.info('Proof of work in msg is insufficient only because it does not meet our higher requirement.') - return - blockMessage = False # Gets set to True if the user shouldn't see the message according to black or white lists. - if BMConfigParser().get('bitmessagesettings', 'blackwhitelist') == 'black': # If we are using a blacklist + if not protocol.isProofOfWorkSufficient( + data, requiredNonceTrialsPerByte, + requiredPayloadLengthExtraBytes): + return logger.info( + 'Proof of work in msg is insufficient only because' + ' it does not meet our higher requirement.') + # Gets set to True if the user shouldn't see the message according + # to black or white lists. + blockMessage = False + # If we are using a blacklist + if config.get( + 'bitmessagesettings', 'blackwhitelist') == 'black': queryreturn = sqlQuery( - '''SELECT label FROM blacklist where address=? and enabled='1' ''', + "SELECT label FROM blacklist where address=? and enabled='1'", fromAddress) if queryreturn != []: logger.info('Message ignored because address is in blacklist.') @@ -499,18 +638,17 @@ def processmsg(self, data): blockMessage = True else: # We're using a whitelist queryreturn = sqlQuery( - '''SELECT label FROM whitelist where address=? and enabled='1' ''', + "SELECT label FROM whitelist where address=? and enabled='1'", fromAddress) if queryreturn == []: - logger.info('Message ignored because address not in whitelist.') + logger.info( + 'Message ignored because address not in whitelist.') blockMessage = True - toLabel = BMConfigParser().get(toAddress, 'label') - if toLabel == '': - toLabel = toAddress - + # toLabel = config.safeGet(toAddress, 'label', toAddress) try: - decodedMessage = helper_msgcoding.MsgDecode(messageEncodingType, message) + decodedMessage = helper_msgcoding.MsgDecode( + messageEncodingType, message) except helper_msgcoding.MsgDecodeException: return subject = decodedMessage.subject @@ -522,8 +660,9 @@ def processmsg(self, data): blockMessage = True if not blockMessage: if messageEncodingType != 0: - t = (inventoryHash, toAddress, fromAddress, subject, int( - time.time()), body, 'inbox', messageEncodingType, 0, sigHash) + t = (inventoryHash, toAddress, fromAddress, subject, + int(time.time()), body, 'inbox', messageEncodingType, + 0, sigHash) helper_inbox.insert(t) queues.UISignalQueue.put(('displayNewInboxMessage', ( @@ -532,139 +671,157 @@ def processmsg(self, data): # 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'): - try: - apiNotifyPath = BMConfigParser().get( - 'bitmessagesettings', 'apinotifypath') - except: - apiNotifyPath = '' - if apiNotifyPath != '': - call([apiNotifyPath, "newMessage"]) + if config.safeGetBoolean( + 'bitmessagesettings', 'apienabled'): + apiNotifyPath = config.safeGet( + 'bitmessagesettings', 'apinotifypath') + if apiNotifyPath: + 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') and messageEncodingType != 0: - try: - mailingListName = BMConfigParser().get( - toAddress, 'mailinglistname') - except: - mailingListName = '' + if config.safeGetBoolean(toAddress, 'mailinglist') \ + and messageEncodingType != 0: + mailingListName = config.safeGet( + toAddress, 'mailinglistname', '') # Let us send out this message as a broadcast subject = self.addMailingListNameToSubject( subject, mailingListName) # Let us now send this message out as a broadcast - message = time.strftime("%a, %Y-%m-%d %H:%M:%S UTC", time.gmtime( - )) + ' Message ostensibly from ' + fromAddress + ':\n\n' + body - fromAddress = toAddress # The fromAddress for the broadcast that we are about to send is the toAddress (my address) for the msg message we are currently processing. - # We don't actually need the ackdata for acknowledgement since this is a broadcast message but we can use it to update the user interface when the POW is done generating. - streamNumber = decodeAddress(fromAddress)[2] - - ackdata = genAckPayload(streamNumber, 0) + message = time.strftime( + "%a, %Y-%m-%d %H:%M:%S UTC", time.gmtime() + ) + ' Message ostensibly from ' + fromAddress \ + + ':\n\n' + body + # The fromAddress for the broadcast that we are about to + # send is the toAddress (my address) for the msg message + # we are currently processing. + fromAddress = toAddress + # We don't actually need the ackdata for acknowledgement + # since this is a broadcast message but we can use it to + # update the user interface when the POW is done generating. toAddress = '[Broadcast subscribers]' - ripe = '' - - # We really should have a discussion about how to - # set the TTL for mailing list broadcasts. This is obviously - # hard-coded. - TTL = 2*7*24*60*60 # 2 weeks - t = ('', - toAddress, - ripe, - fromAddress, - subject, - message, - ackdata, - int(time.time()), # sentTime (this doesn't change) - int(time.time()), # lastActionTime - 0, - 'broadcastqueued', - 0, - 'sent', - messageEncodingType, - TTL) - helper_sent.insert(t) - - queues.UISignalQueue.put(('displayNewSentMessage', ( - toAddress, '[Broadcast subscribers]', fromAddress, subject, message, ackdata))) + + ackdata = helper_sent.insert( + fromAddress=fromAddress, + status='broadcastqueued', + subject=subject, + message=message, + encoding=messageEncodingType) + + queues.UISignalQueue.put(( + 'displayNewSentMessage', ( + toAddress, '[Broadcast subscribers]', fromAddress, + subject, message, ackdata) + )) queues.workerQueue.put(('sendbroadcast', '')) - # Don't send ACK if invalid, blacklisted senders, invisible messages, disabled or chan - if self.ackDataHasAValidHeader(ackData) and \ - not blockMessage and \ - messageEncodingType != 0 and \ - not BMConfigParser().safeGetBoolean(toAddress, 'dontsendack') and \ - not BMConfigParser().safeGetBoolean(toAddress, 'chan'): - shared.checkAndShareObjectWithPeers(ackData[24:]) + # Don't send ACK if invalid, blacklisted senders, invisible + # messages, disabled or chan + if ( + self.ackDataHasAValidHeader(ackData) and not blockMessage + and messageEncodingType != 0 + and not config.safeGetBoolean(toAddress, 'dontsendack') + and not config.safeGetBoolean(toAddress, 'chan') + ): + ackPayload = ackData[24:] + objectType, toStreamNumber, expiresTime = \ + protocol.decodeObjectParameters(ackPayload) + inventoryHash = highlevelcrypto.calculateInventoryHash(ackPayload) + state.Inventory[inventoryHash] = ( + objectType, toStreamNumber, ackPayload, expiresTime, b'') + invQueue.put((toStreamNumber, inventoryHash)) # Display timing data timeRequiredToAttemptToDecryptMessage = time.time( ) - messageProcessingStartTime - shared.successfullyDecryptMessageTimings.append( + self.successfullyDecryptMessageTimings.append( timeRequiredToAttemptToDecryptMessage) - sum = 0 - for item in shared.successfullyDecryptMessageTimings: - sum += item - logger.debug('Time to decrypt this message successfully: %s\n\ - Average time for all message decryption successes since startup: %s.' % - (timeRequiredToAttemptToDecryptMessage, sum / len(shared.successfullyDecryptMessageTimings)) - ) - + timing_sum = 0 + for item in self.successfullyDecryptMessageTimings: + timing_sum += item + logger.debug( + 'Time to decrypt this message successfully: %s' + '\nAverage time for all message decryption successes since' + ' startup: %s.', + timeRequiredToAttemptToDecryptMessage, + timing_sum / len(self.successfullyDecryptMessageTimings) + ) def processbroadcast(self, data): + """Process a broadcast object""" messageProcessingStartTime = time.time() - shared.numberOfBroadcastsProcessed += 1 + 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]) readPosition += broadcastVersionLength if broadcastVersion < 4 or broadcastVersion > 5: - logger.info('Cannot decode incoming broadcast versions less than 4 or higher than 5. Assuming the sender isn\'t being silly, you should upgrade Bitmessage because this message shall be ignored.') - return + return logger.info( + 'Cannot decode incoming broadcast versions less than 4' + ' or higher than 5. Assuming the sender isn\'t being silly,' + ' you should upgrade Bitmessage because this message shall' + ' be ignored.' + ) cleartextStreamNumber, cleartextStreamNumberLength = decodeVarint( data[readPosition:readPosition + 10]) readPosition += cleartextStreamNumberLength if broadcastVersion == 4: - """ - v4 broadcasts are encrypted the same way the msgs are encrypted. To see if we are interested in a - v4 broadcast, we try to decrypt it. This was replaced with v5 broadcasts which include a tag which - we check instead, just like we do with v4 pubkeys. - """ + # v4 broadcasts are encrypted the same way the msgs are + # encrypted. To see if we are interested in a v4 broadcast, + # we try to decrypt it. This was replaced with v5 broadcasts + # which include a tag which we check instead, just like we do + # with v4 pubkeys. signedData = data[8:readPosition] initialDecryptionSuccessful = False - for key, cryptorObject in sorted(shared.MyECSubscriptionCryptorObjects.items(), key=lambda x: random.random()): + for key, cryptorObject in sorted( + shared.MyECSubscriptionCryptorObjects.items(), + key=lambda x: random.random()): # nosec B311 try: - if initialDecryptionSuccessful: # continue decryption attempts to avoid timing attacks + # continue decryption attempts to avoid timing attacks + if initialDecryptionSuccessful: cryptorObject.decrypt(data[readPosition:]) else: - decryptedData = cryptorObject.decrypt(data[readPosition:]) - toRipe = key # This is the RIPE hash of the sender's pubkey. We need this below to compare to the RIPE hash of the sender's address to verify that it was encrypted by with their key rather than some other key. + decryptedData = cryptorObject.decrypt( + data[readPosition:]) + # This is the RIPE hash of the sender's pubkey. + # We need this below to compare to the RIPE hash + # of the sender's address to verify that it was + # encrypted by with their key rather than some + # other key. + toRipe = key initialDecryptionSuccessful = True - logger.info('EC decryption successful using key associated with ripe hash: %s' % hexlify(key)) - except Exception as err: - pass - # print 'cryptorObject.decrypt Exception:', err + logger.info( + 'EC decryption successful using key associated' + ' with ripe hash: %s', hexlify(key)) + except Exception: + logger.debug( + 'cryptorObject.decrypt Exception:', exc_info=True) if not initialDecryptionSuccessful: # This is not a broadcast I am interested in. - logger.debug('Length of time program spent failing to decrypt this v4 broadcast: %s seconds.' % (time.time() - messageProcessingStartTime,)) - return + return logger.debug( + 'Length of time program spent failing to decrypt this' + ' v4 broadcast: %s seconds.', + time.time() - messageProcessingStartTime) elif broadcastVersion == 5: - embeddedTag = data[readPosition:readPosition+32] + embeddedTag = data[readPosition:readPosition + 32] readPosition += 32 if embeddedTag not in shared.MyECSubscriptionCryptorObjects: - logger.debug('We\'re not interested in this broadcast.') + logger.debug('We\'re not interested in this broadcast.') return # We are interested in this broadcast because of its tag. - signedData = data[8:readPosition] # We're going to add some more data which is signed further down. + # We're going to add some more data which is signed further down. + signedData = data[8:readPosition] cryptorObject = shared.MyECSubscriptionCryptorObjects[embeddedTag] try: decryptedData = cryptorObject.decrypt(data[readPosition:]) logger.debug('EC decryption successful') - except Exception as err: - logger.debug('Broadcast version %s decryption Unsuccessful.' % broadcastVersion) - return + except Exception: + return logger.debug( + 'Broadcast version %s decryption Unsuccessful.', + broadcastVersion) # At this point this is a broadcast I have decrypted and am # interested in. readPosition = 0 @@ -672,20 +829,30 @@ def processbroadcast(self, data): decryptedData[readPosition:readPosition + 9]) if broadcastVersion == 4: if sendersAddressVersion < 2 or sendersAddressVersion > 3: - logger.warning('Cannot decode senderAddressVersion other than 2 or 3. Assuming the sender isn\'t being silly, you should upgrade Bitmessage because this message shall be ignored.') - return + return logger.warning( + 'Cannot decode senderAddressVersion other than 2 or 3.' + ' Assuming the sender isn\'t being silly, you should' + ' upgrade Bitmessage because this message shall be' + ' ignored.' + ) elif broadcastVersion == 5: if sendersAddressVersion < 4: - logger.info('Cannot decode senderAddressVersion less than 4 for broadcast version number 5. Assuming the sender isn\'t being silly, you should upgrade Bitmessage because this message shall be ignored.') - return + return logger.info( + 'Cannot decode senderAddressVersion less than 4 for' + ' broadcast version number 5. Assuming the sender' + ' isn\'t being silly, you should upgrade Bitmessage' + ' because this message shall be ignored.' + ) readPosition += sendersAddressVersionLength sendersStream, sendersStreamLength = decodeVarint( decryptedData[readPosition:readPosition + 9]) if sendersStream != cleartextStreamNumber: - logger.info('The stream number outside of the encryption on which the POW was completed doesn\'t match the stream number inside the encryption. Ignoring broadcast.') - return + return logger.info( + 'The stream number outside of the encryption on which the' + ' POW was completed doesn\'t match the stream number' + ' inside the encryption. Ignoring broadcast.' + ) readPosition += sendersStreamLength - behaviorBitfield = decryptedData[readPosition:readPosition + 4] readPosition += 4 sendersPubSigningKey = '\x04' + \ decryptedData[readPosition:readPosition + 64] @@ -694,32 +861,41 @@ def processbroadcast(self, data): decryptedData[readPosition:readPosition + 64] readPosition += 64 if sendersAddressVersion >= 3: - requiredAverageProofOfWorkNonceTrialsPerByte, varintLength = decodeVarint( - decryptedData[readPosition:readPosition + 10]) + requiredAverageProofOfWorkNonceTrialsPerByte, varintLength = \ + decodeVarint(decryptedData[readPosition:readPosition + 10]) readPosition += varintLength - logger.debug('sender\'s requiredAverageProofOfWorkNonceTrialsPerByte is %s' % requiredAverageProofOfWorkNonceTrialsPerByte) + logger.debug( + 'sender\'s requiredAverageProofOfWorkNonceTrialsPerByte' + ' is %s', requiredAverageProofOfWorkNonceTrialsPerByte) requiredPayloadLengthExtraBytes, varintLength = decodeVarint( decryptedData[readPosition:readPosition + 10]) readPosition += varintLength - logger.debug('sender\'s requiredPayloadLengthExtraBytes is %s' % requiredPayloadLengthExtraBytes) + logger.debug( + 'sender\'s requiredPayloadLengthExtraBytes is %s', + requiredPayloadLengthExtraBytes) endOfPubkeyPosition = readPosition - sha = hashlib.new('sha512') - sha.update(sendersPubSigningKey + sendersPubEncryptionKey) - ripeHasher = hashlib.new('ripemd160') - ripeHasher.update(sha.digest()) - calculatedRipe = ripeHasher.digest() + calculatedRipe = highlevelcrypto.to_ripe( + sendersPubSigningKey, sendersPubEncryptionKey) if broadcastVersion == 4: if toRipe != calculatedRipe: - logger.info('The encryption key used to encrypt this message doesn\'t match the keys inbedded in the message itself. Ignoring message.') - return + return logger.info( + 'The encryption key used to encrypt this message' + ' doesn\'t match the keys inbedded in the message' + ' itself. Ignoring message.' + ) elif broadcastVersion == 5: - calculatedTag = hashlib.sha512(hashlib.sha512(encodeVarint( - sendersAddressVersion) + encodeVarint(sendersStream) + calculatedRipe).digest()).digest()[32:] + calculatedTag = highlevelcrypto.double_sha512( + encodeVarint(sendersAddressVersion) + + encodeVarint(sendersStream) + calculatedRipe + )[32:] if calculatedTag != embeddedTag: - logger.debug('The tag and encryption key used to encrypt this message doesn\'t match the keys inbedded in the message itself. Ignoring message.') - return + return logger.debug( + 'The tag and encryption key used to encrypt this' + ' message doesn\'t match the keys inbedded in the' + ' message itself. Ignoring message.' + ) messageEncodingType, messageEncodingTypeLength = decodeVarint( decryptedData[readPosition:readPosition + 9]) if messageEncodingType == 0: @@ -737,15 +913,17 @@ def processbroadcast(self, data): signature = decryptedData[ readPosition:readPosition + signatureLength] signedData += decryptedData[:readPositionAtBottomOfMessage] - if not highlevelcrypto.verify(signedData, signature, hexlify(sendersPubSigningKey)): + if not highlevelcrypto.verify( + signedData, signature, hexlify(sendersPubSigningKey)): logger.debug('ECDSA verify failed') return logger.debug('ECDSA verify passed') - sigHash = hashlib.sha512(hashlib.sha512(signature).digest()).digest()[32:] # Used to detect and ignore duplicate messages in our inbox + # Used to detect and ignore duplicate messages in our inbox + sigHash = highlevelcrypto.double_sha512(signature)[32:] fromAddress = encodeAddress( sendersAddressVersion, sendersStream, calculatedRipe) - logger.info('fromAddress: %s' % fromAddress) + logger.info('fromAddress: %s', fromAddress) # Let's store the public key in case we want to reply to this person. sqlExecute('''INSERT INTO pubkeys VALUES (?,?,?,?,?)''', @@ -760,12 +938,9 @@ def processbroadcast(self, data): # and send it. self.possibleNewPubkey(fromAddress) - fromAddress = encodeAddress( - sendersAddressVersion, sendersStream, calculatedRipe) - logger.debug('fromAddress: ' + fromAddress) - try: - decodedMessage = helper_msgcoding.MsgDecode(messageEncodingType, message) + decodedMessage = helper_msgcoding.MsgDecode( + messageEncodingType, message) except helper_msgcoding.MsgDecodeException: return subject = decodedMessage.subject @@ -785,79 +960,91 @@ def processbroadcast(self, data): # 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'): - try: - apiNotifyPath = BMConfigParser().get( - 'bitmessagesettings', 'apinotifypath') - except: - apiNotifyPath = '' - if apiNotifyPath != '': - call([apiNotifyPath, "newBroadcast"]) + if config.safeGetBoolean('bitmessagesettings', 'apienabled'): + apiNotifyPath = config.safeGet( + 'bitmessagesettings', 'apinotifypath') + if apiNotifyPath: + subprocess.call([apiNotifyPath, "newBroadcast"]) # nosec B603 # Display timing data - logger.info('Time spent processing this interesting broadcast: %s' % (time.time() - messageProcessingStartTime,)) - + logger.info( + 'Time spent processing this interesting broadcast: %s', + time.time() - messageProcessingStartTime) def possibleNewPubkey(self, address): """ - We have inserted a pubkey into our pubkey table which we received from a - pubkey, msg, or broadcast message. It might be one that we have been - waiting for. Let's check. + We have inserted a pubkey into our pubkey table which we received + from a pubkey, msg, or broadcast message. It might be one that we + have been waiting for. Let's check. """ - - # For address versions <= 3, we wait on a key with the correct address version, - # stream number, and RIPE hash. - status, addressVersion, streamNumber, ripe = decodeAddress(address) - if addressVersion <=3: + + # For address versions <= 3, we wait on a key with the correct + # address version, stream number and RIPE hash. + addressVersion, streamNumber, ripe = decodeAddress(address)[1:] + if addressVersion <= 3: if address in state.neededPubkeys: del state.neededPubkeys[address] self.sendMessages(address) else: - logger.debug('We don\'t need this pub key. We didn\'t ask for it. For address: %s' % address) + logger.debug( + 'We don\'t need this pub key. We didn\'t ask for it.' + ' For address: %s', address) # For address versions >= 4, we wait on a pubkey with the correct tag. # Let us create the tag from the address and see if we were waiting # for it. elif addressVersion >= 4: - tag = hashlib.sha512(hashlib.sha512(encodeVarint( - addressVersion) + encodeVarint(streamNumber) + ripe).digest()).digest()[32:] + tag = highlevelcrypto.double_sha512( + encodeVarint(addressVersion) + encodeVarint(streamNumber) + + ripe + )[32:] if tag in state.neededPubkeys: del state.neededPubkeys[tag] self.sendMessages(address) - def sendMessages(self, address): + @staticmethod + def sendMessages(address): """ - This function is called by the possibleNewPubkey function when - that function sees that we now have the necessary pubkey - to send one or more messages. + This method is called by the `possibleNewPubkey` when it sees + that we now have the necessary pubkey to send one or more messages. """ logger.info('We have been awaiting the arrival of this pubkey.') sqlExecute( - '''UPDATE sent SET status='doingmsgpow', retrynumber=0 WHERE toaddress=? AND (status='awaitingpubkey' or status='doingpubkeypow') AND folder='sent' ''', - address) + "UPDATE sent SET status='doingmsgpow', retrynumber=0" + " WHERE toaddress=?" + " AND (status='awaitingpubkey' OR status='doingpubkeypow')" + " AND folder='sent'", address) queues.workerQueue.put(('sendmessage', '')) - def ackDataHasAValidHeader(self, ackData): + @staticmethod + def ackDataHasAValidHeader(ackData): + """Checking ackData with valid Header, not sending ackData when false""" if len(ackData) < protocol.Header.size: - logger.info('The length of ackData is unreasonably short. Not sending ackData.') + logger.info( + 'The length of ackData is unreasonably short. Not sending' + ' ackData.') return False - - magic,command,payloadLength,checksum = protocol.Header.unpack(ackData[:protocol.Header.size]) - if magic != 0xE9BEB4D9: + + magic, command, payloadLength, checksum = protocol.Header.unpack( + ackData[:protocol.Header.size]) + if magic != protocol.magic: logger.info('Ackdata magic bytes were wrong. Not sending ackData.') return False payload = ackData[protocol.Header.size:] if len(payload) != payloadLength: - logger.info('ackData payload length doesn\'t match the payload length specified in the header. Not sending ackdata.') + logger.info( + 'ackData payload length doesn\'t match the payload length' + ' specified in the header. Not sending ackdata.') return False - if payloadLength > 1600100: # ~1.6 MB which is the maximum possible size of an inv message. - """ - The largest message should be either an inv or a getdata message at 1.6 MB in size. - That doesn't mean that the object may be that big. The - shared.checkAndShareObjectWithPeers function will verify that it is no larger than - 2^18 bytes. - """ + # ~1.6 MB which is the maximum possible size of an inv message. + if payloadLength > 1600100: + # The largest message should be either an inv or a getdata + # message at 1.6 MB in size. + # That doesn't mean that the object may be that big. The + # shared.checkAndShareObjectWithPeers function will verify + # that it is no larger than 2^18 bytes. return False - if checksum != hashlib.sha512(payload).digest()[0:4]: # test the checksum in the message. + # test the checksum in the message. + if checksum != hashlib.sha512(payload).digest()[0:4]: logger.info('ackdata checksum wrong. Not sending ackdata.') return False command = command.rstrip('\x00') @@ -865,27 +1052,12 @@ def ackDataHasAValidHeader(self, ackData): return False return True - def addMailingListNameToSubject(self, subject, mailingListName): + @staticmethod + def addMailingListNameToSubject(subject, mailingListName): + """Adding mailingListName to subject""" subject = subject.strip() if subject[:3] == 'Re:' or subject[:3] == 'RE:': subject = subject[3:].strip() if '[' + mailingListName + ']' in subject: return subject - else: - return '[' + mailingListName + '] ' + subject - - def decodeType2Message(self, message): - bodyPositionIndex = string.find(message, '\nBody:') - if bodyPositionIndex > 1: - subject = message[8:bodyPositionIndex] - # Only save and show the first 500 characters of the subject. - # Any more is probably an attack. - subject = subject[:500] - body = message[bodyPositionIndex + 6:] - else: - subject = '' - body = message - # Throw away any extra lines (headers) after the subject. - if subject: - subject = subject.splitlines()[0] - return subject, body + return '[' + mailingListName + '] ' + subject diff --git a/src/class_objectProcessorQueue.py b/src/class_objectProcessorQueue.py deleted file mode 100644 index 6309e994a7..0000000000 --- a/src/class_objectProcessorQueue.py +++ /dev/null @@ -1,27 +0,0 @@ -import Queue -import threading -import time - -class ObjectProcessorQueue(Queue.Queue): - maxSize = 32000000 - - def __init__(self): - Queue.Queue.__init__(self) - self.sizeLock = threading.Lock() - self.curSize = 0 # in Bytes. We maintain this to prevent nodes from flooing us with objects which take up too much memory. If this gets too big we'll sleep before asking for further objects. - - def put(self, item, block = True, timeout = None): - while self.curSize >= self.maxSize: - time.sleep(1) - with self.sizeLock: - self.curSize += len(item[1]) - Queue.Queue.put(self, item, block, timeout) - - def get(self, block = True, timeout = None): - try: - item = Queue.Queue.get(self, block, timeout) - except Queue.Empty as e: - raise Queue.Empty() - with self.sizeLock: - self.curSize -= len(item[1]) - return item diff --git a/src/class_singleCleaner.py b/src/class_singleCleaner.py index b9f356200d..06153dcf28 100644 --- a/src/class_singleCleaner.py +++ b/src/class_singleCleaner.py @@ -1,136 +1,135 @@ -import gc -import threading -import shared -import time -import os - -import tr#anslate -from bmconfigparser import BMConfigParser -from helper_sql import * -from helper_threading import * -from inventory import Inventory -from network.connectionpool import BMConnectionPool -from debug import logger -import knownnodes -import queues -import state - """ -The singleCleaner class is a timer-driven thread that cleans data structures -to free memory, resends messages when a remote node doesn't respond, and +The `singleCleaner` class is a timer-driven thread that cleans data structures +to free memory, resends messages when a remote node doesn't respond, and sends pong messages to keep connections alive if the network isn't busy. + It cleans these data structures in memory: -inventory (moves data to the on-disk sql database) -inventorySets (clears then reloads data out of sql database) + - inventory (moves data to the on-disk sql database) + - inventorySets (clears then reloads data out of sql database) It cleans these tables on the disk: -inventory (clears expired objects) -pubkeys (clears pubkeys older than 4 weeks old which we have not used personally) -knownNodes (clears addresses which have not been online for over 3 days) + - inventory (clears expired objects) + - pubkeys (clears pubkeys older than 4 weeks old which we have not used + personally) + - knownNodes (clears addresses which have not been online for over 3 days) It resends messages when there has been no response: -resends getpubkey messages in 5 days (then 10 days, then 20 days, etc...) -resends msg messages in 5 days (then 10 days, then 20 days, etc...) + - resends getpubkey messages in 5 days (then 10 days, then 20 days, etc...) + - resends msg messages in 5 days (then 10 days, then 20 days, etc...) """ +import gc +import os +import time -class singleCleaner(threading.Thread, StoppableThread): +import queues +import state +from bmconfigparser import config +from helper_sql import sqlExecute, sqlQuery +from network import connectionpool, knownnodes, StoppableThread +from tr import _translate + + +#: Equals 4 weeks. You could make this longer if you want +#: but making it shorter would not be advisable because +#: there is a very small possibility that it could keep you +#: from obtaining a needed pubkey for a period of time. +lengthOfTimeToHoldOnToAllPubkeys = 2419200 + + +class singleCleaner(StoppableThread): + """The singleCleaner thread class""" + name = "singleCleaner" cycleLength = 300 expireDiscoveredPeers = 300 - def __init__(self): - threading.Thread.__init__(self, name="singleCleaner") - self.initStop() - - def run(self): + def run(self): # pylint: disable=too-many-branches gc.disable() timeWeLastClearedInventoryAndPubkeysTables = 0 try: - shared.maximumLengthOfTimeToBotherResendingMessages = (float(BMConfigParser().get('bitmessagesettings', 'stopresendingafterxdays')) * 24 * 60 * 60) + (float(BMConfigParser().get('bitmessagesettings', 'stopresendingafterxmonths')) * (60 * 60 * 24 *365)/12) - except: - # Either the user hasn't set stopresendingafterxdays and stopresendingafterxmonths yet or the options are missing from the config file. - shared.maximumLengthOfTimeToBotherResendingMessages = float('inf') - - # initial wait - if state.shutdown == 0: - self.stop.wait(singleCleaner.cycleLength) + state.maximumLengthOfTimeToBotherResendingMessages = ( + config.getfloat( + 'bitmessagesettings', 'stopresendingafterxdays') + * 24 * 60 * 60 + ) + ( + config.getfloat( + 'bitmessagesettings', 'stopresendingafterxmonths') + * (60 * 60 * 24 * 365) / 12) + except: # noqa:E722 + # Either the user hasn't set stopresendingafterxdays and + # stopresendingafterxmonths yet or the options are missing + # from the config file. + state.maximumLengthOfTimeToBotherResendingMessages = float('inf') while state.shutdown == 0: + self.stop.wait(self.cycleLength) queues.UISignalQueue.put(( - 'updateStatusBar', 'Doing housekeeping (Flushing inventory in memory to disk...)')) - Inventory().flush() + 'updateStatusBar', + 'Doing housekeeping (Flushing inventory in memory to disk...)' + )) + state.Inventory.flush() queues.UISignalQueue.put(('updateStatusBar', '')) - + # If we are running as a daemon then we are going to fill up the UI # queue which will never be handled by a UI. We should clear it to # save memory. - if shared.thisapp.daemon or not state.enableGUI: # FIXME redundant? + # FIXME redundant? + if state.thisapp.daemon or not state.enableGUI: queues.UISignalQueue.queue.clear() - if timeWeLastClearedInventoryAndPubkeysTables < int(time.time()) - 7380: - timeWeLastClearedInventoryAndPubkeysTables = int(time.time()) - Inventory().clean() + + tick = int(time.time()) + if timeWeLastClearedInventoryAndPubkeysTables < tick - 7380: + timeWeLastClearedInventoryAndPubkeysTables = tick + state.Inventory.clean() + queues.workerQueue.put(('sendOnionPeerObj', '')) # pubkeys sqlExecute( - '''DELETE FROM pubkeys WHERE time?) ''', - int(time.time()), - int(time.time()) - shared.maximumLengthOfTimeToBotherResendingMessages) - for row in queryreturn: - if len(row) < 2: - logger.error('Something went wrong in the singleCleaner thread: a query did not return the requested fields. ' + repr(row)) - self.stop.wait(3) - break - toAddress, ackData, status = row + "SELECT toaddress, ackdata, status FROM sent" + " WHERE ((status='awaitingpubkey' OR status='msgsent')" + " AND folder='sent' AND sleeptill?)", + tick, + tick - state.maximumLengthOfTimeToBotherResendingMessages + ) + for toAddress, ackData, status in queryreturn: if status == 'awaitingpubkey': - resendPubkeyRequest(toAddress) + self.resendPubkeyRequest(toAddress) elif status == 'msgsent': - resendMsg(ackData) - - # cleanup old nodes - now = int(time.time()) - with knownnodes.knownNodesLock: - for stream in knownnodes.knownNodes: - keys = knownnodes.knownNodes[stream].keys() - for node in keys: - try: - # scrap old nodes - if now - knownnodes.knownNodes[stream][node]["lastseen"] > 2419200: # 28 days - shared.needToWriteKnownNodesToDisk = True - del knownnodes.knownNodes[stream][node] - continue - # scrap old nodes with low rating - if now - knownnodes.knownNodes[stream][node]["lastseen"] > 10800 and knownnodes.knownNodes[stream][node]["rating"] <= knownnodes.knownNodesForgetRating: - shared.needToWriteKnownNodesToDisk = True - del knownnodes.knownNodes[stream][node] - continue - except TypeError: - print "Error in %s" % (str(node)) - keys = [] - - # Let us write out the knowNodes to disk if there is anything new to write out. - if shared.needToWriteKnownNodesToDisk: - try: - knownnodes.saveKnownNodes() - except Exception as err: - if "Errno 28" in str(err): - logger.fatal('(while receiveDataThread knownnodes.needToWriteKnownNodesToDisk) Alert: Your disk or data storage volume is full. ') - queues.UISignalQueue.put(('alert', (tr._translate("MainWindow", "Disk full"), tr._translate("MainWindow", 'Alert: Your disk or data storage volume is full. Bitmessage will now exit.'), True))) - if shared.thisapp.daemon or not state.enableGUI: # FIXME redundant? - os._exit(0) - shared.needToWriteKnownNodesToDisk = False - -# # clear download queues -# for thread in threading.enumerate(): -# if thread.isAlive() and hasattr(thread, 'downloadQueue'): -# thread.downloadQueue.clear() + self.resendMsg(ackData) + + try: + # Cleanup knownnodes and handle possible severe exception + # while writing it to disk + if state.enableNetwork: + knownnodes.cleanupKnownNodes(connectionpool.pool) + except Exception as err: + if "Errno 28" in str(err): + self.logger.fatal( + '(while writing knownnodes to disk)' + ' Alert: Your disk or data storage volume is full.' + ) + queues.UISignalQueue.put(( + 'alert', + (_translate("MainWindow", "Disk full"), + _translate( + "MainWindow", + 'Alert: Your disk or data storage volume' + ' is full. Bitmessage will now exit.'), + True) + )) + # FIXME redundant? + if state.thisapp.daemon or not state.enableGUI: + os._exit(1) # pylint: disable=protected-access # inv/object tracking - for connection in BMConnectionPool().inboundConnections.values() + BMConnectionPool().outboundConnections.values(): + for connection in connectionpool.pool.connections(): connection.clean() # discovery tracking @@ -141,34 +140,48 @@ def run(self): del state.discoveredPeers[k] except KeyError: pass - # TODO: cleanup pending upload / download + # ..todo:: cleanup pending upload / download gc.collect() - if state.shutdown == 0: - self.stop.wait(singleCleaner.cycleLength) - - -def resendPubkeyRequest(address): - logger.debug('It has been a long time and we haven\'t heard a response to our getpubkey request. Sending again.') - try: - del state.neededPubkeys[ - address] # We need to take this entry out of the neededPubkeys structure because the queues.workerQueue checks to see whether the entry is already present and will not do the POW and send the message because it assumes that it has already done it recently. - except: - pass - - queues.UISignalQueue.put(( - 'updateStatusBar', 'Doing work necessary to again attempt to request a public key...')) - sqlExecute( - '''UPDATE sent SET status='msgqueued' WHERE toaddress=?''', - address) - queues.workerQueue.put(('sendmessage', '')) - -def resendMsg(ackdata): - logger.debug('It has been a long time and we haven\'t heard an acknowledgement to our msg. Sending again.') - sqlExecute( - '''UPDATE sent SET status='msgqueued' WHERE ackdata=?''', - ackdata) - queues.workerQueue.put(('sendmessage', '')) - queues.UISignalQueue.put(( - 'updateStatusBar', 'Doing work necessary to again attempt to deliver a message...')) + def resendPubkeyRequest(self, address): + """Resend pubkey request for address""" + self.logger.debug( + 'It has been a long time and we haven\'t heard a response to our' + ' getpubkey request. Sending again.' + ) + try: + # We need to take this entry out of the neededPubkeys structure + # because the queues.workerQueue checks to see whether the entry + # is already present and will not do the POW and send the message + # because it assumes that it has already done it recently. + del state.neededPubkeys[address] + except KeyError: + pass + except RuntimeError: + self.logger.warning( + "Can't remove %s from neededPubkeys, requesting pubkey will be delayed", address, exc_info=True) + + queues.UISignalQueue.put(( + 'updateStatusBar', + 'Doing work necessary to again attempt to request a public key...' + )) + sqlExecute( + "UPDATE sent SET status = 'msgqueued'" + " WHERE toaddress = ? AND folder = 'sent'", address) + queues.workerQueue.put(('sendmessage', '')) + + def resendMsg(self, ackdata): + """Resend message by ackdata""" + self.logger.debug( + 'It has been a long time and we haven\'t heard an acknowledgement' + ' to our msg. Sending again.' + ) + sqlExecute( + "UPDATE sent SET status = 'msgqueued'" + " WHERE ackdata = ? AND folder = 'sent'", ackdata) + queues.workerQueue.put(('sendmessage', '')) + queues.UISignalQueue.put(( + 'updateStatusBar', + 'Doing work necessary to again attempt to deliver a message...' + )) diff --git a/src/class_singleWorker.py b/src/class_singleWorker.py index c95d484ae2..da9d64c6ad 100644 --- a/src/class_singleWorker.py +++ b/src/class_singleWorker.py @@ -1,40 +1,42 @@ +""" +Thread for performing PoW +""" +# pylint: disable=protected-access,too-many-branches,too-many-statements +# pylint: disable=no-self-use,too-many-lines,too-many-locals + from __future__ import division -import time -import threading import hashlib -from struct import pack -# used when the API must execute an outside program -from subprocess import call +import time from binascii import hexlify, unhexlify +from struct import pack +from subprocess import call # nosec -import tr +from six.moves import configparser, queue +from six.moves.reprlib import repr + +import defaults +import helper_inbox +import helper_msgcoding +import helper_random +import helper_sql +import highlevelcrypto import l10n +import proofofwork import protocol import queues -import state import shared -import defaults -import highlevelcrypto -import proofofwork -import helper_inbox -import helper_random -import helper_msgcoding -from bmconfigparser import BMConfigParser -from debug import logger -from inventory import Inventory -from addresses import ( - decodeAddress, encodeVarint, decodeVarint, calculateInventoryHash -) -# from helper_generic import addDataPadding -from helper_threading import StoppableThread -from helper_sql import sqlQuery, sqlExecute - +import state +import tr +from addresses import decodeAddress, decodeVarint, encodeVarint +from bmconfigparser import config +from helper_sql import sqlExecute, sqlQuery +from network import StoppableThread, invQueue, knownnodes -# This thread, of which there is only one, does the heavy lifting: -# calculating POWs. def sizeof_fmt(num, suffix='h/s'): + """Format hashes per seconds nicely (SI prefix)""" + for unit in ['', 'k', 'M', 'G', 'T', 'P', 'E', 'Z']: if abs(num) < 1000.0: return "%3.1f%s%s" % (num, unit, suffix) @@ -42,25 +44,29 @@ def sizeof_fmt(num, suffix='h/s'): return "%.1f%s%s" % (num, 'Yi', suffix) -class singleWorker(threading.Thread, StoppableThread): +class singleWorker(StoppableThread): + """Thread for performing PoW""" def __init__(self): - # QThread.__init__(self, parent) - threading.Thread.__init__(self, name="singleWorker") - self.initStop() + super(singleWorker, self).__init__(name="singleWorker") + self.digestAlg = config.safeGet( + 'bitmessagesettings', 'digestalg', 'sha256') proofofwork.init() def stopThread(self): + """Signal through the queue that the thread should be stopped""" + try: queues.workerQueue.put(("stopThread", "data")) - except: - pass + except queue.Full: + self.logger.error('workerQueue is Full') super(singleWorker, self).stopThread() def run(self): + # pylint: disable=attribute-defined-outside-init - while not state.sqlReady and state.shutdown == 0: - self.stop.wait(2) + while not helper_sql.sql_ready.wait(1.0) and state.shutdown == 0: + self.stop.wait(1.0) if state.shutdown > 0: return @@ -68,18 +74,16 @@ def run(self): 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( - encodeVarint(toAddressVersionNumber) + - encodeVarint(toStreamNumber) + toRipe - ).digest()).digest() + doubleHashOfAddressData = highlevelcrypto.double_sha512( + encodeVarint(toAddressVersionNumber) + + encodeVarint(toStreamNumber) + toRipe + ) # Note that this is the first half of the sha512 hash. privEncryptionKey = doubleHashOfAddressData[:32] tag = doubleHashOfAddressData[32:] @@ -91,37 +95,51 @@ def run(self): hexlify(privEncryptionKey)) ) - # Initialize the shared.ackdataForWhichImWatching data structure + # Initialize the state.ackdataForWhichImWatching data structure queryreturn = sqlQuery( - '''SELECT ackdata FROM sent WHERE status = 'msgsent' ''') + '''SELECT ackdata FROM sent WHERE status = 'msgsent' AND folder = 'sent' ''') for row in queryreturn: ackdata, = row - logger.info('Watching for ackdata ' + hexlify(ackdata)) - shared.ackdataForWhichImWatching[ackdata] = 0 + self.logger.info('Watching for ackdata %s', hexlify(ackdata)) + state.ackdataForWhichImWatching[ackdata] = 0 # Fix legacy (headerless) watched ackdata to include header - for oldack in shared.ackdataForWhichImWatching.keys(): - if (len(oldack) == 32): + for oldack in state.ackdataForWhichImWatching: + if len(oldack) == 32: # attach legacy header, always constant (msg/1/1) newack = '\x00\x00\x00\x02\x01\x01' + oldack - shared.ackdataForWhichImWatching[newack] = 0 + state.ackdataForWhichImWatching[newack] = 0 sqlExecute( - 'UPDATE sent SET ackdata=? WHERE ackdata=?', + '''UPDATE sent SET ackdata=? WHERE ackdata=? AND folder = 'sent' ''', newack, oldack ) - del shared.ackdataForWhichImWatching[oldack] + del state.ackdataForWhichImWatching[oldack] + + # For the case if user deleted knownnodes + # but is still having onionpeer objects in inventory + if not knownnodes.knownNodesActual: + for item in state.Inventory.by_type_and_tag(protocol.OBJECT_ONIONPEER): + queues.objectProcessorQueue.put(( + protocol.OBJECT_ONIONPEER, item.payload + )) + # FIXME: should also delete from inventory # give some time for the GUI to start # before we start on existing POW tasks. self.stop.wait(10) - if state.shutdown == 0: - # just in case there are any pending tasks for msg - # messages that have yet to be sent. - queues.workerQueue.put(('sendmessage', '')) - # just in case there are any tasks for Broadcasts - # that have yet to be sent. - queues.workerQueue.put(('sendbroadcast', '')) + if state.shutdown: + return + + # just in case there are any pending tasks for msg + # messages that have yet to be sent. + queues.workerQueue.put(('sendmessage', '')) + # just in case there are any tasks for Broadcasts + # that have yet to be sent. + queues.workerQueue.put(('sendbroadcast', '')) + + # send onionpeer object + queues.workerQueue.put(('sendOnionPeerObj', '')) while state.shutdown == 0: self.busy = 0 @@ -130,56 +148,65 @@ def run(self): if command == 'sendmessage': try: self.sendMsg() - except: - pass + except: # noqa:E722 + self.logger.warning("sendMsg didn't work") elif command == 'sendbroadcast': try: self.sendBroadcast() - except: - pass + except: # noqa:E722 + self.logger.warning("sendBroadcast didn't work") elif command == 'doPOWForMyV2Pubkey': try: self.doPOWForMyV2Pubkey(data) - except: - pass + except: # noqa:E722 + self.logger.warning("doPOWForMyV2Pubkey didn't work") elif command == 'sendOutOrStoreMyV3Pubkey': try: self.sendOutOrStoreMyV3Pubkey(data) - except: - pass + except: # noqa:E722 + self.logger.warning("sendOutOrStoreMyV3Pubkey didn't work") elif command == 'sendOutOrStoreMyV4Pubkey': try: self.sendOutOrStoreMyV4Pubkey(data) - except: - pass + except: # noqa:E722 + self.logger.warning("sendOutOrStoreMyV4Pubkey didn't work") + elif command == 'sendOnionPeerObj': + try: + self.sendOnionPeerObj(data) + except: # noqa:E722 + self.logger.warning("sendOnionPeerObj didn't work") elif command == 'resetPoW': try: proofofwork.resetPoW() - except: - pass + except: # noqa:E722 + self.logger.warning("proofofwork.resetPoW didn't work") elif command == 'stopThread': self.busy = 0 return else: - logger.error( + self.logger.error( 'Probable programming error: The command sent' ' to the workerThread is weird. It is: %s\n', command ) queues.workerQueue.task_done() - logger.info("Quitting...") + self.logger.info("Quitting...") def _getKeysForAddress(self, address): - privSigningKeyBase58 = BMConfigParser().get( - address, 'privsigningkey') - privEncryptionKeyBase58 = BMConfigParser().get( - address, 'privencryptionkey') - - privSigningKeyHex = hexlify(shared.decodeWalletImportFormat( - privSigningKeyBase58)) - privEncryptionKeyHex = hexlify(shared.decodeWalletImportFormat( - privEncryptionKeyBase58)) + 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(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 @@ -192,56 +219,45 @@ def _getKeysForAddress(self, address): return privSigningKeyHex, privEncryptionKeyHex, \ pubSigningKey, pubEncryptionKey - def _doPOWDefaults(self, payload, TTL, - log_prefix='', - log_time=False): - target = 2 ** 64 / ( - defaults.networkDefaultProofOfWorkNonceTrialsPerByte * ( - len(payload) + 8 + - defaults.networkDefaultPayloadLengthExtraBytes + (( - TTL * ( - len(payload) + 8 + - defaults.networkDefaultPayloadLengthExtraBytes - )) / (2 ** 16)) - )) - initialHash = hashlib.sha512(payload).digest() - logger.info( + @classmethod + def _doPOWDefaults( + cls, payload, TTL, + nonceTrialsPerByte=None, payloadLengthExtraBytes=None, + log_prefix='', log_time=False + ): + if not nonceTrialsPerByte: + nonceTrialsPerByte = \ + defaults.networkDefaultProofOfWorkNonceTrialsPerByte + if not payloadLengthExtraBytes: + payloadLengthExtraBytes = \ + defaults.networkDefaultPayloadLengthExtraBytes + cls.logger.info( '%s Doing proof of work... TTL set to %s', log_prefix, TTL) if log_time: start_time = time.time() - trialValue, nonce = proofofwork.run(target, initialHash) - logger.info( + trialValue, nonce = proofofwork.calculate( + payload, TTL, nonceTrialsPerByte, payloadLengthExtraBytes) + cls.logger.info( '%s Found proof of work %s Nonce: %s', log_prefix, trialValue, nonce ) try: delta = time.time() - start_time - logger.info( + cls.logger.info( 'PoW took %.1f seconds, speed %s.', delta, sizeof_fmt(nonce / delta) ) - except: # NameError + except NameError: # no start_time - no logging pass payload = pack('>Q', nonce) + payload - # inventoryHash = calculateInventoryHash(payload) return payload - # This function also broadcasts out the pubkey message - # once it is done with the POW - def doPOWForMyV2Pubkey(self, hash): + def doPOWForMyV2Pubkey(self, adressHash): + """ This function also broadcasts out the pubkey + message once it is done with the POW""" # Look up my stream number based on my address hash - """configSections = shared.config.addresses() - for addressInKeysFile in configSections: - if addressInKeysFile != 'bitmessagesettings': - status, addressVersionNumber, streamNumber, \ - hashFromThisParticularAddress = \ - decodeAddress(addressInKeysFile) - if hash == hashFromThisParticularAddress: - myAddress = addressInKeysFile - break""" - myAddress = shared.myAddressesByHash[hash] - # status - _, addressVersionNumber, streamNumber, hash = decodeAddress(myAddress) + myAddress = shared.myAddressesByHash[adressHash] + 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)) @@ -254,15 +270,15 @@ def doPOWForMyV2Pubkey(self, hash): payload += protocol.getBitfield(myAddress) try: - # privSigningKeyHex, privEncryptionKeyHex - _, _, pubSigningKey, pubEncryptionKey = \ - self._getKeysForAddress(myAddress) - except Exception as err: - logger.error( + 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 @@ -271,53 +287,58 @@ def doPOWForMyV2Pubkey(self, hash): 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, '') - logger.info('broadcasting inv with hash: %s', hexlify(inventoryHash)) + self.logger.info( + 'broadcasting inv with hash: %s', hexlify(inventoryHash)) - queues.invQueue.put((streamNumber, inventoryHash)) + invQueue.put((streamNumber, inventoryHash)) queues.UISignalQueue.put(('updateStatusBar', '')) try: - BMConfigParser().set( + config.set( myAddress, 'lastpubkeysendtime', str(int(time.time()))) - BMConfigParser().save() - except: + 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("config.set didn't work") - # If this isn't a chan address, this function assembles the pubkey data, - # does the necessary POW and sends it out. If it *is* a chan then it - # assembles the pubkey and stores is in the pubkey table so that we can - # send messages to "ourselves". - def sendOutOrStoreMyV3Pubkey(self, hash): + def sendOutOrStoreMyV3Pubkey(self, adressHash): + """ + If this isn't a chan address, this function assembles the pubkey data, does the necessary POW and sends it out. + If it *is* a chan then it assembles the pubkey and stores is in the pubkey table so that we can send messages + to "ourselves". + """ try: - myAddress = shared.myAddressesByHash[hash] - except: - # The address has been deleted. + myAddress = shared.myAddressesByHash[adressHash] + except KeyError: + self.logger.warning( # The address has been deleted. + "Can't find %s in myAddressByHash", hexlify(adressHash)) return - if BMConfigParser().safeGetBoolean(myAddress, 'chan'): - logger.info('This is a chan address. Not sending pubkey.') + if config.safeGetBoolean(myAddress, 'chan'): + self.logger.info('This is a chan address. Not sending pubkey.') return - status, addressVersionNumber, streamNumber, hash = decodeAddress( + _, addressVersionNumber, streamNumber, adressHash = decodeAddress( myAddress) # 28 days from now plus or minus five minutes TTL = int(28 * 24 * 60 * 60 + helper_random.randomrandrange(-300, 300)) embeddedTime = int(time.time() + TTL) + # signedTimeForProtocolV2 = embeddedTime - TTL - """ - According to the protocol specification, the expiresTime - along with the pubkey information is signed. But to be - backwards compatible during the upgrade period, we shall sign - not the expiresTime but rather the current time. There must be - precisely a 28 day difference between the two. After the upgrade - period we'll switch to signing the whole payload with the - expiresTime time. - """ + # According to the protocol specification, the expiresTime + # along with the pubkey information is signed. But to be + # backwards compatible during the upgrade period, we shall sign + # not the expiresTime but rather the current time. There must be + # precisely a 28 day difference between the two. After the upgrade + # period we'll switch to signing the whole payload with the + # expiresTime time. + payload = pack('>Q', (embeddedTime)) payload += '\x00\x00\x00\x01' # object type: pubkey payload += encodeVarint(addressVersionNumber) # Address version number @@ -329,22 +350,24 @@ def sendOutOrStoreMyV3Pubkey(self, hash): # , privEncryptionKeyHex privSigningKeyHex, _, pubSigningKey, pubEncryptionKey = \ self._getKeysForAddress(myAddress) - except Exception as err: - logger.error( + 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 @@ -352,34 +375,40 @@ def sendOutOrStoreMyV3Pubkey(self, hash): 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, '') - logger.info('broadcasting inv with hash: ' + hexlify(inventoryHash)) + self.logger.info( + 'broadcasting inv with hash: %s', hexlify(inventoryHash)) - queues.invQueue.put((streamNumber, inventoryHash)) + invQueue.put((streamNumber, inventoryHash)) queues.UISignalQueue.put(('updateStatusBar', '')) try: - BMConfigParser().set( + config.set( myAddress, 'lastpubkeysendtime', str(int(time.time()))) - BMConfigParser().save() - except: + 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") - # If this isn't a chan address, this function assembles - # the pubkey data, does the necessary POW and sends it out. def sendOutOrStoreMyV4Pubkey(self, myAddress): - if not BMConfigParser().has_section(myAddress): + """ + It doesn't send directly anymore. It put is to a queue for another thread to send at an appropriate time, + whereas in the past it directly appended it to the outgoing buffer, I think. Same with all the other methods in + this class. + """ + if not config.has_section(myAddress): # The address has been deleted. return - if shared.BMConfigParser().safeGetBoolean(myAddress, 'chan'): - logger.info('This is a chan address. Not sending pubkey.') + if config.safeGetBoolean(myAddress, 'chan'): + self.logger.info('This is a chan address. Not sending pubkey.') return - status, addressVersionNumber, streamNumber, hash = decodeAddress( + _, addressVersionNumber, streamNumber, addressHash = decodeAddress( myAddress) # 28 days from now plus or minus five minutes @@ -395,19 +424,20 @@ def sendOutOrStoreMyV4Pubkey(self, myAddress): # , privEncryptionKeyHex privSigningKeyHex, _, pubSigningKey, pubEncryptionKey = \ self._getKeysForAddress(myAddress) - except Exception as err: - logger.error( + 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 @@ -417,14 +447,13 @@ def sendOutOrStoreMyV4Pubkey(self, myAddress): # 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( - encodeVarint(addressVersionNumber) + - encodeVarint(streamNumber) + hash - ).digest()).digest() + doubleHashOfAddressData = highlevelcrypto.double_sha512( + encodeVarint(addressVersionNumber) + + encodeVarint(streamNumber) + addressHash + ) payload += doubleHashOfAddressData[32:] # the tag signature = highlevelcrypto.sign( - payload + dataToEncrypt, privSigningKeyHex - ) + payload + dataToEncrypt, privSigningKeyHex, self.digestAlg) dataToEncrypt += encodeVarint(len(signature)) dataToEncrypt += signature @@ -437,32 +466,73 @@ def sendOutOrStoreMyV4Pubkey(self, myAddress): 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:] ) - logger.info('broadcasting inv with hash: ' + hexlify(inventoryHash)) + self.logger.info( + 'broadcasting inv with hash: %s', hexlify(inventoryHash)) - queues.invQueue.put((streamNumber, inventoryHash)) + 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: - logger.error( + self.logger.error( 'Error: Couldn\'t add the lastpubkeysendtime' ' to the keys.dat file. Error message: %s', err ) + def sendOnionPeerObj(self, peer=None): + """Send onionpeer object representing peer""" + if not peer: # find own onionhostname + for peer in state.ownAddresses: + if peer.host.endswith('.onion'): + break + else: + return + TTL = int(7 * 24 * 60 * 60 + helper_random.randomrandrange(-300, 300)) + embeddedTime = int(time.time() + TTL) + streamNumber = 1 # Don't know yet what should be here + objectType = protocol.OBJECT_ONIONPEER + # FIXME: ideally the objectPayload should be signed + objectPayload = encodeVarint(peer.port) + protocol.encodeHost(peer.host) + tag = highlevelcrypto.calculateInventoryHash(objectPayload) + + if state.Inventory.by_type_and_tag(objectType, tag): + return # not expired + + payload = pack('>Q', embeddedTime) + payload += pack('>I', objectType) + payload += encodeVarint(2 if len(peer.host) == 22 else 3) + payload += encodeVarint(streamNumber) + payload += objectPayload + + payload = self._doPOWDefaults( + payload, TTL, log_prefix='(For onionpeer object)') + + 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', + hexlify(inventoryHash)) + invQueue.put((streamNumber, inventoryHash)) + def sendBroadcast(self): + """Send a broadcast-type object (assemble the object, perform PoW and put it to the inv announcement queue)""" # Reset just in case sqlExecute( '''UPDATE sent SET status='broadcastqueued' ''' - '''WHERE status = 'doingbroadcastpow' ''') + + '''WHERE status = 'doingbroadcastpow' AND folder = 'sent' ''') queryreturn = sqlQuery( '''SELECT fromaddress, subject, message, ''' ''' ackdata, ttl, encodingtype FROM sent ''' @@ -474,7 +544,7 @@ def sendBroadcast(self): _, addressVersionNumber, streamNumber, ripe = \ decodeAddress(fromaddress) if addressVersionNumber <= 1: - logger.error( + self.logger.error( 'Error: In the singleWorker thread, the ' ' sendBroadcast function doesn\'t understand' ' the address version.\n') @@ -485,7 +555,7 @@ def sendBroadcast(self): # , privEncryptionKeyHex privSigningKeyHex, _, pubSigningKey, pubEncryptionKey = \ self._getKeysForAddress(fromaddress) - except: + except ValueError: queues.UISignalQueue.put(( 'updateSentItemStatusByAckdata', ( ackdata, @@ -495,11 +565,27 @@ def sendBroadcast(self): " (your address) in the keys.dat file.")) )) continue + except Exception as err: + self.logger.error( + 'Error within sendBroadcast. Could not read' + ' the keys from the keys.dat file for a requested' + ' address. %s\n', err + ) + queues.UISignalQueue.put(( + 'updateSentItemStatusByAckdata', ( + ackdata, + tr._translate( + "MainWindow", + "Error, can't send.")) + )) + continue - sqlExecute( - '''UPDATE sent SET status='doingbroadcastpow' ''' - ''' WHERE ackdata=? AND status='broadcastqueued' ''', - ackdata) + if not sqlExecute( + '''UPDATE sent SET status='doingbroadcastpow' ''' + ''' WHERE ackdata=? AND status='broadcastqueued' ''' + ''' AND folder='sent' ''', + ackdata): + continue # At this time these pubkeys are 65 bytes long # because they include the encoding byte which we won't @@ -524,10 +610,10 @@ def sendBroadcast(self): payload += encodeVarint(streamNumber) if addressVersionNumber >= 4: - doubleHashOfAddressData = hashlib.sha512(hashlib.sha512( - encodeVarint(addressVersionNumber) + - encodeVarint(streamNumber) + ripe - ).digest()).digest() + doubleHashOfAddressData = highlevelcrypto.double_sha512( + encodeVarint(addressVersionNumber) + + encodeVarint(streamNumber) + ripe + ) tag = doubleHashOfAddressData[32:] payload += tag else: @@ -539,9 +625,9 @@ def sendBroadcast(self): 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) @@ -552,7 +638,7 @@ def sendBroadcast(self): dataToSign = payload + dataToEncrypt signature = highlevelcrypto.sign( - dataToSign, privSigningKeyHex) + dataToSign, privSigningKeyHex, self.digestAlg) dataToEncrypt += encodeVarint(len(signature)) dataToEncrypt += signature @@ -565,8 +651,8 @@ def sendBroadcast(self): # Internet connections and being stored on the disk of 3rd parties. if addressVersionNumber <= 3: privEncryptionKey = hashlib.sha512( - encodeVarint(addressVersionNumber) + - encodeVarint(streamNumber) + ripe + encodeVarint(addressVersionNumber) + + encodeVarint(streamNumber) + ripe ).digest()[:32] else: privEncryptionKey = doubleHashOfAddressData[:32] @@ -590,23 +676,23 @@ def sendBroadcast(self): # to not let the user try to send a message this large # until we implement message continuation. if len(payload) > 2 ** 18: # 256 KiB - logger.critical( + self.logger.critical( 'This broadcast object is too large to send.' ' This should never happen. Object size: %s', len(payload) ) continue - inventoryHash = calculateInventoryHash(payload) + inventoryHash = highlevelcrypto.calculateInventoryHash(payload) objectType = 3 - Inventory()[inventoryHash] = ( + state.Inventory[inventoryHash] = ( objectType, streamNumber, payload, embeddedTime, tag) - logger.info( + self.logger.info( 'sending inv (within sendBroadcast function)' ' for object: %s', hexlify(inventoryHash) ) - queues.invQueue.put((streamNumber, inventoryHash)) + invQueue.put((streamNumber, inventoryHash)) queues.UISignalQueue.put(( 'updateSentItemStatusByAckdata', ( @@ -620,16 +706,19 @@ def sendBroadcast(self): # Update the status of the message in the 'sent' table to have # a 'broadcastsent' status sqlExecute( - 'UPDATE sent SET msgid=?, status=?, lastactiontime=?' - ' WHERE ackdata=?', + '''UPDATE sent SET msgid=?, status=?, lastactiontime=? ''' + ''' WHERE ackdata=? AND folder='sent' ''', inventoryHash, 'broadcastsent', int(time.time()), ackdata ) def sendMsg(self): + """Send a message-type object (assemble the object, perform PoW and put it to the inv announcement queue)""" + # pylint: disable=too-many-nested-blocks # Reset just in case sqlExecute( '''UPDATE sent SET status='msgqueued' ''' - ''' WHERE status IN ('doingpubkeypow', 'doingmsgpow')''') + ''' WHERE status IN ('doingpubkeypow', 'doingmsgpow') ''' + ''' AND folder='sent' ''') queryreturn = sqlQuery( '''SELECT toaddress, fromaddress, subject, message, ''' ''' ackdata, status, ttl, retrynumber, encodingtype FROM ''' @@ -664,12 +753,13 @@ def sendMsg(self): # 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): - sqlExecute( + elif config.has_section(toaddress): + if not sqlExecute( '''UPDATE sent SET status='doingmsgpow' ''' - ''' WHERE toaddress=? AND status='msgqueued' ''', + ''' WHERE toaddress=? AND status='msgqueued' AND folder='sent' ''', toaddress - ) + ): + continue status = 'doingmsgpow' elif status == 'msgqueued': # Let's see if we already have the pubkey in our pubkeys table @@ -680,11 +770,12 @@ def sendMsg(self): # If we have the needed pubkey in the pubkey table already, if queryreturn != []: # set the status of this msg to doingmsgpow - sqlExecute( + if not sqlExecute( '''UPDATE sent SET status='doingmsgpow' ''' - ''' WHERE toaddress=? AND status='msgqueued' ''', + ''' WHERE toaddress=? AND status='msgqueued' AND folder='sent' ''', toaddress - ) + ): + continue status = 'doingmsgpow' # mark the pubkey as 'usedpersonally' so that # we don't delete it later. If the pubkey version @@ -701,10 +792,10 @@ def sendMsg(self): if toAddressVersionNumber <= 3: toTag = '' else: - toTag = hashlib.sha512(hashlib.sha512( - encodeVarint(toAddressVersionNumber) + - encodeVarint(toStreamNumber) + toRipe - ).digest()).digest()[32:] + toTag = highlevelcrypto.double_sha512( + encodeVarint(toAddressVersionNumber) + + encodeVarint(toStreamNumber) + toRipe + )[32:] if toaddress in state.neededPubkeys or \ toTag in state.neededPubkeys: # We already sent a request for the pubkey @@ -738,13 +829,11 @@ def sendMsg(self): # 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. @@ -755,10 +844,10 @@ def sendMsg(self): 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 shared.decryptAndCheckPubkeyPayload( + if protocol.decryptAndCheckPubkeyPayload( value.payload, toaddress ) == 'successful': needToRequestPubkey = False @@ -769,7 +858,8 @@ def sendMsg(self): ''' toaddress=? AND ''' ''' (status='msgqueued' or ''' ''' status='awaitingpubkey' or ''' - ''' status='doingpubkeypow')''', + ''' status='doingpubkeypow') AND ''' + ''' folder='sent' ''', toaddress) del state.neededPubkeys[tag] break @@ -786,7 +876,7 @@ def sendMsg(self): sqlExecute( '''UPDATE sent SET ''' ''' status='doingpubkeypow' WHERE ''' - ''' toaddress=? AND status='msgqueued' ''', + ''' toaddress=? AND status='msgqueued' AND folder='sent' ''', toaddress ) queues.UISignalQueue.put(( @@ -812,8 +902,8 @@ def sendMsg(self): embeddedTime = int(time.time() + TTL) # if we aren't sending this to ourselves or a chan - if not BMConfigParser().has_section(toaddress): - shared.ackdataForWhichImWatching[ackdata] = 0 + if not config.has_section(toaddress): + state.ackdataForWhichImWatching[ackdata] = 0 queues.UISignalQueue.put(( 'updateSentItemStatusByAckdata', ( ackdata, @@ -821,8 +911,8 @@ def sendMsg(self): "MainWindow", "Looking up the receiver\'s public key")) )) - logger.info('Sending a message.') - logger.debug( + self.logger.info('Sending a message.') + self.logger.debug( 'First 150 characters of message: %s', repr(message[:150]) ) @@ -833,7 +923,7 @@ def sendMsg(self): queryreturn = sqlQuery( 'SELECT transmitdata FROM pubkeys WHERE address=?', toaddress) - for row in queryreturn: + for row in queryreturn: # pylint: disable=redefined-outer-name pubkeyPayload, = row # The pubkey message is stored with the following items @@ -848,7 +938,7 @@ def sendMsg(self): # to bypass the address version whose length is definitely 1 readPosition = 1 - streamNumber, streamNumberLength = decodeVarint( + _, streamNumberLength = decodeVarint( pubkeyPayload[readPosition:readPosition + 10]) readPosition += streamNumberLength behaviorBitfield = pubkeyPayload[readPosition:readPosition + 4] @@ -860,13 +950,13 @@ def sendMsg(self): # if receiver is a mobile device who expects that their # address RIPE is included unencrypted on the front of # the message.. - if shared.isBitSetWithinBitfield(behaviorBitfield, 30): + 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' ): - logger.info( + self.logger.info( 'The receiver is a mobile user but the' ' sender (you) has not selected that you' ' are willing to send to mobiles. Aborting' @@ -925,58 +1015,61 @@ def sendMsg(self): # regardless of what they say is allowed in order # to get our message to propagate through the network. if requiredAverageProofOfWorkNonceTrialsPerByte < \ - defaults.networkDefaultProofOfWorkNonceTrialsPerByte: + defaults.networkDefaultProofOfWorkNonceTrialsPerByte: requiredAverageProofOfWorkNonceTrialsPerByte = \ defaults.networkDefaultProofOfWorkNonceTrialsPerByte if requiredPayloadLengthExtraBytes < \ defaults.networkDefaultPayloadLengthExtraBytes: requiredPayloadLengthExtraBytes = \ defaults.networkDefaultPayloadLengthExtraBytes - logger.debug( + self.logger.debug( 'Using averageProofOfWorkNonceTrialsPerByte: %s' ' and payloadLengthExtraBytes: %s.', requiredAverageProofOfWorkNonceTrialsPerByte, requiredPayloadLengthExtraBytes ) - queues.UISignalQueue.put(( - 'updateSentItemStatusByAckdata', ( - ackdata, - tr._translate( - "MainWindow", - "Doing work necessary to send message.\n" - "Receiver\'s required difficulty: %1" - " and %2" - ).arg(str(float( - requiredAverageProofOfWorkNonceTrialsPerByte) / - defaults.networkDefaultProofOfWorkNonceTrialsPerByte - )).arg(str(float( - requiredPayloadLengthExtraBytes) / - defaults.networkDefaultPayloadLengthExtraBytes - ))))) + + queues.UISignalQueue.put( + ( + 'updateSentItemStatusByAckdata', + ( + ackdata, + tr._translate( + "MainWindow", + "Doing work necessary to send message.\n" + "Receiver\'s required difficulty: %1" + " and %2" + ).arg( + str( + float(requiredAverageProofOfWorkNonceTrialsPerByte) + / defaults.networkDefaultProofOfWorkNonceTrialsPerByte + ) + ).arg( + str( + float(requiredPayloadLengthExtraBytes) + / defaults.networkDefaultPayloadLengthExtraBytes + ) + ) + ) + ) + ) + if status != 'forcepow': - if (requiredAverageProofOfWorkNonceTrialsPerByte - > BMConfigParser().getint( - 'bitmessagesettings', - 'maxacceptablenoncetrialsperbyte' - ) and - BMConfigParser().getint( - 'bitmessagesettings', - 'maxacceptablenoncetrialsperbyte' - ) != 0) or ( - requiredPayloadLengthExtraBytes - > BMConfigParser().getint( - 'bitmessagesettings', - 'maxacceptablepayloadlengthextrabytes' - ) and - BMConfigParser().getint( - 'bitmessagesettings', - 'maxacceptablepayloadlengthextrabytes' - ) != 0): + maxacceptablenoncetrialsperbyte = config.getint( + 'bitmessagesettings', 'maxacceptablenoncetrialsperbyte') + maxacceptablepayloadlengthextrabytes = config.getint( + 'bitmessagesettings', 'maxacceptablepayloadlengthextrabytes') + cond1 = maxacceptablenoncetrialsperbyte and \ + requiredAverageProofOfWorkNonceTrialsPerByte > maxacceptablenoncetrialsperbyte + cond2 = maxacceptablepayloadlengthextrabytes and \ + requiredPayloadLengthExtraBytes > maxacceptablepayloadlengthextrabytes + + if cond1 or cond2: # The demanded difficulty is more than # we are willing to do. sqlExecute( '''UPDATE sent SET status='toodifficult' ''' - ''' WHERE ackdata=? ''', + ''' WHERE ackdata=? AND folder='sent' ''', ackdata) queues.UISignalQueue.put(( 'updateSentItemStatusByAckdata', ( @@ -987,25 +1080,22 @@ def sendMsg(self): " the recipient (%1 and %2) is" " more difficult than you are" " willing to do. %3" - ).arg(str(float( - requiredAverageProofOfWorkNonceTrialsPerByte) - / defaults.networkDefaultProofOfWorkNonceTrialsPerByte - )).arg(str(float( - requiredPayloadLengthExtraBytes) - / defaults.networkDefaultPayloadLengthExtraBytes - )).arg(l10n.formatTimestamp())) - )) + ).arg(str(float(requiredAverageProofOfWorkNonceTrialsPerByte) + / defaults.networkDefaultProofOfWorkNonceTrialsPerByte) + ).arg(str(float(requiredPayloadLengthExtraBytes) + / defaults.networkDefaultPayloadLengthExtraBytes) + ).arg(l10n.formatTimestamp())))) continue else: # if we are sending a message to ourselves or a chan.. - logger.info('Sending a message.') - logger.debug( + self.logger.info('Sending a message.') + self.logger.debug( 'First 150 characters of message: %r', message[:150]) behaviorBitfield = protocol.getBitfield(fromaddress) try: - privEncryptionKeyBase58 = BMConfigParser().get( + privEncryptionKeyBase58 = config.get( toaddress, 'privencryptionkey') - except Exception as err: + except (configparser.NoSectionError, configparser.NoOptionError) as err: queues.UISignalQueue.put(( 'updateSentItemStatusByAckdata', ( ackdata, @@ -1018,13 +1108,14 @@ def sendMsg(self): " message. %1" ).arg(l10n.formatTimestamp())) )) - logger.error( + self.logger.error( 'Error within sendMsg. Could not read the keys' ' 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 = \ @@ -1053,7 +1144,7 @@ def sendMsg(self): privSigningKeyHex, privEncryptionKeyHex, \ pubSigningKey, pubEncryptionKey = self._getKeysForAddress( fromaddress) - except: + except ValueError: queues.UISignalQueue.put(( 'updateSentItemStatusByAckdata', ( ackdata, @@ -1063,6 +1154,20 @@ def sendMsg(self): " (your address) in the keys.dat file.")) )) continue + except Exception as err: + self.logger.error( + 'Error within sendMsg. Could not read' + ' the keys from the keys.dat file for a requested' + ' address. %s\n', err + ) + queues.UISignalQueue.put(( + 'updateSentItemStatusByAckdata', ( + ackdata, + tr._translate( + "MainWindow", + "Error, can't send.")) + )) + continue payload += pubSigningKey + pubEncryptionKey @@ -1078,9 +1183,9 @@ def sendMsg(self): 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 @@ -1093,15 +1198,15 @@ def sendMsg(self): ) payload += encodeVarint(encodedMessage.length) payload += encodedMessage.data - if BMConfigParser().has_section(toaddress): - logger.info( + if config.has_section(toaddress): + self.logger.info( 'Not bothering to include ackdata because we are' ' sending to ourselves or a chan.' ) fullAckPayload = '' elif not protocol.checkBitfield( behaviorBitfield, protocol.BITFIELD_DOESACK): - logger.info( + self.logger.info( 'Not bothering to include ackdata because' ' the receiver said that they won\'t relay it anyway.' ) @@ -1116,7 +1221,8 @@ def sendMsg(self): 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 @@ -1125,9 +1231,10 @@ def sendMsg(self): encrypted = highlevelcrypto.encrypt( payload, "04" + hexlify(pubEncryptionKeyBase256) ) - except: + except: # noqa:E722 + self.logger.warning("highlevelcrypto.encrypt didn't work") sqlExecute( - '''UPDATE sent SET status='badkey' WHERE ackdata=?''', + '''UPDATE sent SET status='badkey' WHERE ackdata=? AND folder='sent' ''', ackdata ) queues.UISignalQueue.put(( @@ -1145,69 +1252,39 @@ def sendMsg(self): encryptedPayload += '\x00\x00\x00\x02' # object type: msg encryptedPayload += encodeVarint(1) # msg version encryptedPayload += encodeVarint(toStreamNumber) + encrypted - target = 2 ** 64 / ( - requiredAverageProofOfWorkNonceTrialsPerByte * ( - len(encryptedPayload) + 8 + - requiredPayloadLengthExtraBytes + (( - TTL * ( - len(encryptedPayload) + 8 + - requiredPayloadLengthExtraBytes - )) / (2 ** 16)) - )) - logger.info( - '(For msg message) Doing proof of work. Total required' - ' difficulty: %f. Required small message difficulty: %f.', - 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) Found proof of work %s Nonce: %s', - trialValue, nonce + encryptedPayload = self._doPOWDefaults( + encryptedPayload, TTL, + requiredAverageProofOfWorkNonceTrialsPerByte, + requiredPayloadLengthExtraBytes, + log_prefix='(For msg message)', log_time=True ) - try: - logger.info( - 'PoW took %.1f seconds, speed %s.', - time.time() - powStartTime, - sizeof_fmt(nonce / (time.time() - powStartTime)) - ) - except: - pass - - encryptedPayload = pack('>Q', nonce) + encryptedPayload # Sanity check. The encryptedPayload size should never be # larger than 256 KiB. There should be checks elsewhere # in the code to not let the user try to send a message # this large until we implement message continuation. if len(encryptedPayload) > 2 ** 18: # 256 KiB - logger.critical( + self.logger.critical( 'This msg object is too large to send. This should' ' never happen. Object size: %i', len(encryptedPayload) ) 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 \ - not protocol.checkBitfield( - behaviorBitfield, protocol.BITFIELD_DOESACK): + if config.has_section(toaddress) or \ + not protocol.checkBitfield(behaviorBitfield, protocol.BITFIELD_DOESACK): queues.UISignalQueue.put(( 'updateSentItemStatusByAckdata', ( ackdata, tr._translate( "MainWindow", "Message sent. Sent at %1" - ).arg(l10n.formatTimestamp())) - )) + ).arg(l10n.formatTimestamp())))) else: # not sending to a chan or one of my addresses queues.UISignalQueue.put(( @@ -1219,17 +1296,16 @@ def sendMsg(self): " Sent on %1" ).arg(l10n.formatTimestamp())) )) - logger.info( + self.logger.info( 'Broadcasting inv for my msg(within sendmsg function): %s', hexlify(inventoryHash) ) - queues.invQueue.put((toStreamNumber, inventoryHash)) + invQueue.put((toStreamNumber, inventoryHash)) # Update the sent message in the sent table with the # necessary information. - if BMConfigParser().has_section(toaddress) or \ - not protocol.checkBitfield( - behaviorBitfield, protocol.BITFIELD_DOESACK): + if config.has_section(toaddress) or \ + not protocol.checkBitfield(behaviorBitfield, protocol.BITFIELD_DOESACK): newStatus = 'msgsentnoackexpected' else: newStatus = 'msgsent' @@ -1237,17 +1313,16 @@ def sendMsg(self): sleepTill = int(time.time() + TTL * 1.1) sqlExecute( '''UPDATE sent SET msgid=?, status=?, retrynumber=?, ''' - ''' sleeptill=?, lastactiontime=? WHERE ackdata=?''', + ''' sleeptill=?, lastactiontime=? WHERE ackdata=? AND folder='sent' ''', inventoryHash, newStatus, retryNumber + 1, sleepTill, int(time.time()), ackdata ) # 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) @@ -1258,21 +1333,23 @@ def sendMsg(self): # 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'): - try: - apiNotifyPath = BMConfigParser().get( - 'bitmessagesettings', 'apinotifypath') - except: - apiNotifyPath = '' - if apiNotifyPath != '': - call([apiNotifyPath, "newMessage"]) + + apiNotifyPath = config.safeGet( + 'bitmessagesettings', 'apinotifypath') + + if apiNotifyPath: + # There is no additional risk of remote exploitation or + # privilege escalation + call([apiNotifyPath, "newMessage"]) # nosec B603 def requestPubKey(self, toAddress): + """Send a getpubkey object""" toStatus, addressVersionNumber, streamNumber, ripe = decodeAddress( toAddress) if toStatus != 'success': - logger.error( + self.logger.error( 'Very abnormal error occurred in requestPubKey.' ' toAddress is: %r. Please report this error to Atheros.', toAddress @@ -1282,11 +1359,11 @@ def requestPubKey(self, toAddress): queryReturn = sqlQuery( '''SELECT retrynumber FROM sent WHERE toaddress=? ''' ''' AND (status='doingpubkeypow' OR status='awaitingpubkey') ''' - ''' LIMIT 1''', + ''' AND folder='sent' LIMIT 1''', toAddress ) - if len(queryReturn) == 0: - logger.critical( + if not queryReturn: + self.logger.critical( 'BUG: Why are we requesting the pubkey for %s' ' if there are no messages in the sent folder' ' to that address?', toAddress @@ -1302,16 +1379,13 @@ def requestPubKey(self, toAddress): # 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( - encodeVarint(addressVersionNumber) + - encodeVarint(streamNumber) + ripe - ).digest()).digest()[:32] + doubleHashOfAddressData = highlevelcrypto.double_sha512( + encodeVarint(addressVersionNumber) + + encodeVarint(streamNumber) + ripe + ) + 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. @@ -1334,14 +1408,13 @@ def requestPubKey(self, toAddress): payload += encodeVarint(streamNumber) if addressVersionNumber <= 3: payload += ripe - logger.info( + self.logger.info( 'making request for pubkey with ripe: %s', hexlify(ripe)) else: payload += tag - logger.info( + self.logger.info( 'making request for v4 pubkey with tag: %s', hexlify(tag)) - # print 'trial value', trialValue statusbar = 'Doing the computations necessary to request' +\ ' the recipient\'s public key.' queues.UISignalQueue.put(('updateStatusBar', statusbar)) @@ -1355,12 +1428,12 @@ def requestPubKey(self, toAddress): payload = self._doPOWDefaults(payload, TTL) - inventoryHash = calculateInventoryHash(payload) + inventoryHash = highlevelcrypto.calculateInventoryHash(payload) objectType = 1 - Inventory()[inventoryHash] = ( + state.Inventory[inventoryHash] = ( objectType, streamNumber, payload, embeddedTime, '') - logger.info('sending inv (for the getpubkey message)') - queues.invQueue.put((streamNumber, inventoryHash)) + self.logger.info('sending inv (for the getpubkey message)') + invQueue.put((streamNumber, inventoryHash)) # wait 10% past expiration sleeptill = int(time.time() + TTL * 1.1) @@ -1368,7 +1441,7 @@ def requestPubKey(self, toAddress): '''UPDATE sent SET lastactiontime=?, ''' ''' status='awaitingpubkey', retrynumber=?, sleeptill=? ''' ''' WHERE toaddress=? AND (status='doingpubkeypow' OR ''' - ''' status='awaitingpubkey') ''', + ''' status='awaitingpubkey') AND folder='sent' ''', int(time.time()), retryNumber + 1, sleeptill, toAddress) queues.UISignalQueue.put(( @@ -1388,16 +1461,14 @@ def requestPubKey(self, toAddress): ).arg(l10n.formatTimestamp())) )) - def generateFullAckMessage(self, ackdata, toStreamNumber, TTL): - # It might be perfectly fine to just use the same TTL for - # the ackdata that we use for the message. But I would rather - # it be more difficult for attackers to associate ackData with - # the associated msg object. However, users would want the TTL - # of the acknowledgement to be about the same as they set - # for the message itself. So let's set the TTL of the - # acknowledgement to be in one of three 'buckets': 1 hour, 7 - # days, or 28 days, whichever is relatively close to what the - # user specified. + def generateFullAckMessage(self, ackdata, _, TTL): + """ + It might be perfectly fine to just use the same TTL for the ackdata that we use for the message. But I would + rather it be more difficult for attackers to associate ackData with the associated msg object. However, users + would want the TTL of the acknowledgement to be about the same as they set for the message itself. So let's set + the TTL of the acknowledgement to be in one of three 'buckets': 1 hour, 7 days, or 28 days, whichever is + relatively close to what the user specified. + """ if TTL < 24 * 60 * 60: # 1 day TTL = 24 * 60 * 60 # 1 day elif TTL < 7 * 24 * 60 * 60: # 1 week diff --git a/src/class_smtpDeliver.py b/src/class_smtpDeliver.py index bb659ebef3..634dee171a 100644 --- a/src/class_smtpDeliver.py +++ b/src/class_smtpDeliver.py @@ -1,66 +1,70 @@ -from email.mime.text import MIMEText -from email.header import Header +""" +SMTP client thread for delivering emails +""" +# pylint: disable=unused-variable + import smtplib -import sys -import threading -import urlparse +from email.header import Header + +from six.moves import email_mime_text +from six.moves.urllib import parse as urlparse -from bmconfigparser import BMConfigParser -from debug import logger -from helper_threading import * import queues import state +from bmconfigparser import config +from network.threads import StoppableThread SMTPDOMAIN = "bmaddr.lan" -class smtpDeliver(threading.Thread, StoppableThread): + +class smtpDeliver(StoppableThread): + """SMTP client thread for delivery""" + name = "smtpDeliver" _instance = None - def __init__(self, parent=None): - threading.Thread.__init__(self, name="smtpDeliver") - self.initStop() - def stopThread(self): - try: - queues.UISignallerQueue.put(("stopThread", "data")) - except: - pass + """Relay shutdown instruction""" + queues.UISignalQueue.put(("stopThread", "data")) super(smtpDeliver, self).stopThread() @classmethod def get(cls): + """(probably) Singleton functionality""" if not cls._instance: cls._instance = smtpDeliver() return cls._instance def run(self): + # pylint: disable=too-many-branches,too-many-statements,too-many-locals + # pylint: disable=deprecated-lambda while state.shutdown == 0: command, data = queues.UISignalQueue.get() if command == 'writeNewAddressToTable': label, address, streamNumber = data - pass elif command == 'updateStatusBar': pass elif command == 'updateSentItemStatusByToAddress': toAddress, message = data - pass elif command == 'updateSentItemStatusByAckdata': ackData, message = data - pass elif command == 'displayNewInboxMessage': inventoryHash, toAddress, fromAddress, subject, body = data - dest = BMConfigParser().safeGet("bitmessagesettings", "smtpdeliver", '') + dest = config.safeGet("bitmessagesettings", "smtpdeliver", '') if dest == '': continue try: u = urlparse.urlparse(dest) to = urlparse.parse_qs(u.query)['to'] client = smtplib.SMTP(u.hostname, u.port) - msg = MIMEText(body, 'plain', 'utf-8') + msg = email_mime_text(body, 'plain', 'utf-8') msg['Subject'] = Header(subject, 'utf-8') msg['From'] = fromAddress + '@' + SMTPDOMAIN - toLabel = map (lambda y: BMConfigParser().safeGet(y, "label"), filter(lambda x: x == toAddress, BMConfigParser().addresses())) - if len(toLabel) > 0: + toLabel = map( + lambda y: config.safeGet(y, "label"), + filter( + lambda x: x == toAddress, config.addresses()) + ) + if toLabel: msg['To'] = "\"%s\" <%s>" % (Header(toLabel[0], 'utf-8'), toAddress + '@' + SMTPDOMAIN) else: msg['To'] = toAddress + '@' + SMTPDOMAIN @@ -68,13 +72,14 @@ def run(self): client.starttls() client.ehlo() client.sendmail(msg['From'], [to], msg.as_string()) - logger.info("Delivered via SMTP to %s through %s:%i ...", to, u.hostname, u.port) + self.logger.info( + 'Delivered via SMTP to %s through %s:%i ...', + to, u.hostname, u.port) client.quit() - except: - logger.error("smtp delivery error", exc_info=True) + except: # noqa:E722 + self.logger.error('smtp delivery error', exc_info=True) elif command == 'displayNewSentMessage': toAddress, fromLabel, fromAddress, subject, message, ackdata = data - pass elif command == 'updateNetworkStatusTab': pass elif command == 'updateNumberOfMessagesProcessed': @@ -103,9 +108,8 @@ def run(self): pass elif command == 'alert': title, text, exitAfterUserClicksOk = data - pass elif command == 'stopThread': break else: - sys.stderr.write( - 'Command sent to smtpDeliver not recognized: %s\n' % command) + self.logger.warning( + 'Command sent to smtpDeliver not recognized: %s', command) diff --git a/src/class_smtpServer.py b/src/class_smtpServer.py index b62a713002..44ea7c9cc4 100644 --- a/src/class_smtpServer.py +++ b/src/class_smtpServer.py @@ -1,30 +1,42 @@ +""" +SMTP server thread +""" import asyncore import base64 import email -from email.parser import Parser -from email.header import decode_header +import logging import re import signal import smtpd -import socket import threading import time +from email.header import decode_header +from email.parser import Parser +import queues from addresses import decodeAddress -from bmconfigparser import BMConfigParser -from debug import logger -from helper_sql import sqlExecute +from bmconfigparser import config from helper_ackPayload import genAckPayload -from helper_threading import StoppableThread -from pyelliptic.openssl import OpenSSL -import queues +from helper_sql import sqlExecute +from network.threads import StoppableThread from version import softwareVersion SMTPDOMAIN = "bmaddr.lan" LISTENPORT = 8425 +logger = logging.getLogger('default') +# pylint: disable=attribute-defined-outside-init + + +class SmtpServerChannelException(Exception): + """Generic smtp server channel exception.""" + pass + + class smtpServerChannel(smtpd.SMTPChannel): + """Asyncore channel for SMTP protocol (server)""" def smtp_EHLO(self, arg): + """Process an EHLO""" if not arg: self.push('501 Syntax: HELO hostname') return @@ -32,43 +44,48 @@ def smtp_EHLO(self, arg): self.push('250 AUTH PLAIN') def smtp_AUTH(self, arg): + """Process AUTH""" if not arg or arg[0:5] not in ["PLAIN"]: self.push('501 Syntax: AUTH PLAIN') return authstring = arg[6:] try: decoded = base64.b64decode(authstring) - correctauth = "\x00" + BMConfigParser().safeGet("bitmessagesettings", "smtpdusername", "") + \ - "\x00" + BMConfigParser().safeGet("bitmessagesettings", "smtpdpassword", "") - logger.debug("authstring: %s / %s", correctauth, decoded) + correctauth = "\x00" + config.safeGet( + "bitmessagesettings", "smtpdusername", "") + "\x00" + config.safeGet( + "bitmessagesettings", "smtpdpassword", "") + logger.debug('authstring: %s / %s', correctauth, decoded) if correctauth == decoded: self.auth = True self.push('235 2.7.0 Authentication successful') else: - raise Exception("Auth fail") - except: + raise SmtpServerChannelException("Auth fail") + except: # noqa:E722 self.push('501 Authentication fail') def smtp_DATA(self, arg): + """Process DATA""" if not hasattr(self, "auth") or not self.auth: - self.push ("530 Authentication required") + self.push('530 Authentication required') return smtpd.SMTPChannel.smtp_DATA(self, arg) class smtpServerPyBitmessage(smtpd.SMTPServer): + """Asyncore SMTP server class""" def handle_accept(self): + """Accept a connection""" pair = self.accept() if pair is not None: conn, addr = pair -# print >> DEBUGSTREAM, 'Incoming connection from %s' % repr(addr) self.channel = smtpServerChannel(self, conn, addr) def send(self, fromAddress, toAddress, subject, message): - status, addressVersionNumber, streamNumber, ripe = decodeAddress(toAddress) - stealthLevel = BMConfigParser().safeGetInt('bitmessagesettings', 'ackstealthlevel') + """Send a bitmessage""" + # pylint: disable=arguments-differ + streamNumber, ripe = decodeAddress(toAddress)[2:] + stealthLevel = config.safeGetInt('bitmessagesettings', 'ackstealthlevel') ackdata = genAckPayload(streamNumber, stealthLevel) - t = () sqlExecute( '''INSERT INTO sent VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''', '', @@ -78,64 +95,66 @@ def send(self, fromAddress, toAddress, subject, message): subject, message, ackdata, - int(time.time()), # sentTime (this will never change) - int(time.time()), # lastActionTime - 0, # sleepTill time. This will get set when the POW gets done. + int(time.time()), # sentTime (this will never change) + int(time.time()), # lastActionTime + 0, # sleepTill time. This will get set when the POW gets done. 'msgqueued', - 0, # retryNumber - 'sent', # folder - 2, # encodingtype - min(BMConfigParser().getint('bitmessagesettings', 'ttl'), 86400 * 2) # not necessary to have a TTL higher than 2 days + 0, # retryNumber + 'sent', # folder + 2, # encodingtype + # not necessary to have a TTL higher than 2 days + min(config.getint('bitmessagesettings', 'ttl'), 86400 * 2) ) queues.workerQueue.put(('sendmessage', toAddress)) def decode_header(self, hdr): + """Email header decoding""" ret = [] for h in decode_header(self.msg_headers[hdr]): if h[1]: - ret.append(unicode(h[0], h[1])) + ret.append(h[0].decode(h[1])) else: ret.append(h[0].decode("utf-8", errors='replace')) - - return ret + return ret def process_message(self, peer, mailfrom, rcpttos, data): -# print 'Receiving message from:', peer + """Process an email""" + # pylint: disable=too-many-locals, too-many-branches p = re.compile(".*<([^>]+)>") if not hasattr(self.channel, "auth") or not self.channel.auth: - logger.error("Missing or invalid auth") + logger.error('Missing or invalid auth') return try: self.msg_headers = Parser().parsestr(data) - except: - logger.error("Invalid headers") + except: # noqa:E722 + logger.error('Invalid headers') return try: sender, domain = p.sub(r'\1', mailfrom).split("@") if domain != SMTPDOMAIN: - raise Exception("Bad domain %s", domain) - if sender not in BMConfigParser().addresses(): - raise Exception("Nonexisting user %s", sender) + raise Exception("Bad domain %s" % domain) + if sender not in config.addresses(): + raise Exception("Nonexisting user %s" % sender) except Exception as err: - logger.debug("Bad envelope from %s: %s", mailfrom, repr(err)) + logger.debug('Bad envelope from %s: %r', mailfrom, err) msg_from = self.decode_header("from") try: msg_from = p.sub(r'\1', self.decode_header("from")[0]) sender, domain = msg_from.split("@") if domain != SMTPDOMAIN: - raise Exception("Bad domain %s", domain) - if sender not in BMConfigParser().addresses(): - raise Exception("Nonexisting user %s", sender) + raise Exception("Bad domain %s" % domain) + if sender not in config.addresses(): + raise Exception("Nonexisting user %s" % sender) except Exception as err: - logger.error("Bad headers from %s: %s", msg_from, repr(err)) + logger.error('Bad headers from %s: %r', msg_from, err) return try: msg_subject = self.decode_header('subject')[0] - except: + except: # noqa:E722 msg_subject = "Subject missing..." msg_tmp = email.message_from_string(data) @@ -148,54 +167,51 @@ def process_message(self, peer, mailfrom, rcpttos, data): try: rcpt, domain = p.sub(r'\1', to).split("@") if domain != SMTPDOMAIN: - raise Exception("Bad domain %s", domain) - logger.debug("Sending %s to %s about %s", sender, rcpt, msg_subject) + raise Exception("Bad domain %s" % domain) + logger.debug( + 'Sending %s to %s about %s', sender, rcpt, msg_subject) self.send(sender, rcpt, msg_subject, body) - logger.info("Relayed %s to %s", sender, rcpt) + logger.info('Relayed %s to %s', sender, rcpt) except Exception as err: - logger.error( "Bad to %s: %s", to, repr(err)) + logger.error('Bad to %s: %r', to, err) continue return -class smtpServer(threading.Thread, StoppableThread): - def __init__(self, parent=None): - threading.Thread.__init__(self, name="smtpServerThread") - self.initStop() + +class smtpServer(StoppableThread): + """SMTP server thread""" + def __init__(self, _=None): + super(smtpServer, self).__init__(name="smtpServerThread") self.server = smtpServerPyBitmessage(('127.0.0.1', LISTENPORT), None) - + def stopThread(self): super(smtpServer, self).stopThread() self.server.close() return - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) -# for ip in ('127.0.0.1', BMConfigParser().get('bitmessagesettings', 'onionbindip')): - for ip in ('127.0.0.1'): - try: - s.connect((ip, LISTENPORT)) - s.shutdown(socket.SHUT_RDWR) - s.close() - break - except: - pass def run(self): asyncore.loop(1) -def signals(signal, frame): - print "Got signal, terminating" + +def signals(_, __): + """Signal handler""" + logger.warning('Got signal, terminating') for thread in threading.enumerate(): if thread.isAlive() and isinstance(thread, StoppableThread): thread.stopThread() + def runServer(): - print "Running SMTPd thread" + """Run SMTP server as a standalone python process""" + logger.warning('Running SMTPd thread') smtpThread = smtpServer() smtpThread.start() signal.signal(signal.SIGINT, signals) signal.signal(signal.SIGTERM, signals) - print "Processing" + logger.warning('Processing') smtpThread.join() - print "The end" + logger.warning('The end') + if __name__ == "__main__": runServer() diff --git a/src/class_sqlThread.py b/src/class_sqlThread.py index a774a24bb3..144976f6b7 100644 --- a/src/class_sqlThread.py +++ b/src/class_sqlThread.py @@ -1,64 +1,87 @@ -import threading -from bmconfigparser import BMConfigParser -import sqlite3 -import time +""" +sqlThread is defined here +""" + +import os import shutil # used for moving the messages.dat file +import sqlite3 import sys -import os -from debug import logger -import defaults -import helper_sql -from namecoin import ensureNamecoinOptions -import paths -import queues -import random -import state -import string -import tr#anslate -import helper_random -# This thread exists because SQLITE3 is so un-threadsafe that we must -# submit queries to it and it puts results back in a different queue. They -# won't let us just use locks. +import threading +import time + +from six.moves.reprlib import repr + +try: + import helper_sql + import helper_startup + import paths + import queues + import state + from addresses import encodeAddress + 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 config, config_ready + from .debug import logger + from .tr import _translate class sqlThread(threading.Thread): + """A thread for all SQL operations""" def __init__(self): threading.Thread.__init__(self, name="SQL") - def run(self): + 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() - + self.cur.execute('PRAGMA secure_delete = true') + # call create_function for encode address + self.create_function() + try: self.cur.execute( - '''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 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)''') self.cur.execute( - '''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 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)''') self.cur.execute( - '''CREATE TABLE subscriptions (label text, address text, enabled bool)''' ) + '''CREATE TABLE subscriptions (label text, address text, enabled bool)''') self.cur.execute( - '''CREATE TABLE addressbook (label text, address text)''' ) + '''CREATE TABLE addressbook (label text, address text, UNIQUE(address) ON CONFLICT IGNORE)''') self.cur.execute( - '''CREATE TABLE blacklist (label text, address text, enabled bool)''' ) + '''CREATE TABLE blacklist (label text, address text, enabled bool)''') self.cur.execute( - '''CREATE TABLE whitelist (label text, address text, enabled bool)''' ) + '''CREATE TABLE whitelist (label text, address text, enabled bool)''') self.cur.execute( - '''CREATE TABLE pubkeys (address text, addressversion int, transmitdata blob, time int, usedpersonally text, UNIQUE(address) ON CONFLICT REPLACE)''' ) + '''CREATE TABLE pubkeys (address text, addressversion int, transmitdata blob, time int,''' + ''' usedpersonally text, UNIQUE(address) ON CONFLICT REPLACE)''') self.cur.execute( - '''CREATE TABLE inventory (hash blob, objecttype int, streamnumber int, payload blob, expirestime integer, tag blob, UNIQUE(hash) ON CONFLICT REPLACE)''' ) + '''CREATE TABLE inventory (hash blob, objecttype int, streamnumber int, payload blob,''' + ''' expirestime integer, tag blob, UNIQUE(hash) ON CONFLICT REPLACE)''') self.cur.execute( - '''INSERT INTO subscriptions VALUES('Bitmessage new releases/announcements','BM-GtovgYdgs7qXPkoYaRgrLFuFKz1SFpsw',1)''') + '''INSERT INTO subscriptions VALUES''' + '''('Bitmessage new releases/announcements','BM-GtovgYdgs7qXPkoYaRgrLFuFKz1SFpsw',1)''') self.cur.execute( - '''CREATE TABLE settings (key blob, value blob, UNIQUE(key) ON CONFLICT REPLACE)''' ) - self.cur.execute( '''INSERT INTO settings VALUES('version','10')''') - self.cur.execute( '''INSERT INTO settings VALUES('lastvacuumtime',?)''', ( + '''CREATE TABLE settings (key blob, value blob, UNIQUE(key) ON CONFLICT REPLACE)''') + self.cur.execute('''INSERT INTO settings VALUES('version','11')''') + self.cur.execute('''INSERT INTO settings VALUES('lastvacuumtime',?)''', ( int(time.time()),)) self.cur.execute( - '''CREATE TABLE objectprocessorqueue (objecttype int, data blob, UNIQUE(objecttype, data) ON CONFLICT REPLACE)''' ) + '''CREATE TABLE objectprocessorqueue''' + ''' (objecttype int, data blob, UNIQUE(objecttype, data) ON CONFLICT REPLACE)''') self.conn.commit() logger.info('Created messages database file') except Exception as err: @@ -70,35 +93,26 @@ def run(self): 'ERROR trying to create database file (message.dat). Error message: %s\n' % str(err)) os._exit(0) - if BMConfigParser().getint('bitmessagesettings', 'settingsversion') == 1: - BMConfigParser().set('bitmessagesettings', 'settingsversion', '2') - # 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. - BMConfigParser().set('bitmessagesettings', 'socksproxytype', 'none') - BMConfigParser().set('bitmessagesettings', 'sockshostname', 'localhost') - BMConfigParser().set('bitmessagesettings', 'socksport', '9050') - BMConfigParser().set('bitmessagesettings', 'socksauthentication', 'false') - BMConfigParser().set('bitmessagesettings', 'socksusername', '') - BMConfigParser().set('bitmessagesettings', 'sockspassword', '') - BMConfigParser().set('bitmessagesettings', 'sockslisten', 'false') - BMConfigParser().set('bitmessagesettings', 'keysencrypted', 'false') - BMConfigParser().set('bitmessagesettings', 'messagesencrypted', 'false') + # 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 = config.getint( + 'bitmessagesettings', 'settingsversion') # People running earlier versions of PyBitmessage do not have the # usedpersonally field in their pubkeys table. Let's add it. - if BMConfigParser().getint('bitmessagesettings', 'settingsversion') == 2: + if settingsversion == 2: item = '''ALTER TABLE pubkeys ADD usedpersonally text DEFAULT 'no' ''' parameters = '' self.cur.execute(item, parameters) self.conn.commit() - BMConfigParser().set('bitmessagesettings', 'settingsversion', '3') + settingsversion = 3 # People running earlier versions of PyBitmessage do not have the # encodingtype field in their inbox and sent tables or the read field # in the inbox table. Let's add them. - if BMConfigParser().getint('bitmessagesettings', 'settingsversion') == 3: + if settingsversion == 3: item = '''ALTER TABLE inbox ADD encodingtype int DEFAULT '2' ''' parameters = '' self.cur.execute(item, parameters) @@ -112,21 +126,13 @@ def run(self): self.cur.execute(item, parameters) self.conn.commit() - BMConfigParser().set('bitmessagesettings', 'settingsversion', '4') + settingsversion = 4 - if BMConfigParser().getint('bitmessagesettings', 'settingsversion') == 4: - BMConfigParser().set('bitmessagesettings', 'defaultnoncetrialsperbyte', str( - defaults.networkDefaultProofOfWorkNonceTrialsPerByte)) - BMConfigParser().set('bitmessagesettings', 'defaultpayloadlengthextrabytes', str( - defaults.networkDefaultPayloadLengthExtraBytes)) - BMConfigParser().set('bitmessagesettings', 'settingsversion', '5') + config.set( + 'bitmessagesettings', 'settingsversion', str(settingsversion)) + config.save() - if BMConfigParser().getint('bitmessagesettings', 'settingsversion') == 5: - BMConfigParser().set( - 'bitmessagesettings', 'maxacceptablenoncetrialsperbyte', '0') - BMConfigParser().set( - 'bitmessagesettings', 'maxacceptablepayloadlengthextrabytes', '0') - BMConfigParser().set('bitmessagesettings', 'settingsversion', '6') + helper_startup.updateConfig() # From now on, let us keep a 'version' embedded in the messages.dat # file so that when we make changes to the database, the database @@ -137,35 +143,41 @@ def run(self): self.cur.execute(item, parameters) if self.cur.fetchall() == []: # The settings table doesn't exist. We need to make it. - logger.debug('In messages.dat database, creating new \'settings\' table.') + logger.debug( + "In messages.dat database, creating new 'settings' table.") self.cur.execute( - '''CREATE TABLE settings (key text, value blob, UNIQUE(key) ON CONFLICT REPLACE)''' ) - self.cur.execute( '''INSERT INTO settings VALUES('version','1')''') - self.cur.execute( '''INSERT INTO settings VALUES('lastvacuumtime',?)''', ( + '''CREATE TABLE settings (key text, value blob, UNIQUE(key) ON CONFLICT REPLACE)''') + self.cur.execute('''INSERT INTO settings VALUES('version','1')''') + self.cur.execute('''INSERT INTO settings VALUES('lastvacuumtime',?)''', ( int(time.time()),)) logger.debug('In messages.dat database, removing an obsolete field from the pubkeys table.') self.cur.execute( - '''CREATE TEMPORARY TABLE pubkeys_backup(hash blob, transmitdata blob, time int, usedpersonally text, UNIQUE(hash) ON CONFLICT REPLACE);''') + '''CREATE TEMPORARY TABLE pubkeys_backup(hash blob, transmitdata blob, time int,''' + ''' usedpersonally text, UNIQUE(hash) ON CONFLICT REPLACE);''') self.cur.execute( '''INSERT INTO pubkeys_backup SELECT hash, transmitdata, time, usedpersonally FROM pubkeys;''') - self.cur.execute( '''DROP TABLE pubkeys''') + self.cur.execute('''DROP TABLE pubkeys''') self.cur.execute( - '''CREATE TABLE pubkeys (hash blob, transmitdata blob, time int, usedpersonally text, UNIQUE(hash) ON CONFLICT REPLACE)''' ) + '''CREATE TABLE pubkeys''' + ''' (hash blob, transmitdata blob, time int, usedpersonally text, UNIQUE(hash) ON CONFLICT REPLACE)''') self.cur.execute( '''INSERT INTO pubkeys SELECT hash, transmitdata, time, usedpersonally FROM pubkeys_backup;''') - self.cur.execute( '''DROP TABLE pubkeys_backup;''') - logger.debug('Deleting all pubkeys from inventory. They will be redownloaded and then saved with the correct times.') + self.cur.execute('''DROP TABLE pubkeys_backup;''') + logger.debug( + 'Deleting all pubkeys from inventory.' + ' They will be redownloaded and then saved with the correct times.') self.cur.execute( '''delete from inventory where objecttype = 'pubkey';''') logger.debug('replacing Bitmessage announcements mailing list with a new one.') self.cur.execute( '''delete from subscriptions where address='BM-BbkPSZbzPwpVcYZpU4yHwf9ZPEapN5Zx' ''') self.cur.execute( - '''INSERT INTO subscriptions VALUES('Bitmessage new releases/announcements','BM-GtovgYdgs7qXPkoYaRgrLFuFKz1SFpsw',1)''') + '''INSERT INTO subscriptions VALUES''' + '''('Bitmessage new releases/announcements','BM-GtovgYdgs7qXPkoYaRgrLFuFKz1SFpsw',1)''') logger.debug('Commiting.') self.conn.commit() logger.debug('Vacuuming message.dat. You might notice that the file size gets much smaller.') - self.cur.execute( ''' VACUUM ''') + self.cur.execute(''' VACUUM ''') # After code refactoring, the possible status values for sent messages # have changed. @@ -178,41 +190,32 @@ def run(self): self.cur.execute( '''update sent set status='broadcastqueued' where status='broadcastpending' ''') self.conn.commit() - - if not BMConfigParser().has_option('bitmessagesettings', 'sockslisten'): - BMConfigParser().set('bitmessagesettings', 'sockslisten', 'false') - - ensureNamecoinOptions() - - """# Add a new column to the inventory table to store the first 20 bytes of encrypted messages to support Android app - item = '''SELECT value FROM settings WHERE key='version';''' - parameters = '' - self.cur.execute(item, parameters) - if int(self.cur.fetchall()[0][0]) == 1: - print 'upgrading database' - item = '''ALTER TABLE inventory ADD first20bytesofencryptedmessage blob DEFAULT '' ''' - parameters = '' - self.cur.execute(item, parameters) - item = '''update settings set value=? WHERE key='version';''' - parameters = (2,) - self.cur.execute(item, parameters)""" - # Let's get rid of the first20bytesofencryptedmessage field in the inventory table. + # Let's get rid of the first20bytesofencryptedmessage field in + # the inventory table. item = '''SELECT value FROM settings WHERE key='version';''' parameters = '' self.cur.execute(item, parameters) if int(self.cur.fetchall()[0][0]) == 2: - logger.debug('In messages.dat database, removing an obsolete field from the inventory table.') + logger.debug( + 'In messages.dat database, removing an obsolete field from' + ' the inventory table.') self.cur.execute( - '''CREATE TEMPORARY TABLE inventory_backup(hash blob, objecttype text, streamnumber int, payload blob, receivedtime integer, UNIQUE(hash) ON CONFLICT REPLACE);''') + '''CREATE TEMPORARY TABLE inventory_backup''' + '''(hash blob, objecttype text, streamnumber int, payload blob,''' + ''' receivedtime integer, UNIQUE(hash) ON CONFLICT REPLACE);''') self.cur.execute( - '''INSERT INTO inventory_backup SELECT hash, objecttype, streamnumber, payload, receivedtime FROM inventory;''') - self.cur.execute( '''DROP TABLE inventory''') + '''INSERT INTO inventory_backup SELECT hash, objecttype, streamnumber, payload, receivedtime''' + ''' FROM inventory;''') + self.cur.execute('''DROP TABLE inventory''') self.cur.execute( - '''CREATE TABLE inventory (hash blob, objecttype text, streamnumber int, payload blob, receivedtime integer, UNIQUE(hash) ON CONFLICT REPLACE)''' ) + '''CREATE TABLE inventory''' + ''' (hash blob, objecttype text, streamnumber int, payload blob, receivedtime integer,''' + ''' UNIQUE(hash) ON CONFLICT REPLACE)''') self.cur.execute( - '''INSERT INTO inventory SELECT hash, objecttype, streamnumber, payload, receivedtime FROM inventory_backup;''') - self.cur.execute( '''DROP TABLE inventory_backup;''') + '''INSERT INTO inventory SELECT hash, objecttype, streamnumber, payload, receivedtime''' + ''' FROM inventory_backup;''') + self.cur.execute('''DROP TABLE inventory_backup;''') item = '''update settings set value=? WHERE key='version';''' parameters = (3,) self.cur.execute(item, parameters) @@ -223,7 +226,9 @@ def run(self): self.cur.execute(item, parameters) currentVersion = int(self.cur.fetchall()[0][0]) if currentVersion == 1 or currentVersion == 3: - logger.debug('In messages.dat database, adding tag field to the inventory table.') + logger.debug( + 'In messages.dat database, adding tag field to' + ' the inventory table.') item = '''ALTER TABLE inventory ADD tag blob DEFAULT '' ''' parameters = '' self.cur.execute(item, parameters) @@ -231,19 +236,6 @@ def run(self): parameters = (4,) self.cur.execute(item, parameters) - if not BMConfigParser().has_option('bitmessagesettings', 'userlocale'): - BMConfigParser().set('bitmessagesettings', 'userlocale', 'system') - if not BMConfigParser().has_option('bitmessagesettings', 'sendoutgoingconnections'): - BMConfigParser().set('bitmessagesettings', 'sendoutgoingconnections', 'True') - - # Raise the default required difficulty from 1 to 2 - # With the change to protocol v3, this is obsolete. - if BMConfigParser().getint('bitmessagesettings', 'settingsversion') == 6: - """if int(shared.config.get('bitmessagesettings','defaultnoncetrialsperbyte')) == defaults.networkDefaultProofOfWorkNonceTrialsPerByte: - shared.config.set('bitmessagesettings','defaultnoncetrialsperbyte', str(defaults.networkDefaultProofOfWorkNonceTrialsPerByte * 2)) - """ - BMConfigParser().set('bitmessagesettings', 'settingsversion', '7') - # 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. item = '''SELECT value FROM settings WHERE key='version';''' @@ -251,27 +243,15 @@ def run(self): self.cur.execute(item, parameters) currentVersion = int(self.cur.fetchall()[0][0]) if currentVersion == 4: - self.cur.execute( '''DROP TABLE pubkeys''') + self.cur.execute('''DROP TABLE pubkeys''') self.cur.execute( - '''CREATE TABLE pubkeys (hash blob, addressversion int, transmitdata blob, time int, usedpersonally text, UNIQUE(hash, addressversion) ON CONFLICT REPLACE)''' ) + '''CREATE TABLE pubkeys (hash blob, addressversion int, transmitdata blob, time int,''' + '''usedpersonally text, UNIQUE(hash, addressversion) ON CONFLICT REPLACE)''') self.cur.execute( '''delete from inventory where objecttype = 'pubkey';''') item = '''update settings set value=? WHERE key='version';''' parameters = (5,) self.cur.execute(item, parameters) - - if not BMConfigParser().has_option('bitmessagesettings', 'useidenticons'): - BMConfigParser().set('bitmessagesettings', 'useidenticons', 'True') - if not BMConfigParser().has_option('bitmessagesettings', 'identiconsuffix'): # acts as a salt - BMConfigParser().set('bitmessagesettings', 'identiconsuffix', ''.join(helper_random.randomchoice("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz") for x in range(12)))# a twelve character pseudo-password to salt the identicons - - #Add settings to support no longer resending messages after a certain period of time even if we never get an ack - if BMConfigParser().getint('bitmessagesettings', 'settingsversion') == 7: - BMConfigParser().set( - 'bitmessagesettings', 'stopresendingafterxdays', '') - BMConfigParser().set( - 'bitmessagesettings', 'stopresendingafterxmonths', '') - BMConfigParser().set('bitmessagesettings', 'settingsversion', '8') # Add a new table: objectprocessorqueue with which to hold objects # that have yet to be processed if the user shuts down Bitmessage. @@ -280,79 +260,57 @@ def run(self): self.cur.execute(item, parameters) currentVersion = int(self.cur.fetchall()[0][0]) if currentVersion == 5: - self.cur.execute( '''DROP TABLE knownnodes''') + self.cur.execute('''DROP TABLE knownnodes''') self.cur.execute( - '''CREATE TABLE objectprocessorqueue (objecttype text, data blob, UNIQUE(objecttype, data) ON CONFLICT REPLACE)''' ) + '''CREATE TABLE objectprocessorqueue''' + ''' (objecttype text, data blob, UNIQUE(objecttype, data) ON CONFLICT REPLACE)''') item = '''update settings set value=? WHERE key='version';''' parameters = (6,) self.cur.execute(item, parameters) - + # changes related to protocol v3 - # In table inventory and objectprocessorqueue, objecttype is now an integer (it was a human-friendly string previously) + # In table inventory and objectprocessorqueue, objecttype is now + # an integer (it was a human-friendly string previously) item = '''SELECT value FROM settings WHERE key='version';''' parameters = '' self.cur.execute(item, parameters) currentVersion = int(self.cur.fetchall()[0][0]) if currentVersion == 6: - logger.debug('In messages.dat database, dropping and recreating the inventory table.') - self.cur.execute( '''DROP TABLE inventory''') - self.cur.execute( '''CREATE TABLE inventory (hash blob, objecttype int, streamnumber int, payload blob, expirestime integer, tag blob, UNIQUE(hash) ON CONFLICT REPLACE)''' ) - self.cur.execute( '''DROP TABLE objectprocessorqueue''') - self.cur.execute( '''CREATE TABLE objectprocessorqueue (objecttype int, data blob, UNIQUE(objecttype, data) ON CONFLICT REPLACE)''' ) + logger.debug( + 'In messages.dat database, dropping and recreating' + ' the inventory table.') + self.cur.execute('''DROP TABLE inventory''') + self.cur.execute( + '''CREATE TABLE inventory''' + ''' (hash blob, objecttype int, streamnumber int, payload blob, expirestime integer,''' + ''' tag blob, UNIQUE(hash) ON CONFLICT REPLACE)''') + self.cur.execute('''DROP TABLE objectprocessorqueue''') + self.cur.execute( + '''CREATE TABLE objectprocessorqueue''' + ''' (objecttype int, data blob, UNIQUE(objecttype, data) ON CONFLICT REPLACE)''') item = '''update settings set value=? WHERE key='version';''' parameters = (7,) self.cur.execute(item, parameters) - logger.debug('Finished dropping and recreating the inventory table.') - - # With the change to protocol version 3, reset the user-settable difficulties to 1 - if BMConfigParser().getint('bitmessagesettings', 'settingsversion') == 8: - BMConfigParser().set('bitmessagesettings','defaultnoncetrialsperbyte', str(defaults.networkDefaultProofOfWorkNonceTrialsPerByte)) - BMConfigParser().set('bitmessagesettings','defaultpayloadlengthextrabytes', str(defaults.networkDefaultPayloadLengthExtraBytes)) - previousTotalDifficulty = int(BMConfigParser().getint('bitmessagesettings', 'maxacceptablenoncetrialsperbyte')) / 320 - previousSmallMessageDifficulty = int(BMConfigParser().getint('bitmessagesettings', 'maxacceptablepayloadlengthextrabytes')) / 14000 - BMConfigParser().set('bitmessagesettings','maxacceptablenoncetrialsperbyte', str(previousTotalDifficulty * 1000)) - BMConfigParser().set('bitmessagesettings','maxacceptablepayloadlengthextrabytes', str(previousSmallMessageDifficulty * 1000)) - BMConfigParser().set('bitmessagesettings', 'settingsversion', '9') - - # Adjust the required POW values for each of this user's addresses to conform to protocol v3 norms. - if BMConfigParser().getint('bitmessagesettings', 'settingsversion') == 9: - for addressInKeysFile in BMConfigParser().addressses(): - try: - previousTotalDifficulty = float(BMConfigParser().getint(addressInKeysFile, 'noncetrialsperbyte')) / 320 - previousSmallMessageDifficulty = float(BMConfigParser().getint(addressInKeysFile, 'payloadlengthextrabytes')) / 14000 - if previousTotalDifficulty <= 2: - previousTotalDifficulty = 1 - if previousSmallMessageDifficulty < 1: - previousSmallMessageDifficulty = 1 - BMConfigParser().set(addressInKeysFile,'noncetrialsperbyte', str(int(previousTotalDifficulty * 1000))) - BMConfigParser().set(addressInKeysFile,'payloadlengthextrabytes', str(int(previousSmallMessageDifficulty * 1000))) - except: - continue - BMConfigParser().set('bitmessagesettings', 'maxdownloadrate', '0') - BMConfigParser().set('bitmessagesettings', 'maxuploadrate', '0') - BMConfigParser().set('bitmessagesettings', 'settingsversion', '10') - BMConfigParser().save() - - # sanity check - if BMConfigParser().getint('bitmessagesettings', 'maxacceptablenoncetrialsperbyte') == 0: - BMConfigParser().set('bitmessagesettings','maxacceptablenoncetrialsperbyte', str(defaults.ridiculousDifficulty * defaults.networkDefaultProofOfWorkNonceTrialsPerByte)) - if BMConfigParser().getint('bitmessagesettings', 'maxacceptablepayloadlengthextrabytes') == 0: - BMConfigParser().set('bitmessagesettings','maxacceptablepayloadlengthextrabytes', str(defaults.ridiculousDifficulty * defaults.networkDefaultPayloadLengthExtraBytes)) + logger.debug( + 'Finished dropping and recreating the inventory table.') # 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. + # clear it, and the pubkeys from inventory, so that they'll + # be re-downloaded. item = '''SELECT value FROM settings WHERE key='version';''' parameters = '' self.cur.execute(item, parameters) currentVersion = int(self.cur.fetchall()[0][0]) if currentVersion == 7: - logger.debug('In messages.dat database, clearing pubkeys table because the data format has been updated.') + logger.debug( + 'In messages.dat database, clearing pubkeys table' + ' because the data format has been updated.') self.cur.execute( '''delete from inventory where objecttype = 1;''') self.cur.execute( '''delete from pubkeys;''') - # Any sending messages for which we *thought* that we had the pubkey must - # be rechecked. + # Any sending messages for which we *thought* that we had + # the pubkey must be rechecked. self.cur.execute( '''UPDATE sent SET status='msgqueued' WHERE status='doingmsgpow' or status='badkey';''') query = '''update settings set value=? WHERE key='version';''' @@ -360,99 +318,114 @@ def run(self): self.cur.execute(query, parameters) logger.debug('Finished clearing currently held pubkeys.') - # 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. + # 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. item = '''SELECT value FROM settings WHERE key='version';''' parameters = '' self.cur.execute(item, parameters) currentVersion = int(self.cur.fetchall()[0][0]) if currentVersion == 8: - logger.debug('In messages.dat database, adding sighash field to the inbox table.') + logger.debug( + 'In messages.dat database, adding sighash field to' + ' the inbox table.') item = '''ALTER TABLE inbox ADD sighash blob DEFAULT '' ''' parameters = '' self.cur.execute(item, parameters) item = '''update settings set value=? WHERE key='version';''' parameters = (9,) self.cur.execute(item, parameters) - - # TTL is now user-specifiable. Let's add an option to save whatever the user selects. - if not BMConfigParser().has_option('bitmessagesettings', 'ttl'): - BMConfigParser().set('bitmessagesettings', 'ttl', '367200') - # We'll also need a `sleeptill` field and a `ttl` field. Also we can combine - # the pubkeyretrynumber and msgretrynumber into one. + + # We'll also need a `sleeptill` field and a `ttl` field. Also we + # can combine the pubkeyretrynumber and msgretrynumber into one. + item = '''SELECT value FROM settings WHERE key='version';''' parameters = '' self.cur.execute(item, parameters) currentVersion = int(self.cur.fetchall()[0][0]) if currentVersion == 9: - logger.info('In messages.dat database, making TTL-related changes: combining the pubkeyretrynumber and msgretrynumber fields into the retrynumber field and adding the sleeptill and ttl fields...') - self.cur.execute( - '''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)''' ) - self.cur.execute( - '''INSERT INTO sent_backup SELECT msgid, toaddress, toripe, fromaddress, subject, message, ackdata, lastactiontime, status, 0, folder, encodingtype FROM sent;''') - self.cur.execute( '''DROP TABLE sent''') - self.cur.execute( - '''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)''' ) - self.cur.execute( - '''INSERT INTO sent SELECT msgid, toaddress, toripe, fromaddress, subject, message, ackdata, lastactiontime, lastactiontime, 0, status, 0, folder, encodingtype, 216000 FROM sent_backup;''') - self.cur.execute( '''DROP TABLE sent_backup''') + logger.info( + 'In messages.dat database, making TTL-related changes:' + ' combining the pubkeyretrynumber and msgretrynumber' + ' fields into the retrynumber field and adding the' + ' sleeptill and ttl fields...') + self.cur.execute( + '''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)''') + self.cur.execute( + '''INSERT INTO sent_backup SELECT msgid, toaddress, toripe, fromaddress,''' + ''' subject, message, ackdata, lastactiontime,''' + ''' status, 0, folder, encodingtype FROM sent;''') + self.cur.execute('''DROP TABLE sent''') + self.cur.execute( + '''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)''') + self.cur.execute( + '''INSERT INTO sent SELECT msgid, toaddress, toripe, fromaddress, subject, message, ackdata,''' + ''' lastactiontime, lastactiontime, 0, status, 0, folder, encodingtype, 216000 FROM sent_backup;''') + self.cur.execute('''DROP TABLE sent_backup''') logger.info('In messages.dat database, finished making TTL-related changes.') logger.debug('In messages.dat database, adding address field to the pubkeys table.') # We're going to have to calculate the address for each row in the pubkeys - # table. Then we can take out the hash field. - self.cur.execute('''ALTER TABLE pubkeys ADD address text DEFAULT '' ''') - self.cur.execute('''SELECT hash, addressversion FROM pubkeys''') - queryResult = self.cur.fetchall() - from addresses import encodeAddress - for row in queryResult: - hash, addressVersion = row - address = encodeAddress(addressVersion, 1, hash) - item = '''UPDATE pubkeys SET address=? WHERE hash=?;''' - parameters = (address, hash) - self.cur.execute(item, parameters) + # table. Then we can take out the hash field. + self.cur.execute('''ALTER TABLE pubkeys ADD address text DEFAULT '' ;''') + + # replica for loop to update hashed address + self.cur.execute('''UPDATE pubkeys SET address=(enaddr(pubkeys.addressversion, 1, hash)); ''') + # Now we can remove the hash field from the pubkeys table. self.cur.execute( - '''CREATE TEMPORARY TABLE pubkeys_backup (address text, addressversion int, transmitdata blob, time int, usedpersonally text, UNIQUE(address) ON CONFLICT REPLACE)''' ) + '''CREATE TEMPORARY TABLE pubkeys_backup''' + ''' (address text, addressversion int, transmitdata blob, time int,''' + ''' usedpersonally text, UNIQUE(address) ON CONFLICT REPLACE)''') self.cur.execute( - '''INSERT INTO pubkeys_backup SELECT address, addressversion, transmitdata, time, usedpersonally FROM pubkeys;''') - self.cur.execute( '''DROP TABLE pubkeys''') + '''INSERT INTO pubkeys_backup''' + ''' SELECT address, addressversion, transmitdata, time, usedpersonally FROM pubkeys;''') + self.cur.execute('''DROP TABLE pubkeys''') self.cur.execute( - '''CREATE TABLE pubkeys (address text, addressversion int, transmitdata blob, time int, usedpersonally text, UNIQUE(address) ON CONFLICT REPLACE)''' ) + '''CREATE TABLE pubkeys''' + ''' (address text, addressversion int, transmitdata blob, time int, usedpersonally text,''' + ''' UNIQUE(address) ON CONFLICT REPLACE)''') self.cur.execute( - '''INSERT INTO pubkeys SELECT address, addressversion, transmitdata, time, usedpersonally FROM pubkeys_backup;''') - self.cur.execute( '''DROP TABLE pubkeys_backup''') - logger.debug('In messages.dat database, done adding address field to the pubkeys table and removing the hash field.') + '''INSERT INTO pubkeys SELECT''' + ''' address, addressversion, transmitdata, time, usedpersonally FROM pubkeys_backup;''') + self.cur.execute('''DROP TABLE pubkeys_backup''') + logger.debug( + 'In messages.dat database, done adding address field to the pubkeys table' + ' and removing the hash field.') self.cur.execute('''update settings set value=10 WHERE key='version';''') - - if not BMConfigParser().has_option('bitmessagesettings', 'onionhostname'): - BMConfigParser().set('bitmessagesettings', 'onionhostname', '') - if not BMConfigParser().has_option('bitmessagesettings', 'onionport'): - BMConfigParser().set('bitmessagesettings', 'onionport', '8444') - if not BMConfigParser().has_option('bitmessagesettings', 'onionbindip'): - BMConfigParser().set('bitmessagesettings', 'onionbindip', '127.0.0.1') - if not BMConfigParser().has_option('bitmessagesettings', 'smtpdeliver'): - BMConfigParser().set('bitmessagesettings', 'smtpdeliver', '') - if not BMConfigParser().has_option('bitmessagesettings', 'hidetrayconnectionnotifications'): - BMConfigParser().set('bitmessagesettings', 'hidetrayconnectionnotifications', 'false') - if BMConfigParser().has_option('bitmessagesettings', 'maxoutboundconnections'): - try: - if BMConfigParser().getint('bitmessagesettings', 'maxoutboundconnections') < 1: raise ValueError - except ValueError as err: - BMConfigParser().remove_option('bitmessagesettings', 'maxoutboundconnections') - logger.error('Your maximum outbound connections must be a number.') - if not BMConfigParser().has_option('bitmessagesettings', 'maxoutboundconnections'): - logger.info('Setting maximum outbound connections to 8.') - BMConfigParser().set('bitmessagesettings', 'maxoutboundconnections', '8') - - BMConfigParser().save() - + + # Update the address colunm to unique in addressbook table + item = '''SELECT value FROM settings WHERE key='version';''' + parameters = '' + self.cur.execute(item, parameters) + currentVersion = int(self.cur.fetchall()[0][0]) + if currentVersion == 10: + logger.debug( + 'In messages.dat database, updating address column to UNIQUE' + ' in the addressbook table.') + self.cur.execute( + '''ALTER TABLE addressbook RENAME TO old_addressbook''') + self.cur.execute( + '''CREATE TABLE addressbook''' + ''' (label text, address text, UNIQUE(address) ON CONFLICT IGNORE)''') + self.cur.execute( + '''INSERT INTO addressbook SELECT label, address FROM old_addressbook;''') + self.cur.execute('''DROP TABLE old_addressbook''') + self.cur.execute('''update settings set value=11 WHERE key='version';''') + # Are you hoping to add a new option to the keys.dat file of existing - # Bitmessage users or modify the SQLite database? Add it right above this line! - + # Bitmessage users or modify the SQLite database? Add it right + # above this line! + try: testpayload = '\x00\x00' t = ('1234', 1, testpayload, '12345678', 'no') - self.cur.execute( '''INSERT INTO pubkeys VALUES(?,?,?,?,?)''', t) + self.cur.execute('''INSERT INTO pubkeys VALUES(?,?,?,?,?)''', t) self.conn.commit() self.cur.execute( '''SELECT transmitdata FROM pubkeys WHERE address='1234' ''') @@ -462,13 +435,29 @@ def run(self): self.cur.execute('''DELETE FROM pubkeys WHERE address='1234' ''') self.conn.commit() if transmitdata == '': - logger.fatal('Problem: The version of SQLite you have cannot store Null values. Please download and install the latest revision of your version of Python (for example, the latest Python 2.7 revision) and try again.\n') - logger.fatal('PyBitmessage will now exit very abruptly. You may now see threading errors related to this abrupt exit but the problem you need to solve is related to SQLite.\n\n') + logger.fatal( + 'Problem: The version of SQLite you have cannot store Null values.' + ' Please download and install the latest revision of your version of Python' + ' (for example, the latest Python 2.7 revision) and try again.\n') + logger.fatal( + 'PyBitmessage will now exit very abruptly.' + ' You may now see threading errors related to this abrupt exit' + ' but the problem you need to solve is related to SQLite.\n\n') os._exit(0) except Exception as err: if str(err) == 'database or disk is full': - logger.fatal('(While null value test) Alert: Your disk or data storage volume is full. sqlThread will now exit.') - queues.UISignalQueue.put(('alert', (tr._translate("MainWindow", "Disk full"), tr._translate("MainWindow", 'Alert: Your disk or data storage volume is full. Bitmessage will now exit.'), True))) + logger.fatal( + '(While null value test) Alert: Your disk or data storage volume is full.' + ' sqlThread will now exit.') + queues.UISignalQueue.put(( + 'alert', ( + _translate( + "MainWindow", + "Disk full"), + _translate( + "MainWindow", + 'Alert: Your disk or data storage volume is full. Bitmessage will now exit.'), + True))) os._exit(0) else: logger.error(err) @@ -484,17 +473,27 @@ def run(self): if int(value) < int(time.time()) - 86400: logger.info('It has been a long time since the messages.dat file has been vacuumed. Vacuuming now...') try: - self.cur.execute( ''' VACUUM ''') + self.cur.execute(''' VACUUM ''') except Exception as err: if str(err) == 'database or disk is full': - logger.fatal('(While VACUUM) Alert: Your disk or data storage volume is full. sqlThread will now exit.') - queues.UISignalQueue.put(('alert', (tr._translate("MainWindow", "Disk full"), tr._translate("MainWindow", 'Alert: Your disk or data storage volume is full. Bitmessage will now exit.'), True))) + logger.fatal( + '(While VACUUM) Alert: Your disk or data storage volume is full.' + ' sqlThread will now exit.') + queues.UISignalQueue.put(( + 'alert', ( + _translate( + "MainWindow", + "Disk full"), + _translate( + "MainWindow", + 'Alert: Your disk or data storage volume is full. Bitmessage will now exit.'), + True))) os._exit(0) item = '''update settings set value=? WHERE key='lastvacuumtime';''' parameters = (int(time.time()),) self.cur.execute(item, parameters) - state.sqlReady = True + helper_sql.sql_ready.set() while True: item = helper_sql.sqlSubmitQueue.get() @@ -503,8 +502,18 @@ def run(self): self.conn.commit() except Exception as err: if str(err) == 'database or disk is full': - logger.fatal('(While committing) Alert: Your disk or data storage volume is full. sqlThread will now exit.') - queues.UISignalQueue.put(('alert', (tr._translate("MainWindow", "Disk full"), tr._translate("MainWindow", 'Alert: Your disk or data storage volume is full. Bitmessage will now exit.'), True))) + logger.fatal( + '(While committing) Alert: Your disk or data storage volume is full.' + ' sqlThread will now exit.') + queues.UISignalQueue.put(( + 'alert', ( + _translate( + "MainWindow", + "Disk full"), + _translate( + "MainWindow", + 'Alert: Your disk or data storage volume is full. Bitmessage will now exit.'), + True))) os._exit(0) elif item == 'exit': self.conn.close() @@ -518,8 +527,18 @@ def run(self): self.conn.commit() except Exception as err: if str(err) == 'database or disk is full': - logger.fatal('(while movemessagstoprog) Alert: Your disk or data storage volume is full. sqlThread will now exit.') - queues.UISignalQueue.put(('alert', (tr._translate("MainWindow", "Disk full"), tr._translate("MainWindow", 'Alert: Your disk or data storage volume is full. Bitmessage will now exit.'), True))) + logger.fatal( + '(while movemessagstoprog) Alert: Your disk or data storage volume is full.' + ' sqlThread will now exit.') + queues.UISignalQueue.put(( + 'alert', ( + _translate( + "MainWindow", + "Disk full"), + _translate( + "MainWindow", + 'Alert: Your disk or data storage volume is full. Bitmessage will now exit.'), + True))) os._exit(0) self.conn.close() shutil.move( @@ -534,8 +553,18 @@ def run(self): self.conn.commit() except Exception as err: if str(err) == 'database or disk is full': - logger.fatal('(while movemessagstoappdata) Alert: Your disk or data storage volume is full. sqlThread will now exit.') - queues.UISignalQueue.put(('alert', (tr._translate("MainWindow", "Disk full"), tr._translate("MainWindow", 'Alert: Your disk or data storage volume is full. Bitmessage will now exit.'), True))) + logger.fatal( + '(while movemessagstoappdata) Alert: Your disk or data storage volume is full.' + ' sqlThread will now exit.') + queues.UISignalQueue.put(( + 'alert', ( + _translate( + "MainWindow", + "Disk full"), + _translate( + "MainWindow", + 'Alert: Your disk or data storage volume is full. Bitmessage will now exit.'), + True))) os._exit(0) self.conn.close() shutil.move( @@ -548,30 +577,66 @@ def run(self): self.cur.execute('''delete from sent where folder='trash' ''') self.conn.commit() try: - self.cur.execute( ''' VACUUM ''') + self.cur.execute(''' VACUUM ''') except Exception as err: if str(err) == 'database or disk is full': - logger.fatal('(while deleteandvacuume) Alert: Your disk or data storage volume is full. sqlThread will now exit.') - queues.UISignalQueue.put(('alert', (tr._translate("MainWindow", "Disk full"), tr._translate("MainWindow", 'Alert: Your disk or data storage volume is full. Bitmessage will now exit.'), True))) + logger.fatal( + '(while deleteandvacuume) Alert: Your disk or data storage volume is full.' + ' sqlThread will now exit.') + queues.UISignalQueue.put(( + 'alert', ( + _translate( + "MainWindow", + "Disk full"), + _translate( + "MainWindow", + 'Alert: Your disk or data storage volume is full. Bitmessage will now exit.'), + True))) os._exit(0) else: parameters = helper_sql.sqlSubmitQueue.get() rowcount = 0 - # print 'item', item - # print 'parameters', parameters try: self.cur.execute(item, parameters) rowcount = self.cur.rowcount except Exception as err: if str(err) == 'database or disk is full': - logger.fatal('(while cur.execute) Alert: Your disk or data storage volume is full. sqlThread will now exit.') - queues.UISignalQueue.put(('alert', (tr._translate("MainWindow", "Disk full"), tr._translate("MainWindow", 'Alert: Your disk or data storage volume is full. Bitmessage will now exit.'), True))) + logger.fatal( + '(while cur.execute) Alert: Your disk or data storage volume is full.' + ' sqlThread will now exit.') + queues.UISignalQueue.put(( + 'alert', ( + _translate( + "MainWindow", + "Disk full"), + _translate( + "MainWindow", + 'Alert: Your disk or data storage volume is full. Bitmessage will now exit.'), + True))) os._exit(0) else: - logger.fatal('Major error occurred when trying to execute a SQL statement within the sqlThread. Please tell Atheros about this error message or post it in the forum! Error occurred while trying to execute statement: "%s" Here are the parameters; you might want to censor this data with asterisks (***) as it can contain private information: %s. Here is the actual error message thrown by the sqlThread: %s', str(item), str(repr(parameters)), str(err)) + logger.fatal( + 'Major error occurred when trying to execute a SQL statement within the sqlThread.' + ' Please tell Atheros about this error message or post it in the forum!' + ' Error occurred while trying to execute statement: "%s" Here are the parameters;' + ' you might want to censor this data with asterisks (***)' + ' as it can contain private information: %s.' + ' Here is the actual error message thrown by the sqlThread: %s', + str(item), + str(repr(parameters)), + str(err)) logger.fatal('This program shall now abruptly exit!') os._exit(0) helper_sql.sqlReturnQueue.put((self.cur.fetchall(), rowcount)) # helper_sql.sqlSubmitQueue.task_done() + + def create_function(self): + # create_function + try: + self.conn.create_function("enaddr", 3, func=encodeAddress, deterministic=True) + except (TypeError, sqlite3.NotSupportedError) as err: + logger.debug( + "Got error while pass deterministic in sqlite create function {}, Passing 3 params".format(err)) + self.conn.create_function("enaddr", 3, encodeAddress) diff --git a/src/debug.py b/src/debug.py index 79c6e64e47..639be123bf 100644 --- a/src/debug.py +++ b/src/debug.py @@ -1,56 +1,92 @@ -# -*- coding: utf-8 -*- -''' +""" Logging and debuging facility -============================= +----------------------------- Levels: - DEBUG Detailed information, typically of interest only when diagnosing problems. - INFO Confirmation that things are working as expected. - WARNING An indication that something unexpected happened, or indicative of some problem in the - near future (e.g. ‘disk space low’). The software is still working as expected. - ERROR Due to a more serious problem, the software has not been able to perform some function. - CRITICAL A serious error, indicating that the program itself may be unable to continue running. - -There are three loggers: `console_only`, `file_only` and `both`. - -Use: `from debug import logger` to import this facility into whatever module you wish to log messages from. - Logging is thread-safe so you don't have to worry about locks, just import and log. -''' + + DEBUG + Detailed information, typically of interest only when diagnosing problems. + INFO + Confirmation that things are working as expected. + WARNING + An indication that something unexpected happened, or indicative of + some problem in the near future (e.g. 'disk space low'). The software + is still working as expected. + ERROR + Due to a more serious problem, the software has not been able to + perform some function. + CRITICAL + A serious error, indicating that the program itself may be unable to + continue running. + +There are three loggers by default: `console_only`, `file_only` and `both`. +You can configure logging in the logging.dat in the appdata dir. +It's format is described in the :func:`logging.config.fileConfig` doc. + +Use: + +>>> import logging +>>> logger = logging.getLogger('default') + +The old form: ``from debug import logger`` is also may be used, +but only in the top level modules. + +Logging is thread-safe so you don't have to worry about locks, +just import and log. +""" + import logging import logging.config import os import sys + +from six.moves import configparser + import helper_startup import state + 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 +# Now can be overriden from a config file, which uses standard python +# logging.config.fileConfig interface +# examples are here: +# https://web.archive.org/web/20170712122006/https://bitmessage.org/forum/index.php/topic,4820.msg11163.html#msg11163 log_level = 'WARNING' + def log_uncaught_exceptions(ex_cls, ex, tb): + """The last resort logging function used for sys.excepthook""" logging.critical('Unhandled exception', exc_info=(ex_cls, ex, tb)) + def configureLogging(): - have_logging = False + """ + Configure logging, + using either logging.dat file in the state.appdata dir + or dictionary with hardcoded settings. + """ + sys.excepthook = log_uncaught_exceptions + fail_msg = '' try: - logging.config.fileConfig(os.path.join (state.appdata, 'logging.dat')) - have_logging = True - print "Loaded logger configuration from %s" % (os.path.join(state.appdata, 'logging.dat')) - except: - if os.path.isfile(os.path.join(state.appdata, 'logging.dat')): - print "Failed to load logger configuration from %s, using default logging config" % (os.path.join(state.appdata, 'logging.dat')) - print sys.exc_info() + logging_config = os.path.join(state.appdata, 'logging.dat') + logging.config.fileConfig( + logging_config, disable_existing_loggers=False) + return ( + False, + 'Loaded logger configuration from %s' % logging_config + ) + except (OSError, configparser.NoSectionError, KeyError): + if os.path.isfile(logging_config): + fail_msg = \ + 'Failed to load logger configuration from %s, using default' \ + ' logging config\n%s' % \ + (logging_config, sys.exc_info()) else: - # no need to confuse the user if the logger config is missing entirely - print "Using default logger configuration" - - sys.excepthook = log_uncaught_exceptions - - if have_logging: - return False + # no need to confuse the user if the logger config + # is missing entirely + fail_msg = 'Using default logger configuration' - logging.config.dictConfig({ + logging_config = { 'version': 1, 'formatters': { 'default': { @@ -68,8 +104,8 @@ def configureLogging(): 'class': 'logging.handlers.RotatingFileHandler', 'formatter': 'default', 'level': log_level, - 'filename': state.appdata + 'debug.log', - 'maxBytes': 2097152, # 2 MiB + 'filename': os.path.join(state.appdata, 'debug.log'), + 'maxBytes': 2097152, # 2 MiB 'backupCount': 1, 'encoding': 'UTF-8', } @@ -77,45 +113,45 @@ def configureLogging(): 'loggers': { 'console_only': { 'handlers': ['console'], - 'propagate' : 0 + 'propagate': 0 }, 'file_only': { 'handlers': ['file'], - 'propagate' : 0 + 'propagate': 0 }, 'both': { 'handlers': ['console', 'file'], - 'propagate' : 0 + 'propagate': 0 }, }, 'root': { 'level': log_level, 'handlers': ['console'], }, - }) - return True - -# TODO (xj9): Get from a config file. -#logger = logging.getLogger('console_only') -if configureLogging(): - if '-c' in sys.argv: - logger = logging.getLogger('file_only') - else: - logger = logging.getLogger('both') -else: - logger = logging.getLogger('default') + } + + logging_config['loggers']['default'] = logging_config['loggers'][ + 'file_only' if '-c' in sys.argv else 'both'] + logging.config.dictConfig(logging_config) + + return True, fail_msg -def restartLoggingInUpdatedAppdataLocation(): + +def resetLogging(): + """Reconfigure logging in runtime when state.appdata dir changed""" + # pylint: disable=global-statement, used-before-assignment global logger - for i in list(logger.handlers): + for i in logger.handlers: logger.removeHandler(i) i.flush() i.close() - if configureLogging(): - if '-c' in sys.argv: - logger = logging.getLogger('file_only') - else: - logger = logging.getLogger('both') - else: - logger = logging.getLogger('default') + configureLogging() + logger = logging.getLogger('default') + + +# ! +preconfigured, msg = configureLogging() +logger = logging.getLogger('default') +if msg: + logger.log(logging.WARNING if preconfigured else logging.INFO, msg) diff --git a/src/default.ini b/src/default.ini new file mode 100644 index 0000000000..d4420ba55b --- /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/defaultKnownNodes.py b/src/defaultKnownNodes.py deleted file mode 100644 index 05b650141a..0000000000 --- a/src/defaultKnownNodes.py +++ /dev/null @@ -1,82 +0,0 @@ -import pickle -import socket -from struct import * -import time -import random -import sys -from time import strftime, localtime -import state - -def createDefaultKnownNodes(appdata): - ############## Stream 1 ################ - stream1 = {} - - #stream1[state.Peer('2604:2000:1380:9f:82e:148b:2746:d0c7', 8080)] = int(time.time()) - stream1[state.Peer('5.45.99.75', 8444)] = {"lastseen": int(time.time()), "rating": 0, "self": False} - stream1[state.Peer('75.167.159.54', 8444)] = {"lastseen": int(time.time()), "rating": 0, "self": False} - stream1[state.Peer('95.165.168.168', 8444)] = {"lastseen": int(time.time()), "rating": 0, "self": False} - stream1[state.Peer('85.180.139.241', 8444)] = {"lastseen": int(time.time()), "rating": 0, "self": False} - stream1[state.Peer('158.222.217.190', 8080)] = {"lastseen": int(time.time()), "rating": 0, "self": False} - stream1[state.Peer('178.62.12.187', 8448)] = {"lastseen": int(time.time()), "rating": 0, "self": False} - stream1[state.Peer('24.188.198.204', 8111)] = {"lastseen": int(time.time()), "rating": 0, "self": False} - stream1[state.Peer('109.147.204.113', 1195)] = {"lastseen": int(time.time()), "rating": 0, "self": False} - stream1[state.Peer('178.11.46.221', 8444)] = {"lastseen": int(time.time()), "rating": 0, "self": False} - - ############# Stream 2 ################# - stream2 = {} - # None yet - - ############# Stream 3 ################# - stream3 = {} - # None yet - - allKnownNodes = {} - allKnownNodes[1] = stream1 - allKnownNodes[2] = stream2 - allKnownNodes[3] = stream3 - - #print stream1 - #print allKnownNodes - - with open(appdata + 'knownnodes.dat', 'wb') as output: - # Pickle dictionary using protocol 0. - pickle.dump(allKnownNodes, output) - - return allKnownNodes - -def readDefaultKnownNodes(appdata): - pickleFile = open(appdata + 'knownnodes.dat', 'rb') - knownNodes = pickle.load(pickleFile) - pickleFile.close() - for stream, storedValue in knownNodes.items(): - for host,value in storedValue.items(): - try: - # Old knownNodes format. - port, storedtime = value - except: - # New knownNodes format. - host, port = host - storedtime = value - print host, '\t', port, '\t', unicode(strftime('%a, %d %b %Y %I:%M %p',localtime(storedtime)),'utf-8') - -if __name__ == "__main__": - - APPNAME = "PyBitmessage" - from os import path, environ - if sys.platform == 'darwin': - from AppKit import NSSearchPathForDirectoriesInDomains # @UnresolvedImport - # http://developer.apple.com/DOCUMENTATION/Cocoa/Reference/Foundation/Miscellaneous/Foundation_Functions/Reference/reference.html#//apple_ref/c/func/NSSearchPathForDirectoriesInDomains - # NSApplicationSupportDirectory = 14 - # NSUserDomainMask = 1 - # True for expanding the tilde into a fully qualified path - appdata = path.join(NSSearchPathForDirectoriesInDomains(14, 1, True)[0], APPNAME) + '/' - elif 'win' in sys.platform: - appdata = path.join(environ['APPDATA'], APPNAME) + '\\' - else: - appdata = path.expanduser(path.join("~", "." + APPNAME + "/")) - - - print 'New list of all known nodes:', createDefaultKnownNodes(appdata) - readDefaultKnownNodes(appdata) - - diff --git a/src/defaults.py b/src/defaults.py index 8401bf2efc..32162b565f 100644 --- a/src/defaults.py +++ b/src/defaults.py @@ -1,13 +1,24 @@ -# sanity check, prevent doing ridiculous PoW -# 20 million PoWs equals approximately 2 days on dev's dual R9 290 +""" +Common default values +""" + +#: sanity check, prevent doing ridiculous PoW +#: 20 million PoWs equals approximately 2 days on dev's dual R9 290 ridiculousDifficulty = 20000000 -# Remember here the RPC port read from namecoin.conf so we can restore to -# it as default whenever the user changes the "method" selection for -# namecoin integration to "namecoind". +#: Remember here the RPC port read from namecoin.conf so we can restore to +#: it as default whenever the user changes the "method" selection for +#: namecoin integration to "namecoind". namecoinDefaultRpcPort = "8336" -#If changed, these values will cause particularly unexpected behavior: You won't be able to either send or receive messages because the proof of work you do (or demand) won't match that done or demanded by others. Don't change them! -networkDefaultProofOfWorkNonceTrialsPerByte = 1000 #The amount of work that should be performed (and demanded) per byte of the payload. -networkDefaultPayloadLengthExtraBytes = 1000 #To make sending short messages a little more difficult, this value is added to the payload length for use in calculating the proof of work target. - +# If changed, these values will cause particularly unexpected behavior: +# You won't be able to either send or receive messages because the proof +# of work you do (or demand) won't match that done or demanded by others. +# Don't change them! +#: The amount of work that should be performed (and demanded) per byte +#: of the payload. +networkDefaultProofOfWorkNonceTrialsPerByte = 1000 +#: To make sending short messages a little more difficult, this value is +#: added to the payload length for use in calculating the proof of work +#: target. +networkDefaultPayloadLengthExtraBytes = 1000 diff --git a/src/depends.py b/src/depends.py index 4c79322a3d..15ddc94ac2 100755 --- a/src/depends.py +++ b/src/depends.py @@ -1,54 +1,201 @@ -#! python +""" +Utility functions to check the availability of dependencies +and suggest how it may be installed +""" -import sys import os -import pyelliptic.openssl +import re +import sys + +import six # Only really old versions of Python don't have sys.hexversion. We don't # support them. The logging module was introduced in Python 2.3 if not hasattr(sys, 'hexversion') or sys.hexversion < 0x20300F0: - sys.stdout.write('Python version: ' + sys.version) - sys.stdout.write('PyBitmessage requires Python 2.7.3 or greater (but not Python 3)') - sys.exit() + sys.exit( + 'Python version: %s\n' + 'PyBitmessage requires Python 2.7.4 or greater (but not Python 3)' + % sys.version + ) + +import logging # noqa:E402 +import subprocess # nosec B404 +from importlib import import_module # We can now use logging so set up a simple configuration -import logging -formatter = logging.Formatter( - '%(levelname)s: %(message)s' -) +formatter = logging.Formatter('%(levelname)s: %(message)s') handler = logging.StreamHandler(sys.stdout) handler.setFormatter(formatter) -logger = logging.getLogger(__name__) +logger = logging.getLogger('both') logger.addHandler(handler) logger.setLevel(logging.ERROR) -# We need to check hashlib for RIPEMD-160, as it won't be available -# if OpenSSL is not linked against or the linked OpenSSL has RIPEMD -# disabled. +OS_RELEASE = { + "Debian GNU/Linux".lower(): "Debian", + "fedora": "Fedora", + "opensuse": "openSUSE", + "ubuntu": "Ubuntu", + "gentoo": "Gentoo", + "calculate": "Gentoo" +} + +PACKAGE_MANAGER = { + "OpenBSD": "pkg_add", + "FreeBSD": "pkg install", + "Debian": "apt-get install", + "Ubuntu": "apt-get install", + "Ubuntu 12": "apt-get install", + "Ubuntu 20": "apt-get install", + "openSUSE": "zypper install", + "Fedora": "dnf install", + "Guix": "guix package -i", + "Gentoo": "emerge" +} + +PACKAGES = { + "PyQt4": { + "OpenBSD": "py-qt4", + "FreeBSD": "py27-qt4", + "Debian": "python-qt4", + "Ubuntu": "python-qt4", + "Ubuntu 12": "python-qt4", + "Ubuntu 20": "", + "openSUSE": "python-qt", + "Fedora": "PyQt4", + "Guix": "python2-pyqt@4.11.4", + "Gentoo": "dev-python/PyQt4", + "optional": True, + "description": + "You only need PyQt if you want to use the GUI." + " When only running as a daemon, this can be skipped.\n" + "However, you would have to install it manually" + " because setuptools does not support PyQt." + }, + "msgpack": { + "OpenBSD": "py-msgpack", + "FreeBSD": "py27-msgpack-python", + "Debian": "python-msgpack", + "Ubuntu": "python-msgpack", + "Ubuntu 12": "msgpack-python", + "Ubuntu 20": "", + "openSUSE": "python-msgpack-python", + "Fedora": "python2-msgpack", + "Guix": "python2-msgpack", + "Gentoo": "dev-python/msgpack", + "optional": True, + "description": + "python-msgpack is recommended for improved performance of" + " message encoding/decoding" + }, + "pyopencl": { + "FreeBSD": "py27-pyopencl", + "Debian": "python-pyopencl", + "Ubuntu": "python-pyopencl", + "Ubuntu 12": "python-pyopencl", + "Ubuntu 20": "", + "Fedora": "python2-pyopencl", + "openSUSE": "", + "OpenBSD": "", + "Guix": "", + "Gentoo": "dev-python/pyopencl", + "optional": True, + "description": + "If you install pyopencl, you will be able to use" + " GPU acceleration for proof of work.\n" + "You also need a compatible GPU and drivers." + }, + "setuptools": { + "OpenBSD": "py-setuptools", + "FreeBSD": "py27-setuptools", + "Debian": "python-setuptools", + "Ubuntu": "python-setuptools", + "Ubuntu 12": "python-setuptools", + "Ubuntu 20": "python-setuptools", + "Fedora": "python2-setuptools", + "openSUSE": "python-setuptools", + "Guix": "python2-setuptools", + "Gentoo": "dev-python/setuptools", + "optional": False, + }, + "six": { + "OpenBSD": "py-six", + "FreeBSD": "py27-six", + "Debian": "python-six", + "Ubuntu": "python-six", + "Ubuntu 12": "python-six", + "Ubuntu 20": "python-six", + "Fedora": "python-six", + "openSUSE": "python-six", + "Guix": "python-six", + "Gentoo": "dev-python/six", + "optional": False, + } +} + + +def detectOS(): + """Finding out what Operating System is running""" + if detectOS.result is not None: + return detectOS.result + if sys.platform.startswith('openbsd'): + detectOS.result = "OpenBSD" + elif sys.platform.startswith('freebsd'): + detectOS.result = "FreeBSD" + elif sys.platform.startswith('win'): + detectOS.result = "Windows" + elif os.path.isfile("/etc/os-release"): + detectOSRelease() + elif os.path.isfile("/etc/config.scm"): + detectOS.result = "Guix" + return detectOS.result + + +detectOS.result = None + + +def detectOSRelease(): + """Detecting the release of OS""" + with open("/etc/os-release", 'r') as osRelease: + version = None + for line in osRelease: + if line.startswith("NAME="): + detectOS.result = OS_RELEASE.get( + line.replace('"', '').split("=")[-1].strip().lower()) + elif line.startswith("VERSION_ID="): + try: + version = float(line.split("=")[1].replace("\"", "")) + except ValueError: + pass + if detectOS.result == "Ubuntu" and version < 14: + detectOS.result = "Ubuntu 12" + elif detectOS.result == "Ubuntu" and version >= 20: + detectOS.result = "Ubuntu 20" + + +def try_import(module, log_extra=False): + """Try to import the non imported packages""" + try: + return import_module(module) + except ImportError: + module = module.split('.')[0] + logger.error('The %s module is not available.', module) + if log_extra: + logger.error(log_extra) + dist = detectOS() + logger.error( + 'On %s, try running "%s %s" as root.', + dist, PACKAGE_MANAGER[dist], PACKAGES[module][dist]) + return False -def check_hashlib(): - """Do hashlib check. - The hashlib module check with version as if it included or not - in The Python Standard library , its a module containing an - interface to the most popular hashing algorithms. hashlib - implements some of the algorithms, however if OpenSSL - installed, hashlib is able to use this algorithms as well. - """ - if sys.hexversion < 0x020500F0: - logger.error('The hashlib module is not included in this version of Python.') - return False - import hashlib - if '_hashlib' not in hashlib.__dict__: - logger.error('The RIPEMD-160 hash algorithm is not available. The hashlib module is not linked against OpenSSL.') - return False +def check_ripemd160(): + """Check availability of the RIPEMD160 hash function""" try: - hashlib.new('ripemd160') - except ValueError: - logger.error('The RIPEMD-160 hash algorithm is not available. The hashlib module utilizes an OpenSSL library with RIPEMD disabled.') + from fallback import RIPEMD160Hash # pylint: disable=relative-import + except ImportError: return False - return True + return RIPEMD160Hash is not None def check_sqlite(): @@ -58,35 +205,46 @@ def check_sqlite(): support in python version for specifieed platform. """ if sys.hexversion < 0x020500F0: - logger.error('The sqlite3 module is not included in this version of Python.') + logger.error( + 'The sqlite3 module is not included in this version of Python.') if sys.platform.startswith('freebsd'): - logger.error('On FreeBSD, try running "pkg install py27-sqlite3" as root.') + logger.error( + 'On FreeBSD, try running "pkg install py27-sqlite3" as root.') return False - try: - import sqlite3 - except ImportError: - logger.error('The sqlite3 module is not available') + + sqlite3 = try_import('sqlite3') + if not sqlite3: return False - logger.info('sqlite3 Module Version: ' + sqlite3.version) - logger.info('SQLite Library Version: ' + sqlite3.sqlite_version) - #sqlite_version_number formula: https://sqlite.org/c3ref/c_source_id.html - sqlite_version_number = sqlite3.sqlite_version_info[0] * 1000000 + sqlite3.sqlite_version_info[1] * 1000 + sqlite3.sqlite_version_info[2] + logger.info('sqlite3 Module Version: %s', sqlite3.version) + logger.info('SQLite Library Version: %s', sqlite3.sqlite_version) + # sqlite_version_number formula: https://sqlite.org/c3ref/c_source_id.html + sqlite_version_number = ( + sqlite3.sqlite_version_info[0] * 1000000 + + sqlite3.sqlite_version_info[1] * 1000 + + sqlite3.sqlite_version_info[2] + ) conn = None try: try: conn = sqlite3.connect(':memory:') if sqlite_version_number >= 3006018: - sqlite_source_id = conn.execute('SELECT sqlite_source_id();').fetchone()[0] - logger.info('SQLite Library Source ID: ' + sqlite_source_id) + sqlite_source_id = conn.execute( + 'SELECT sqlite_source_id();' + ).fetchone()[0] + logger.info('SQLite Library Source ID: %s', sqlite_source_id) if sqlite_version_number >= 3006023: - compile_options = ', '.join(map(lambda row: row[0], conn.execute('PRAGMA compile_options;'))) - logger.info('SQLite Library Compile Options: ' + compile_options) - #There is no specific version requirement as yet, so we just use the - #first version that was included with Python. + compile_options = ', '.join( + [row[0] for row in conn.execute('PRAGMA compile_options;')]) + logger.info( + 'SQLite Library Compile Options: %s', compile_options) + # There is no specific version requirement as yet, so we just + # use the first version that was included with Python. if sqlite_version_number < 3000008: - logger.error('This version of SQLite is too old. PyBitmessage requires SQLite 3.0.8 or later') + logger.error( + 'This version of SQLite is too old.' + ' PyBitmessage requires SQLite 3.0.8 or later') return False return True except sqlite3.Error: @@ -96,23 +254,24 @@ def check_sqlite(): if conn: conn.close() + def check_openssl(): """Do openssl dependency check. Here we are checking for openssl with its all dependent libraries and version checking. """ - try: - import ctypes - except ImportError: - logger.error('Unable to check OpenSSL. The ctypes module is not available.') + # pylint: disable=too-many-branches, too-many-return-statements + # pylint: disable=protected-access, redefined-outer-name + ctypes = try_import('ctypes') + if not ctypes: + logger.error('Unable to check OpenSSL.') return False - #We need to emulate the way PyElliptic searches for OpenSSL. + # We need to emulate the way PyElliptic searches for OpenSSL. if sys.platform == 'win32': paths = ['libeay32.dll'] if getattr(sys, 'frozen', False): - import os.path paths.insert(0, os.path.join(sys._MEIPASS, 'libeay32.dll')) else: paths = ['libcrypto.so', 'libcrypto.so.1.0.0'] @@ -122,14 +281,14 @@ def check_openssl(): '/usr/local/opt/openssl/lib/libcrypto.dylib', './../Frameworks/libcrypto.dylib' ]) - import re + if re.match(r'linux|darwin|freebsd', sys.platform): try: import ctypes.util path = ctypes.util.find_library('ssl') if path not in paths: paths.append(path) - except: + except: # nosec B110 # pylint:disable=bare-except pass openssl_version = None @@ -138,135 +297,126 @@ def check_openssl(): cflags_regex = re.compile(r'(?:OPENSSL_NO_)(AES|EC|ECDH|ECDSA)(?!\w)') + import pyelliptic.openssl + for path in paths: - logger.info('Checking OpenSSL at ' + path) + logger.info('Checking OpenSSL at %s', path) try: library = ctypes.CDLL(path) except OSError: continue - logger.info('OpenSSL Name: ' + library._name) - openssl_version, openssl_hexversion, openssl_cflags = pyelliptic.openssl.get_version(library) + logger.info('OpenSSL Name: %s', library._name) + try: + openssl_version, openssl_hexversion, openssl_cflags = \ + pyelliptic.openssl.get_version(library) + except AttributeError: # sphinx chokes + return True if not openssl_version: logger.error('Cannot determine version of this OpenSSL library.') return False - logger.info('OpenSSL Version: ' + openssl_version) - logger.info('OpenSSL Compile Options: ' + openssl_cflags) - #PyElliptic uses EVP_CIPHER_CTX_new and EVP_CIPHER_CTX_free which were - #introduced in 0.9.8b. + logger.info('OpenSSL Version: %s', openssl_version) + logger.info('OpenSSL Compile Options: %s', openssl_cflags) + # PyElliptic uses EVP_CIPHER_CTX_new and EVP_CIPHER_CTX_free which were + # introduced in 0.9.8b. if openssl_hexversion < 0x90802F: - logger.error('This OpenSSL library is too old. PyBitmessage requires OpenSSL 0.9.8b or later with AES, Elliptic Curves (EC), ECDH, and ECDSA enabled.') + logger.error( + 'This OpenSSL library is too old. PyBitmessage requires' + ' OpenSSL 0.9.8b or later with AES, Elliptic Curves (EC),' + ' ECDH, and ECDSA enabled.') return False - matches = cflags_regex.findall(openssl_cflags) - if len(matches) > 0: - logger.error('This OpenSSL library is missing the following required features: ' + ', '.join(matches) + '. PyBitmessage requires OpenSSL 0.9.8b or later with AES, Elliptic Curves (EC), ECDH, and ECDSA enabled.') + matches = cflags_regex.findall(openssl_cflags.decode('utf-8', "ignore")) + if matches: + logger.error( + 'This OpenSSL library is missing the following required' + ' features: %s. PyBitmessage requires OpenSSL 0.9.8b' + ' or later with AES, Elliptic Curves (EC), ECDH,' + ' and ECDSA enabled.', ', '.join(matches)) return False return True return False -#TODO: The minimum versions of pythondialog and dialog need to be determined + +# ..todo:: The minimum versions of pythondialog and dialog need to be determined def check_curses(): """Do curses dependency check. - Here we are checking for curses if available or not with check - as interface requires the pythondialog\ package and the dialog - utility. + Here we are checking for curses if available or not with check as interface + requires the `pythondialog `_ package + and the dialog utility. """ if sys.hexversion < 0x20600F0: - logger.error('The curses interface requires the pythondialog package and the dialog utility.') + logger.error( + 'The curses interface requires the pythondialog package and' + ' the dialog utility.') return False - try: - import curses - except ImportError: - logger.error('The curses interface can not be used. The curses module is not available.') + curses = try_import('curses') + if not curses: + logger.error('The curses interface can not be used.') + return False + + logger.info('curses Module Version: %s', curses.version) + + dialog = try_import('dialog') + if not dialog: + logger.error('The curses interface can not be used.') return False - logger.info('curses Module Version: ' + curses.version) + try: - import dialog - except ImportError: - logger.error('The curses interface can not be used. The pythondialog package is not available.') + subprocess.check_call(['which', 'dialog']) # nosec B603, B607 + except subprocess.CalledProcessError: + logger.error( + 'Curses requires the `dialog` command to be installed as well as' + ' the python library.') return False - logger.info('pythondialog Package Version: ' + dialog.__version__) + + logger.info('pythondialog Package Version: %s', dialog.__version__) dialog_util_version = dialog.Dialog().cached_backend_version - #The pythondialog author does not like Python2 str, so we have to use - #unicode for just the version otherwise we get the repr form which includes - #the module and class names along with the actual version. - logger.info('dialog Utility Version' + unicode(dialog_util_version)) + # The pythondialog author does not like Python2 str, so we have to use + # unicode for just the version otherwise we get the repr form which + # includes the module and class names along with the actual version. + logger.info('dialog Utility Version %s', dialog_util_version.decode('utf-8')) return True + def check_pyqt(): """Do pyqt dependency check. Here we are checking for PyQt4 with its version, as for it require - PyQt 4.7 or later. + PyQt 4.8 or later. """ - try: - import PyQt4.QtCore - except ImportError: - logger.error('The PyQt4 package is not available. PyBitmessage requires PyQt 4.8 or later and Qt 4.7 or later.') - if sys.platform.startswith('openbsd'): - logger.error('On OpenBSD, try running "pkg_add py-qt4" as root.') - elif sys.platform.startswith('freebsd'): - logger.error('On FreeBSD, try running "pkg install py27-qt4" as root.') - elif os.path.isfile("/etc/os-release"): - with open("/etc/os-release", 'rt') as osRelease: - for line in osRelease: - if line.startswith("NAME="): - if "fedora" in line.lower(): - logger.error('On Fedora, try running "dnf install PyQt4" as root.') - elif "opensuse" in line.lower(): - logger.error('On openSUSE, try running "zypper install python-qt" as root.') - elif "ubuntu" in line.lower(): - logger.error('On Ubuntu, try running "apt-get install python-qt4" as root.') - elif "debian" in line.lower(): - logger.error('On Debian, try running "apt-get install python-qt4" as root.') - else: - logger.error('If your package manager does not have this package, try running "pip install PyQt4".') + QtCore = try_import( + 'PyQt4.QtCore', 'PyBitmessage requires PyQt 4.8 or later and Qt 4.7 or later.') + + if not QtCore: return False - logger.info('PyQt Version: ' + PyQt4.QtCore.PYQT_VERSION_STR) - logger.info('Qt Version: ' + PyQt4.QtCore.QT_VERSION_STR) + + logger.info('PyQt Version: %s', QtCore.PYQT_VERSION_STR) + logger.info('Qt Version: %s', QtCore.QT_VERSION_STR) passed = True - if PyQt4.QtCore.PYQT_VERSION < 0x40800: - logger.error('This version of PyQt is too old. PyBitmessage requries PyQt 4.8 or later.') + if QtCore.PYQT_VERSION < 0x40800: + logger.error( + 'This version of PyQt is too old. PyBitmessage requries' + ' PyQt 4.8 or later.') passed = False - if PyQt4.QtCore.QT_VERSION < 0x40700: - logger.error('This version of Qt is too old. PyBitmessage requries Qt 4.7 or later.') + if QtCore.QT_VERSION < 0x40700: + logger.error( + 'This version of Qt is too old. PyBitmessage requries' + ' Qt 4.7 or later.') passed = False return passed + def check_msgpack(): """Do sgpack module check. - simply checking if msgpack package with all its dependency + simply checking if msgpack package with all its dependency is available or not as recommended for messages coding. """ - try: - import msgpack - except ImportError: - logger.error( - 'The msgpack package is not available.' - 'It is highly recommended for messages coding.') - if sys.platform.startswith('openbsd'): - logger.error('On OpenBSD, try running "pkg_add py-msgpack" as root.') - elif sys.platform.startswith('freebsd'): - logger.error('On FreeBSD, try running "pkg install py27-msgpack-python" as root.') - elif os.path.isfile("/etc/os-release"): - with open("/etc/os-release", 'rt') as osRelease: - for line in osRelease: - if line.startswith("NAME="): - if "fedora" in line.lower(): - logger.error('On Fedora, try running "dnf install python2-msgpack" as root.') - elif "opensuse" in line.lower(): - logger.error('On openSUSE, try running "zypper install python-msgpack-python" as root.') - elif "ubuntu" in line.lower(): - logger.error('On Ubuntu, try running "apt-get install python-msgpack" as root.') - elif "debian" in line.lower(): - logger.error('On Debian, try running "apt-get install python-msgpack" as root.') - else: - logger.error('If your package manager does not have this package, try running "pip install msgpack-python".') + return try_import( + 'msgpack', 'It is highly recommended for messages coding.') is not False - return True -def check_dependencies(verbose = False, optional = False): +def check_dependencies(verbose=False, optional=False): """Do dependency check. It identifies project dependencies and checks if there are @@ -279,33 +429,43 @@ def check_dependencies(verbose = False, optional = False): has_all_dependencies = True - #Python 2.7.3 is the required minimum. Python 3+ is not supported, but it is - #still useful to provide information about our other requirements. + # Python 2.7.4 is the required minimum. + # (https://bitmessage.org/forum/index.php?topic=4081.0) + # Python 3+ is not supported, but it is still useful to provide + # information about our other requirements. logger.info('Python version: %s', sys.version) - if sys.hexversion < 0x20703F0: - logger.error('PyBitmessage requires Python 2.7.3 or greater (but not Python 3+)') - has_all_dependencies = False - if sys.hexversion >= 0x3000000: - logger.error('PyBitmessage does not support Python 3+. Python 2.7.3 or greater is required.') + if sys.hexversion < 0x20704F0: + logger.error( + 'PyBitmessage requires Python 2.7.4 or greater' + ' (but not Python 3+)') has_all_dependencies = False + if six.PY3: + logger.error( + 'PyBitmessage does not support Python 3+. Python 2.7.4' + ' or greater is required. Python 2.7.18 is recommended.') + sys.exit() - check_functions = [check_hashlib, check_sqlite, check_openssl, check_msgpack] + # FIXME: This needs to be uncommented when more of the code is python3 compatible + # if sys.hexversion >= 0x3000000 and sys.hexversion < 0x3060000: + # print("PyBitmessage requires python >= 3.6 if using python 3") + + check_functions = [check_ripemd160, check_sqlite, check_openssl] if optional: - check_functions.extend([check_pyqt, check_curses]) + check_functions.extend([check_msgpack, check_pyqt, check_curses]) - #Unexpected exceptions are handled here + # Unexpected exceptions are handled here for check in check_functions: try: has_all_dependencies &= check() - except: - logger.exception(check.__name__ + ' failed unexpectedly.') + except: # noqa:E722 + logger.exception('%s failed unexpectedly.', check.__name__) has_all_dependencies = False - + if not has_all_dependencies: - logger.critical('PyBitmessage cannot start. One or more dependencies are unavailable.') - sys.exit() + sys.exit( + 'PyBitmessage cannot start. One or more dependencies are' + ' unavailable.' + ) -if __name__ == '__main__': - """Check Dependencies""" - check_dependencies(True, True) +logger.setLevel(0) diff --git a/src/fallback/__init__.py b/src/fallback/__init__.py index e69de29bb2..f65999a1ff 100644 --- a/src/fallback/__init__.py +++ b/src/fallback/__init__.py @@ -0,0 +1,32 @@ +""" +Fallback expressions help PyBitmessage modules to run without some external +dependencies. + + +RIPEMD160Hash +------------- + +We need to check :mod:`hashlib` for RIPEMD-160, as it won't be available +if OpenSSL is not linked against or the linked OpenSSL has RIPEMD disabled. +Try to use `pycryptodome `_ +in that case. +""" + +import hashlib + +try: + hashlib.new('ripemd160') +except ValueError: + try: + from Crypto.Hash import RIPEMD160 + except ImportError: + RIPEMD160Hash = None + else: + RIPEMD160Hash = RIPEMD160.new +else: + def RIPEMD160Hash(data=None): + """hashlib based RIPEMD160Hash""" + hasher = hashlib.new('ripemd160') + if data: + hasher.update(data) + return hasher diff --git a/src/fallback/umsgpack/umsgpack.py b/src/fallback/umsgpack/umsgpack.py index cd7a2037e6..3493861448 100644 --- a/src/fallback/umsgpack/umsgpack.py +++ b/src/fallback/umsgpack/umsgpack.py @@ -31,6 +31,9 @@ # THE SOFTWARE. # """ +src/fallback/umsgpack/umsgpack.py +================================= + u-msgpack-python v2.4.1 - v at sergeev.io https://github.com/vsergeev/u-msgpack-python @@ -43,10 +46,13 @@ License: MIT """ -import struct +# pylint: disable=too-many-lines,too-many-branches,too-many-statements,global-statement,too-many-return-statements +# pylint: disable=unused-argument + import collections -import sys import io +import struct +import sys __version__ = "2.4.1" "Module version string" @@ -60,7 +66,7 @@ ############################################################################## # Extension type for application-defined types and data -class Ext: +class Ext: # pylint: disable=old-style-class """ The Ext class facilitates creating a serializable extension object to store an application-defined type and data byte array. @@ -87,6 +93,8 @@ def __init__(self, type, data): Ext Object (Type: 0x05, Data: 01 02 03) >>> """ + # pylint:disable=redefined-builtin + # Application ext type should be 0 <= type <= 127 if not isinstance(type, int) or not (type >= 0 and type <= 127): raise TypeError("ext type out of range") @@ -205,7 +213,7 @@ class DuplicateKeyException(UnpackException): loads = None compatibility = False -""" +u""" Compatibility mode boolean. When compatibility mode is enabled, u-msgpack-python will serialize both @@ -412,7 +420,7 @@ def _pack2(obj, fp, **options): _pack_ext(ext_handlers[obj.__class__](obj), fp, options) elif isinstance(obj, bool): _pack_boolean(obj, fp, options) - elif isinstance(obj, int) or isinstance(obj, long): + elif isinstance(obj, (int, long)): _pack_integer(obj, fp, options) elif isinstance(obj, float): _pack_float(obj, fp, options) @@ -424,7 +432,7 @@ def _pack2(obj, fp, **options): _pack_string(obj, fp, options) elif isinstance(obj, str): _pack_binary(obj, fp, options) - elif isinstance(obj, list) or isinstance(obj, tuple): + elif isinstance(obj, (list, tuple)): _pack_array(obj, fp, options) elif isinstance(obj, dict): _pack_map(obj, fp, options) @@ -494,7 +502,7 @@ def _pack3(obj, fp, **options): _pack_string(obj, fp, options) elif isinstance(obj, bytes): _pack_binary(obj, fp, options) - elif isinstance(obj, list) or isinstance(obj, tuple): + elif isinstance(obj, (list, tuple)): _pack_array(obj, fp, options) elif isinstance(obj, dict): _pack_map(obj, fp, options) @@ -723,7 +731,7 @@ def _unpack_array(code, fp, options): else: raise Exception("logic error, not array: 0x%02x" % ord(code)) - return [_unpack(fp, options) for i in xrange(length)] + return [_unpack(fp, options) for _ in xrange(length)] def _deep_list_to_tuple(obj): @@ -957,6 +965,8 @@ def _unpackb3(s, **options): def __init(): + # pylint: disable=global-variable-undefined + global pack global packb global unpack @@ -989,7 +999,7 @@ def __init(): unpackb = _unpackb3 load = _unpack3 loads = _unpackb3 - xrange = range + xrange = range # pylint: disable=redefined-builtin else: pack = _pack2 packb = _packb2 diff --git a/src/helper_ackPayload.py b/src/helper_ackPayload.py index acdbadf7a0..1c5ddf98fd 100644 --- a/src/helper_ackPayload.py +++ b/src/helper_ackPayload.py @@ -1,41 +1,47 @@ -"""This module is for generating ack payload.""" +""" +This module is for generating ack payload +""" -import highlevelcrypto -import helper_random from binascii import hexlify from struct import pack -from addresses import encodeVarint -# This function generates payload objects for message acknowledgements -# Several stealth levels are available depending on the privacy needs; -# a higher level means better stealth, but also higher cost (size+POW) -# - level 0: a random 32-byte sequence with a message header appended -# - level 1: a getpubkey request for a (random) dummy key hash -# - level 2: a standard message, encrypted to a random pubkey +import helper_random +import highlevelcrypto +from addresses import encodeVarint def genAckPayload(streamNumber=1, stealthLevel=0): - """Generate and return payload obj.""" - if (stealthLevel == 2): # Generate privacy-enhanced payload + """ + Generate and return payload obj. + + This function generates payload objects for message acknowledgements + Several stealth levels are available depending on the privacy needs; + a higher level means better stealth, but also higher cost (size+POW) + + - level 0: a random 32-byte sequence with a message header appended + - 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 # 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 new file mode 100644 index 0000000000..6d35411330 --- /dev/null +++ b/src/helper_addressbook.py @@ -0,0 +1,14 @@ +""" +Insert value into addressbook +""" + +from bmconfigparser import config +from helper_sql import sqlExecute + + +def insert(address, label): + """perform insert into addressbook""" + + if address not in config.addresses(): + return sqlExecute('''INSERT INTO addressbook VALUES (?,?)''', label, address) == 1 + return False diff --git a/src/helper_bitcoin.py b/src/helper_bitcoin.py index d56e395b96..d4f1d10585 100644 --- a/src/helper_bitcoin.py +++ b/src/helper_bitcoin.py @@ -1,10 +1,19 @@ +""" +Calculates bitcoin and testnet address from pubkey +""" + import hashlib + +from debug import logger from pyelliptic import arithmetic -# This function expects that pubkey begin with \x04 + def calculateBitcoinAddressFromPubkey(pubkey): + """Calculate bitcoin address from given pubkey (65 bytes long hex string)""" if len(pubkey) != 65: - print 'Could not calculate Bitcoin address from pubkey because function was passed a pubkey that was', len(pubkey), 'bytes long rather than 65.' + logger.error('Could not calculate Bitcoin address from pubkey because' + ' function was passed a pubkey that was' + ' %i bytes long rather than 65.', len(pubkey)) return "error" ripe = hashlib.new('ripemd160') sha = hashlib.new('sha256') @@ -24,8 +33,11 @@ def calculateBitcoinAddressFromPubkey(pubkey): def calculateTestnetAddressFromPubkey(pubkey): + """This function expects that pubkey begin with the testnet prefix""" if len(pubkey) != 65: - print 'Could not calculate Bitcoin address from pubkey because function was passed a pubkey that was', len(pubkey), 'bytes long rather than 65.' + logger.error('Could not calculate Bitcoin address from pubkey because' + ' function was passed a pubkey that was' + ' %i bytes long rather than 65.', len(pubkey)) return "error" ripe = hashlib.new('ripemd160') sha = hashlib.new('sha256') diff --git a/src/helper_bootstrap.py b/src/helper_bootstrap.py deleted file mode 100644 index 0ba8634826..0000000000 --- a/src/helper_bootstrap.py +++ /dev/null @@ -1,110 +0,0 @@ -import socket -import defaultKnownNodes -import pickle -import time - -from bmconfigparser import BMConfigParser -from debug import logger -import knownnodes -import socks -import state - - -def addKnownNode(stream, peer, lastseen=None, self=False): - if lastseen is None: - lastseen = time.time() - knownnodes.knownNodes[stream][peer] = { - "lastseen": lastseen, - "rating": 0, - "self": self, - } - - -def knownNodes(): - try: - with open(state.appdata + 'knownnodes.dat', 'rb') as pickleFile: - with knownnodes.knownNodesLock: - knownnodes.knownNodes = pickle.load(pickleFile) - # the old format was {Peer:lastseen, ...} - # the new format is {Peer:{"lastseen":i, "rating":f}} - for stream in knownnodes.knownNodes.keys(): - for node, params in knownnodes.knownNodes[stream].items(): - if isinstance(params, (float, int)): - addKnownNode(stream, node, params) - except: - knownnodes.knownNodes = defaultKnownNodes.createDefaultKnownNodes(state.appdata) - # your own onion address, if setup - if BMConfigParser().has_option('bitmessagesettings', 'onionhostname') and ".onion" in BMConfigParser().get('bitmessagesettings', 'onionhostname'): - addKnownNode(1, state.Peer(BMConfigParser().get('bitmessagesettings', 'onionhostname'), BMConfigParser().getint('bitmessagesettings', 'onionport')), self=True) - if BMConfigParser().getint('bitmessagesettings', 'settingsversion') > 10: - logger.error('Bitmessage cannot read future versions of the keys file (keys.dat). Run the newer version of Bitmessage.') - raise SystemExit - - -def dns(): - # DNS bootstrap. This could be programmed to use the SOCKS proxy to do the - # DNS lookup some day but for now we will just rely on the entries in - # defaultKnownNodes.py. Hopefully either they are up to date or the user - # has run Bitmessage recently without SOCKS turned on and received good - # bootstrap nodes using that method. - def try_add_known_node(stream, addr, port, method=''): - try: - socket.inet_aton(addr) - except (TypeError, socket.error): - return - logger.info( - 'Adding %s to knownNodes based on %s DNS bootstrap method', - addr, method) - addKnownNode(stream, state.Peer(addr, port)) - - proxy_type = BMConfigParser().get('bitmessagesettings', 'socksproxytype') - - if proxy_type == 'none': - for port in [8080, 8444]: - try: - for item in socket.getaddrinfo( - 'bootstrap%s.bitmessage.org' % port, 80): - try_add_known_node(1, item[4][0], port) - except: - logger.error( - 'bootstrap%s.bitmessage.org DNS bootstrapping failed.', - port, exc_info=True - ) - elif proxy_type == 'SOCKS5': - addKnownNode(1, state.Peer('quzwelsuziwqgpt2.onion', 8444)) - logger.debug("Adding quzwelsuziwqgpt2.onion:8444 to knownNodes.") - for port in [8080, 8444]: - logger.debug("Resolving %i through SOCKS...", port) - address_family = socket.AF_INET - sock = socks.socksocket(address_family, socket.SOCK_STREAM) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - sock.settimeout(20) - proxytype = socks.PROXY_TYPE_SOCKS5 - sockshostname = BMConfigParser().get( - 'bitmessagesettings', 'sockshostname') - socksport = BMConfigParser().getint( - 'bitmessagesettings', 'socksport') - rdns = True # Do domain name lookups through the proxy; though this setting doesn't really matter since we won't be doing any domain name lookups anyway. - if BMConfigParser().getboolean('bitmessagesettings', 'socksauthentication'): - socksusername = BMConfigParser().get( - 'bitmessagesettings', 'socksusername') - sockspassword = BMConfigParser().get( - 'bitmessagesettings', 'sockspassword') - sock.setproxy( - proxytype, sockshostname, socksport, rdns, socksusername, sockspassword) - else: - sock.setproxy( - proxytype, sockshostname, socksport, rdns) - try: - ip = sock.resolve("bootstrap" + str(port) + ".bitmessage.org") - sock.shutdown(socket.SHUT_RDWR) - sock.close() - except: - logger.error("SOCKS DNS resolving failed", exc_info=True) - else: - try_add_known_node(1, ip, port, 'SOCKS') - else: - logger.info( - 'DNS bootstrap skipped because the proxy type does not support' - ' DNS resolution.' - ) diff --git a/src/helper_generic.py b/src/helper_generic.py deleted file mode 100644 index 03d962b5d5..0000000000 --- a/src/helper_generic.py +++ /dev/null @@ -1,102 +0,0 @@ -""" -Helper Generic perform generic oprations for threading. - -Also perform some conversion operations. -""" - -import socket -import sys -from binascii import hexlify, unhexlify -from multiprocessing import current_process -import threading -import traceback - -import shared -import state -from debug import logger -import queues -import shutdown - - -def powQueueSize(): - curWorkerQueue = queues.workerQueue.qsize() - for thread in threading.enumerate(): - try: - if thread.name == "singleWorker": - curWorkerQueue += thread.busy - except Exception as err: - logger.info("Thread error %s", err) - return curWorkerQueue - - -def convertIntToString(n): - a = __builtins__.hex(n) - if a[-1:] == 'L': - a = a[:-1] - if (len(a) % 2) == 0: - return unhexlify(a[2:]) - else: - return unhexlify('0' + a[2:]) - - -def convertStringToInt(s): - return int(hexlify(s), 16) - - -def allThreadTraceback(frame): - id2name = dict([(th.ident, th.name) for th in threading.enumerate()]) - code = [] - for threadId, stack in sys._current_frames().items(): - code.append("\n# Thread: %s(%d)" % ( - id2name.get(threadId, ""), threadId)) - for filename, lineno, name, line in traceback.extract_stack(stack): - code.append('File: "%s", line %d, in %s' % ( - filename, lineno, name)) - if line: - code.append(" %s" % (line.strip())) - print "\n".join(code) - - -def signal_handler(signal, frame): - logger.error("Got signal %i in %s/%s", signal, - current_process().name, - threading.current_thread().name) - if "PoolWorker" in current_process().name: - raise SystemExit - if threading.current_thread().name not in ( - "PyBitmessage", "MainThread"): - return - logger.error("Got signal %i", signal) - if shared.thisapp.daemon or not state.enableGUI: # FIXME redundant? - shutdown.doCleanShutdown() - else: - allThreadTraceback(frame) - print 'Unfortunately you cannot use Ctrl+C when running the UI because the UI captures the signal.' - - -def isHostInPrivateIPRange(host): - if ":" in host: # IPv6 - hostAddr = socket.inet_pton(socket.AF_INET6, host) - if hostAddr == ('\x00' * 15) + '\x01': - return False - if hostAddr[0] == '\xFE' and (ord(hostAddr[1]) & 0xc0) == 0x80: - return False - if (ord(hostAddr[0]) & 0xfe) == 0xfc: - return False - elif ".onion" not in host: - if host[:3] == '10.': - return True - if host[:4] == '172.': - if host[6] == '.': - if int(host[4:6]) >= 16 and int(host[4:6]) <= 31: - return True - if host[:8] == '192.168.': - return True - # Multicast - if host[:3] >= 224 and host[:3] <= 239 and host[4] == '.': - return True - return False - - -def addDataPadding(data, desiredMsgLength=12, paddingChar='\x00'): - return data + paddingChar * (desiredMsgLength - len(data)) diff --git a/src/helper_inbox.py b/src/helper_inbox.py index 95214743d4..555795df19 100644 --- a/src/helper_inbox.py +++ b/src/helper_inbox.py @@ -1,10 +1,11 @@ -"""Helper Inbox performs inbox messagese related operations.""" +"""Helper Inbox performs inbox messages related operations""" -from helper_sql import sqlExecute, sqlQuery import queues +from helper_sql import sqlExecute, sqlQuery def insert(t): + """Perform an insert into the "inbox" table""" sqlExecute('''INSERT INTO inbox VALUES (?,?,?,?,?,?,?,?,?,?)''', *t) # shouldn't emit changedInboxUnread and displayNewInboxMessage # at the same time @@ -12,11 +13,23 @@ def insert(t): def trash(msgid): + """Mark a message in the `inbox` as `trash`""" sqlExecute('''UPDATE inbox SET folder='trash' WHERE msgid=?''', 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) + + def isMessageAlreadyInInbox(sigHash): + """Check for previous instances of this message""" queryReturn = sqlQuery( '''SELECT COUNT(*) FROM inbox WHERE sighash=?''', sigHash) return queryReturn[0][0] != 0 diff --git a/src/helper_msgcoding.py b/src/helper_msgcoding.py index e644c0a40a..05fa1c1b7a 100644 --- a/src/helper_msgcoding.py +++ b/src/helper_msgcoding.py @@ -1,4 +1,14 @@ -#!/usr/bin/python2.7 +""" +Message encoding end decoding functions +""" + +import string +import zlib + +import messagetypes +from bmconfigparser import config +from debug import logger +from tr import _translate try: import msgpack @@ -7,14 +17,6 @@ import umsgpack as msgpack except ImportError: import fallback.umsgpack.umsgpack as msgpack -import string -import zlib - -from bmconfigparser import BMConfigParser -from debug import logger -import messagetypes -from tr import _translate -import helper_random BITMESSAGE_ENCODING_IGNORE = 0 BITMESSAGE_ENCODING_TRIVIAL = 1 @@ -23,19 +25,24 @@ class MsgEncodeException(Exception): + """Exception during message encoding""" pass class MsgDecodeException(Exception): + """Exception during message decoding""" pass class DecompressionSizeException(MsgDecodeException): + # pylint: disable=super-init-not-called + """Decompression resulted in too much data (attack protection)""" def __init__(self, size): self.size = size class MsgEncode(object): + """Message encoder class""" def __init__(self, message, encoding=BITMESSAGE_ENCODING_SIMPLE): self.data = None self.encoding = encoding @@ -50,6 +57,7 @@ def __init__(self, message, encoding=BITMESSAGE_ENCODING_SIMPLE): raise MsgEncodeException("Unknown encoding %i" % (encoding)) def encodeExtended(self, message): + """Handle extended encoding""" try: msgObj = messagetypes.message.Message() self.data = zlib.compress(msgpack.dumps(msgObj.encode(message)), 9) @@ -62,31 +70,41 @@ def encodeExtended(self, message): self.length = len(self.data) def encodeSimple(self, message): - self.data = 'Subject:' + message['subject'] + '\n' + 'Body:' + message['body'] + """Handle simple encoding""" + self.data = 'Subject:%(subject)s\nBody:%(body)s' % message self.length = len(self.data) def encodeTrivial(self, message): + """Handle trivial encoding""" self.data = message['body'] self.length = len(self.data) class MsgDecode(object): + """Message decoder class""" def __init__(self, encoding, data): self.encoding = encoding if self.encoding == BITMESSAGE_ENCODING_EXTENDED: self.decodeExtended(data) - elif self.encoding in [BITMESSAGE_ENCODING_SIMPLE, BITMESSAGE_ENCODING_TRIVIAL]: + elif self.encoding in ( + BITMESSAGE_ENCODING_SIMPLE, BITMESSAGE_ENCODING_TRIVIAL): self.decodeSimple(data) else: - self.body = _translate("MsgDecode", "The message has an unknown encoding.\nPerhaps you should upgrade Bitmessage.") + self.body = _translate( + "MsgDecode", + "The message has an unknown encoding.\n" + "Perhaps you should upgrade Bitmessage.") self.subject = _translate("MsgDecode", "Unknown encoding") def decodeExtended(self, data): + """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") + 1 - len(tmp)) + got = dc.decompress( + data, config.safeGetInt("zlib", "maxsize") + + 1 - len(tmp)) # EOF if got == "": break @@ -116,13 +134,14 @@ def decodeExtended(self, data): raise MsgDecodeException("Malformed message") try: msgObj.process() - except: + except: # noqa:E722 raise MsgDecodeException("Malformed message") if msgType == "message": self.subject = msgObj.subject self.body = msgObj.body def decodeSimple(self, data): + """Handle simple encoding""" bodyPositionIndex = string.find(data, '\nBody:') if bodyPositionIndex > 1: subject = data[8:bodyPositionIndex] @@ -138,24 +157,3 @@ def decodeSimple(self, data): subject = subject.splitlines()[0] self.subject = subject self.body = body - -if __name__ == '__main__': - import random - messageData = { - "subject": ''.join(helper_random.randomchoice(string.ascii_lowercase + string.digits) for _ in range(40)), - "body": ''.join(helper_random.randomchoice(string.ascii_lowercase + string.digits) for _ in range(10000)) - } - obj1 = MsgEncode(messageData, 1) - obj2 = MsgEncode(messageData, 2) - obj3 = MsgEncode(messageData, 3) - print "1:%i 2:%i 3:%i" %(len(obj1.data), len(obj2.data), len(obj3.data)) - - obj1e = MsgDecode(1, obj1.data) - # no subject in trivial encoding - assert messageData["body"] == obj1e.body - obj2e = MsgDecode(2, obj2.data) - assert messageData["subject"] == obj2e.subject - assert messageData["body"] == obj2e.body - obj3e = MsgDecode(3, obj3.data) - assert messageData["subject"] == obj3e.subject - assert messageData["body"] == obj3e.body diff --git a/src/helper_random.py b/src/helper_random.py index 5650187159..ff99678a6a 100644 --- a/src/helper_random.py +++ b/src/helper_random.py @@ -1,15 +1,14 @@ -import os +"""Convenience functions for random operations. Not suitable for security / cryptography operations.""" + import random -from pyelliptic.openssl import OpenSSL + + NoneType = type(None) -def randomBytes(n): - """Method randomBytes.""" - try: - return os.urandom(n) - except NotImplementedError: - return OpenSSL.rand(n) +def seed(): + """Initialize random number generator""" + random.seed() def randomshuffle(population): @@ -36,7 +35,7 @@ def randomsample(population, k): without replacement, its called partial shuffle. """ - return random.sample(population, k) + return random.sample(population, k) # nosec B311 def randomrandrange(x, y=None): @@ -48,9 +47,8 @@ def randomrandrange(x, y=None): but doesnt actually build a range object. """ if isinstance(y, NoneType): - return random.randrange(x) - else: - return random.randrange(x, y) + return random.randrange(x) # nosec + return random.randrange(x, y) # nosec def randomchoice(population): @@ -60,4 +58,4 @@ def randomchoice(population): sequence seq. If seq is empty, raises IndexError. """ - return random.choice(population) + return random.choice(population) # nosec diff --git a/src/helper_search.py b/src/helper_search.py index b3d4f92372..85a9e97172 100644 --- a/src/helper_search.py +++ b/src/helper_search.py @@ -1,85 +1,112 @@ -#!/usr/bin/python2.7 +""" +Additional SQL helper for searching messages. +Used by :mod:`.bitmessageqt`. +""" -from helper_sql import * +from helper_sql import sqlQuery +from tr import _translate -try: - from PyQt4 import QtGui - haveQt = True -except Exception: - haveQt = False -def search_translate (context, text): - if haveQt: - return QtGui.QApplication.translate(context, text) - else: - return text.lower() +def search_sql( + xAddress='toaddress', account=None, folder='inbox', where=None, + what=None, unreadOnly=False +): + """ + Search for messages from given account and folder having search term + in one of it's fields. -def search_sql(xAddress = "toaddress", account = None, folder = "inbox", where = None, what = None, unreadOnly = False): - if what is not None and what != "": - what = "%" + what + "%" - if where == search_translate("MainWindow", "To"): - where = "toaddress" - elif where == search_translate("MainWindow", "From"): - where = "fromaddress" - elif where == search_translate("MainWindow", "Subject"): - where = "subject" - elif where == search_translate("MainWindow", "Message"): - where = "message" + :param str xAddress: address field checked + ('fromaddress', 'toaddress' or 'both') + :param account: the account which is checked + :type account: :class:`.bitmessageqt.account.BMAccount` + instance + :param str folder: the folder which is checked + :param str where: message field which is checked ('toaddress', + 'fromaddress', 'subject' or 'message'), by default check any field + :param str what: the search term + :param bool unreadOnly: if True, search only for unread messages + :return: all messages where field contains + :rtype: list[list] + """ + # pylint: disable=too-many-branches + if what: + what = '%' + what + '%' + if where == _translate("MainWindow", "To"): + where = 'toaddress' + elif where == _translate("MainWindow", "From"): + where = 'fromaddress' + elif where == _translate("MainWindow", "Subject"): + where = 'subject' + elif where == _translate("MainWindow", "Message"): + where = 'message' else: - where = "toaddress || fromaddress || subject || message" - else: - what = None + where = 'toaddress || fromaddress || subject || message' - if folder == "sent": - sqlStatementBase = ''' - SELECT toaddress, fromaddress, subject, status, ackdata, lastactiontime - FROM sent ''' - else: - sqlStatementBase = '''SELECT folder, msgid, toaddress, fromaddress, subject, received, read - FROM inbox ''' + sqlStatementBase = 'SELECT toaddress, fromaddress, subject, ' + ( + 'status, ackdata, lastactiontime FROM sent ' if folder == 'sent' + else 'folder, msgid, received, read FROM inbox ' + ) sqlStatementParts = [] sqlArguments = [] if account is not None: if xAddress == 'both': - sqlStatementParts.append("(fromaddress = ? OR toaddress = ?)") + sqlStatementParts.append('(fromaddress = ? OR toaddress = ?)') sqlArguments.append(account) sqlArguments.append(account) else: - sqlStatementParts.append(xAddress + " = ? ") + sqlStatementParts.append(xAddress + ' = ? ') sqlArguments.append(account) if folder is not None: - if folder == "new": - folder = "inbox" + if folder == 'new': + folder = 'inbox' unreadOnly = True - sqlStatementParts.append("folder = ? ") + sqlStatementParts.append('folder = ? ') sqlArguments.append(folder) else: - sqlStatementParts.append("folder != ?") - sqlArguments.append("trash") - if what is not None: - sqlStatementParts.append("%s LIKE ?" % (where)) + sqlStatementParts.append('folder != ?') + sqlArguments.append('trash') + if what: + sqlStatementParts.append('%s LIKE ?' % (where)) sqlArguments.append(what) if unreadOnly: - sqlStatementParts.append("read = 0") - if len(sqlStatementParts) > 0: - sqlStatementBase += "WHERE " + " AND ".join(sqlStatementParts) - if folder == "sent": - sqlStatementBase += " ORDER BY lastactiontime" + sqlStatementParts.append('read = 0') + if sqlStatementParts: + sqlStatementBase += 'WHERE ' + ' AND '.join(sqlStatementParts) + if folder == 'sent': + sqlStatementBase += ' ORDER BY lastactiontime' return sqlQuery(sqlStatementBase, sqlArguments) -def check_match(toAddress, fromAddress, subject, message, where = None, what = None): - if what is not None and what != "": - if where in (search_translate("MainWindow", "To"), search_translate("MainWindow", "All")): - if what.lower() not in toAddress.lower(): - return False - elif where in (search_translate("MainWindow", "From"), search_translate("MainWindow", "All")): - if what.lower() not in fromAddress.lower(): - return False - elif where in (search_translate("MainWindow", "Subject"), search_translate("MainWindow", "All")): - if what.lower() not in subject.lower(): - return False - elif where in (search_translate("MainWindow", "Message"), search_translate("MainWindow", "All")): - if what.lower() not in message.lower(): - return False + +def check_match( + toAddress, fromAddress, subject, message, where=None, what=None): + """ + Check if a single message matches a filter (used when new messages + are added to messagelists) + """ + if not what: + return True + + if where in ( + _translate("MainWindow", "To"), _translate("MainWindow", "All") + ): + if what.lower() not in toAddress.lower(): + return False + elif where in ( + _translate("MainWindow", "From"), _translate("MainWindow", "All") + ): + if what.lower() not in fromAddress.lower(): + return False + elif where in ( + _translate("MainWindow", "Subject"), + _translate("MainWindow", "All") + ): + if what.lower() not in subject.lower(): + return False + elif where in ( + _translate("MainWindow", "Message"), + _translate("MainWindow", "All") + ): + if what.lower() not in message.lower(): + return False return True diff --git a/src/helper_sent.py b/src/helper_sent.py index 8dde7215d3..aa76e756a8 100644 --- a/src/helper_sent.py +++ b/src/helper_sent.py @@ -1,4 +1,69 @@ -from helper_sql import * +""" +Insert values into sent table +""" -def insert(t): - sqlExecute('''INSERT INTO sent VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''', *t) +import time +import uuid +from addresses import decodeAddress +from bmconfigparser import config +from helper_ackPayload import genAckPayload +from helper_sql import sqlExecute, sqlQuery + + +# pylint: disable=too-many-arguments +def insert(msgid=None, toAddress='[Broadcast subscribers]', fromAddress=None, subject=None, + message=None, status='msgqueued', ripe=None, ackdata=None, sentTime=None, + lastActionTime=None, sleeptill=0, retryNumber=0, encoding=2, ttl=None, folder='sent'): + """Perform an insert into the `sent` table""" + # pylint: disable=unused-variable + # pylint: disable-msg=too-many-locals + + valid_addr = True + if not ripe or not ackdata: + addr = fromAddress if toAddress == '[Broadcast subscribers]' else toAddress + new_status, addressVersionNumber, streamNumber, new_ripe = decodeAddress(addr) + valid_addr = True if new_status == 'success' else False + if not ripe: + ripe = new_ripe + + if not ackdata: + stealthLevel = config.safeGetInt( + 'bitmessagesettings', 'ackstealthlevel') + new_ackdata = genAckPayload(streamNumber, stealthLevel) + ackdata = new_ackdata + if valid_addr: + msgid = msgid if msgid else uuid.uuid4().bytes + 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 config.getint('bitmessagesettings', 'ttl') + + t = (msgid, toAddress, ripe, fromAddress, subject, message, ackdata, + sentTime, lastActionTime, sleeptill, status, retryNumber, folder, + encoding, ttl) + + sqlExecute('''INSERT INTO sent VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''', *t) + 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 18e05e03f0..8dee9e0cb5 100644 --- a/src/helper_sql.py +++ b/src/helper_sql.py @@ -1,19 +1,53 @@ -"""Helper Sql performs sql operations.""" +""" +SQL-related functions defined here are really pass the queries (or other SQL +commands) to :class:`.threads.sqlThread` through `sqlSubmitQueue` queue and check +or return the result got from `sqlReturnQueue`. + +This is done that way because :mod:`sqlite3` is so thread-unsafe that they +won't even let you call it from different threads using your own locks. +SQLite objects can only be used from one thread. + +.. note:: This actually only applies for certain deployments, and/or + really old version of sqlite. I haven't actually seen it anywhere. + Current versions do have support for threading and multiprocessing. + I don't see an urgent reason to refactor this, but it should be noted + in the comment that the problem is mostly not valid. Sadly, last time + I checked, there is no reliable way to check whether the library is + or isn't thread-safe. +""" import threading -import Queue -sqlSubmitQueue = Queue.Queue() -# SQLITE3 is so thread-unsafe that they won't even let you call it from different threads using your own locks. -# SQL objects #can only be called from one thread. -sqlReturnQueue = Queue.Queue() -sqlLock = threading.Lock() - - -def sqlQuery(sqlStatement, *args): - """SQLLITE execute statement and return query.""" - sqlLock.acquire() - sqlSubmitQueue.put(sqlStatement) +from six.moves import queue + + +sqlSubmitQueue = queue.Queue() +"""the queue for SQL""" +sqlReturnQueue = queue.Queue() +"""the queue for results""" +sql_lock = threading.Lock() +""" lock to prevent queueing a new request until the previous response + is available """ +sql_available = False +"""set to True by `.threads.sqlThread` immediately upon start""" +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): + """ + Query sqlite and return results + + :param str sql_statement: SQL statement string + :param list args: SQL query parameters + :rtype: list + """ + assert sql_available + sql_lock.acquire() + sqlSubmitQueue.put(sql_statement) if args == (): sqlSubmitQueue.put('') @@ -22,44 +56,48 @@ def sqlQuery(sqlStatement, *args): else: sqlSubmitQueue.put(args) queryreturn, _ = sqlReturnQueue.get() - sqlLock.release() + sql_lock.release() return queryreturn -def sqlExecuteChunked(sqlStatement, idCount, *args): +def sqlExecuteChunked(sql_statement, idCount, *args): + """Execute chunked SQL statement to avoid argument limit""" # SQLITE_MAX_VARIABLE_NUMBER, # unfortunately getting/setting isn't exposed to python + assert sql_available sqlExecuteChunked.chunkSize = 999 if idCount == 0 or idCount > len(args): return 0 - totalRowCount = 0 - with sqlLock: + total_row_count = 0 + with sql_lock: for i in range( - len(args) - idCount, len(args), - sqlExecuteChunked.chunkSize - (len(args) - idCount) + len(args) - idCount, len(args), + sqlExecuteChunked.chunkSize - (len(args) - idCount) ): chunk_slice = args[ i:i + sqlExecuteChunked.chunkSize - (len(args) - idCount) ] sqlSubmitQueue.put( - sqlStatement.format(','.join('?' * len(chunk_slice))) + sql_statement.format(','.join('?' * len(chunk_slice))) ) # first static args, and then iterative chunk sqlSubmitQueue.put( args[0:len(args) - idCount] + chunk_slice ) - retVal = sqlReturnQueue.get() - totalRowCount += retVal[1] + ret_val = sqlReturnQueue.get() + total_row_count += ret_val[1] sqlSubmitQueue.put('commit') - return totalRowCount + return total_row_count -def sqlExecute(sqlStatement, *args): - sqlLock.acquire() - sqlSubmitQueue.put(sqlStatement) +def sqlExecute(sql_statement, *args): + """Execute SQL statement (optionally with arguments)""" + assert sql_available + sql_lock.acquire() + sqlSubmitQueue.put(sql_statement) if args == (): sqlSubmitQueue.put('') @@ -67,30 +105,47 @@ def sqlExecute(sqlStatement, *args): sqlSubmitQueue.put(args) _, rowcount = sqlReturnQueue.get() sqlSubmitQueue.put('commit') - sqlLock.release() + sql_lock.release() return rowcount + +def sqlExecuteScript(sql_statement): + """Execute SQL script statement""" + + statements = sql_statement.split(";") + with SqlBulkExecute() as sql: + for q in statements: + sql.execute("{}".format(q)) + + def sqlStoredProcedure(procName): - sqlLock.acquire() + """Schedule procName to be run""" + assert sql_available + sql_lock.acquire() sqlSubmitQueue.put(procName) - sqlLock.release() + if procName == "exit": + sqlSubmitQueue.task_done() + sqlSubmitQueue.put("terminate") + sql_lock.release() -class SqlBulkExecute: +class SqlBulkExecute(object): """This is used when you have to execute the same statement in a cycle.""" def __enter__(self): - sqlLock.acquire() + sql_lock.acquire() return self def __exit__(self, exc_type, value, traceback): sqlSubmitQueue.put('commit') - sqlLock.release() + sql_lock.release() @staticmethod - def execute(sqlStatement, *args): + def execute(sql_statement, *args): """Used for statements that do not return results.""" - sqlSubmitQueue.put(sqlStatement) + assert sql_available + sqlSubmitQueue.put(sql_statement) + if args == (): sqlSubmitQueue.put('') else: diff --git a/src/helper_startup.py b/src/helper_startup.py index aaab59d90c..52e1bf7ae4 100644 --- a/src/helper_startup.py +++ b/src/helper_startup.py @@ -1,155 +1,373 @@ -"""Helper Start performs all the startup operations.""" +""" +Startup operations. +""" +# pylint: disable=too-many-branches,too-many-statements -import ConfigParser -from bmconfigparser import BMConfigParser -import defaults -import sys +import ctypes +import logging import os import platform +import socket +import sys +import time from distutils.version import StrictVersion +from struct import pack +from six.moves import configparser -from namecoin import ensureNamecoinOptions -import paths -import state -import helper_random +try: + import defaults + import helper_random + import paths + import state + from bmconfigparser import config, config_ready +except ImportError: + from . import defaults, helper_random, paths, state + from .bmconfigparser import config, config_ready -StoreConfigFilesInSameDirectoryAsProgramByDefault = False -# The user may de-select Portable Mode in the settings if they want the config -# files to stay in the application data folder. +try: + from plugins.plugin import get_plugin +except ImportError: + get_plugin = None -def _loadTrustedPeer(): - try: - trustedPeer = BMConfigParser().get('bitmessagesettings', 'trustedpeer') - except ConfigParser.Error: - # This probably means the trusted peer wasn't specified so we - # can just leave it as None - return +logger = logging.getLogger('default') - host, port = trustedPeer.split(':') - state.trustedPeer = state.Peer(host, int(port)) +# The user may de-select Portable Mode in the settings if they want +# the config files to stay in the application data folder. +StoreConfigFilesInSameDirectoryAsProgramByDefault = False def loadConfig(): + """Load the config""" if state.appdata: - BMConfigParser().read(state.appdata + 'keys.dat') + config.read(state.appdata + 'keys.dat') # state.appdata must have been specified as a startup option. - try: - BMConfigParser().get('bitmessagesettings', 'settingsversion') - print 'Loading config files from directory specified on startup: ' + state.appdata - needToCreateKeysFile = False - except Exception: - needToCreateKeysFile = True - + needToCreateKeysFile = config.safeGet( + 'bitmessagesettings', 'settingsversion') is None + if not needToCreateKeysFile: + logger.info( + 'Loading config files from directory specified' + ' on startup: %s', state.appdata) else: - BMConfigParser().read(paths.lookupExeFolder() + 'keys.dat') - try: - BMConfigParser().get('bitmessagesettings', 'settingsversion') - print 'Loading config files from same directory as program.' + config.read(paths.lookupExeFolder() + 'keys.dat') + + if config.safeGet('bitmessagesettings', 'settingsversion'): + logger.info('Loading config files from same directory as program.') needToCreateKeysFile = False state.appdata = paths.lookupExeFolder() - except Exception: + else: # Could not load the keys.dat file in the program directory. # Perhaps it is in the appdata directory. state.appdata = paths.lookupAppdataFolder() - BMConfigParser().read(state.appdata + 'keys.dat') - try: - BMConfigParser().get('bitmessagesettings', 'settingsversion') - print 'Loading existing config files from', state.appdata - needToCreateKeysFile = False - except Exception: - needToCreateKeysFile = True + config.read(state.appdata + 'keys.dat') + needToCreateKeysFile = config.safeGet( + 'bitmessagesettings', 'settingsversion') is None + if not needToCreateKeysFile: + logger.info( + 'Loading existing config files from %s', state.appdata) if needToCreateKeysFile: + # This appears to be the first time running the program; there is # no config file (or it cannot be accessed). Create config file. - BMConfigParser().add_section('bitmessagesettings') - BMConfigParser().set('bitmessagesettings', 'settingsversion', '10') - BMConfigParser().set('bitmessagesettings', 'port', '8444') - BMConfigParser().set( - 'bitmessagesettings', 'timeformat', '%%c') - BMConfigParser().set('bitmessagesettings', 'blackwhitelist', 'black') - BMConfigParser().set('bitmessagesettings', 'startonlogon', 'false') + # config.add_section('bitmessagesettings') + config.read() + config.set('bitmessagesettings', 'settingsversion', '10') if 'linux' in sys.platform: - BMConfigParser().set( - 'bitmessagesettings', 'minimizetotray', 'false') - # This isn't implimented yet and when True on - # Ubuntu causes Bitmessage to disappear while - # running when minimized. + config.set('bitmessagesettings', 'minimizetotray', 'false') + # This isn't implimented yet and when True on + # Ubuntu causes Bitmessage to disappear while + # running when minimized. else: - BMConfigParser().set( - 'bitmessagesettings', 'minimizetotray', 'true') - BMConfigParser().set( - 'bitmessagesettings', 'showtraynotifications', 'true') - BMConfigParser().set('bitmessagesettings', 'startintray', 'false') - BMConfigParser().set('bitmessagesettings', 'socksproxytype', 'none') - BMConfigParser().set( - 'bitmessagesettings', 'sockshostname', 'localhost') - BMConfigParser().set('bitmessagesettings', 'socksport', '9050') - BMConfigParser().set( - 'bitmessagesettings', 'socksauthentication', 'false') - BMConfigParser().set( - 'bitmessagesettings', 'sockslisten', 'false') - BMConfigParser().set('bitmessagesettings', 'socksusername', '') - BMConfigParser().set('bitmessagesettings', 'sockspassword', '') - BMConfigParser().set('bitmessagesettings', 'keysencrypted', 'false') - BMConfigParser().set( - 'bitmessagesettings', 'messagesencrypted', 'false') - BMConfigParser().set('bitmessagesettings', 'defaultnoncetrialsperbyte', str( - defaults.networkDefaultProofOfWorkNonceTrialsPerByte)) - BMConfigParser().set('bitmessagesettings', 'defaultpayloadlengthextrabytes', str( - defaults.networkDefaultPayloadLengthExtraBytes)) - BMConfigParser().set('bitmessagesettings', 'minimizeonclose', 'false') - BMConfigParser().set( - 'bitmessagesettings', 'maxacceptablenoncetrialsperbyte', '0') - BMConfigParser().set( - 'bitmessagesettings', 'maxacceptablepayloadlengthextrabytes', '0') - BMConfigParser().set('bitmessagesettings', 'dontconnect', 'true') - BMConfigParser().set('bitmessagesettings', 'userlocale', 'system') - BMConfigParser().set('bitmessagesettings', 'useidenticons', 'True') - BMConfigParser().set('bitmessagesettings', 'identiconsuffix', ''.join(helper_random.randomchoice("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz") for x in range(12)))# a twelve character pseudo-password to salt the identicons - BMConfigParser().set('bitmessagesettings', 'replybelow', 'False') - BMConfigParser().set('bitmessagesettings', 'maxdownloadrate', '0') - BMConfigParser().set('bitmessagesettings', 'maxuploadrate', '0') - BMConfigParser().set('bitmessagesettings', 'maxoutboundconnections', '8') - BMConfigParser().set('bitmessagesettings', 'ttl', '367200') - #start:UI setting to stop trying to send messages after X days/months - BMConfigParser().set( - 'bitmessagesettings', 'stopresendingafterxdays', '') - BMConfigParser().set( - 'bitmessagesettings', 'stopresendingafterxmonths', '') - #BMConfigParser().set( - # 'bitmessagesettings', 'timeperiod', '-1') - #end + config.set('bitmessagesettings', 'minimizetotray', 'true') + config.set( + 'bitmessagesettings', 'defaultnoncetrialsperbyte', + str(defaults.networkDefaultProofOfWorkNonceTrialsPerByte)) + config.set( + 'bitmessagesettings', 'defaultpayloadlengthextrabytes', + str(defaults.networkDefaultPayloadLengthExtraBytes)) + config.set('bitmessagesettings', 'dontconnect', 'true') + # UI setting to stop trying to send messages after X days/months + # 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 # the first time. But you must also add it to the keys.dat file of - # existing users. To do that, search the class_sqlThread.py file for the - # text: "right above this line!" - - ensureNamecoinOptions() + # existing users. To do that, search the class_sqlThread.py file + # for the text: "right above this line!" if StoreConfigFilesInSameDirectoryAsProgramByDefault: # Just use the same directory as the program and forget about # the appdata folder state.appdata = '' - print 'Creating new config files in same directory as program.' + logger.info( + 'Creating new config files in same directory as program.') else: - print 'Creating new config files in', state.appdata + logger.info('Creating new config files in %s', state.appdata) if not os.path.exists(state.appdata): os.makedirs(state.appdata) if not sys.platform.startswith('win'): os.umask(0o077) - BMConfigParser().save() + config.save() + else: + updateConfig() + config_ready.set() + + +def updateConfig(): + """Save the config""" + settingsversion = config.getint('bitmessagesettings', 'settingsversion') + if settingsversion == 1: + 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', 'sockslisten', 'false') + config.set('bitmessagesettings', 'keysencrypted', 'false') + config.set('bitmessagesettings', 'messagesencrypted', 'false') + settingsversion = 2 + # let class_sqlThread update SQL and continue + elif settingsversion == 4: + config.set( + 'bitmessagesettings', 'defaultnoncetrialsperbyte', + str(defaults.networkDefaultProofOfWorkNonceTrialsPerByte)) + config.set( + 'bitmessagesettings', 'defaultpayloadlengthextrabytes', + str(defaults.networkDefaultPayloadLengthExtraBytes)) + settingsversion = 5 + + if settingsversion == 5: + config.set( + 'bitmessagesettings', 'maxacceptablenoncetrialsperbyte', '0') + config.set( + 'bitmessagesettings', 'maxacceptablepayloadlengthextrabytes', '0') + settingsversion = 7 + + if not config.has_option('bitmessagesettings', 'sockslisten'): + config.set('bitmessagesettings', 'sockslisten', 'false') + + if not config.has_option('bitmessagesettings', 'userlocale'): + config.set('bitmessagesettings', 'userlocale', 'system') + + if not config.has_option('bitmessagesettings', 'sendoutgoingconnections'): + config.set('bitmessagesettings', 'sendoutgoingconnections', 'True') - _loadTrustedPeer() + if not config.has_option('bitmessagesettings', 'useidenticons'): + config.set('bitmessagesettings', 'useidenticons', 'True') + if not config.has_option('bitmessagesettings', 'identiconsuffix'): + # acts as a salt + config.set( + 'bitmessagesettings', 'identiconsuffix', ''.join( + helper_random.randomchoice( + "123456789ABCDEFGHJKLMNPQRSTUVWXYZ" + "abcdefghijkmnopqrstuvwxyz") for x in range(12)) + ) # a twelve character pseudo-password to salt the identicons -def isOurOperatingSystemLimitedToHavingVeryFewHalfOpenConnections(): + # Add settings to support no longer resending messages after + # a certain period of time even if we never get an ack + if settingsversion == 7: + config.set('bitmessagesettings', 'stopresendingafterxdays', '') + config.set('bitmessagesettings', 'stopresendingafterxmonths', '') + settingsversion = 8 + + # With the change to protocol version 3, reset the user-settable + # difficulties to 1 + if settingsversion == 8: + config.set( + 'bitmessagesettings', 'defaultnoncetrialsperbyte', + str(defaults.networkDefaultProofOfWorkNonceTrialsPerByte)) + config.set( + 'bitmessagesettings', 'defaultpayloadlengthextrabytes', + str(defaults.networkDefaultPayloadLengthExtraBytes)) + previousTotalDifficulty = int( + config.getint( + 'bitmessagesettings', 'maxacceptablenoncetrialsperbyte') + ) / 320 + previousSmallMessageDifficulty = int( + config.getint( + 'bitmessagesettings', 'maxacceptablepayloadlengthextrabytes') + ) / 14000 + config.set( + 'bitmessagesettings', 'maxacceptablenoncetrialsperbyte', + str(previousTotalDifficulty * 1000)) + config.set( + 'bitmessagesettings', 'maxacceptablepayloadlengthextrabytes', + str(previousSmallMessageDifficulty * 1000)) + settingsversion = 9 + + # Adjust the required POW values for each of this user's addresses + # to conform to protocol v3 norms. + if settingsversion == 9: + for addressInKeysFile in config.addresses(): + try: + previousTotalDifficulty = float( + config.getint( + addressInKeysFile, 'noncetrialsperbyte')) / 320 + previousSmallMessageDifficulty = float( + config.getint( + addressInKeysFile, 'payloadlengthextrabytes')) / 14000 + if previousTotalDifficulty <= 2: + previousTotalDifficulty = 1 + if previousSmallMessageDifficulty < 1: + previousSmallMessageDifficulty = 1 + config.set( + addressInKeysFile, 'noncetrialsperbyte', + str(int(previousTotalDifficulty * 1000))) + config.set( + addressInKeysFile, 'payloadlengthextrabytes', + str(int(previousSmallMessageDifficulty * 1000))) + except (ValueError, TypeError, configparser.NoSectionError, + configparser.NoOptionError): + continue + config.set('bitmessagesettings', 'maxdownloadrate', '0') + config.set('bitmessagesettings', 'maxuploadrate', '0') + settingsversion = 10 + + # sanity check + if config.safeGetInt( + 'bitmessagesettings', 'maxacceptablenoncetrialsperbyte') == 0: + config.set( + 'bitmessagesettings', 'maxacceptablenoncetrialsperbyte', + str(defaults.ridiculousDifficulty + * defaults.networkDefaultProofOfWorkNonceTrialsPerByte) + ) + if config.safeGetInt( + 'bitmessagesettings', 'maxacceptablepayloadlengthextrabytes') == 0: + config.set( + 'bitmessagesettings', 'maxacceptablepayloadlengthextrabytes', + str(defaults.ridiculousDifficulty + * defaults.networkDefaultPayloadLengthExtraBytes) + ) + + if not config.has_option('bitmessagesettings', 'onionport'): + config.set('bitmessagesettings', 'onionport', '8444') + if not config.has_option('bitmessagesettings', 'onionbindip'): + config.set('bitmessagesettings', 'onionbindip', '127.0.0.1') + if not config.has_option('bitmessagesettings', 'smtpdeliver'): + config.set('bitmessagesettings', 'smtpdeliver', '') + if not config.has_option( + 'bitmessagesettings', 'hidetrayconnectionnotifications'): + config.set( + 'bitmessagesettings', 'hidetrayconnectionnotifications', 'false') + if config.safeGetInt('bitmessagesettings', 'maxoutboundconnections') < 1: + config.set('bitmessagesettings', 'maxoutboundconnections', '8') + logger.warning('Your maximum outbound connections must be a number.') + + # TTL is now user-specifiable. Let's add an option to save + # whatever the user selects. + if not config.has_option('bitmessagesettings', 'ttl'): + config.set('bitmessagesettings', 'ttl', '367200') + + config.set('bitmessagesettings', 'settingsversion', str(settingsversion)) + config.save() + + +def adjustHalfOpenConnectionsLimit(): + """Check and satisfy half-open connections limit (mainly XP and Vista)""" + if config.safeGet( + 'bitmessagesettings', 'socksproxytype', 'none') != 'none': + state.maximumNumberOfHalfOpenConnections = 4 + return + + is_limited = False try: if sys.platform[0:3] == "win": + # Some XP and Vista systems can only have 10 outgoing + # connections at a time. VER_THIS = StrictVersion(platform.version()) - return StrictVersion("5.1.2600")<=VER_THIS and StrictVersion("6.0.6000")>=VER_THIS - return False - except Exception: - return False + is_limited = ( + StrictVersion("5.1.2600") <= VER_THIS + and StrictVersion("6.0.6000") >= VER_THIS + ) + except ValueError: + pass + + state.maximumNumberOfHalfOpenConnections = 9 if is_limited else 64 + + +def fixSocket(): + """Add missing socket options and methods mainly on Windows""" + if sys.platform.startswith('linux'): + socket.SO_BINDTODEVICE = 25 + + if not sys.platform.startswith('win'): + return + + # Python 2 on Windows doesn't define a wrapper for + # socket.inet_ntop but we can make one ourselves using ctypes + if not hasattr(socket, 'inet_ntop'): + addressToString = ctypes.windll.ws2_32.WSAAddressToStringA + + def inet_ntop(family, host): + """Converting an IP address in packed + binary format to string format""" + if family == socket.AF_INET: + if len(host) != 4: + raise ValueError("invalid IPv4 host") + host = pack("hH4s8s", socket.AF_INET, 0, host, "\0" * 8) + elif family == socket.AF_INET6: + if len(host) != 16: + raise ValueError("invalid IPv6 host") + host = pack("hHL16sL", socket.AF_INET6, 0, 0, host, 0) + else: + raise ValueError("invalid address family") + buf = "\0" * 64 + lengthBuf = pack("I", len(buf)) + addressToString(host, len(host), None, buf, lengthBuf) + return buf[0:buf.index("\0")] + socket.inet_ntop = inet_ntop + + # Same for inet_pton + if not hasattr(socket, 'inet_pton'): + stringToAddress = ctypes.windll.ws2_32.WSAStringToAddressA + + def inet_pton(family, host): + """Converting an IP address in string format + to a packed binary format""" + buf = "\0" * 28 + lengthBuf = pack("I", len(buf)) + if stringToAddress(str(host), + int(family), + None, + buf, + lengthBuf) != 0: + raise socket.error("illegal IP address passed to inet_pton") + if family == socket.AF_INET: + return buf[4:8] + elif family == socket.AF_INET6: + return buf[8:24] + else: + raise ValueError("invalid address family") + socket.inet_pton = inet_pton + + # These sockopts are needed on for IPv6 support + if not hasattr(socket, 'IPPROTO_IPV6'): + socket.IPPROTO_IPV6 = 41 + if not hasattr(socket, 'IPV6_V6ONLY'): + socket.IPV6_V6ONLY = 27 + + +def start_proxyconfig(): + """Check socksproxytype and start any proxy configuration plugin""" + if not get_plugin: + return + config_ready.wait() + proxy_type = config.safeGet('bitmessagesettings', 'socksproxytype') + if proxy_type and proxy_type not in ('none', 'SOCKS4a', 'SOCKS5'): + try: + proxyconfig_start = time.time() + if not get_plugin('proxyconfig', name=proxy_type)(config): + raise TypeError() + except TypeError: + # cannot import shutdown here ): + logger.error( + 'Failed to run proxy config plugin %s', + proxy_type, exc_info=True) + config.setTemp('bitmessagesettings', 'dontconnect', 'true') + else: + logger.info( + 'Started proxy config plugin %s in %s sec', + proxy_type, time.time() - proxyconfig_start) diff --git a/src/helper_threading.py b/src/helper_threading.py deleted file mode 100644 index 6b6a5e2527..0000000000 --- a/src/helper_threading.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Helper threading perform all the threading operations.""" - -from contextlib import contextmanager -import threading - -try: - import prctl -except ImportError: - def set_thread_name(name): - """Set the thread name for external use (visible from the OS).""" - threading.current_thread().name = name -else: - def set_thread_name(name): - """Set a name for the thread for python internal use.""" - prctl.set_name(name) - - def _thread_name_hack(self): - set_thread_name(self.name) - threading.Thread.__bootstrap_original__(self) - - threading.Thread.__bootstrap_original__ = threading.Thread._Thread__bootstrap - threading.Thread._Thread__bootstrap = _thread_name_hack - - -class StoppableThread(object): - def initStop(self): - self.stop = threading.Event() - self._stopped = False - - def stopThread(self): - self._stopped = True - self.stop.set() - - -class BusyError(threading.ThreadError): - pass - -@contextmanager -def nonBlocking(lock): - locked = lock.acquire(False) - if not locked: - raise BusyError - try: - yield - finally: - lock.release() diff --git a/src/highlevelcrypto.py b/src/highlevelcrypto.py index 8729ec5c66..b83da2f34a 100644 --- a/src/highlevelcrypto.py +++ b/src/highlevelcrypto.py @@ -1,101 +1,238 @@ +""" +High level cryptographic functions based on `.pyelliptic` OpenSSL bindings. + +.. note:: + Upstream pyelliptic was upgraded from SHA1 to SHA256 for signing. We must + `upgrade PyBitmessage gracefully. `_ + `More discussion. `_ +""" + +import hashlib +import os from binascii import hexlify -from bmconfigparser import BMConfigParser -import pyelliptic -from pyelliptic import arithmetic as a, OpenSSL -def makeCryptor(privkey): - private_key = a.changebase(privkey, 16, 256, minlen=32) - public_key = pointMult(private_key) - privkey_bin = '\x02\xca\x00\x20' + private_key - pubkey_bin = '\x02\xca\x00\x20' + public_key[1:-32] + '\x00\x20' + public_key[-32:] - cryptor = pyelliptic.ECC(curve='secp256k1',privkey=privkey_bin,pubkey=pubkey_bin) - return cryptor + +try: + import pyelliptic + from fallback import RIPEMD160Hash + from pyelliptic import OpenSSL + from pyelliptic import arithmetic as a +except ImportError: + from pybitmessage import pyelliptic + from pybitmessage.fallback import RIPEMD160Hash + from pybitmessage.pyelliptic import OpenSSL + from pybitmessage.pyelliptic import arithmetic as a + + +__all__ = [ + 'decodeWalletImportFormat', 'deterministic_keys', + 'double_sha512', 'calculateInventoryHash', 'encodeWalletImportFormat', + 'encrypt', 'makeCryptor', 'pointMult', 'privToPub', 'randomBytes', + 'random_keys', 'sign', 'to_ripe', 'verify'] + + +# 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): - pubkey_raw = a.changebase(pubkey[2:],16,256,minlen=64) - pubkey_bin = '\x02\xca\x00 '+pubkey_raw[:32]+'\x00 '+pubkey_raw[32:] + """Convert a pubkey from hex to binary""" + pubkey_raw = a.changebase(pubkey[2:], 16, 256, minlen=64) + pubkey_bin = b'\x02\xca\x00 ' + pubkey_raw[:32] + b'\x00 ' + pubkey_raw[32:] return pubkey_bin -def makePubCryptor(pubkey): - pubkey_bin = hexToPubkey(pubkey) - return pyelliptic.ECC(curve='secp256k1',pubkey=pubkey_bin) -# Converts hex private key into hex public key + + def privToPub(privkey): + """Converts hex private key into hex public key""" private_key = a.changebase(privkey, 16, 256, minlen=32) public_key = pointMult(private_key) return hexlify(public_key) -# Encrypts message with hex public key -def encrypt(msg,hexPubkey): - return pyelliptic.ECC(curve='secp256k1').encrypt(msg,hexToPubkey(hexPubkey)) -# Decrypts message with hex private key -def decrypt(msg,hexPrivkey): - return makeCryptor(hexPrivkey).decrypt(msg) -# Decrypts message with an existing pyelliptic.ECC.ECC object -def decryptFast(msg,cryptor): - return cryptor.decrypt(msg) -# Signs with hex private key -def sign(msg,hexPrivkey): - # pyelliptic is upgrading from SHA1 to SHA256 for signing. We must - # upgrade PyBitmessage gracefully. - # https://github.com/yann2192/pyelliptic/pull/33 - # More discussion: https://github.com/yann2192/pyelliptic/issues/32 - digestAlg = BMConfigParser().safeGet('bitmessagesettings', 'digestalg', 'sha1') - 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)) -# Verifies with hex public key -def verify(msg,sig,hexPubkey): - # 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: - sigVerifyPassed = makePubCryptor(hexPubkey).verify(sig,msg,digest_alg=OpenSSL.digest_ecdsa_sha1) # old SHA1 algorithm. - 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 -# Does an EC point multiplication; turns a private key into a public key. + def pointMult(secret): + """ + Does an EC point multiplication; turns a private key into a public key. + + Evidently, this type of error can occur very rarely: + + >>> File "highlevelcrypto.py", line 54, in pointMult + >>> group = OpenSSL.EC_KEY_get0_group(k) + >>> WindowsError: exception: access violation reading 0x0000000000000008 + """ while True: try: - """ - Evidently, this type of error can occur very rarely: - - File "highlevelcrypto.py", line 54, in pointMult - group = OpenSSL.EC_KEY_get0_group(k) - WindowsError: exception: access violation reading 0x0000000000000008 - """ - k = OpenSSL.EC_KEY_new_by_curve_name(OpenSSL.get_curve('secp256k1')) + k = OpenSSL.EC_KEY_new_by_curve_name( + OpenSSL.get_curve('secp256k1')) priv_key = OpenSSL.BN_bin2bn(secret, 32, None) group = OpenSSL.EC_KEY_get0_group(k) pub_key = OpenSSL.EC_POINT_new(group) - + OpenSSL.EC_POINT_mul(group, pub_key, priv_key, None, None, None) OpenSSL.EC_KEY_set_private_key(k, priv_key) OpenSSL.EC_KEY_set_public_key(k, pub_key) - + size = OpenSSL.i2o_ECPublicKey(k, None) 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 as e: + except Exception: import traceback 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/blue-plus-icon-12.png b/src/images/blue-plus-icon-12.png new file mode 100644 index 0000000000..f9007861a4 Binary files /dev/null and b/src/images/blue-plus-icon-12.png differ diff --git a/src/images/kivy/down-arrow.png b/src/images/kivy/down-arrow.png new file mode 100644 index 0000000000..bf3e864c80 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 0000000000..9fc38f3107 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 0000000000..256f9be6b6 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 0000000000..34ab194314 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 0000000000..33302ff87d 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 0000000000..3a63af11de 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 0000000000..f02090f81c 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 0000000000..69550edd56 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 0000000000..f994130dd2 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 0000000000..8f136a774a 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 0000000000..42a1e45a82 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 0000000000..bac2f2461d 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 0000000000..2b8b63e3a7 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 0000000000..3918f6d358 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 0000000000..0cf202e95b 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 0000000000..f9d612dd8b 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 0000000000..f2ab33e1a8 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 0000000000..09d6e56efa 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 0000000000..e385a954ba 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 0000000000..55fc4f77d4 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 0000000000..2a3fa76f9c 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 0000000000..81ad908457 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 0000000000..64ed6110e2 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 0000000000..2db56c1fca 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 0000000000..47a4052c83 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 0000000000..2549ffc2e8 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 0000000000..5d6316114e 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 0000000000..43086f3896 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 0000000000..32d1709d31 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 0000000000..279bd1ce6d 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 0000000000..c88f048d7a 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 0000000000..1533117129 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 0000000000..9afcadd714 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 0000000000..e841b9d90a 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 0000000000..10de35e9e5 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 0000000000..2d235d0618 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 0000000000..c0cc972ad6 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 0000000000..57ec501241 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 0000000000..27ffd18b47 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 0000000000..090646f5d6 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 0000000000..444419cf0d 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 0000000000..ace7b36b25 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 0000000000..a47f326ebf 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 0000000000..da07d0ac16 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 0000000000..a00f9d7c78 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 0000000000..be919fc4b9 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 0000000000..4819bbd197 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 0000000000..7d1c8e0168 Binary files /dev/null and b/src/images/kivy/text_images/Z.png differ diff --git a/src/images/plus-4-xxl.png b/src/images/plus-4-xxl.png new file mode 100644 index 0000000000..1f178267fa Binary files /dev/null and b/src/images/plus-4-xxl.png differ diff --git a/src/images/plus.png b/src/images/plus.png new file mode 100644 index 0000000000..4fd3478c69 Binary files /dev/null and b/src/images/plus.png differ diff --git a/src/inventory.py b/src/inventory.py index 7432b0f130..5b739e84bf 100644 --- a/src/inventory.py +++ b/src/inventory.py @@ -1,26 +1,48 @@ -from bmconfigparser import BMConfigParser -from singleton import Singleton +"""The Inventory""" # TODO make this dynamic, and watch out for frozen, like with messagetypes -import storage.sqlite import storage.filesystem +import storage.sqlite +from bmconfigparser import config + + +def create_inventory_instance(backend="sqlite"): + """ + Create an instance of the inventory class + defined in `storage.`. + """ + return getattr( + getattr(storage, backend), + "{}Inventory".format(backend.title()))() -@Singleton -class Inventory(): + +class Inventory: + """ + Inventory class which uses storage backends + to manage the inventory. + """ def __init__(self): - #super(self.__class__, self).__init__() - 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 def __getattr__(self, attr): + if attr == "__contains__": + self.numberOfInventoryLookupsPerformed += 1 try: - if attr == "__contains__": - self.numberOfInventoryLookupsPerformed += 1 realRet = getattr(self._realInventory, attr) except AttributeError: - raise AttributeError("%s instance has no attribute '%s'" %(self.__class__.__name__, attr)) + raise AttributeError( + "%s instance has no attribute '%s'" % + (self.__class__.__name__, attr) + ) else: return realRet + + # 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/knownnodes.py b/src/knownnodes.py deleted file mode 100644 index aa0801280d..0000000000 --- a/src/knownnodes.py +++ /dev/null @@ -1,49 +0,0 @@ -import pickle -import os -import threading - -from bmconfigparser import BMConfigParser -import state - -knownNodesLock = threading.Lock() -knownNodes = {} - -knownNodesTrimAmount = 2000 - -# forget a node after rating is this low -knownNodesForgetRating = -0.5 - -def saveKnownNodes(dirName = None): - if dirName is None: - dirName = state.appdata - with knownNodesLock: - with open(os.path.join(dirName, 'knownnodes.dat'), 'wb') as output: - pickle.dump(knownNodes, output) - -def increaseRating(peer): - increaseAmount = 0.1 - maxRating = 1 - with knownNodesLock: - for stream in knownNodes.keys(): - try: - knownNodes[stream][peer]["rating"] = min(knownNodes[stream][peer]["rating"] + increaseAmount, maxRating) - except KeyError: - pass - -def decreaseRating(peer): - decreaseAmount = 0.1 - minRating = -1 - with knownNodesLock: - for stream in knownNodes.keys(): - try: - knownNodes[stream][peer]["rating"] = max(knownNodes[stream][peer]["rating"] - decreaseAmount, minRating) - except KeyError: - pass - -def trimKnownNodes(recAddrStream = 1): - if len(knownNodes[recAddrStream]) < int(BMConfigParser().get("knownnodes", "maxnodes")): - return - with knownNodesLock: - oldestList = sorted(knownNodes[recAddrStream], key=lambda x: x['lastseen'])[:knownNodesTrimAmount] - for oldest in oldestList: - del knownNodes[recAddrStream][oldest] diff --git a/src/l10n.py b/src/l10n.py index b3b163419a..441562afa2 100644 --- a/src/l10n.py +++ b/src/l10n.py @@ -1,21 +1,33 @@ +"""Localization helpers""" import logging import os +import re import time -from bmconfigparser import BMConfigParser +import six +from six.moves import range +from bmconfigparser import config -#logger = logging.getLogger(__name__) -logger = logging.getLogger('file_only') - +logger = logging.getLogger('default') DEFAULT_ENCODING = 'ISO8859-1' DEFAULT_LANGUAGE = 'en_US' DEFAULT_TIME_FORMAT = '%Y-%m-%d %H:%M:%S' -encoding = DEFAULT_ENCODING -language = DEFAULT_LANGUAGE +try: + import locale + encoding = locale.getpreferredencoding(True) or DEFAULT_ENCODING + language = ( + locale.getlocale()[0] or locale.getdefaultlocale()[0] + or DEFAULT_LANGUAGE) +except (ImportError, AttributeError): # FIXME: it never happens + logger.exception('Could not determine language or encoding') + locale = None + encoding = DEFAULT_ENCODING + language = DEFAULT_LANGUAGE + windowsLanguageMap = { "ar": "arabic", @@ -40,87 +52,91 @@ "zh_TW": "chinese-traditional" } -try: - import locale - encoding = locale.getpreferredencoding(True) or DEFAULT_ENCODING - language = locale.getlocale()[0] or locale.getdefaultlocale()[0] or DEFAULT_LANGUAGE -except: - logger.exception('Could not determine language or encoding') +time_format = config.safeGet( + 'bitmessagesettings', 'timeformat', DEFAULT_TIME_FORMAT) -if BMConfigParser().has_option('bitmessagesettings', 'timeformat'): - time_format = BMConfigParser().get('bitmessagesettings', 'timeformat') - #Test the format string - try: - time.strftime(time_format) - except: - logger.exception('Could not format timestamp') - time_format = DEFAULT_TIME_FORMAT -else: +if not re.search(r'\d', time.strftime(time_format)): time_format = DEFAULT_TIME_FORMAT -#It seems some systems lie about the encoding they use so we perform -#comprehensive decoding tests -if time_format != DEFAULT_TIME_FORMAT: +# It seems some systems lie about the encoding they use +# so we perform comprehensive decoding tests +elif six.PY2: try: - #Check day names - for i in xrange(7): - unicode(time.strftime(time_format, (0, 0, 0, 0, 0, 0, i, 0, 0)), encoding) - #Check month names - for i in xrange(1, 13): - unicode(time.strftime(time_format, (0, i, 0, 0, 0, 0, 0, 0, 0)), encoding) - #Check AM/PM - unicode(time.strftime(time_format, (0, 0, 0, 11, 0, 0, 0, 0, 0)), encoding) - unicode(time.strftime(time_format, (0, 0, 0, 13, 0, 0, 0, 0, 0)), encoding) - #Check DST - unicode(time.strftime(time_format, (0, 0, 0, 0, 0, 0, 0, 0, 1)), encoding) - except: + # Check day names + for i in range(7): + time.strftime( + time_format, (0, 0, 0, 0, 0, 0, i, 0, 0)).decode(encoding) + # Check month names + for i in range(1, 13): + time.strftime( + time_format, (0, i, 0, 0, 0, 0, 0, 0, 0)).decode(encoding) + # Check AM/PM + time.strftime( + time_format, (0, 0, 0, 11, 0, 0, 0, 0, 0)).decode(encoding) + time.strftime( + time_format, (0, 0, 0, 13, 0, 0, 0, 0, 0)).decode(encoding) + # Check DST + time.strftime( + time_format, (0, 0, 0, 0, 0, 0, 0, 0, 1)).decode(encoding) + except Exception: # TODO: write tests and determine exception types logger.exception('Could not decode locale formatted timestamp') - time_format = DEFAULT_TIME_FORMAT + # time_format = DEFAULT_TIME_FORMAT encoding = DEFAULT_ENCODING -def setlocale(category, newlocale): - locale.setlocale(category, newlocale) + +def setlocale(newlocale): + """Set the locale""" + try: + locale.setlocale(locale.LC_ALL, newlocale) + except AttributeError: # locale is None + pass # it looks like some stuff isn't initialised yet when this is called the # first time and its init gets the locale settings from the environment os.environ["LC_ALL"] = newlocale -def formatTimestamp(timestamp = None, as_unicode = True): - #For some reason some timestamps are strings so we need to sanitize. + +def formatTimestamp(timestamp=None): + """Return a formatted timestamp""" + # For some reason some timestamps are strings so we need to sanitize. if timestamp is not None and not isinstance(timestamp, int): try: timestamp = int(timestamp) - except: + except (ValueError, TypeError): timestamp = None - #timestamp can't be less than 0. + # timestamp can't be less than 0. if timestamp is not None and timestamp < 0: timestamp = None if timestamp is None: timestring = time.strftime(time_format) else: - #In case timestamp is too far in the future + # In case timestamp is too far in the future try: timestring = time.strftime(time_format, time.localtime(timestamp)) except ValueError: timestring = time.strftime(time_format) - if as_unicode: - return unicode(timestring, encoding) + if six.PY2: + return timestring.decode(encoding) return timestring + def getTranslationLanguage(): - userlocale = None - if BMConfigParser().has_option('bitmessagesettings', 'userlocale'): - userlocale = BMConfigParser().get('bitmessagesettings', 'userlocale') + """Return the user's language choice""" + userlocale = config.safeGet( + 'bitmessagesettings', 'userlocale', 'system') + return userlocale if userlocale and userlocale != 'system' else language - if userlocale in [None, '', 'system']: - return language - return userlocale - def getWindowsLocale(posixLocale): + """ + Get the Windows locale + Technically this converts the locale string from UNIX to Windows format, + because they use different ones in their + libraries. E.g. "en_EN.UTF-8" to "english". + """ if posixLocale in windowsLanguageMap: return windowsLanguageMap[posixLocale] if "." in posixLocale: diff --git a/src/main-android-live.py b/src/main-android-live.py new file mode 100644 index 0000000000..e164443614 --- /dev/null +++ b/src/main-android-live.py @@ -0,0 +1,13 @@ +"""This module is for thread start.""" +import state +import sys +from bitmessagemain import main +from termcolor import colored +print(colored('kivy is not supported at the moment for this version..', 'red')) +sys.exit() + + +if __name__ == '__main__': + state.kivy = True + print("Kivy Loading......") + main() diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000000..ce042b84d3 --- /dev/null +++ b/src/main.py @@ -0,0 +1,31 @@ +# 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 mockbm import multiqueue +import state + +from mockbm.class_addressGenerator import FakeAddressGenerator # noqa:E402 +from bitmessagekivy.mpybit import NavigateApp # noqa:E402 +from mockbm import network # noqa:E402 + +stats = network.stats +objectracker = network.objectracker + + +def main(): + """main method for starting threads""" + addressGeneratorThread = FakeAddressGenerator() + addressGeneratorThread.daemon = True + addressGeneratorThread.start() + state.kivyapp = NavigateApp() + state.kivyapp.run() + addressGeneratorThread.stopThread() + + +if __name__ == "__main__": + os.environ['INSTALL_TESTS'] = "True" + main() diff --git a/src/message_data_reader.py b/src/message_data_reader.py deleted file mode 100644 index a0659807cd..0000000000 --- a/src/message_data_reader.py +++ /dev/null @@ -1,112 +0,0 @@ -#This program can be used to print out everything in your Inbox or Sent folders and also take things out of the trash. -#Scroll down to the bottom to see the functions that you can uncomment. Save then run this file. -#The functions which only read the database file seem to function just fine even if you have Bitmessage running but you should definitly close it before running the functions that make changes (like taking items out of the trash). - -import sqlite3 -from time import strftime, localtime -import sys -import paths -import queues -import state -from binascii import hexlify - -appdata = paths.lookupAppdataFolder() - -conn = sqlite3.connect( appdata + 'messages.dat' ) -conn.text_factory = str -cur = conn.cursor() - -def readInbox(): - print 'Printing everything in inbox table:' - item = '''select * from inbox''' - parameters = '' - cur.execute(item, parameters) - output = cur.fetchall() - for row in output: - print row - -def readSent(): - print 'Printing everything in Sent table:' - item = '''select * from sent where folder !='trash' ''' - parameters = '' - cur.execute(item, parameters) - output = cur.fetchall() - for row in output: - msgid, toaddress, toripe, fromaddress, subject, message, ackdata, lastactiontime, sleeptill, status, retrynumber, folder, encodingtype, ttl = row - print hexlify(msgid), toaddress, 'toripe:', hexlify(toripe), 'fromaddress:', fromaddress, 'ENCODING TYPE:', encodingtype, 'SUBJECT:', repr(subject), 'MESSAGE:', repr(message), 'ACKDATA:', hexlify(ackdata), lastactiontime, status, retrynumber, folder - -def readSubscriptions(): - print 'Printing everything in subscriptions table:' - item = '''select * from subscriptions''' - parameters = '' - cur.execute(item, parameters) - output = cur.fetchall() - for row in output: - print row - -def readPubkeys(): - print 'Printing everything in pubkeys table:' - item = '''select address, transmitdata, time, usedpersonally from pubkeys''' - parameters = '' - cur.execute(item, parameters) - output = cur.fetchall() - for row in output: - address, transmitdata, time, usedpersonally = row - print 'Address:', address, '\tTime first broadcast:', unicode(strftime('%a, %d %b %Y %I:%M %p',localtime(time)),'utf-8'), '\tUsed by me personally:', usedpersonally, '\tFull pubkey message:', hexlify(transmitdata) - -def readInventory(): - print 'Printing everything in inventory table:' - item = '''select hash, objecttype, streamnumber, payload, expirestime from inventory''' - parameters = '' - cur.execute(item, parameters) - output = cur.fetchall() - for row in output: - hash, objecttype, streamnumber, payload, expirestime = row - print 'Hash:', hexlify(hash), objecttype, streamnumber, '\t', hexlify(payload), '\t', unicode(strftime('%a, %d %b %Y %I:%M %p',localtime(expirestime)),'utf-8') - - -def takeInboxMessagesOutOfTrash(): - item = '''update inbox set folder='inbox' where folder='trash' ''' - parameters = '' - cur.execute(item, parameters) - output = cur.fetchall() - conn.commit() - print 'done' - -def takeSentMessagesOutOfTrash(): - item = '''update sent set folder='sent' where folder='trash' ''' - parameters = '' - cur.execute(item, parameters) - output = cur.fetchall() - conn.commit() - print 'done' - -def markAllInboxMessagesAsUnread(): - item = '''update inbox set read='0' ''' - parameters = '' - cur.execute(item, parameters) - output = cur.fetchall() - conn.commit() - queues.UISignalQueue.put(('changedInboxUnread', None)) - print 'done' - -def vacuum(): - item = '''VACUUM''' - parameters = '' - cur.execute(item, parameters) - output = cur.fetchall() - conn.commit() - print 'done' - -#takeInboxMessagesOutOfTrash() -#takeSentMessagesOutOfTrash() -#markAllInboxMessagesAsUnread() -readInbox() -#readSent() -#readPubkeys() -#readSubscriptions() -#readInventory() -#vacuum() #will defragment and clean empty space from the messages.dat file. - - - diff --git a/src/messagetypes/__init__.py b/src/messagetypes/__init__.py index 06783eac76..b9ddd1e9d2 100644 --- a/src/messagetypes/__init__.py +++ b/src/messagetypes/__init__.py @@ -1,49 +1,54 @@ -from importlib import import_module -from os import path, listdir -from string import lower +import logging -from debug import logger -import messagetypes -import paths +from importlib import import_module -class MsgBase(object): - def encode(self): - self.data = {"": lower(type(self).__name__)} +logger = logging.getLogger('default') def constructObject(data): + """Constructing an object""" whitelist = ["message"] if data[""] not in whitelist: return None try: - classBase = getattr(getattr(messagetypes, data[""]), data[""].title()) - except (NameError, AttributeError): + classBase = getattr(import_module(".{}".format(data[""]), __name__), data[""].title()) + except (NameError, AttributeError, ValueError, ImportError): + logger.error("Don't know how to handle message type: \"%s\"", data[""], exc_info=True) + return None + except: # noqa:E722 logger.error("Don't know how to handle message type: \"%s\"", data[""], exc_info=True) return None + try: returnObj = classBase() returnObj.decode(data) except KeyError as e: logger.error("Missing mandatory key %s", e) return None - except: + except: # noqa:E722 logger.error("classBase fail", exc_info=True) return None else: return returnObj -if paths.frozen is not None: - import messagetypes.message - import messagetypes.vote + +try: + from pybitmessage import paths +except ImportError: + paths = None + +if paths and paths.frozen is not None: + from . import message, vote # noqa: F401 flake8: disable=unused-import else: - for mod in listdir(path.dirname(__file__)): + import os + for mod in os.listdir(os.path.dirname(__file__)): if mod == "__init__.py": continue - splitted = path.splitext(mod) + splitted = os.path.splitext(mod) if splitted[1] != ".py": continue try: - import_module(".{}".format(splitted[0]), "messagetypes") + import_module(".{}".format(splitted[0]), __name__) except ImportError: logger.error("Error importing %s", mod, exc_info=True) else: diff --git a/src/messagetypes/message.py b/src/messagetypes/message.py index ab61e375e2..245c753fde 100644 --- a/src/messagetypes/message.py +++ b/src/messagetypes/message.py @@ -1,31 +1,45 @@ -from debug import logger -from messagetypes import MsgBase +import logging +logger = logging.getLogger('default') -class Message(MsgBase): + +class MsgBase(object): # pylint: disable=too-few-public-methods + """Base class for message types""" def __init__(self): - return + self.data = {"": type(self).__name__.lower()} + + +class Message(MsgBase): + """Encapsulate a message""" + # pylint: disable=attribute-defined-outside-init def decode(self, data): + """Decode a message""" # UTF-8 and variable type validator - if type(data["subject"]) is str: - self.subject = unicode(data["subject"], 'utf-8', 'replace') - else: - self.subject = unicode(str(data["subject"]), 'utf-8', 'replace') - if type(data["body"]) is str: - self.body = unicode(data["body"], 'utf-8', 'replace') - else: - self.body = unicode(str(data["body"]), 'utf-8', 'replace') + subject = data.get("subject", "") + body = data.get("body", "") + try: + data["subject"] = subject.decode('utf-8', 'replace') + except: + data["subject"] = '' - def encode(self, data): - super(Message, self).encode() try: - self.data["subject"] = data["subject"] - self.data["body"] = data["body"] - except KeyError as e: - logger.error("Missing key %s", e.name) + data["body"] = body.decode('utf-8', 'replace') + except: + data["body"] = '' + + self.subject = data["subject"] + self.body = data["body"] + + def encode(self, data): + """Encode a message""" + super(Message, self).__init__() + self.data["subject"] = data.get("subject", "") + self.data["body"] = data.get("body", "") + return self.data def process(self): + """Process a message""" logger.debug("Subject: %i bytes", len(self.subject)) logger.debug("Body: %i bytes", len(self.body)) diff --git a/src/messagetypes/vote.py b/src/messagetypes/vote.py index df8d267fc8..b3e96513ee 100644 --- a/src/messagetypes/vote.py +++ b/src/messagetypes/vote.py @@ -1,23 +1,30 @@ -from debug import logger -from messagetypes import MsgBase +import logging + +from .message import MsgBase + +logger = logging.getLogger('default') + class Vote(MsgBase): - def __init__(self): - return + """Module used to vote""" def decode(self, data): + """decode a vote""" + # pylint: disable=attribute-defined-outside-init self.msgid = data["msgid"] self.vote = data["vote"] def encode(self, data): - super(Vote, self).encode() + """Encode a vote""" + super(Vote, self).__init__() try: self.data["msgid"] = data["msgid"] self.data["vote"] = data["vote"] except KeyError as e: - logger.error("Missing key %s", e.name) + logger.error("Missing key %s", e) return self.data def process(self): + """Encode a vote""" logger.debug("msgid: %s", self.msgid) logger.debug("vote: %s", self.vote) diff --git a/src/mockbm/__init__.py b/src/mockbm/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/mockbm/class_addressGenerator.py b/src/mockbm/class_addressGenerator.py new file mode 100644 index 0000000000..c84b92d517 --- /dev/null +++ b/src/mockbm/class_addressGenerator.py @@ -0,0 +1,98 @@ +""" +A thread for creating addresses +""" + +import logging +import random +import threading + +from six.moves import queue + +from pybitmessage import state +from pybitmessage import queues + +from pybitmessage.bmconfigparser import config + +# from 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 StoppableThread(threading.Thread): + """Base class for application threads with stopThread method""" + name = None + logger = logging.getLogger('default') + + def __init__(self, name=None): + if name: + self.name = name + super(StoppableThread, self).__init__(name=self.name) + self.stop = threading.Event() + self._stopped = False + random.seed() + self.logger.info('Init thread %s', self.name) + + def stopThread(self): + """Stop the thread""" + self._stopped = True + self.stop.set() + + +class FakeAddressGenerator(StoppableThread): + """A thread for creating fake addresses""" + name = "addressGenerator" + address_list = list(fake_addresses.keys()) + + def stopThread(self): + try: + queues.addressGeneratorQueue.put(("stopThread", "data")) + except queue.Full: + self.logger.warning('addressGeneratorQueue is Full') + super(FakeAddressGenerator, self).stopThread() + + 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) + label = queueValue[3] + + config.add_section(address) + config.set(address, 'label', label) + config.set(address, 'enabled', 'true') + config.set( + address, 'privsigningkey', fake_addresses[address]['privsigningkey']) + config.set( + address, 'privencryptionkey', fake_addresses[address]['privencryptionkey']) + config.save() + + queues.addressGeneratorQueue.task_done() + except IndexError: + self.logger.error( + 'Program error: you can only create 5 fake addresses') diff --git a/src/mockbm/helper_startup.py b/src/mockbm/helper_startup.py new file mode 100644 index 0000000000..2dc06878b7 --- /dev/null +++ b/src/mockbm/helper_startup.py @@ -0,0 +1,16 @@ +import os +from pybitmessage.bmconfigparser import config + + +def loadConfig(): + """Loading mock test data""" + try: + config.read(os.path.join(os.environ['BITMESSAGE_HOME'], 'keys.dat')) + except KeyError: + pass + + +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 0000000000..4cf4da9823 --- /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 0000000000..8ec76920a7 --- /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 0000000000..3f33c91b2f --- /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 9b3c3c3ed2..595067e5a7 100644 --- a/src/namecoin.py +++ b/src/namecoin.py @@ -1,54 +1,41 @@ -# Copyright (C) 2013 by Daniel Kraft -# This file is part of the Bitmessage project. -# -# 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 the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# 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. +""" +Namecoin queries +""" +# pylint: disable=too-many-branches,protected-access import base64 -import httplib import json +import os import socket import sys -import os -from bmconfigparser import BMConfigParser -import defaults -import tr # translate +from six.moves import http_client as httplib -# FIXME: from debug import logger crashes PyBitmessage due to a circular -# dependency. The debug module will also override/disable logging.getLogger() -# loggers so module level logging functions are used instead -import logging as logger +import defaults +from addresses import decodeAddress +from bmconfigparser import config +from debug import logger +from tr import _translate # translate configSection = "bitmessagesettings" -# Error thrown when the RPC call returns an error. -class RPCError (Exception): + +class RPCError(Exception): + """Error thrown when the RPC call returns an error.""" + error = None - def __init__ (self, data): + def __init__(self, data): + super(RPCError, self).__init__() self.error = data - + def __str__(self): - return '{0}: {1}'.format(type(self).__name__, self.error) + return "{0}: {1}".format(type(self).__name__, self.error) + + +class namecoinConnection(object): + """This class handles the Namecoin identity integration.""" -# This class handles the Namecoin identity integration. -class namecoinConnection (object): user = None password = None host = None @@ -58,47 +45,60 @@ class namecoinConnection (object): queryid = 1 con = None - # Initialise. If options are given, take the connection settings from - # them instead of loading from the configs. This can be used to test - # currently entered connection settings in the config dialog without - # actually changing the values (yet). - def __init__ (self, options = None): + def __init__(self, options=None): + """ + Initialise. If options are given, take the connection settings from + them instead of loading from the configs. This can be used to test + currently entered connection settings in the config dialog without + actually changing the values (yet). + """ if options is None: - self.nmctype = BMConfigParser().get (configSection, "namecoinrpctype") - self.host = BMConfigParser().get (configSection, "namecoinrpchost") - self.port = int(BMConfigParser().get (configSection, "namecoinrpcport")) - self.user = BMConfigParser().get (configSection, "namecoinrpcuser") - self.password = BMConfigParser().get (configSection, - "namecoinrpcpassword") + self.nmctype = config.get( + configSection, "namecoinrpctype") + self.host = config.get( + configSection, "namecoinrpchost") + self.port = int(config.get( + configSection, "namecoinrpcport")) + self.user = config.get( + configSection, "namecoinrpcuser") + self.password = config.get( + configSection, "namecoinrpcpassword") else: - self.nmctype = options["type"] - self.host = options["host"] - self.port = int(options["port"]) - self.user = options["user"] - self.password = options["password"] + self.nmctype = options["type"] + self.host = options["host"] + self.port = int(options["port"]) + self.user = options["user"] + self.password = options["password"] assert self.nmctype == "namecoind" or self.nmctype == "nmcontrol" if self.nmctype == "namecoind": - self.con = httplib.HTTPConnection(self.host, self.port, timeout = 3) - - # Query for the bitmessage address corresponding to the given identity - # string. If it doesn't contain a slash, id/ is prepended. We return - # the result as (Error, Address) pair, where the Error is an error - # message to display or None in case of success. - def query (self, string): - slashPos = string.find ("/") + self.con = httplib.HTTPConnection(self.host, self.port, timeout=3) + + def query(self, identity): + """ + Query for the bitmessage address corresponding to the given identity + string. If it doesn't contain a slash, id/ is prepended. We return + the result as (Error, Address) pair, where the Error is an error + message to display or None in case of success. + """ + slashPos = identity.find("/") if slashPos < 0: - string = "id/" + string + display_name = identity + identity = "id/" + identity + else: + display_name = identity.split("/")[1] try: if self.nmctype == "namecoind": - res = self.callRPC ("name_show", [string]) + res = self.callRPC("name_show", [identity]) res = res["value"] elif self.nmctype == "nmcontrol": - res = self.callRPC ("data", ["getValue", string]) + res = self.callRPC("data", ["getValue", identity]) res = res["reply"] - if res == False: - return (tr._translate("MainWindow",'The name %1 was not found.').arg(unicode(string)), None) + if not res: + return (_translate( + "MainWindow", "The name %1 was not found." + ).arg(identity.decode("utf-8", "ignore")), None) else: assert False except RPCError as exc: @@ -107,29 +107,44 @@ def query (self, string): errmsg = exc.error["message"] else: errmsg = exc.error - return (tr._translate("MainWindow",'The namecoin query failed (%1)').arg(unicode(errmsg)), None) - except Exception as exc: + return (_translate( + "MainWindow", "The namecoin query failed (%1)" + ).arg(errmsg.decode("utf-8", "ignore")), None) + except AssertionError: + return (_translate( + "MainWindow", "Unknown namecoin interface type: %1" + ).arg(self.nmctype.decode("utf-8", "ignore")), None) + except Exception: logger.exception("Namecoin query exception") - return (tr._translate("MainWindow",'The namecoin query failed.'), None) + return (_translate( + "MainWindow", "The namecoin query failed."), None) try: - val = json.loads (res) - except: - logger.exception("Namecoin query json exception") - return (tr._translate("MainWindow",'The name %1 has no valid JSON data.').arg(unicode(string)), None) - - if "bitmessage" in val: - if "name" in val: - ret = "%s <%s>" % (val["name"], val["bitmessage"]) - else: - ret = val["bitmessage"] - return (None, ret) - return (tr._translate("MainWindow",'The name %1 has no associated Bitmessage address.').arg(unicode(string)), None) + res = json.loads(res) + except ValueError: + pass + else: + try: + display_name = res["name"] + except KeyError: + pass + res = res.get("bitmessage") + + valid = decodeAddress(res)[0] == "success" + return ( + None, "%s <%s>" % (display_name, res) + ) if valid else ( + _translate( + "MainWindow", + "The name %1 has no associated Bitmessage address." + ).arg(identity.decode("utf-8", "ignore")), None) - # Test the connection settings. This routine tries to query a "getinfo" - # command, and builds either an error message or a success message with - # some info from it. def test(self): + """ + Test the connection settings. This routine tries to query a "getinfo" + command, and builds either an error message or a success message with + some info from it. + """ try: if self.nmctype == "namecoind": try: @@ -143,45 +158,65 @@ def test(self): vers = vers / 100 v1 = vers if v3 == 0: - versStr = "0.%d.%d" % (v1, v2) + versStr = "0.%d.%d" % (v1, v2) else: - versStr = "0.%d.%d.%d" % (v1, v2, v3) - return ('success', tr._translate("MainWindow",'Success! Namecoind version %1 running.').arg(unicode(versStr)) ) + versStr = "0.%d.%d.%d" % (v1, v2, v3) + message = ( + "success", + _translate( + "MainWindow", + "Success! Namecoind version %1 running.").arg( + versStr.decode("utf-8", "ignore"))) elif self.nmctype == "nmcontrol": - res = self.callRPC ("data", ["status"]) + res = self.callRPC("data", ["status"]) prefix = "Plugin data running" if ("reply" in res) and res["reply"][:len(prefix)] == prefix: - return ('success', tr._translate("MainWindow",'Success! NMControll is up and running.')) + return ( + "success", + _translate( + "MainWindow", + "Success! NMControll is up and running." + ) + ) logger.error("Unexpected nmcontrol reply: %s", res) - return ('failed', tr._translate("MainWindow",'Couldn\'t understand NMControl.')) + message = ( + "failed", + _translate( + "MainWindow", + "Couldn\'t understand NMControl." + ) + ) else: - assert False + sys.exit("Unsupported Namecoin type") + + return message except Exception: logger.info("Namecoin connection test failure") return ( - 'failed', - tr._translate( + "failed", + _translate( "MainWindow", "The connection to namecoin failed.") ) - # Helper routine that actually performs an JSON RPC call. - def callRPC (self, method, params): + def callRPC(self, method, params): + """Helper routine that actually performs an JSON RPC call.""" + data = {"method": method, "params": params, "id": self.queryid} if self.nmctype == "namecoind": - resp = self.queryHTTP (json.dumps (data)) + resp = self.queryHTTP(json.dumps(data)) elif self.nmctype == "nmcontrol": - resp = self.queryServer (json.dumps (data)) + resp = self.queryServer(json.dumps(data)) else: - assert False - val = json.loads (resp) + assert False + val = json.loads(resp) if val["id"] != self.queryid: - raise Exception ("ID mismatch in JSON RPC answer.") - + raise Exception("ID mismatch in JSON RPC answer.") + if self.nmctype == "namecoind": self.queryid = self.queryid + 1 @@ -190,11 +225,12 @@ def callRPC (self, method, params): return val["result"] if isinstance(error, bool): - raise RPCError (val["result"]) - raise RPCError (error) + raise RPCError(val["result"]) + raise RPCError(error) + + def queryHTTP(self, data): + """Query the server via HTTP.""" - # Query the server via HTTP. - def queryHTTP (self, data): result = None try: @@ -206,57 +242,71 @@ def queryHTTP (self, data): self.con.putheader("Content-Length", str(len(data))) self.con.putheader("Accept", "application/json") authstr = "%s:%s" % (self.user, self.password) - self.con.putheader("Authorization", "Basic %s" % base64.b64encode (authstr)) + self.con.putheader( + "Authorization", "Basic %s" % base64.b64encode(authstr)) self.con.endheaders() self.con.send(data) - try: - resp = self.con.getresponse() - result = resp.read() - if resp.status != 200: - raise Exception ("Namecoin returned status %i: %s", resp.status, resp.reason) - except: - logger.info("HTTP receive error") - except: + except: # noqa:E722 logger.info("HTTP connection error") + return None + + try: + resp = self.con.getresponse() + result = resp.read() + if resp.status != 200: + raise Exception( + "Namecoin returned status" + " %i: %s" % (resp.status, resp.reason)) + except: # noqa:E722 + logger.info("HTTP receive error") + return None return result - # Helper routine sending data to the RPC server and returning the result. - def queryServer (self, data): + def queryServer(self, data): + """Helper routine sending data to the RPC " + "server and returning the result.""" + try: - s = socket.socket (socket.AF_INET, socket.SOCK_STREAM) - s.setsockopt (socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - s.settimeout(3) - s.connect ((self.host, self.port)) - s.sendall (data) + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.settimeout(3) + s.connect((self.host, self.port)) + s.sendall(data) result = "" while True: - tmp = s.recv (self.bufsize) + tmp = s.recv(self.bufsize) if not tmp: - break + break result += tmp - s.close () + s.close() return result except socket.error as exc: - raise Exception ("Socket error in RPC connection: %s" % str (exc)) + raise Exception("Socket error in RPC connection: %s" % exc) + + +def lookupNamecoinFolder(): + """ + Look up the namecoin data folder. + + .. todo:: Check whether this works on other platforms as well! + """ -# Look up the namecoin data folder. -# FIXME: Check whether this works on other platforms as well! -def lookupNamecoinFolder (): app = "namecoin" - from os import path, environ + from os import environ, path if sys.platform == "darwin": if "HOME" in environ: - dataFolder = path.join (os.environ["HOME"], - "Library/Application Support/", app) + '/' + dataFolder = path.join(os.environ["HOME"], + "Library/Application Support/", app) + "/" else: - print ("Could not find home folder, please report this message" - + " and your OS X version to the BitMessage Github.") - sys.exit() + 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 = path.join(environ["APPDATA"], app) + "\\" @@ -265,34 +315,38 @@ def lookupNamecoinFolder (): return dataFolder -# Ensure all namecoin options are set, by setting those to default values -# that aren't there. -def ensureNamecoinOptions (): - if not BMConfigParser().has_option (configSection, "namecoinrpctype"): - BMConfigParser().set (configSection, "namecoinrpctype", "namecoind") - if not BMConfigParser().has_option (configSection, "namecoinrpchost"): - BMConfigParser().set (configSection, "namecoinrpchost", "localhost") - hasUser = BMConfigParser().has_option (configSection, "namecoinrpcuser") - hasPass = BMConfigParser().has_option (configSection, "namecoinrpcpassword") - hasPort = BMConfigParser().has_option (configSection, "namecoinrpcport") +def ensureNamecoinOptions(): + """ + Ensure all namecoin options are set, by setting those to default values + that aren't there. + """ + + 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 = 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 = "" defaultPass = "" - nmcFolder = lookupNamecoinFolder () + nmcFolder = lookupNamecoinFolder() nmcConfig = nmcFolder + "namecoin.conf" try: - nmc = open (nmcConfig, "r") + nmc = open(nmcConfig, "r") while True: - line = nmc.readline () + line = nmc.readline() if line == "": break - parts = line.split ("=") - if len (parts) == 2: + parts = line.split("=") + if len(parts) == 2: key = parts[0] - val = parts[1].rstrip () + val = parts[1].rstrip() if key == "rpcuser" and not hasUser: defaultUser = val @@ -300,20 +354,21 @@ def ensureNamecoinOptions (): defaultPass = val if key == "rpcport": defaults.namecoinDefaultRpcPort = val - - nmc.close () + + nmc.close() except IOError: - logger.error("%s unreadable or missing, Namecoin support deactivated", nmcConfig) - except Exception as exc: + logger.warning( + "%s unreadable or missing, Namecoin support deactivated", + nmcConfig) + except Exception: logger.warning("Error processing namecoin.conf", exc_info=True) # If still nothing found, set empty at least. - if (not hasUser): - BMConfigParser().set (configSection, "namecoinrpcuser", defaultUser) - if (not hasPass): - BMConfigParser().set (configSection, "namecoinrpcpassword", defaultPass) + if not hasUser: + config.set(configSection, "namecoinrpcuser", defaultUser) + if not hasPass: + config.set(configSection, "namecoinrpcpassword", defaultPass) # Set default port now, possibly to found value. - if (not hasPort): - BMConfigParser().set (configSection, "namecoinrpcport", - defaults.namecoinDefaultRpcPort) + if not hasPort: + config.set(configSection, "namecoinrpcport", defaults.namecoinDefaultRpcPort) diff --git a/src/network/__init__.py b/src/network/__init__.py index e69de29bb2..c87ad64d6c 100644 --- a/src/network/__init__.py +++ b/src/network/__init__.py @@ -0,0 +1,54 @@ +""" +Network subsystem package +""" +from six.moves import queue +from .dandelion import Dandelion +from .threads import StoppableThread +from .multiqueue import MultiQueue + +dandelion_ins = Dandelion() + +# network queues +invQueue = MultiQueue() +addrQueue = MultiQueue() +portCheckerQueue = queue.Queue() +receiveDataQueue = queue.Queue() + +__all__ = ["StoppableThread"] + + +def start(config, state): + """Start network threads""" + from .announcethread import AnnounceThread + import connectionpool # pylint: disable=relative-import + from .addrthread import AddrThread + from .downloadthread import DownloadThread + from .invthread import InvThread + from .networkthread import BMNetworkThread + from .knownnodes import readKnownNodes + from .receivequeuethread import ReceiveQueueThread + from .uploadthread import UploadThread + + # check and set dandelion enabled value at network startup + dandelion_ins.init_dandelion_enabled(config) + # pass pool instance into dandelion class instance + dandelion_ins.init_pool(connectionpool.pool) + + readKnownNodes() + 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 5b0ea638b5..81e4450609 100644 --- a/src/network/addrthread.py +++ b/src/network/addrthread.py @@ -1,34 +1,47 @@ -import Queue -import threading +""" +Announce addresses as they are received from other hosts +""" +import random +from six.moves import queue -import addresses -from helper_threading import StoppableThread -from network.connectionpool import BMConnectionPool -from queues import addrQueue -import protocol -import state +# magic imports! +import connectionpool +from protocol import assembleAddrMessage +from network import addrQueue # FIXME: init with queue -class AddrThread(threading.Thread, StoppableThread): - def __init__(self): - threading.Thread.__init__(self, name="AddrBroadcaster") - self.initStop() - self.name = "AddrBroadcaster" +from threads import StoppableThread + + +class AddrThread(StoppableThread): + """(Node) address broadcasting thread""" + name = "AddrBroadcaster" def run(self): - while not state.shutdown: + while not self._stopped: chunk = [] while True: try: data = addrQueue.get(False) - chunk.append((data[0], data[1])) - if len(data) > 2: - source = BMConnectionPool().getConnectionByAddr(data[2]) - except Queue.Empty: + chunk.append(data) + except queue.Empty: break - except KeyError: - continue - #finish + if chunk: + # Choose peers randomly + connections = connectionpool.pool.establishedConnections() + random.shuffle(connections) + for i in connections: + random.shuffle(chunk) + filtered = [] + for stream, peer, seen, destination in chunk: + # peer's own address or address received from peer + if i.destination in (peer, destination): + continue + if stream not in i.streams: + continue + filtered.append((stream, peer, seen)) + if filtered: + i.append_write_buf(assembleAddrMessage(filtered)) addrQueue.iterate() for i in range(len(chunk)): diff --git a/src/network/advanceddispatcher.py b/src/network/advanceddispatcher.py index d426dbe8d8..49f0d19d7f 100644 --- a/src/network/advanceddispatcher.py +++ b/src/network/advanceddispatcher.py @@ -1,24 +1,37 @@ +""" +Improved version of asyncore dispatcher +""" import socket import threading import time -import asyncore_pollchoose as asyncore -from debug import logger -from helper_threading import BusyError, nonBlocking +import network.asyncore_pollchoose as asyncore import state +from threads import BusyError, nonBlocking + class ProcessingError(Exception): + """General class for protocol parser exception, + use as a base for others.""" pass + class UnknownStateError(ProcessingError): + """Parser points to an unknown (unimplemented) state.""" pass + class AdvancedDispatcher(asyncore.dispatcher): - _buf_len = 131072 # 128kB + """Improved version of asyncore dispatcher, + with buffers and protocol state.""" + # pylint: disable=too-many-instance-attributes + _buf_len = 131072 # 128kB def __init__(self, sock=None): if not hasattr(self, '_map'): asyncore.dispatcher.__init__(self, sock) + self.connectedAt = 0 + self.close_reason = None self.read_buf = bytearray() self.write_buf = bytearray() self.state = "init" @@ -29,8 +42,10 @@ def __init__(self, sock=None): self.readLock = threading.RLock() self.writeLock = threading.RLock() self.processingLock = threading.RLock() + self.uploadChunk = self.downloadChunk = 0 def append_write_buf(self, data): + """Append binary data to the end of stream write buffer.""" if data: if isinstance(data, list): with self.writeLock: @@ -41,6 +56,7 @@ def append_write_buf(self, data): self.write_buf.extend(data) def slice_write_buf(self, length=0): + """Cut the beginning of the stream write buffer.""" if length > 0: with self.writeLock: if length >= len(self.write_buf): @@ -49,6 +65,7 @@ def slice_write_buf(self, length=0): del self.write_buf[0:length] def slice_read_buf(self, length=0): + """Cut the beginning of the stream read buffer.""" if length > 0: with self.readLock: if length >= len(self.read_buf): @@ -57,6 +74,8 @@ def slice_read_buf(self, length=0): del self.read_buf[0:length] def process(self): + """Process (parse) data that's in the buffer, + as long as there is enough data and the connection is open.""" while self.connected and not state.shutdown: try: with nonBlocking(self.processingLock): @@ -67,42 +86,50 @@ def process(self): try: cmd = getattr(self, "state_" + str(self.state)) except AttributeError: - logger.error("Unknown state %s", self.state, exc_info=True) - raise UnknownState(self.state) + self.logger.error( + 'Unknown state %s', self.state, exc_info=True) + raise UnknownStateError(self.state) if not cmd(): break except BusyError: return False return False - def set_state(self, state, length=0, expectBytes=0): + def set_state(self, state_str, length=0, expectBytes=0): + """Set the next processing state.""" self.expectBytes = expectBytes self.slice_read_buf(length) - self.state = state + self.state = state_str def writable(self): + """Is data from the write buffer ready to be sent to the network?""" self.uploadChunk = AdvancedDispatcher._buf_len if asyncore.maxUploadRate > 0: self.uploadChunk = int(asyncore.uploadBucket) self.uploadChunk = min(self.uploadChunk, len(self.write_buf)) - return asyncore.dispatcher.writable(self) and \ - (self.connecting or (self.connected and self.uploadChunk > 0)) + return asyncore.dispatcher.writable(self) and ( + self.connecting or ( + self.connected and self.uploadChunk > 0)) def readable(self): + """Is the read buffer ready to accept data from the network?""" self.downloadChunk = AdvancedDispatcher._buf_len if asyncore.maxDownloadRate > 0: self.downloadChunk = int(asyncore.downloadBucket) try: if self.expectBytes > 0 and not self.fullyEstablished: - self.downloadChunk = min(self.downloadChunk, self.expectBytes - len(self.read_buf)) + self.downloadChunk = min( + self.downloadChunk, self.expectBytes - len(self.read_buf)) if self.downloadChunk < 0: self.downloadChunk = 0 except AttributeError: pass - return asyncore.dispatcher.readable(self) and \ - (self.connecting or self.accepting or (self.connected and self.downloadChunk > 0)) + return asyncore.dispatcher.readable(self) and ( + self.connecting or self.accepting or ( + self.connected and self.downloadChunk > 0)) def handle_read(self): + """Append incoming data to the read buffer.""" self.lastTx = time.time() newData = self.recv(self.downloadChunk) self.receivedBytes += len(newData) @@ -111,6 +138,7 @@ def handle_read(self): self.read_buf.extend(newData) def handle_write(self): + """Send outgoing data from write buffer.""" self.lastTx = time.time() written = self.send(self.write_buf[0:self.uploadChunk]) asyncore.update_sent(written) @@ -118,19 +146,25 @@ def handle_write(self): self.slice_write_buf(written) def handle_connect_event(self): + """Callback for connection established event.""" try: asyncore.dispatcher.handle_connect_event(self) except socket.error as e: + # pylint: disable=protected-access if e.args[0] not in asyncore._DISCONNECTED: raise def handle_connect(self): + """Method for handling connection established implementations.""" self.lastTx = time.time() - def state_close(self): + def state_close(self): # pylint: disable=no-self-use + """Signal to the processing loop to end.""" return False def handle_close(self): + """Callback for connection being closed, + but can also be called directly when you want connection to close.""" with self.readLock: self.read_buf = bytearray() with self.writeLock: diff --git a/src/network/announcethread.py b/src/network/announcethread.py index a94eeb3653..7cb35e779f 100644 --- a/src/network/announcethread.py +++ b/src/network/announcethread.py @@ -1,35 +1,43 @@ -import threading +""" +Announce myself (node address) +""" import time -from bmconfigparser import BMConfigParser -from debug import logger -from helper_threading import StoppableThread -from network.bmproto import BMProto -from network.connectionpool import BMConnectionPool -from network.udp import UDPSocket -import state +# magic imports! +import connectionpool +from bmconfigparser import config +from protocol import assembleAddrMessage -class AnnounceThread(threading.Thread, StoppableThread): - def __init__(self): - threading.Thread.__init__(self, name="Announcer") - self.initStop() - self.name = "Announcer" - logger.info("init announce thread") +from node import Peer +from threads import StoppableThread + + +class AnnounceThread(StoppableThread): + """A thread to manage regular announcing of this node""" + name = "Announcer" + announceInterval = 60 def run(self): lastSelfAnnounced = 0 - while not self._stopped and state.shutdown == 0: + while not self._stopped: processed = 0 - if lastSelfAnnounced < time.time() - UDPSocket.announceInterval: + if lastSelfAnnounced < time.time() - self.announceInterval: self.announceSelf() lastSelfAnnounced = time.time() if processed == 0: self.stop.wait(10) - def announceSelf(self): - for connection in BMConnectionPool().udpSockets.values(): + @staticmethod + def announceSelf(): + """Announce our presence""" + for connection in connectionpool.pool.udpSockets.values(): if not connection.announcing: continue - for stream in state.streamsInWhichIAmParticipating: - addr = (stream, state.Peer('127.0.0.1', BMConfigParser().safeGetInt("bitmessagesettings", "port")), time.time()) - connection.append_write_buf(BMProto.assembleAddr([addr])) + for stream in connectionpool.pool.streams: + addr = ( + stream, + Peer( + '127.0.0.1', + config.safeGetInt('bitmessagesettings', 'port')), + int(time.time())) + connection.append_write_buf(assembleAddrMessage([addr])) diff --git a/src/network/asyncore_pollchoose.py b/src/network/asyncore_pollchoose.py index e50be61b87..3948ba7920 100644 --- a/src/network/asyncore_pollchoose.py +++ b/src/network/asyncore_pollchoose.py @@ -1,67 +1,26 @@ +""" +Basic infrastructure for asynchronous socket service clients and servers. +""" # -*- Mode: Python -*- # Id: asyncore.py,v 2.51 2000/09/07 22:29:26 rushing Exp # Author: Sam Rushing - -# ====================================================================== -# Copyright 1996 by Sam Rushing -# -# All Rights Reserved -# -# Permission to use, copy, modify, and distribute this software and -# its documentation for any purpose and without fee is hereby -# granted, provided that the above copyright notice appear in all -# copies and that both that copyright notice and this permission -# notice appear in supporting documentation, and that the name of Sam -# Rushing not be used in advertising or publicity pertaining to -# distribution of the software without specific, written prior -# permission. -# -# SAM RUSHING DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, -# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN -# NO EVENT SHALL SAM RUSHING BE LIABLE FOR ANY SPECIAL, INDIRECT OR -# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS -# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, -# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -# ====================================================================== - -"""Basic infrastructure for asynchronous socket service clients and servers. - -There are only two ways to have a program on a single processor do "more -than one thing at a time". Multi-threaded programming is the simplest and -most popular way to do it, but there is another very different technique, -that lets you have nearly all the advantages of multi-threading, without -actually using multiple threads. it's really only practical if your program -is largely I/O bound. If your program is CPU bound, then pre-emptive -scheduled threads are probably what you really need. Network servers are -rarely CPU-bound, however. - -If your operating system supports the select() system call in its I/O -library (and nearly all do), then you can use it to juggle multiple -communication channels at once; doing other work while your I/O is taking -place in the "background." Although this strategy can seem strange and -complex, especially at first, it is in many ways easier to understand and -control than multi-threaded programming. The module documented here solves -many of the difficult problems for you, making the task of building -sophisticated high-performance network servers and clients a snap. -""" - -# randomise object order for bandwidth balancing -import random +# pylint: disable=too-many-branches,too-many-lines,global-statement +# pylint: disable=redefined-builtin,no-self-use +import os import select import socket +import random import sys import time -from threading import current_thread import warnings +from errno import ( + EADDRINUSE, EAGAIN, EALREADY, EBADF, ECONNABORTED, ECONNREFUSED, + ECONNRESET, EHOSTUNREACH, EINPROGRESS, EINTR, EINVAL, EISCONN, ENETUNREACH, + ENOTCONN, ENOTSOCK, EPIPE, ESHUTDOWN, ETIMEDOUT, EWOULDBLOCK, errorcode +) +from threading import current_thread + -import os -import helper_random -from errno import EALREADY, EINPROGRESS, EWOULDBLOCK, ECONNRESET, EINVAL, \ - ENOTCONN, ESHUTDOWN, EISCONN, EBADF, ECONNABORTED, EPIPE, EAGAIN, \ - ECONNREFUSED, EHOSTUNREACH, ENETUNREACH, ENOTSOCK, EINTR, ETIMEDOUT, \ - EADDRINUSE, \ - errorcode try: from errno import WSAEWOULDBLOCK except (ImportError, AttributeError): @@ -75,13 +34,15 @@ except (ImportError, AttributeError): WSAECONNRESET = ECONNRESET try: - from errno import WSAEADDRINUSE + # Desirable side-effects on Windows; imports winsock error numbers + from errno import WSAEADDRINUSE # pylint: disable=unused-import except (ImportError, AttributeError): WSAEADDRINUSE = EADDRINUSE -_DISCONNECTED = frozenset((ECONNRESET, ENOTCONN, ESHUTDOWN, ECONNABORTED, EPIPE, - EBADF, ECONNREFUSED, EHOSTUNREACH, ENETUNREACH, ETIMEDOUT, - WSAECONNRESET)) + +_DISCONNECTED = frozenset(( + ECONNRESET, ENOTCONN, ESHUTDOWN, ECONNABORTED, EPIPE, EBADF, ECONNREFUSED, + EHOSTUNREACH, ENETUNREACH, ETIMEDOUT, WSAECONNRESET)) OP_READ = 1 OP_WRITE = 2 @@ -91,17 +52,22 @@ except NameError: socket_map = {} + def _strerror(err): try: return os.strerror(err) except (ValueError, OverflowError, NameError): if err in errorcode: return errorcode[err] - return "Unknown error %s" %err + return "Unknown error %s" % err + class ExitNow(Exception): + """We don't use directly but may be necessary as we replace + asyncore due to some library raising or expecting it""" pass + _reraised_exceptions = (ExitNow, KeyboardInterrupt, SystemExit) maxDownloadRate = 0 @@ -113,28 +79,39 @@ class ExitNow(Exception): uploadBucket = 0 sentBytes = 0 + def read(obj): + """Event to read from the object, i.e. its network socket.""" + if not can_receive(): return try: obj.handle_read_event() except _reraised_exceptions: raise - except: + except BaseException: obj.handle_error() + def write(obj): + """Event to write to the object, i.e. its network socket.""" + if not can_send(): return try: obj.handle_write_event() except _reraised_exceptions: raise - except: + except BaseException: obj.handle_error() + def set_rates(download, upload): - global maxDownloadRate, maxUploadRate, downloadBucket, uploadBucket, downloadTimestamp, uploadTimestamp + """Set throttling rates""" + + global maxDownloadRate, maxUploadRate, downloadBucket + global uploadBucket, downloadTimestamp, uploadTimestamp + maxDownloadRate = float(download) * 1024 maxUploadRate = float(upload) * 1024 downloadBucket = maxDownloadRate @@ -142,26 +119,41 @@ def set_rates(download, upload): downloadTimestamp = time.time() uploadTimestamp = time.time() + def can_receive(): + """Predicate indicating whether the download throttle is in effect""" + return maxDownloadRate == 0 or downloadBucket > 0 + def can_send(): + """Predicate indicating whether the upload throttle is in effect""" + return maxUploadRate == 0 or uploadBucket > 0 + def update_received(download=0): + """Update the receiving throttle""" + global receivedBytes, downloadBucket, downloadTimestamp + currentTimestamp = time.time() receivedBytes += download if maxDownloadRate > 0: - bucketIncrease = maxDownloadRate * (currentTimestamp - downloadTimestamp) + bucketIncrease = \ + maxDownloadRate * (currentTimestamp - downloadTimestamp) downloadBucket += bucketIncrease if downloadBucket > maxDownloadRate: downloadBucket = int(maxDownloadRate) downloadBucket -= download downloadTimestamp = currentTimestamp + def update_sent(upload=0): + """Update the sending throttle""" + global sentBytes, uploadBucket, uploadTimestamp + currentTimestamp = time.time() sentBytes += upload if maxUploadRate > 0: @@ -172,15 +164,21 @@ def update_sent(upload=0): uploadBucket -= upload uploadTimestamp = currentTimestamp + def _exception(obj): + """Handle exceptions as appropriate""" + try: obj.handle_expt_event() except _reraised_exceptions: raise - except: + except BaseException: obj.handle_error() + def readwrite(obj, flags): + """Read and write any pending data to/from the object""" + try: if flags & select.POLLIN and can_receive(): obj.handle_read_event() @@ -197,15 +195,19 @@ def readwrite(obj, flags): obj.handle_close() except _reraised_exceptions: raise - except: + except BaseException: obj.handle_error() + def select_poller(timeout=0.0, map=None): """A poller which uses select(), available on most platforms.""" + if map is None: map = socket_map if map: - r = []; w = []; e = [] + r = [] + w = [] + e = [] for fd, obj in list(map.items()): is_r = obj.readable() is_w = obj.writable() @@ -231,13 +233,13 @@ def select_poller(timeout=0.0, map=None): if err.args[0] in (WSAENOTSOCK, ): return - for fd in helper_random.randomsample(r, len(r)): + for fd in random.sample(r, len(r)): # nosec B311 obj = map.get(fd) if obj is None: continue read(obj) - for fd in helper_random.randomsample(w, len(w)): + for fd in random.sample(w, len(w)): # nosec B311 obj = map.get(fd) if obj is None: continue @@ -251,13 +253,15 @@ def select_poller(timeout=0.0, map=None): else: current_thread().stop.wait(timeout) + def poll_poller(timeout=0.0, map=None): """A poller which uses poll(), available on most UNIXen.""" + if map is None: map = socket_map if timeout is not None: # timeout is in milliseconds - timeout = int(timeout*1000) + timeout = int(timeout * 1000) try: poll_poller.pollster except AttributeError: @@ -293,7 +297,7 @@ def poll_poller(timeout=0.0, map=None): except socket.error as err: if err.args[0] in (EBADF, WSAENOTSOCK, EINTR): return - for fd, flags in helper_random.randomsample(r, len(r)): + for fd, flags in random.sample(r, len(r)): # nosec B311 obj = map.get(fd) if obj is None: continue @@ -301,12 +305,15 @@ def poll_poller(timeout=0.0, map=None): else: current_thread().stop.wait(timeout) + # Aliases for backward compatibility poll = select_poller poll2 = poll3 = poll_poller + def epoll_poller(timeout=0.0, map=None): """A poller which uses epoll(), supported on Linux 2.5.44 and newer.""" + if map is None: map = socket_map try: @@ -346,20 +353,23 @@ def epoll_poller(timeout=0.0, map=None): if e.errno != EINTR: raise r = [] - except select.error, err: + except select.error as err: if err.args[0] != EINTR: raise r = [] - for fd, flags in helper_random.randomsample(r, len(r)): + for fd, flags in random.sample(r, len(r)): # nosec B311 obj = map.get(fd) if obj is None: continue - readwrite(obj, flags) + readwrite(obj, flags) else: current_thread().stop.wait(timeout) + def kqueue_poller(timeout=0.0, map=None): """A poller which uses kqueue(), BSD specific.""" + # pylint: disable=no-member,too-many-statements + if map is None: map = socket_map try: @@ -386,14 +396,20 @@ def kqueue_poller(timeout=0.0, map=None): poller_flags |= select.KQ_EV_ENABLE else: poller_flags |= select.KQ_EV_DISABLE - updates.append(select.kevent(fd, filter=select.KQ_FILTER_READ, flags=poller_flags)) + updates.append( + select.kevent( + fd, filter=select.KQ_FILTER_READ, + flags=poller_flags)) if kq_filter & 2 != obj.poller_filter & 2: poller_flags = select.KQ_EV_ADD if kq_filter & 2: poller_flags |= select.KQ_EV_ENABLE else: poller_flags |= select.KQ_EV_DISABLE - updates.append(select.kevent(fd, filter=select.KQ_FILTER_WRITE, flags=poller_flags)) + updates.append( + select.kevent( + fd, filter=select.KQ_FILTER_WRITE, + flags=poller_flags)) obj.poller_filter = kq_filter if not selectables: @@ -404,11 +420,11 @@ def kqueue_poller(timeout=0.0, map=None): events = kqueue_poller.pollster.control(updates, selectables, timeout) if len(events) > 1: - events = helper_random.randomsample(events, len(events)) + events = random.sample(events, len(events)) # nosec B311 for event in events: fd = event.ident - obj = map.get(fd) + obj = map.get(fd) if obj is None: continue if event.flags & select.KQ_EV_ERROR: @@ -425,13 +441,14 @@ def kqueue_poller(timeout=0.0, map=None): current_thread().stop.wait(timeout) -def loop(timeout=30.0, use_poll=False, map=None, count=None, - poller=None): +def loop(timeout=30.0, use_poll=False, map=None, count=None, poller=None): + """Poll in a loop, until count or timeout is reached""" + if map is None: map = socket_map if count is None: - count = True - # code which grants backward compatibility with "use_poll" + count = True + # code which grants backward compatibility with "use_poll" # argument which should no longer be used in favor of # "poller" @@ -460,10 +477,13 @@ def loop(timeout=30.0, use_poll=False, map=None, count=None, break # then poll poller(subtimeout, map) - if type(count) is int: + if isinstance(count, int): count = count - 1 -class dispatcher: + +class dispatcher(object): + """Dispatcher for socket objects""" + # pylint: disable=too-many-public-methods,too-many-instance-attributes debug = False connected = False @@ -510,7 +530,7 @@ def __init__(self, sock=None, map=None): self.socket = None def __repr__(self): - status = [self.__class__.__module__+"."+self.__class__.__name__] + status = [self.__class__.__module__ + "." + self.__class__.__name__] if self.accepting and self.addr: status.append('listening') elif self.connected: @@ -525,7 +545,8 @@ def __repr__(self): __str__ = __repr__ def add_channel(self, map=None): - #self.log_info('adding channel %s' % self) + """Add a channel""" + # pylint: disable=attribute-defined-outside-init if map is None: map = self._map map[self._fileno] = self @@ -533,20 +554,22 @@ def add_channel(self, map=None): self.poller_filter = 0 def del_channel(self, map=None): + """Delete a channel""" fd = self._fileno if map is None: map = self._map if fd in map: - #self.log_info('closing channel %d:%s' % (fd, self)) del map[fd] if self._fileno: try: - kqueue_poller.pollster.control([select.kevent(fd, select.KQ_FILTER_READ, select.KQ_EV_DELETE)], 0) - except (AttributeError, KeyError, TypeError, IOError, OSError): + kqueue_poller.pollster.control([select.kevent( + fd, select.KQ_FILTER_READ, select.KQ_EV_DELETE)], 0) + except(AttributeError, KeyError, TypeError, IOError, OSError): pass try: - kqueue_poller.pollster.control([select.kevent(fd, select.KQ_FILTER_WRITE, select.KQ_EV_DELETE)], 0) - except (AttributeError, KeyError, TypeError, IOError, OSError): + kqueue_poller.pollster.control([select.kevent( + fd, select.KQ_FILTER_WRITE, select.KQ_EV_DELETE)], 0) + except(AttributeError, KeyError, TypeError, IOError, OSError): pass try: epoll_poller.pollster.unregister(fd) @@ -563,26 +586,28 @@ def del_channel(self, map=None): self.poller_filter = 0 self.poller_registered = False - def create_socket(self, family=socket.AF_INET, socket_type=socket.SOCK_STREAM): + def create_socket( + self, family=socket.AF_INET, socket_type=socket.SOCK_STREAM): + """Create a socket""" + # pylint: disable=attribute-defined-outside-init self.family_and_type = family, socket_type sock = socket.socket(family, socket_type) sock.setblocking(0) self.set_socket(sock) def set_socket(self, sock, map=None): + """Set socket""" self.socket = sock -## self.__dict__['socket'] = sock self._fileno = sock.fileno() self.add_channel(map) def set_reuse_addr(self): - # try to re-use a server port if possible + """try to re-use a server port if possible""" try: self.socket.setsockopt( - socket.SOL_SOCKET, socket.SO_REUSEADDR, - self.socket.getsockopt(socket.SOL_SOCKET, - socket.SO_REUSEADDR) | 1 - ) + socket.SOL_SOCKET, socket.SO_REUSEADDR, self.socket.getsockopt( + socket.SOL_SOCKET, socket.SO_REUSEADDR) | 1 + ) except socket.error: pass @@ -593,11 +618,13 @@ def set_reuse_addr(self): # ================================================== def readable(self): + """Predicate to indicate download throttle status""" if maxDownloadRate > 0: return downloadBucket > dispatcher.minTx return True def writable(self): + """Predicate to indicate upload throttle status""" if maxUploadRate > 0: return uploadBucket > dispatcher.minTx return True @@ -607,21 +634,24 @@ def writable(self): # ================================================== def listen(self, num): + """Listen on a port""" self.accepting = True if os.name == 'nt' and num > 5: num = 5 return self.socket.listen(num) def bind(self, addr): + """Bind to an address""" self.addr = addr return self.socket.bind(addr) def connect(self, address): + """Connect to an address""" self.connected = False self.connecting = True err = self.socket.connect_ex(address) if err in (EINPROGRESS, EALREADY, EWOULDBLOCK, WSAEWOULDBLOCK) \ - or err == EINVAL and os.name in ('nt', 'ce'): + or err == EINVAL and os.name in ('nt', 'ce'): self.addr = address return if err in (0, EISCONN): @@ -631,13 +661,16 @@ def connect(self, address): raise socket.error(err, errorcode[err]) def accept(self): - # XXX can return either an address pair or None + """Accept incoming connections. + Returns either an address pair or None.""" try: conn, addr = self.socket.accept() except TypeError: return None except socket.error as why: - if why.args[0] in (EWOULDBLOCK, WSAEWOULDBLOCK, ECONNABORTED, EAGAIN, ENOTCONN): + if why.args[0] in ( + EWOULDBLOCK, WSAEWOULDBLOCK, ECONNABORTED, + EAGAIN, ENOTCONN): return None else: raise @@ -645,6 +678,7 @@ def accept(self): return conn, addr def send(self, data): + """Send data""" try: result = self.socket.send(data) return result @@ -658,6 +692,7 @@ def send(self, data): raise def recv(self, buffer_size): + """Receive data""" try: data = self.socket.recv(buffer_size) if not data: @@ -665,8 +700,7 @@ def recv(self, buffer_size): # a read condition, and having recv() return 0. self.handle_close() return b'' - else: - return data + return data except socket.error as why: # winsock sometimes raises ENOTCONN if why.args[0] in (EAGAIN, EWOULDBLOCK, WSAEWOULDBLOCK): @@ -678,6 +712,7 @@ def recv(self, buffer_size): raise def close(self): + """Close connection""" self.connected = False self.accepting = False self.connecting = False @@ -694,11 +729,12 @@ def __getattr__(self, attr): try: retattr = getattr(self.socket, attr) except AttributeError: - raise AttributeError("%s instance has no attribute '%s'" - %(self.__class__.__name__, attr)) + raise AttributeError( + "%s instance has no attribute '%s'" + % (self.__class__.__name__, attr)) else: - msg = "%(me)s.%(attr)s is deprecated; use %(me)s.socket.%(attr)s " \ - "instead" % {'me' : self.__class__.__name__, 'attr' : attr} + msg = "%(me)s.%(attr)s is deprecated; use %(me)s.socket.%(attr)s"\ + " instead" % {'me': self.__class__.__name__, 'attr': attr} warnings.warn(msg, DeprecationWarning, stacklevel=2) return retattr @@ -707,13 +743,16 @@ def __getattr__(self, attr): # and 'log_info' is for informational, warning and error logging. def log(self, message): + """Log a message to stderr""" sys.stderr.write('log: %s\n' % str(message)) def log_info(self, message, log_type='info'): + """Conditionally print a message""" if log_type not in self.ignore_log_types: print('%s: %s' % (log_type, message)) def handle_read_event(self): + """Handle a read event""" if self.accepting: # accepting sockets are never connected, they "spawn" new # sockets that are connected @@ -726,6 +765,7 @@ def handle_read_event(self): self.handle_read() def handle_connect_event(self): + """Handle a connection event""" err = self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) if err != 0: raise socket.error(err, _strerror(err)) @@ -734,6 +774,7 @@ def handle_connect_event(self): self.connecting = False def handle_write_event(self): + """Handle a write event""" if self.accepting: # Accepting sockets shouldn't get a write event. # We will pretend it didn't happen. @@ -745,6 +786,7 @@ def handle_write_event(self): self.handle_write() def handle_expt_event(self): + """Handle expected exceptions""" # handle_expt_event() is called if there might be an error on the # socket, or if there is OOB data # check for the error condition first @@ -763,103 +805,117 @@ def handle_expt_event(self): self.handle_expt() def handle_error(self): - nil, t, v, tbinfo = compact_traceback() + """Handle unexpected exceptions""" + _, t, v, tbinfo = compact_traceback() # sometimes a user repr method will crash. try: self_repr = repr(self) - except: + except BaseException: self_repr = '<__repr__(self) failed for object at %0x>' % id(self) self.log_info( 'uncaptured python exception, closing channel %s (%s:%s %s)' % ( - self_repr, - t, - v, - tbinfo - ), - 'error' - ) + self_repr, t, v, tbinfo), + 'error') self.handle_close() + def handle_accept(self): + """Handle an accept event""" + pair = self.accept() + if pair is not None: + self.handle_accepted(*pair) + def handle_expt(self): + """Log that the subclass does not implement handle_expt""" self.log_info('unhandled incoming priority event', 'warning') def handle_read(self): + """Log that the subclass does not implement handle_read""" self.log_info('unhandled read event', 'warning') def handle_write(self): + """Log that the subclass does not implement handle_write""" self.log_info('unhandled write event', 'warning') def handle_connect(self): + """Log that the subclass does not implement handle_connect""" self.log_info('unhandled connect event', 'warning') - def handle_accept(self): - pair = self.accept() - if pair is not None: - self.handle_accepted(*pair) - def handle_accepted(self, sock, addr): + """Log that the subclass does not implement handle_accepted""" sock.close() self.log_info('unhandled accepted event on %s' % (addr), 'warning') def handle_close(self): + """Log that the subclass does not implement handle_close""" self.log_info('unhandled close event', 'warning') self.close() -# --------------------------------------------------------------------------- -# adds simple buffered output capability, useful for simple clients. -# [for more sophisticated usage use asynchat.async_chat] -# --------------------------------------------------------------------------- class dispatcher_with_send(dispatcher): + """ + adds simple buffered output capability, useful for simple clients. + [for more sophisticated usage use asynchat.async_chat] + """ def __init__(self, sock=None, map=None): dispatcher.__init__(self, sock, map) self.out_buffer = b'' def initiate_send(self): + """Initiate a send""" num_sent = 0 num_sent = dispatcher.send(self, self.out_buffer[:512]) self.out_buffer = self.out_buffer[num_sent:] def handle_write(self): + """Handle a write event""" self.initiate_send() def writable(self): - return (not self.connected) or len(self.out_buffer) + """Predicate to indicate if the object is writable""" + return not self.connected or len(self.out_buffer) def send(self, data): + """Send data""" if self.debug: self.log_info('sending %s' % repr(data)) self.out_buffer = self.out_buffer + data self.initiate_send() + # --------------------------------------------------------------------------- # used for debugging. # --------------------------------------------------------------------------- + def compact_traceback(): + """Return a compact traceback""" t, v, tb = sys.exc_info() tbinfo = [] - if not tb: # Must have a traceback + # Must have a traceback + if not tb: raise AssertionError("traceback does not exist") while tb: tbinfo.append(( tb.tb_frame.f_code.co_filename, tb.tb_frame.f_code.co_name, str(tb.tb_lineno) - )) + )) tb = tb.tb_next # just to be safe del tb - file, function, line = tbinfo[-1] + filename, function, line = tbinfo[-1] info = ' '.join(['[%s|%s|%s]' % x for x in tbinfo]) - return (file, function, line), t, v, info + return (filename, function, line), t, v, info + def close_all(map=None, ignore_all=False): + """Close all connections""" + if map is None: map = socket_map for x in list(map.values()): @@ -872,11 +928,12 @@ def close_all(map=None, ignore_all=False): raise except _reraised_exceptions: raise - except: + except BaseException: if not ignore_all: raise map.clear() + # Asynchronous File I/O: # # After a little research (reading man pages on various unixen, and @@ -890,41 +947,50 @@ def close_all(map=None, ignore_all=False): # # Regardless, this is useful for pipes, and stdin/stdout... + if os.name == 'posix': import fcntl - class file_wrapper: - # Here we override just enough to make a file - # look like a socket for the purposes of asyncore. - # The passed fd is automatically os.dup()'d + class file_wrapper: # pylint: disable=old-style-class + """ + Here we override just enough to make a file look + like a socket for the purposes of asyncore. + + The passed fd is automatically os.dup()'d + """ def __init__(self, fd): self.fd = os.dup(fd) def recv(self, *args): + """Fake recv()""" return os.read(self.fd, *args) def send(self, *args): + """Fake send()""" return os.write(self.fd, *args) def getsockopt(self, level, optname, buflen=None): - if (level == socket.SOL_SOCKET and - optname == socket.SO_ERROR and - not buflen): + """Fake getsockopt()""" + if (level == socket.SOL_SOCKET and optname == socket.SO_ERROR + and not buflen): return 0 - raise NotImplementedError("Only asyncore specific behaviour " - "implemented.") + raise NotImplementedError( + "Only asyncore specific behaviour implemented.") read = recv write = send def close(self): + """Fake close()""" os.close(self.fd) def fileno(self): + """Fake fileno()""" return self.fd class file_dispatcher(dispatcher): + """A dispatcher for file_wrapper objects""" def __init__(self, fd, map=None): dispatcher.__init__(self, None, map) @@ -940,6 +1006,7 @@ def __init__(self, fd, map=None): fcntl.fcntl(fd, fcntl.F_SETFL, flags) def set_file(self, fd): + """Set file""" self.socket = file_wrapper(fd) self._fileno = self.socket.fileno() self.add_channel() diff --git a/src/network/bmobject.py b/src/network/bmobject.py index 2e7dd09297..1a54fc7b7a 100644 --- a/src/network/bmobject.py +++ b/src/network/bmobject.py @@ -1,44 +1,63 @@ -from binascii import hexlify +""" +BMObject and it's exceptions. +""" +import logging import time -from addresses import calculateInventoryHash -from debug import logger -from inventory import Inventory -from network.dandelion import Dandelion import protocol import state +import connectionpool +from network import dandelion_ins +from highlevelcrypto import calculateInventoryHash -class BMObjectInsufficientPOWError(Exception): - errorCodes = ("Insufficient proof of work") +logger = logging.getLogger('default') -class BMObjectInvalidDataError(Exception): - errorCodes = ("Data invalid") +class BMObjectInsufficientPOWError(Exception): + """Exception indicating the object + doesn't have sufficient proof of work.""" + errorCodes = ("Insufficient proof of work") class BMObjectExpiredError(Exception): + """Exception indicating the object's lifetime has expired.""" errorCodes = ("Object expired") class BMObjectUnwantedStreamError(Exception): + """Exception indicating the object is in a stream + we didn't advertise as being interested in.""" errorCodes = ("Object in unwanted stream") class BMObjectInvalidError(Exception): + """The object's data does not match object specification.""" errorCodes = ("Invalid object") class BMObjectAlreadyHaveError(Exception): + """We received a duplicate object (one we already have)""" errorCodes = ("Already have this object") class BMObject(object): + """Bitmessage Object as a class.""" + # max TTL, 28 days and 3 hours maxTTL = 28 * 24 * 60 * 60 + 10800 # min TTL, 3 hour (in the past minTTL = -3600 - def __init__(self, nonce, expiresTime, objectType, version, streamNumber, data, payloadOffset): + def __init__( + self, + nonce, + expiresTime, + objectType, + version, + streamNumber, + data, + payloadOffset + ): self.nonce = nonce self.expiresTime = expiresTime self.objectType = objectType @@ -47,39 +66,61 @@ def __init__(self, nonce, expiresTime, objectType, version, streamNumber, data, self.inventoryHash = calculateInventoryHash(data) # copy to avoid memory issues self.data = bytearray(data) - self.tag = self.data[payloadOffset:payloadOffset+32] + self.tag = self.data[payloadOffset:payloadOffset + 32] def checkProofOfWorkSufficient(self): + """Perform a proof of work check for sufficiency.""" # Let us check to make sure that the proof of work is sufficient. if not protocol.isProofOfWorkSufficient(self.data): logger.info('Proof of work is insufficient.') raise BMObjectInsufficientPOWError() def checkEOLSanity(self): + """Check if object's lifetime + isn't ridiculously far in the past or future.""" # EOL sanity check if self.expiresTime - int(time.time()) > BMObject.maxTTL: - logger.info('This object\'s End of Life time is too far in the future. Ignoring it. Time is %i', self.expiresTime) - # TODO: remove from download queue + logger.info( + 'This object\'s End of Life time is too far in the future.' + ' Ignoring it. Time is %i', self.expiresTime) + # .. todo:: remove from download queue raise BMObjectExpiredError() if self.expiresTime - int(time.time()) < BMObject.minTTL: - logger.info('This object\'s End of Life time was too long ago. Ignoring the object. Time is %i', self.expiresTime) - # TODO: remove from download queue + logger.info( + 'This object\'s End of Life time was too long ago.' + ' Ignoring the object. Time is %i', self.expiresTime) + # .. todo:: remove from download queue raise BMObjectExpiredError() def checkStream(self): - if self.streamNumber not in state.streamsInWhichIAmParticipating: - logger.debug('The streamNumber %i isn\'t one we are interested in.', self.streamNumber) + """Check if object's stream matches streams we are interested in""" + 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) raise BMObjectUnwantedStreamError() def checkAlreadyHave(self): + """ + Check if we already have the object + (so that we don't duplicate it in inventory + or advertise it unnecessarily) + """ # if it's a stem duplicate, pretend we don't have it - if Dandelion().hasHash(self.inventoryHash): + if dandelion_ins.hasHash(self.inventoryHash): return - if self.inventoryHash in Inventory(): + if self.inventoryHash in state.Inventory: raise BMObjectAlreadyHaveError() def checkObjectByType(self): + """Call a object type specific check + (objects can have additional checks based on their types)""" if self.objectType == protocol.OBJECT_GETPUBKEY: self.checkGetpubkey() elif self.objectType == protocol.OBJECT_PUBKEY: @@ -90,22 +131,31 @@ def checkObjectByType(self): self.checkBroadcast() # other objects don't require other types of tests - def checkMessage(self): + def checkMessage(self): # pylint: disable=no-self-use + """"Message" object type checks.""" return def checkGetpubkey(self): + """"Getpubkey" object type checks.""" if len(self.data) < 42: - logger.info('getpubkey message doesn\'t contain enough data. Ignoring.') + logger.info( + 'getpubkey message doesn\'t contain enough data. Ignoring.') raise BMObjectInvalidError() def checkPubkey(self): - if len(self.data) < 146 or len(self.data) > 440: # sanity check + """"Pubkey" object type checks.""" + # sanity check + if len(self.data) < 146 or len(self.data) > 440: logger.info('pubkey object too short or too long. Ignoring.') raise BMObjectInvalidError() def checkBroadcast(self): + """"Broadcast" object type checks.""" if len(self.data) < 180: - logger.debug('The payload length of this broadcast packet is unreasonably low. Someone is probably trying funny business. Ignoring message.') + logger.debug( + 'The payload length of this broadcast' + ' packet is unreasonably low. Someone is probably' + ' trying funny business. Ignoring message.') raise BMObjectInvalidError() # this isn't supported anymore diff --git a/src/network/bmproto.py b/src/network/bmproto.py index a0267cadf9..fbb5a06e40 100644 --- a/src/network/bmproto.py +++ b/src/network/bmproto.py @@ -1,63 +1,76 @@ +""" +Class BMProto defines bitmessage's network protocol workflow. +""" + import base64 import hashlib -import random +import logging +import re import socket import struct import time -from bmconfigparser import BMConfigParser -from debug import logger -from inventory import Inventory +# magic imports! +import addresses import knownnodes +import protocol +import state +import connectionpool +from bmconfigparser import config +from queues import objectProcessorQueue +from randomtrackingdict import RandomTrackingDict from network.advanceddispatcher import AdvancedDispatcher -from network.dandelion import Dandelion -from network.bmobject import BMObject, BMObjectInsufficientPOWError, BMObjectInvalidDataError, \ - BMObjectExpiredError, BMObjectUnwantedStreamError, BMObjectInvalidError, BMObjectAlreadyHaveError -import network.connectionpool -from network.node import Node -from network.objectracker import ObjectTracker -from network.proxy import Proxy, ProxyError, GeneralProxyError +from network.bmobject import ( + BMObject, BMObjectAlreadyHaveError, BMObjectExpiredError, + BMObjectInsufficientPOWError, BMObjectInvalidError, + BMObjectUnwantedStreamError +) +from network.proxy import ProxyError +from network import dandelion_ins, invQueue, portCheckerQueue +from node import Node, Peer +from objectracker import ObjectTracker, missingObjects + + +logger = logging.getLogger('default') -import addresses -from queues import objectProcessorQueue, portCheckerQueue, invQueue, addrQueue -import shared -import state -import protocol -import helper_random class BMProtoError(ProxyError): + """A Bitmessage Protocol Base Error""" errorCodes = ("Protocol error") class BMProtoInsufficientDataError(BMProtoError): + """A Bitmessage Protocol Insufficient Data Error""" errorCodes = ("Insufficient data") class BMProtoExcessiveDataError(BMProtoError): + """A Bitmessage Protocol Excessive Data Error""" errorCodes = ("Too much data") class BMProto(AdvancedDispatcher, ObjectTracker): - # ~1.6 MB which is the maximum possible size of an inv message. - maxMessageSize = 1600100 - # 2**18 = 256kB is the maximum size of an object payload - maxObjectPayloadSize = 2**18 - # protocol specification says max 1000 addresses in one addr command - maxAddrCount = 1000 - # protocol specification says max 50000 objects in one inv command - maxObjectCount = 50000 - # address is online if online less than this many seconds ago - addressAlive = 10800 - # maximum time offset - maxTimeOffset = 3600 + """A parser for the Bitmessage Protocol""" + # pylint: disable=too-many-instance-attributes, too-many-public-methods + timeOffsetWrongCount = 0 def __init__(self, address=None, sock=None): + # pylint: disable=unused-argument, super-init-not-called AdvancedDispatcher.__init__(self, sock) self.isOutbound = False # packet/connection from a local IP self.local = False + self.pendingUpload = RandomTrackingDict() + # canonical identifier of network group + self.network_group = None + # userAgent initialization + self.userAgent = '' + # track port check requests, only allow one per connection + # completely disable port checks for now + self.portCheckRequested = True def bm_proto_reset(self): + """Reset the bitmessage object parser""" self.magic = None self.command = None self.payloadLength = 0 @@ -69,62 +82,70 @@ def bm_proto_reset(self): self.object = None def state_bm_header(self): - self.magic, self.command, self.payloadLength, self.checksum = protocol.Header.unpack(self.read_buf[:protocol.Header.size]) + """Process incoming header""" + 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() - logger.debug("Bad magic") + logger.debug('Bad magic') if self.socket.type == socket.SOCK_STREAM: self.close_reason = "Bad magic" self.set_state("close") return False - if self.payloadLength > BMProto.maxMessageSize: + if self.payloadLength > protocol.MAX_MESSAGE_SIZE: self.invalid = True - self.set_state("bm_command", length=protocol.Header.size, expectBytes=self.payloadLength) + self.set_state( + "bm_command", + length=protocol.Header.size, expectBytes=self.payloadLength) return True - - def state_bm_command(self): + + def state_bm_command(self): # pylint: disable=too-many-branches + """Process incoming command""" self.payload = self.read_buf[:self.payloadLength] if self.checksum != hashlib.sha512(self.payload).digest()[0:4]: - logger.debug("Bad checksum, ignoring") + logger.debug('Bad checksum, ignoring') self.invalid = True retval = True - if not self.fullyEstablished and self.command not in ("error", "version", "verack"): - logger.error("Received command %s before connection was fully established, ignoring", self.command) + if not self.fullyEstablished and self.command not in ( + "error", "version", "verack"): + logger.error( + 'Received command %s before connection was fully' + ' established, ignoring', self.command) self.invalid = True if not self.invalid: try: - retval = getattr(self, "bm_command_" + str(self.command).lower())() + retval = getattr( + self, "bm_command_" + str(self.command).lower())() except AttributeError: # unimplemented command - logger.debug("unimplemented command %s", self.command) + logger.debug('unimplemented command %s', self.command) except BMProtoInsufficientDataError: - logger.debug("packet length too short, skipping") + logger.debug('packet length too short, skipping') except BMProtoExcessiveDataError: - logger.debug("too much data, skipping") + logger.debug('too much data, skipping') except BMObjectInsufficientPOWError: - logger.debug("insufficient PoW, skipping") - except BMObjectInvalidDataError: - logger.debug("object invalid data, skipping") + logger.debug('insufficient PoW, skipping') except BMObjectExpiredError: - logger.debug("object expired, skipping") + logger.debug('object expired, skipping') except BMObjectUnwantedStreamError: - logger.debug("object not in wanted stream, skipping") + logger.debug('object not in wanted stream, skipping') except BMObjectInvalidError: - logger.debug("object invalid, skipping") + logger.debug('object invalid, skipping') except BMObjectAlreadyHaveError: - logger.debug("%s:%i already got object, skipping", self.destination.host, self.destination.port) + logger.debug( + '%(host)s:%(port)i already got object, skipping', + self.destination._asdict()) except struct.error: - logger.debug("decoding error, skipping") + logger.debug('decoding error, skipping') elif self.socket.type == socket.SOCK_DGRAM: # broken read, ignore pass else: - #print "Skipping command %s due to invalid data" % (self.command) - logger.debug("Closing due to invalid command %s", self.command) - self.close_reason = "Invalid command %s" % (self.command) + logger.debug('Closing due to invalid command %s', self.command) + self.close_reason = "Invalid command %s" % self.command self.set_state("close") return False if retval: @@ -134,16 +155,21 @@ def state_bm_command(self): return True def decode_payload_string(self, length): - value = self.payload[self.payloadOffset:self.payloadOffset+length] + """Read and return `length` bytes from payload""" + value = self.payload[self.payloadOffset:self.payloadOffset + length] self.payloadOffset += length return value def decode_payload_varint(self): - value, offset = addresses.decodeVarint(self.payload[self.payloadOffset:]) + """Decode a varint from the payload""" + value, offset = addresses.decodeVarint( + self.payload[self.payloadOffset:]) self.payloadOffset += offset return value def decode_payload_node(self): + """Decode node details from the payload""" + # protocol.checkIPAddress() services, host, port = self.decode_payload_content("Q16sH") if host[0:12] == '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFF': host = socket.inet_ntop(socket.AF_INET, str(host[12:16])) @@ -153,38 +179,48 @@ def decode_payload_node(self): else: host = socket.inet_ntop(socket.AF_INET6, str(host)) if host == "": - # This can happen on Windows systems which are not 64-bit compatible - # so let us drop the IPv6 address. + # This can happen on Windows systems which are not 64-bit + # compatible so let us drop the IPv6 address. host = socket.inet_ntop(socket.AF_INET, str(host[12:16])) return Node(services, host, port) - def decode_payload_content(self, pattern = "v"): - # L = varint indicating the length of the next array - # l = varint indicating the length of the next item - # v = varint (or array) - # H = uint16 - # I = uint32 - # Q = uint64 - # i = net_addr (without time and stream number) - # s = string - # 0-9 = length of the next item - # , = end of array + # pylint: disable=too-many-branches,too-many-statements + def decode_payload_content(self, pattern="v"): + """ + Decode the payload depending on pattern: + + L = varint indicating the length of the next array + l = varint indicating the length of the next item + v = varint (or array) + H = uint16 + I = uint32 + Q = uint64 + i = net_addr (without time and stream number) + s = string + 0-9 = length of the next item + , = end of array + """ def decode_simple(self, char="v"): + """Decode the payload using one char pattern""" if char == "v": return self.decode_payload_varint() if char == "i": return self.decode_payload_node() if char == "H": self.payloadOffset += 2 - return struct.unpack(">H", self.payload[self.payloadOffset-2:self.payloadOffset])[0] + return struct.unpack(">H", self.payload[ + self.payloadOffset - 2:self.payloadOffset])[0] if char == "I": self.payloadOffset += 4 - return struct.unpack(">I", self.payload[self.payloadOffset-4:self.payloadOffset])[0] + return struct.unpack(">I", self.payload[ + self.payloadOffset - 4:self.payloadOffset])[0] if char == "Q": self.payloadOffset += 8 - return struct.unpack(">Q", self.payload[self.payloadOffset-8:self.payloadOffset])[0] + return struct.unpack(">Q", self.payload[ + self.payloadOffset - 8:self.payloadOffset])[0] + return None size = None isArray = False @@ -197,16 +233,11 @@ def decode_simple(self, char="v"): # retval (array) parserStack = [[1, 1, False, pattern, 0, []]] - #try: - # sys._getframe(200) - # logger.error("Stack depth warning, pattern: %s", pattern) - # return - #except ValueError: - # pass - while True: i = parserStack[-1][3][parserStack[-1][4]] - if i in "0123456789" and (size is None or parserStack[-1][3][parserStack[-1][4]-1] not in "lL"): + if i in "0123456789" and ( + size is None or parserStack[-1][3][parserStack[-1][4] - 1] + not in "lL"): try: size = size * 10 + int(i) except TypeError: @@ -214,34 +245,43 @@ def decode_simple(self, char="v"): isArray = False elif i in "Ll" and size is None: size = self.decode_payload_varint() - if i == "L": - isArray = True - else: - isArray = False + isArray = i == "L" elif size is not None: if isArray: - parserStack.append([size, size, isArray, parserStack[-1][3][parserStack[-1][4]:], 0, []]) + parserStack.append([ + size, size, isArray, + parserStack[-1][3][parserStack[-1][4]:], 0, [] + ]) parserStack[-2][4] = len(parserStack[-2][3]) else: - for j in range(parserStack[-1][4], len(parserStack[-1][3])): + j = 0 + for j in range( + parserStack[-1][4], len(parserStack[-1][3])): if parserStack[-1][3][j] not in "lL0123456789": break - parserStack.append([size, size, isArray, parserStack[-1][3][parserStack[-1][4]:j+1], 0, []]) + parserStack.append([ + size, size, isArray, + parserStack[-1][3][parserStack[-1][4]:j + 1], 0, [] + ]) parserStack[-2][4] += len(parserStack[-1][3]) - 1 size = None continue elif i == "s": - #if parserStack[-2][2]: - # parserStack[-1][5].append(self.payload[self.payloadOffset:self.payloadOffset + parserStack[-1][0]]) - #else: - parserStack[-1][5] = self.payload[self.payloadOffset:self.payloadOffset + parserStack[-1][0]] + # if parserStack[-2][2]: + # parserStack[-1][5].append(self.payload[ + # self.payloadOffset:self.payloadOffset + # + parserStack[-1][0]]) + # else: + parserStack[-1][5] = self.payload[ + self.payloadOffset:self.payloadOffset + parserStack[-1][0]] self.payloadOffset += parserStack[-1][0] parserStack[-1][1] = 0 parserStack[-1][2] = True - #del parserStack[-1] + # del parserStack[-1] size = None elif i in "viHIQ": - parserStack[-1][5].append(decode_simple(self, parserStack[-1][3][parserStack[-1][4]])) + parserStack[-1][5].append(decode_simple( + self, parserStack[-1][3][parserStack[-1][4]])) size = None else: size = None @@ -252,297 +292,363 @@ def decode_simple(self, char="v"): parserStack[depth][4] = 0 if depth > 0: if parserStack[depth][2]: - parserStack[depth - 1][5].append(parserStack[depth][5]) + parserStack[depth - 1][5].append( + parserStack[depth][5]) else: - parserStack[depth - 1][5].extend(parserStack[depth][5]) + parserStack[depth - 1][5].extend( + parserStack[depth][5]) parserStack[depth][5] = [] if parserStack[depth][1] <= 0: if depth == 0: - # we're done, at depth 0 counter is at 0 and pattern is done parsing + # we're done, at depth 0 counter is at 0 + # and pattern is done parsing return parserStack[depth][5] del parserStack[-1] continue break break if self.payloadOffset > self.payloadLength: - logger.debug("Insufficient data %i/%i", self.payloadOffset, self.payloadLength) + logger.debug( + 'Insufficient data %i/%i', + self.payloadOffset, self.payloadLength) raise BMProtoInsufficientDataError() def bm_command_error(self): - fatalStatus, banTime, inventoryVector, errorText = self.decode_payload_content("vvlsls") - logger.error("%s:%i error: %i, %s", self.destination.host, self.destination.port, fatalStatus, errorText) + """Decode an error message and log it""" + err_values = self.decode_payload_content("vvlsls") + fatalStatus = err_values[0] + # banTime = err_values[1] + # inventoryVector = err_values[2] + errorText = err_values[3] + logger.error( + '%s:%i error: %i, %s', self.destination.host, + self.destination.port, fatalStatus, errorText) return True def bm_command_getdata(self): + """ + Incoming request for object(s). + If we have them and some other conditions are fulfilled, + append them to the write queue. + """ items = self.decode_payload_content("l32s") # skip? - if time.time() < self.skipUntil: + now = time.time() + if now < self.skipUntil: return True - #TODO make this more asynchronous - helper_random.randomshuffle(items) - for i in map(str, items): - if Dandelion().hasHash(i) and \ - self != Dandelion().objectChildStem(i): - self.antiIntersectionDelay() - logger.info('%s asked for a stem object we didn\'t offer to it.', self.destination) - break - else: - try: - self.append_write_buf(protocol.CreatePacket('object', Inventory()[i].payload)) - except KeyError: - self.antiIntersectionDelay() - logger.info('%s asked for an object we don\'t have.', self.destination) - break - # I think that aborting after the first missing/stem object is more secure - # when using random reordering, as the recipient won't know exactly which objects we refuse to deliver + for i in items: + self.pendingUpload[str(i)] = now return True - def _command_inv(self, dandelion=False): + def _command_inv(self, extend_dandelion_stem=False): + """ + Common inv announce implementation: + both inv and dinv depending on *extend_dandelion_stem* kwarg + """ items = self.decode_payload_content("l32s") - if len(items) >= BMProto.maxObjectCount: - logger.error("Too many items in %sinv message!", "d" if dandelion else "") + if len(items) > protocol.MAX_OBJECT_COUNT: + logger.error( + 'Too many items in %sinv message!', 'd' if extend_dandelion_stem else '') raise BMProtoExcessiveDataError() - else: - pass # ignore dinv if dandelion turned off - if dandelion and not state.dandelion: + if extend_dandelion_stem and not dandelion_ins.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 dandelion_ins.hasHash(i): continue - if dandelion and not Dandelion().hasHash(i): - Dandelion().addHash(i, self) + if extend_dandelion_stem and not dandelion_ins.hasHash(i): + dandelion_ins.addHash(i, self) self.handleReceivedInventory(i) return True def bm_command_inv(self): + """Non-dandelion announce""" return self._command_inv(False) def bm_command_dinv(self): - """ - Dandelion stem announce - """ + """Dandelion stem announce""" return self._command_inv(True) def bm_command_object(self): + """Incoming object, process it""" objectOffset = self.payloadOffset - nonce, expiresTime, objectType, version, streamNumber = self.decode_payload_content("QQIvv") - self.object = BMObject(nonce, expiresTime, objectType, version, streamNumber, self.payload, self.payloadOffset) - - if len(self.payload) - self.payloadOffset > BMProto.maxObjectPayloadSize: - logger.info('The payload length of this object is too large (%s bytes). Ignoring it.' % len(self.payload) - self.payloadOffset) + nonce, expiresTime, objectType, version, streamNumber = \ + self.decode_payload_content("QQIvv") + self.object = BMObject( + nonce, expiresTime, objectType, version, streamNumber, + self.payload, self.payloadOffset) + + payload_len = len(self.payload) - self.payloadOffset + 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) raise BMProtoExcessiveDataError() try: self.object.checkProofOfWorkSufficient() self.object.checkEOLSanity() self.object.checkAlreadyHave() - except (BMObjectExpiredError, BMObjectAlreadyHaveError, BMObjectInsufficientPOWError) as e: + except (BMObjectExpiredError, BMObjectAlreadyHaveError, + BMObjectInsufficientPOWError): BMProto.stopDownloadingObject(self.object.inventoryHash) - raise e + raise try: self.object.checkStream() - except (BMObjectUnwantedStreamError,) as e: - BMProto.stopDownloadingObject(self.object.inventoryHash, BMConfigParser().get("inventory", "acceptmismatch")) - if not BMConfigParser().get("inventory", "acceptmismatch"): - raise e + except BMObjectUnwantedStreamError: + 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))) - except BMObjectInvalidError as e: + objectProcessorQueue.put(( + self.object.objectType, buffer(self.object.data))) # noqa: F821 + except BMObjectInvalidError: BMProto.stopDownloadingObject(self.object.inventoryHash, True) else: try: - del state.missingObjects[self.object.inventoryHash] + del missingObjects[self.object.inventoryHash] except KeyError: pass - if self.object.inventoryHash in Inventory() and Dandelion().hasHash(self.object.inventoryHash): - Dandelion().removeHash(self.object.inventoryHash, "cycle detection") - - Inventory()[self.object.inventoryHash] = ( - self.object.objectType, self.object.streamNumber, buffer(self.payload[objectOffset:]), self.object.expiresTime, buffer(self.object.tag)) - self.handleReceivedObject(self.object.streamNumber, self.object.inventoryHash) - invQueue.put((self.object.streamNumber, self.object.inventoryHash, self.destination)) + if self.object.inventoryHash in state.Inventory and dandelion_ins.hasHash( + self.object.inventoryHash): + dandelion_ins.removeHash( + self.object.inventoryHash, "cycle detection") + + state.Inventory[self.object.inventoryHash] = ( + self.object.objectType, self.object.streamNumber, + buffer(self.payload[objectOffset:]), self.object.expiresTime, # noqa: F821 + buffer(self.object.tag) # noqa: F821 + ) + self.handleReceivedObject( + self.object.streamNumber, self.object.inventoryHash) + invQueue.put(( + self.object.streamNumber, self.object.inventoryHash, + self.destination)) return True def _decode_addr(self): return self.decode_payload_content("LQIQ16sH") def bm_command_addr(self): - addresses = self._decode_addr() - for i in addresses: - seenTime, stream, services, ip, port = i - decodedIP = protocol.checkIPAddress(str(ip)) - if stream not in state.streamsInWhichIAmParticipating: + """Incoming addresses, process them""" + # not using services + for seenTime, stream, _, ip, port in self._decode_addr(): + ip = str(ip) + if ( + stream not in connectionpool.pool.streams + # FIXME: should check against complete list + or ip.startswith('bootstrap') + ): continue - if decodedIP is not False and seenTime > time.time() - BMProto.addressAlive: - peer = state.Peer(decodedIP, port) - try: - if knownnodes.knownNodes[stream][peer]["lastseen"] > seenTime: - continue - except KeyError: - pass - if len(knownnodes.knownNodes[stream]) < int(BMConfigParser().get("knownnodes", "maxnodes")): - with knownnodes.knownNodesLock: - try: - knownnodes.knownNodes[stream][peer]["lastseen"] = seenTime - except (TypeError, KeyError): - knownnodes.knownNodes[stream][peer] = { - "lastseen": seenTime, - "rating": 0, - "self": False, - } - addrQueue.put((stream, peer, self.destination)) + decodedIP = protocol.checkIPAddress(ip) + if ( + decodedIP and time.time() - seenTime > 0 + and seenTime > time.time() - protocol.ADDRESS_ALIVE + and port > 0 + ): + peer = Peer(decodedIP, port) + + with knownnodes.knownNodesLock: + # isnew = + knownnodes.addKnownNode(stream, peer, seenTime) + + # since we don't track peers outside of knownnodes, + # only spread if in knownnodes to prevent flood + # DISABLED TO WORKAROUND FLOOD/LEAK + # if isnew: + # addrQueue.put(( + # stream, peer, seenTime, self.destination)) return True def bm_command_portcheck(self): - portCheckerQueue.put(state.Peer(self.destination, self.peerNode.port)) + """Incoming port check request, queue it.""" + if self.isOutbound or self.portCheckRequested: + return True + self.portCheckRequested = True + portCheckerQueue.put(Peer(self.destination.host, + self.peerNode.port)) return True def bm_command_ping(self): + """Incoming ping, respond to it.""" self.append_write_buf(protocol.CreatePacket('pong')) return True - def bm_command_pong(self): + @staticmethod + def bm_command_pong(): + """ + Incoming pong. + Ignore it. PyBitmessage pings connections after about 5 minutes + of inactivity, and leaves it to the TCP stack to handle actual + timeouts. So there is no need to do anything when a pong arrives. + """ # nothing really return True def bm_command_verack(self): + """ + Incoming verack. + If already sent my own verack, handshake is complete (except + potentially waiting for buffers to flush), so we can continue + to the main connection phase. If not sent verack yet, + continue processing. + """ self.verackReceived = True - if self.verackSent: - if self.isSSL: - self.set_state("tls_init", length=self.payloadLength, expectBytes=0) - return False - self.set_state("connection_fully_established", length=self.payloadLength, expectBytes=0) - return False - return True + if not self.verackSent: + return True + self.set_state( + "tls_init" if self.isSSL else "connection_fully_established", + length=self.payloadLength, expectBytes=0) + return False def bm_command_version(self): - self.remoteProtocolVersion, self.services, self.timestamp, self.sockNode, self.peerNode, self.nonce, \ - self.userAgent, self.streams = self.decode_payload_content("IQQiiQlsLv") + """ + Incoming version. + Parse and log, remember important things, like streams, bitfields, etc. + """ + decoded = self.decode_payload_content("IQQiiQlslv") + (self.remoteProtocolVersion, self.services, self.timestamp, + self.sockNode, self.peerNode, self.nonce, self.userAgent + ) = decoded[:7] + self.streams = decoded[7:] self.nonce = struct.pack('>Q', self.nonce) self.timeOffset = self.timestamp - int(time.time()) - logger.debug("remoteProtocolVersion: %i", self.remoteProtocolVersion) - logger.debug("services: 0x%08X", self.services) - logger.debug("time offset: %i", self.timestamp - int(time.time())) - logger.debug("my external IP: %s", self.sockNode.host) - logger.debug("remote node incoming address: %s:%i", self.destination.host, self.peerNode.port) - logger.debug("user agent: %s", self.userAgent) - logger.debug("streams: [%s]", ",".join(map(str,self.streams))) + logger.debug('remoteProtocolVersion: %i', self.remoteProtocolVersion) + logger.debug('services: 0x%08X', self.services) + logger.debug('time offset: %i', self.timeOffset) + logger.debug('my external IP: %s', self.sockNode.host) + logger.debug( + 'remote node incoming address: %s:%i', + self.destination.host, self.peerNode.port) + logger.debug('user agent: %s', self.userAgent) + logger.debug('streams: [%s]', ','.join(map(str, self.streams))) if not self.peerValidityChecks(): - # TODO ABORT + # ABORT afterwards return True - #shared.connectedHostsList[self.destination] = self.streams[0] self.append_write_buf(protocol.CreatePacket('verack')) self.verackSent = True + ua_valid = re.match( + r'^/[a-zA-Z]+:[0-9]+\.?[\w\s\(\)\./:;-]*/$', self.userAgent) + if not ua_valid: + self.userAgent = '/INVALID:0/' if not self.isOutbound: - self.append_write_buf(protocol.assembleVersionMessage(self.destination.host, self.destination.port, \ - network.connectionpool.BMConnectionPool().streams, True, nodeid=self.nodeid)) - #print "%s:%i: Sending version" % (self.destination.host, self.destination.port) - if ((self.services & protocol.NODE_SSL == protocol.NODE_SSL) and - protocol.haveSSL(not self.isOutbound)): + self.append_write_buf(protocol.assembleVersionMessage( + self.destination.host, self.destination.port, + connectionpool.pool.streams, dandelion_ins.enabled, True, + nodeid=self.nodeid)) + logger.debug( + '%(host)s:%(port)i sending version', + self.destination._asdict()) + if ((self.services & protocol.NODE_SSL == protocol.NODE_SSL) + and protocol.haveSSL(not self.isOutbound)): self.isSSL = True - if self.verackReceived: - if self.isSSL: - self.set_state("tls_init", length=self.payloadLength, expectBytes=0) - return False - self.set_state("connection_fully_established", length=self.payloadLength, expectBytes=0) - return False - return True + if not self.verackReceived: + return True + self.set_state( + "tls_init" if self.isSSL else "connection_fully_established", + length=self.payloadLength, expectBytes=0) + return False + # pylint: disable=too-many-return-statements def peerValidityChecks(self): + """Check the validity of the peer""" if self.remoteProtocolVersion < 3: - self.append_write_buf(protocol.assembleErrorMessage(fatal=2, - errorText="Your is using an old protocol. Closing connection.")) - logger.debug ('Closing connection to old protocol version %s, node: %s', - str(self.remoteProtocolVersion), str(self.destination)) + self.append_write_buf(protocol.assembleErrorMessage( + errorText="Your is using an old protocol. Closing connection.", + fatal=2)) + logger.debug( + 'Closing connection to old protocol version %s, node: %s', + self.remoteProtocolVersion, self.destination) return False - if self.timeOffset > BMProto.maxTimeOffset: - self.append_write_buf(protocol.assembleErrorMessage(fatal=2, - errorText="Your time is too far in the future compared to mine. Closing connection.")) - logger.info("%s's time is too far in the future (%s seconds). Closing connection to it.", + 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)) + logger.info( + "%s's time is too far in the future (%s seconds)." + " Closing connection to it.", self.destination, self.timeOffset) - shared.timeOffsetWrongCount += 1 + BMProto.timeOffsetWrongCount += 1 return False - elif self.timeOffset < -BMProto.maxTimeOffset: - self.append_write_buf(protocol.assembleErrorMessage(fatal=2, - errorText="Your time is too far in the past compared to mine. Closing connection.")) - logger.info("%s's time is too far in the past (timeOffset %s seconds). Closing connection to it.", + 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)) + logger.info( + "%s's time is too far in the past" + " (timeOffset %s seconds). Closing connection to it.", self.destination, self.timeOffset) - shared.timeOffsetWrongCount += 1 + BMProto.timeOffsetWrongCount += 1 return False else: - shared.timeOffsetWrongCount = 0 + BMProto.timeOffsetWrongCount = 0 if not self.streams: - self.append_write_buf(protocol.assembleErrorMessage(fatal=2, - errorText="We don't have shared stream interests. Closing connection.")) - logger.debug ('Closed connection to %s because there is no overlapping interest in streams.', - str(self.destination)) + self.append_write_buf(protocol.assembleErrorMessage( + errorText="We don't have shared stream interests." + " Closing connection.", fatal=2)) + logger.debug( + 'Closed connection to %s because there is no overlapping' + ' interest in streams.', self.destination) return False - if self.destination in network.connectionpool.BMConnectionPool().inboundConnections: + if connectionpool.pool.inboundConnections.get( + self.destination): try: if not protocol.checkSocksIP(self.destination.host): - self.append_write_buf(protocol.assembleErrorMessage(fatal=2, - errorText="Too many connections from your IP. Closing connection.")) - logger.debug ('Closed connection to %s because we are already connected to that IP.', - str(self.destination)) + self.append_write_buf(protocol.assembleErrorMessage( + errorText="Too many connections from your IP." + " Closing connection.", fatal=2)) + logger.debug( + 'Closed connection to %s because we are already' + ' connected to that IP.', self.destination) return False - except: + 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 state.Peer(self.destination.host, self.peerNode.port) in \ - network.connectionpool.BMConnectionPool().inboundConnections or \ - len(network.connectionpool.BMConnectionPool().inboundConnections) + \ - len(network.connectionpool.BMConnectionPool().outboundConnections) > \ - BMConfigParser().safeGetInt("bitmessagesettings", "maxtotalconnections") + \ - BMConfigParser().safeGetInt("bitmessagesettings", "maxbootstrapconnections"): - self.append_write_buf(protocol.assembleErrorMessage(fatal=2, - errorText="Server full, please try again later.")) - logger.debug ("Closed connection to %s due to server full or duplicate inbound/outbound.", - str(self.destination)) + # 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.pool.inboundConnections + or len(connectionpool.pool) + > config.safeGetInt( + 'bitmessagesettings', 'maxtotalconnections') + + config.safeGetInt( + 'bitmessagesettings', 'maxbootstrapconnections') + ): + self.append_write_buf(protocol.assembleErrorMessage( + errorText="Server full, please try again later.", fatal=2)) + logger.debug( + 'Closed connection to %s due to server full' + ' or duplicate inbound/outbound.', self.destination) return False - if network.connectionpool.BMConnectionPool().isAlreadyConnected(self.nonce): - self.append_write_buf(protocol.assembleErrorMessage(fatal=2, - errorText="I'm connected to myself. Closing connection.")) - logger.debug ("Closed connection to %s because I'm connected to myself.", - str(self.destination)) + if connectionpool.pool.isAlreadyConnected(self.nonce): + self.append_write_buf(protocol.assembleErrorMessage( + errorText="I'm connected to myself. Closing connection.", + fatal=2)) + logger.debug( + "Closed connection to %s because I'm connected to myself.", + self.destination) return False return True - @staticmethod - def assembleAddr(peerList): - if isinstance(peerList, state.Peer): - peerList = (peerList) - if not peerList: - return b'' - retval = b'' - for i in range(0, len(peerList), BMProto.maxAddrCount): - payload = addresses.encodeVarint(len(peerList[i:i + BMProto.maxAddrCount])) - for address in peerList[i:i + BMProto.maxAddrCount]: - stream, peer, timestamp = address - payload += struct.pack( - '>Q', timestamp) # 64-bit time - payload += struct.pack('>I', stream) - payload += struct.pack( - '>q', 1) # service bit flags offered by this node - payload += protocol.encodeHost(peer.host) - payload += struct.pack('>H', peer.port) # remote port - retval += protocol.CreatePacket('addr', payload) - return retval - @staticmethod def stopDownloadingObject(hashId, forwardAnyway=False): - for connection in network.connectionpool.BMConnectionPool().inboundConnections.values() + \ - network.connectionpool.BMConnectionPool().outboundConnections.values(): + """Stop downloading object *hashId*""" + for connection in connectionpool.pool.connections(): try: del connection.objectsNewToMe[hashId] except KeyError: @@ -554,20 +660,25 @@ def stopDownloadingObject(hashId, forwardAnyway=False): except KeyError: pass try: - del state.missingObjects[hashId] + del missingObjects[hashId] except KeyError: pass def handle_close(self): + """Handle close""" self.set_state("close") if not (self.accepting or self.connecting or self.connected): # already disconnected return try: - logger.debug("%s:%i: closing, %s", self.destination.host, self.destination.port, self.close_reason) + logger.debug( + '%s:%i: closing, %s', self.destination.host, + self.destination.port, self.close_reason) except AttributeError: try: - logger.debug("%s:%i: closing", self.destination.host, self.destination.port) + logger.debug( + '%s:%i: closing', + self.destination.host, self.destination.port) except AttributeError: - logger.debug("Disconnected socket closing") + logger.debug('Disconnected socket closing') AdvancedDispatcher.handle_close(self) diff --git a/src/network/connectionchooser.py b/src/network/connectionchooser.py index e29185e964..e2981d51cc 100644 --- a/src/network/connectionchooser.py +++ b/src/network/connectionchooser.py @@ -1,16 +1,26 @@ -from queues import Queue +""" +Select which node to connect to +""" +# pylint: disable=too-many-branches +import logging import random -from bmconfigparser import BMConfigParser +from six.moves import queue + import knownnodes import protocol -from queues import portCheckerQueue import state -import helper_random + +from bmconfigparser import config +from network import portCheckerQueue + +logger = logging.getLogger('default') + def getDiscoveredPeer(): + """Get a peer from the local peer discovery list""" try: - peer = helper_random.randomchoice(state.discoveredPeers.keys()) + peer = random.choice(state.discoveredPeers.keys()) # nosec B311 except (IndexError, KeyError): raise ValueError try: @@ -19,32 +29,44 @@ def getDiscoveredPeer(): pass return peer + def chooseConnection(stream): - haveOnion = BMConfigParser().safeGet("bitmessagesettings", "socksproxytype")[0:5] == 'SOCKS' - if state.trustedPeer: - return state.trustedPeer + """Returns an appropriate connection""" + haveOnion = config.safeGet( + "bitmessagesettings", "socksproxytype")[0:5] == 'SOCKS' + onionOnly = config.safeGetBoolean( + "bitmessagesettings", "onionservicesonly") try: retval = portCheckerQueue.get(False) portCheckerQueue.task_done() return retval - except Queue.Empty: + except queue.Empty: pass # with a probability of 0.5, connect to a discovered peer - if helper_random.randomchoice((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 = helper_random.randomchoice(knownnodes.knownNodes[stream].keys()) + peer = random.choice( # nosec B311 + knownnodes.knownNodes[stream].keys()) try: - rating = knownnodes.knownNodes[stream][peer]["rating"] + peer_info = knownnodes.knownNodes[stream][peer] + if peer_info.get('self'): + continue + rating = peer_info["rating"] except TypeError: - print "Error in %s" % (peer) + logger.warning('Error in %s', peer) rating = 0 if haveOnion: + # do not connect to raw IP addresses + # --keep all traffic within Tor overlay + if onionOnly and not peer.host.endswith('.onion'): + continue # onion addresses have a higher priority when SOCKS if peer.host.endswith('.onion') and rating > 0: rating = 1 - else: + # TODO: need better check + elif not peer.host.startswith('bootstrap'): encodedAddr = protocol.encodeHost(peer.host) # don't connect to local IPs when using SOCKS if not protocol.checkIPAddress(encodedAddr, False): @@ -52,7 +74,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 7854dec1a1..93e54af8ea 100644 --- a/src/network/connectionpool.py +++ b/src/network/connectionpool.py @@ -1,60 +1,119 @@ -from ConfigParser import NoOptionError, NoSectionError +""" +`BMConnectionPool` class definition +""" import errno +import logging +import re import socket +import sys import time import random -import re -from bmconfigparser import BMConfigParser -from debug import logger -import helper_bootstrap -from network.proxy import Proxy -from network.tcp import TCPServer, Socks5BMConnection, Socks4aBMConnection, TCPConnection -from network.udp import UDPSocket -from network.connectionchooser import chooseConnection -import network.asyncore_pollchoose as asyncore +import asyncore_pollchoose as asyncore +import knownnodes import protocol -from singleton import Singleton import state -import helper_random +from bmconfigparser import config +from connectionchooser import chooseConnection +from node import Peer +from proxy import Proxy +from tcp import ( + bootstrap, Socks4aBMConnection, Socks5BMConnection, + TCPConnection, TCPServer) +from udp import UDPSocket + +logger = logging.getLogger('default') + -@Singleton class BMConnectionPool(object): + """Pool of all existing connections""" + # pylint: disable=too-many-instance-attributes + trustedPeer = None + """ + If the trustedpeer option is specified in keys.dat then this will + contain a Peer which will be connected to instead of using the + addresses advertised by other peers. + + The expected use case is where the user has a trusted server where + they run a Bitmessage daemon permanently. If they then run a second + instance of the client on a local machine periodically when they want + to check for messages it will sync with the network a lot faster + without compromising security. + """ + def __init__(self): asyncore.set_rates( - BMConfigParser().safeGetInt("bitmessagesettings", "maxdownloadrate"), - BMConfigParser().safeGetInt("bitmessagesettings", "maxuploadrate")) + config.safeGetInt( + "bitmessagesettings", "maxdownloadrate"), + config.safeGetInt( + "bitmessagesettings", "maxuploadrate") + ) self.outboundConnections = {} self.inboundConnections = {} self.listeningSockets = {} self.udpSockets = {} self.streams = [] - self.lastSpawned = 0 - self.spawnWait = 2 - self.bootstrapped = False + self._lastSpawned = 0 + self._spawnWait = 2 + self._bootstrapped = False + + trustedPeer = config.safeGet( + 'bitmessagesettings', 'trustedpeer') + try: + if trustedPeer: + host, port = trustedPeer.split(':') + self.trustedPeer = Peer(host, int(port)) + except ValueError: + sys.exit( + 'Bad trustedpeer config setting! It should be set as' + ' trustedpeer=:' + ) + + def __len__(self): + return len(self.outboundConnections) + len(self.inboundConnections) + + def connections(self): + """ + Shortcut for combined list of connections from + `inboundConnections` and `outboundConnections` dicts + """ + return self.inboundConnections.values() + self.outboundConnections.values() + + def establishedConnections(self): + """Shortcut for list of connections having fullyEstablished == True""" + return [ + x for x in self.connections() if x.fullyEstablished] def connectToStream(self, streamNumber): + """Connect to a bitmessage stream""" self.streams.append(streamNumber) def getConnectionByAddr(self, addr): - if addr in self.inboundConnections: + """ + Return an (existing) connection object based on a `Peer` object + (IP and port) + """ + try: return self.inboundConnections[addr] + except KeyError: + pass try: - if addr.host in self.inboundConnections: - return self.inboundConnections[addr.host] - except AttributeError: + return self.inboundConnections[addr.host] + except (KeyError, AttributeError): pass - if addr in self.outboundConnections: + try: return self.outboundConnections[addr] + except KeyError: + pass try: - if addr.host in self.udpSockets: - return self.udpSockets[addr.host] - except AttributeError: + return self.udpSockets[addr.host] + except (KeyError, AttributeError): pass raise KeyError def isAlreadyConnected(self, nodeid): - for i in self.inboundConnections.values() + self.outboundConnections.values(): + """Check if we're already connected to this peer""" + for i in self.connections(): try: if nodeid == i.nodeid: return True @@ -63,6 +122,7 @@ def isAlreadyConnected(self, nodeid): return False def addConnection(self, connection): + """Add a connection object to our internal dict""" if isinstance(connection, UDPSocket): return if connection.isOutbound: @@ -71,13 +131,16 @@ def addConnection(self, connection): if connection.destination.host in self.inboundConnections: self.inboundConnections[connection.destination] = connection else: - self.inboundConnections[connection.destination.host] = connection + self.inboundConnections[connection.destination.host] = \ + connection def removeConnection(self, connection): + """Remove a connection from our internal dict""" if isinstance(connection, UDPSocket): del self.udpSockets[connection.listening.host] elif isinstance(connection, TCPServer): - del self.listeningSockets[state.Peer(connection.destination.host, connection.destination.port)] + del self.listeningSockets[Peer( + connection.destination.host, connection.destination.port)] elif connection.isOutbound: try: del self.outboundConnections[connection.destination] @@ -91,29 +154,41 @@ def removeConnection(self, connection): del self.inboundConnections[connection.destination.host] except KeyError: pass - connection.close() + connection.handle_close() - def getListeningIP(self): - if BMConfigParser().safeGet("bitmessagesettings", "onionhostname").endswith(".onion"): - host = BMConfigParser().safeGet("bitmessagesettings", "onionbindip") + @staticmethod + def getListeningIP(): + """What IP are we supposed to be listening on?""" + 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().get("bitmessagesettings", "socksproxytype") == "none": + if ( + 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 = socket.INADDR_ANY + 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 def startUDPSocket(self, bind=None): + """ + Open an UDP socket. Depending on settings, it can either only + accept incoming UDP packets, or also be able to send them. + """ if bind is None: host = self.getListeningIP() udpSocket = UDPSocket(host=host, announcing=True) @@ -124,40 +199,96 @@ def startUDPSocket(self, bind=None): udpSocket = UDPSocket(host=bind, announcing=True) self.udpSockets[udpSocket.listening.host] = udpSocket - def loop(self): + def startBootstrappers(self): + """Run the process of resolving bootstrap hostnames""" + proxy_type = config.safeGet( + 'bitmessagesettings', 'socksproxytype') + # A plugins may be added here + hostname = None + if not proxy_type or proxy_type == 'none': + connection_base = TCPConnection + elif proxy_type == 'SOCKS5': + connection_base = Socks5BMConnection + hostname = random.choice([ # nosec B311 + 'quzwelsuziwqgpt2.onion', None + ]) + elif proxy_type == 'SOCKS4a': + connection_base = Socks4aBMConnection # FIXME: I cannot test + else: + # This should never happen because socksproxytype setting + # is handled in bitmessagemain before starting the connectionpool + return + + bootstrapper = bootstrap(connection_base) + if not hostname: + port = random.choice([8080, 8444]) # nosec B311 + hostname = 'bootstrap%s.bitmessage.org' % port + else: + port = 8444 + self.addConnection(bootstrapper(hostname, port)) + + def loop(self): # pylint: disable=too-many-branches,too-many-statements + """Main Connectionpool's loop""" + # pylint: disable=too-many-locals # defaults to empty loop if outbound connections are maxed spawnConnections = False acceptConnections = True - if BMConfigParser().safeGetBoolean('bitmessagesettings', 'dontconnect'): + if config.safeGetBoolean( + 'bitmessagesettings', 'dontconnect'): acceptConnections = False - elif BMConfigParser().safeGetBoolean('bitmessagesettings', 'sendoutgoingconnections'): + elif config.safeGetBoolean( + 'bitmessagesettings', 'sendoutgoingconnections'): spawnConnections = True - if BMConfigParser().get('bitmessagesettings', 'socksproxytype')[0:5] == 'SOCKS' and \ - (not BMConfigParser().getboolean('bitmessagesettings', 'sockslisten') and \ - ".onion" not in BMConfigParser().get('bitmessagesettings', 'onionhostname')): + socksproxytype = config.safeGet( + 'bitmessagesettings', 'socksproxytype', '') + onionsocksproxytype = config.safeGet( + 'bitmessagesettings', 'onionsocksproxytype', '') + if ( + socksproxytype[:5] == 'SOCKS' + and not config.safeGetBoolean( + 'bitmessagesettings', 'sockslisten') + and '.onion' not in config.safeGet( + 'bitmessagesettings', 'onionhostname', '') + ): acceptConnections = False + # pylint: disable=too-many-nested-blocks if spawnConnections: - if not self.bootstrapped: - helper_bootstrap.dns() - self.bootstrapped = True - Proxy.proxy = (BMConfigParser().safeGet("bitmessagesettings", "sockshostname"), - BMConfigParser().safeGetInt("bitmessagesettings", "socksport")) + if not knownnodes.knownNodesActual: + self.startBootstrappers() + knownnodes.knownNodesActual = True + if not self._bootstrapped: + self._bootstrapped = True + Proxy.proxy = ( + config.safeGet( + 'bitmessagesettings', 'sockshostname'), + config.safeGetInt( + 'bitmessagesettings', 'socksport') + ) # TODO AUTH # TODO reset based on GUI settings changes try: - if not BMConfigParser().get("network", "onionsocksproxytype").startswith("SOCKS"): - raise NoOptionError - Proxy.onionproxy = (BMConfigParser().get("network", "onionsockshostname"), - BMConfigParser().getint("network", "onionsocksport")) - except (NoOptionError, NoSectionError): - Proxy.onionproxy = None - established = sum(1 for c in self.outboundConnections.values() if (c.connected and c.fullyEstablished)) + if not onionsocksproxytype.startswith("SOCKS"): + raise ValueError + Proxy.onion_proxy = ( + config.safeGet( + 'network', 'onionsockshostname', None), + config.safeGet( + 'network', 'onionsocksport', None) + ) + except ValueError: + Proxy.onion_proxy = None + established = sum( + 1 for c in self.outboundConnections.values() + if (c.connected and c.fullyEstablished)) pending = len(self.outboundConnections) - established - if established < BMConfigParser().safeGetInt("bitmessagesettings", "maxoutboundconnections"): - for i in range(state.maximumNumberOfHalfOpenConnections - pending): + if established < config.safeGetInt( + 'bitmessagesettings', 'maxoutboundconnections'): + for i in range( + state.maximumNumberOfHalfOpenConnections - pending): try: - chosen = chooseConnection(helper_random.randomchoice(self.streams)) + chosen = self.trustedPeer or chooseConnection( + random.choice(self.streams)) # nosec B311 except ValueError: continue if chosen in self.outboundConnections: @@ -167,55 +298,61 @@ def loop(self): # don't connect to self if chosen in state.ownAddresses: continue - - #for c in self.outboundConnections: - # if chosen == c.destination: - # continue - #for c in self.inboundConnections: - # if chosen.host == c.destination.host: - # continue + # don't connect to the hosts from the same + # network group, defense against sibyl attacks + host_network_group = protocol.network_group( + chosen.host) + same_group = False + for j in self.outboundConnections.values(): + if host_network_group == j.network_group: + same_group = True + if chosen.host == j.destination.host: + knownnodes.decreaseRating(chosen) + break + if same_group: + continue + try: - if chosen.host.endswith(".onion") and Proxy.onionproxy is not None: - if BMConfigParser().get("network", "onionsocksproxytype") == "SOCKS5": + if chosen.host.endswith(".onion") and Proxy.onion_proxy: + if onionsocksproxytype == "SOCKS5": self.addConnection(Socks5BMConnection(chosen)) - elif BMConfigParser().get("network", "onionsocksproxytype") == "SOCKS4a": + elif onionsocksproxytype == "SOCKS4a": self.addConnection(Socks4aBMConnection(chosen)) - elif BMConfigParser().safeGet("bitmessagesettings", "socksproxytype") == "SOCKS5": + elif socksproxytype == "SOCKS5": self.addConnection(Socks5BMConnection(chosen)) - elif BMConfigParser().safeGet("bitmessagesettings", "socksproxytype") == "SOCKS4a": + elif socksproxytype == "SOCKS4a": self.addConnection(Socks4aBMConnection(chosen)) else: self.addConnection(TCPConnection(chosen)) except socket.error as e: if e.errno == errno.ENETUNREACH: continue - except (NoSectionError, NoOptionError): - # shouldn't happen - pass - self.lastSpawned = time.time() + self._lastSpawned = time.time() else: - for i in ( - self.inboundConnections.values() + - self.outboundConnections.values() - ): - i.set_state("close") + 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("[^\w.]+", " ", BMConfigParser().safeGet("network", "bind")).split(): + for bind in re.sub( + r'[^\w.]+', ' ', + 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("[^\w.]+", " ", BMConfigParser().safeGet("network", "bind")).split(): + for bind in re.sub( + r'[^\w.]+', ' ', + config.safeGet('network', 'bind') + ).split(): self.startUDPSocket(bind) self.startUDPSocket(False) logger.info('Starting UDP socket(s).') @@ -231,13 +368,13 @@ def loop(self): i.accepting = i.connecting = i.connected = False logger.info('Stopped udp sockets.') - loopTime = float(self.spawnWait) - if self.lastSpawned < time.time() - self.spawnWait: + loopTime = float(self._spawnWait) + if self._lastSpawned < time.time() - self._spawnWait: loopTime = 2.0 asyncore.loop(timeout=loopTime, count=1000) reaper = [] - for i in self.inboundConnections.values() + self.outboundConnections.values(): + for i in self.connections(): minTx = time.time() - 20 if i.fullyEstablished: minTx -= 300 - 20 @@ -245,9 +382,13 @@ def loop(self): if i.fullyEstablished: i.append_write_buf(protocol.CreatePacket('ping')) else: - i.close_reason = "Timeout (%is)" % (time.time() - i.lastTx) + i.close_reason = "Timeout (%is)" % ( + time.time() - i.lastTx) i.set_state("close") - for i in self.inboundConnections.values() + self.outboundConnections.values() + self.listeningSockets.values() + self.udpSockets.values(): + for i in ( + self.connections() + + self.listeningSockets.values() + self.udpSockets.values() + ): if not (i.accepting or i.connecting or i.connected): reaper.append(i) else: @@ -258,3 +399,6 @@ def loop(self): pass for i in reaper: self.removeConnection(i) + + +pool = BMConnectionPool() diff --git a/src/network/dandelion.py b/src/network/dandelion.py index 06ecca24a2..0bb613c2dd 100644 --- a/src/network/dandelion.py +++ b/src/network/dandelion.py @@ -1,14 +1,12 @@ +""" +Dandelion class definition, tracks stages +""" +import logging from collections import namedtuple -from random import choice, sample, expovariate +from random import choice, expovariate, sample from threading import RLock from time import time -from bmconfigparser import BMConfigParser -import network.connectionpool -from debug import logging -from queues import invQueue -from singleton import Singleton -import state # randomise routes after 600 seconds REASSIGN_INTERVAL = 600 @@ -21,8 +19,11 @@ Stem = namedtuple('Stem', ['child', 'stream', 'timeout']) -@Singleton -class Dandelion(): +logger = logging.getLogger('default') + + +class Dandelion: # pylint: disable=old-style-class + """Dandelion class for tracking stem/fluff stages.""" def __init__(self): # currently assignable child stems self.stem = [] @@ -33,33 +34,59 @@ def __init__(self): # when to rerandomise routes self.refresh = time() + REASSIGN_INTERVAL self.lock = RLock() + self.enabled = None + self.pool = None - def poissonTimeout(self, start=None, average=0): + @staticmethod + def poissonTimeout(start=None, average=0): + """Generate deadline using Poisson distribution""" if start is None: start = time() if average == 0: average = FLUFF_TRIGGER_MEAN_DELAY - return start + expovariate(1.0/average) + FLUFF_TRIGGER_FIXED_DELAY + return start + expovariate(1.0 / average) + FLUFF_TRIGGER_FIXED_DELAY + + def init_pool(self, pool): + """pass pool instance""" + self.pool = pool + + def init_dandelion_enabled(self, config): + """Check if Dandelion is enabled and set value in enabled attribute""" + dandelion_enabled = config.safeGetInt('network', 'dandelion') + # dandelion requires outbound connections, without them, + # stem objects will get stuck forever + if not config.safeGetBoolean( + 'bitmessagesettings', 'sendoutgoingconnections'): + dandelion_enabled = 0 + self.enabled = dandelion_enabled def addHash(self, hashId, source=None, stream=1): - if not state.dandelion: - return + """Add inventory vector to dandelion stem return status of dandelion enabled""" + assert self.enabled is not None with self.lock: self.hashMap[hashId] = Stem( - self.getNodeStem(source), - stream, - self.poissonTimeout()) + self.getNodeStem(source), + stream, + self.poissonTimeout()) def setHashStream(self, hashId, stream=1): + """ + Update stream for inventory vector (as inv/dinv commands don't + include streams, we only learn this after receiving the object) + """ with self.lock: if hashId in self.hashMap: self.hashMap[hashId] = Stem( - self.hashMap[hashId].child, - stream, - self.poissonTimeout()) + self.hashMap[hashId].child, + stream, + self.poissonTimeout()) def removeHash(self, hashId, reason="no reason specified"): - logging.debug("%s entering fluff mode due to %s.", ''.join('%02x'%ord(i) for i in hashId), reason) + """Switch inventory vector from stem to fluff mode""" + if logger.isEnabledFor(logging.DEBUG): + logger.debug( + '%s entering fluff mode due to %s.', + ''.join('%02x' % ord(i) for i in hashId), reason) with self.lock: try: del self.hashMap[hashId] @@ -67,38 +94,63 @@ def removeHash(self, hashId, reason="no reason specified"): pass def hasHash(self, hashId): + """Is inventory vector in stem mode?""" return hashId in self.hashMap def objectChildStem(self, hashId): + """Child (i.e. next) node for an inventory vector during stem mode""" return self.hashMap[hashId].child - def maybeAddStem(self, connection): + def maybeAddStem(self, connection, invQueue): + """ + If we had too few outbound connections, add the current one to the + current stem list. Dandelion as designed by the authors should + always have two active stem child connections. + """ # fewer than MAX_STEMS outbound connections at last reshuffle? with self.lock: if len(self.stem) < MAX_STEMS: self.stem.append(connection) for k in (k for k, v in self.nodeMap.iteritems() if v is None): self.nodeMap[k] = connection - for k, v in {k: v for k, v in self.hashMap.iteritems() if v.child is None}.iteritems(): - self.hashMap[k] = Stem(connection, v.stream, self.poissonTimeout()) + for k, v in { + k: v for k, v in self.hashMap.iteritems() + if v.child is None + }.iteritems(): + self.hashMap[k] = Stem( + connection, v.stream, self.poissonTimeout()) invQueue.put((v.stream, k, v.child)) - def maybeRemoveStem(self, connection): + """ + Remove current connection from the stem list (called e.g. when + a connection is closed). + """ # is the stem active? with self.lock: if connection in self.stem: self.stem.remove(connection) # active mappings to pointing to the removed node - for k in (k for k, v in self.nodeMap.iteritems() if v == connection): + for k in ( + k for k, v in self.nodeMap.iteritems() + if v == connection + ): self.nodeMap[k] = None - for k, v in {k: v for k, v in self.hashMap.iteritems() if v.child == connection}.iteritems(): - self.hashMap[k] = Stem(None, v.stream, self.poissonTimeout()) + for k, v in { + k: v for k, v in self.hashMap.iteritems() + if v.child == connection + }.iteritems(): + self.hashMap[k] = Stem( + None, v.stream, self.poissonTimeout()) def pickStem(self, parent=None): + """ + Pick a random active stem, but not the parent one + (the one where an object came from) + """ 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: @@ -112,6 +164,10 @@ def pickStem(self, parent=None): return None def getNodeStem(self, node=None): + """ + Return child stem node for a given parent stem node + (the mapping is static for about 10 minutes, then it reshuffles) + """ with self.lock: try: return self.nodeMap[node] @@ -119,23 +175,34 @@ def getNodeStem(self, node=None): self.nodeMap[node] = self.pickStem(node) return self.nodeMap[node] - def expire(self): + def expire(self, invQueue): + """Switch expired objects from stem to fluff mode""" with self.lock: deadline = time() - # only expire those that have a child node, i.e. those without a child not will stick around - toDelete = [[v.stream, k, v.child] for k, v in self.hashMap.iteritems() if v.timeout < deadline and v.child] + toDelete = [ + [v.stream, k, v.child] for k, v in self.hashMap.iteritems() + if v.timeout < deadline + ] + for row in toDelete: self.removeHash(row[1], 'expiration') - invQueue.put((row[0], row[1], row[2])) + invQueue.put(row) + return toDelete def reRandomiseStems(self): + """Re-shuffle stem mapping (parent <-> child pairs)""" + assert self.pool is not None + if self.refresh > time(): + return + with self.lock: try: # random two connections - self.stem = sample(network.connectionpool.BMConnectionPool().outboundConnections.values(), MAX_STEMS) + self.stem = sample( # nosec B311 + self.pool.outboundConnections.values(), MAX_STEMS) # not enough stems available except ValueError: - self.stem = network.connectionpool.BMConnectionPool().outboundConnections.values() + self.stem = self.pool.outboundConnections.values() self.nodeMap = {} # hashMap stays to cater for pending stems self.refresh = time() + REASSIGN_INTERVAL diff --git a/src/network/downloadthread.py b/src/network/downloadthread.py index ee0b02891f..7c8bccb699 100644 --- a/src/network/downloadthread.py +++ b/src/network/downloadthread.py @@ -1,18 +1,19 @@ -import random -import threading +""" +`DownloadThread` class definition +""" import time - +import random +import state import addresses -from dandelion import Dandelion -from debug import logger -from helper_threading import StoppableThread -from inventory import Inventory -from network.connectionpool import BMConnectionPool import protocol -from state import missingObjects -import helper_random +import connectionpool +from network import dandelion_ins +from objectracker import missingObjects +from threads import StoppableThread -class DownloadThread(threading.Thread, StoppableThread): + +class DownloadThread(StoppableThread): + """Thread-based class for downloading from connections""" minPending = 200 maxRequestChunk = 1000 requestTimeout = 60 @@ -20,16 +21,16 @@ class DownloadThread(threading.Thread, StoppableThread): requestExpires = 3600 def __init__(self): - threading.Thread.__init__(self, name="Downloader") - self.initStop() - self.name = "Downloader" - logger.info("init download thread") + super(DownloadThread, self).__init__(name="Downloader") self.lastCleaned = time.time() def cleanPending(self): - deadline = time.time() - DownloadThread.requestExpires + """Expire pending downloads eventually""" + deadline = time.time() - self.requestExpires try: - toDelete = [k for k, v in missingObjects.iteritems() if v < deadline] + toDelete = [ + k for k, v in missingObjects.iteritems() + if v < deadline] except RuntimeError: pass else: @@ -41,12 +42,12 @@ def run(self): while not self._stopped: requested = 0 # Choose downloading peers randomly - connections = [x for x in BMConnectionPool().inboundConnections.values() + BMConnectionPool().outboundConnections.values() if x.fullyEstablished] - helper_random.randomshuffle(connections) - try: - requestChunk = max(int(min(DownloadThread.maxRequestChunk, len(missingObjects)) / len(connections)), 1) - except ZeroDivisionError: - requestChunk = 1 + connections = connectionpool.pool.establishedConnections() + random.shuffle(connections) + requestChunk = max(int( + min(self.maxRequestChunk, len(missingObjects)) + / len(connections)), 1) if connections else 1 + for i in connections: now = time.time() # avoid unnecessary delay @@ -59,7 +60,7 @@ def run(self): payload = bytearray() chunkCount = 0 for chunk in request: - if chunk in Inventory() and not Dandelion().hasHash(chunk): + if chunk in state.Inventory and not dandelion_ins.hasHash(chunk): try: del i.objectsNewToMe[chunk] except KeyError: @@ -72,9 +73,11 @@ def run(self): continue payload[0:0] = addresses.encodeVarint(chunkCount) i.append_write_buf(protocol.CreatePacket('getdata', payload)) - logger.debug("%s:%i Requesting %i objects", i.destination.host, i.destination.port, chunkCount) + self.logger.debug( + '%s:%i Requesting %i objects', + i.destination.host, i.destination.port, chunkCount) requested += chunkCount - if time.time() >= self.lastCleaned + DownloadThread.cleanInterval: + if time.time() >= self.lastCleaned + self.cleanInterval: self.cleanPending() if not requested: self.stop.wait(1) diff --git a/src/network/http-old.py b/src/network/http-old.py deleted file mode 100644 index 56d2491538..0000000000 --- a/src/network/http-old.py +++ /dev/null @@ -1,49 +0,0 @@ -import asyncore -import socket -import time - -requestCount = 0 -parallel = 50 -duration = 60 - - -class HTTPClient(asyncore.dispatcher): - port = 12345 - - def __init__(self, host, path, connect=True): - if not hasattr(self, '_map'): - asyncore.dispatcher.__init__(self) - if connect: - self.create_socket(socket.AF_INET, socket.SOCK_STREAM) - self.connect((host, HTTPClient.port)) - self.buffer = 'GET %s HTTP/1.0\r\n\r\n' % path - - def handle_close(self): - global requestCount - requestCount += 1 - self.close() - - def handle_read(self): -# print self.recv(8192) - self.recv(8192) - - def writable(self): - return (len(self.buffer) > 0) - - def handle_write(self): - sent = self.send(self.buffer) - self.buffer = self.buffer[sent:] - -if __name__ == "__main__": - # initial fill - for i in range(parallel): - HTTPClient('127.0.0.1', '/') - start = time.time() - while (time.time() - start < duration): - if (len(asyncore.socket_map) < parallel): - for i in range(parallel - len(asyncore.socket_map)): - HTTPClient('127.0.0.1', '/') - print "Active connections: %i" % (len(asyncore.socket_map)) - asyncore.loop(count=len(asyncore.socket_map)/2) - if requestCount % 100 == 0: - print "Processed %i total messages" % (requestCount) diff --git a/src/network/http.py b/src/network/http.py index 55cb81a159..d7a938fab3 100644 --- a/src/network/http.py +++ b/src/network/http.py @@ -2,31 +2,35 @@ from advanceddispatcher import AdvancedDispatcher import asyncore_pollchoose as asyncore -from proxy import Proxy, ProxyError, GeneralProxyError -from socks5 import Socks5Connection, Socks5Resolver, Socks5AuthError, Socks5Error -from socks4a import Socks4aConnection, Socks4aResolver, Socks4aError +from proxy import ProxyError +from socks5 import Socks5Connection, Socks5Resolver +from socks4a import Socks4aConnection, Socks4aResolver -class HttpError(ProxyError): pass + +class HttpError(ProxyError): + pass class HttpConnection(AdvancedDispatcher): - def __init__(self, host, path="/"): + def __init__(self, host, path="/"): # pylint: disable=redefined-outer-name AdvancedDispatcher.__init__(self) self.path = path self.destination = (host, 80) self.create_socket(socket.AF_INET, socket.SOCK_STREAM) self.connect(self.destination) - print "connecting in background to %s:%i" % (self.destination[0], self.destination[1]) + print("connecting in background to %s:%i" % self.destination) def state_init(self): - self.append_write_buf("GET %s HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n" % (self.path, self.destination[0])) - print "Sending %ib" % (len(self.write_buf)) + self.append_write_buf( + "GET %s HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n" % ( + self.path, self.destination[0])) + print("Sending %ib" % len(self.write_buf)) self.set_state("http_request_sent", 0) return False def state_http_request_sent(self): - if len(self.read_buf) > 0: - print "Received %ib" % (len(self.read_buf)) + if self.read_buf: + print("Received %ib" % len(self.read_buf)) self.read_buf = b"" if not self.connected: self.set_state("close", 0) @@ -34,7 +38,7 @@ def state_http_request_sent(self): class Socks5HttpConnection(Socks5Connection, HttpConnection): - def __init__(self, host, path="/"): + def __init__(self, host, path="/"): # pylint: disable=super-init-not-called, redefined-outer-name self.path = path Socks5Connection.__init__(self, address=(host, 80)) @@ -44,7 +48,7 @@ def state_socks_handshake_done(self): class Socks4aHttpConnection(Socks4aConnection, HttpConnection): - def __init__(self, host, path="/"): + def __init__(self, host, path="/"): # pylint: disable=super-init-not-called, redefined-outer-name Socks4aConnection.__init__(self, address=(host, 80)) self.path = path @@ -55,32 +59,31 @@ def state_socks_handshake_done(self): if __name__ == "__main__": # initial fill - for host in ("bootstrap8080.bitmessage.org", "bootstrap8444.bitmessage.org"): proxy = Socks5Resolver(host=host) - while len(asyncore.socket_map) > 0: - print "loop %s, len %i" % (proxy.state, len(asyncore.socket_map)) + while asyncore.socket_map: + print("loop %s, len %i" % (proxy.state, len(asyncore.socket_map))) asyncore.loop(timeout=1, count=1) proxy.resolved() proxy = Socks4aResolver(host=host) - while len(asyncore.socket_map) > 0: - print "loop %s, len %i" % (proxy.state, len(asyncore.socket_map)) + while asyncore.socket_map: + print("loop %s, len %i" % (proxy.state, len(asyncore.socket_map))) asyncore.loop(timeout=1, count=1) proxy.resolved() for host in ("bitmessage.org",): direct = HttpConnection(host) - while len(asyncore.socket_map) > 0: -# print "loop, state = %s" % (direct.state) + while asyncore.socket_map: + # print "loop, state = %s" % (direct.state) asyncore.loop(timeout=1, count=1) proxy = Socks5HttpConnection(host) - while len(asyncore.socket_map) > 0: -# print "loop, state = %s" % (proxy.state) + while asyncore.socket_map: + # print "loop, state = %s" % (proxy.state) asyncore.loop(timeout=1, count=1) proxy = Socks4aHttpConnection(host) - while len(asyncore.socket_map) > 0: -# print "loop, state = %s" % (proxy.state) + while asyncore.socket_map: + # print "loop, state = %s" % (proxy.state) asyncore.loop(timeout=1, count=1) diff --git a/src/network/httpd.py b/src/network/httpd.py index b8b6ba21f1..b69ffa990e 100644 --- a/src/network/httpd.py +++ b/src/network/httpd.py @@ -1,28 +1,34 @@ +""" +src/network/httpd.py +======================= +""" import asyncore import socket from tls import TLSHandshake + class HTTPRequestHandler(asyncore.dispatcher): + """Handling HTTP request""" response = """HTTP/1.0 200 OK\r -Date: Sun, 23 Oct 2016 18:02:00 GMT\r -Content-Type: text/html; charset=UTF-8\r -Content-Encoding: UTF-8\r -Content-Length: 136\r -Last-Modified: Wed, 08 Jan 2003 23:11:55 GMT\r -Server: Apache/1.3.3.7 (Unix) (Red-Hat/Linux)\r -ETag: "3f80f-1b6-3e1cb03b"\r -Accept-Ranges: bytes\r -Connection: close\r -\r - - - An Example Page - - - Hello World, this is a very simple HTML document. - -""" + Date: Sun, 23 Oct 2016 18:02:00 GMT\r + Content-Type: text/html; charset=UTF-8\r + Content-Encoding: UTF-8\r + Content-Length: 136\r + Last-Modified: Wed, 08 Jan 2003 23:11:55 GMT\r + Server: Apache/1.3.3.7 (Unix) (Red-Hat/Linux)\r + ETag: "3f80f-1b6-3e1cb03b"\r + Accept-Ranges: bytes\r + Connection: close\r + \r + + + An Example Page + + + Hello World, this is a very simple HTML document. + + """ def __init__(self, sock): if not hasattr(self, '_map'): @@ -62,11 +68,17 @@ def handle_write(self): class HTTPSRequestHandler(HTTPRequestHandler, TLSHandshake): + """Handling HTTPS request""" def __init__(self, sock): if not hasattr(self, '_map'): - asyncore.dispatcher.__init__(self, sock) -# self.tlsDone = False - TLSHandshake.__init__(self, sock=sock, certfile='/home/shurdeek/src/PyBitmessage/src/sslkeys/cert.pem', keyfile='/home/shurdeek/src/PyBitmessage/src/sslkeys/key.pem', server_side=True) + asyncore.dispatcher.__init__(self, sock) # pylint: disable=non-parent-init-called + # self.tlsDone = False + TLSHandshake.__init__( + self, + sock=sock, + certfile='/home/shurdeek/src/PyBitmessage/src/sslkeys/cert.pem', + keyfile='/home/shurdeek/src/PyBitmessage/src/sslkeys/key.pem', + server_side=True) HTTPRequestHandler.__init__(self, sock) def handle_connect(self): @@ -81,8 +93,7 @@ def handle_close(self): def readable(self): if self.tlsDone: return HTTPRequestHandler.readable(self) - else: - return TLSHandshake.readable(self) + return TLSHandshake.readable(self) def handle_read(self): if self.tlsDone: @@ -93,8 +104,7 @@ def handle_read(self): def writable(self): if self.tlsDone: return HTTPRequestHandler.writable(self) - else: - return TLSHandshake.writable(self) + return TLSHandshake.writable(self) def handle_write(self): if self.tlsDone: @@ -104,6 +114,7 @@ def handle_write(self): class HTTPServer(asyncore.dispatcher): + """Handling HTTP Server""" port = 12345 def __init__(self): @@ -119,14 +130,15 @@ def handle_accept(self): pair = self.accept() if pair is not None: sock, addr = pair -# print 'Incoming connection from %s' % repr(addr) + # print 'Incoming connection from %s' % repr(addr) self.connections += 1 -# if self.connections % 1000 == 0: -# print "Processed %i connections, active %i" % (self.connections, len(asyncore.socket_map)) + # if self.connections % 1000 == 0: + # print "Processed %i connections, active %i" % (self.connections, len(asyncore.socket_map)) HTTPRequestHandler(sock) class HTTPSServer(HTTPServer): + """Handling HTTPS Server""" port = 12345 def __init__(self): @@ -137,12 +149,13 @@ def handle_accept(self): pair = self.accept() if pair is not None: sock, addr = pair -# print 'Incoming connection from %s' % repr(addr) + # print 'Incoming connection from %s' % repr(addr) self.connections += 1 -# if self.connections % 1000 == 0: -# print "Processed %i connections, active %i" % (self.connections, len(asyncore.socket_map)) + # if self.connections % 1000 == 0: + # print "Processed %i connections, active %i" % (self.connections, len(asyncore.socket_map)) HTTPSRequestHandler(sock) + if __name__ == "__main__": client = HTTPSServer() asyncore.loop() diff --git a/src/network/https.py b/src/network/https.py index 151efcb88e..a7b8b57ceb 100644 --- a/src/network/https.py +++ b/src/network/https.py @@ -1,10 +1,18 @@ import asyncore from http import HTTPClient -import paths from tls import TLSHandshake -# self.sslSock = ssl.wrap_socket(self.sock, keyfile = os.path.join(paths.codePath(), 'sslkeys', 'key.pem'), certfile = os.path.join(paths.codePath(), 'sslkeys', 'cert.pem'), server_side = not self.initiatedConnection, ssl_version=ssl.PROTOCOL_TLSv1, do_handshake_on_connect=False, ciphers='AECDH-AES256-SHA') +""" +self.sslSock = ssl.wrap_socket( + self.sock, + keyfile=os.path.join(paths.codePath(), 'sslkeys', 'key.pem'), + certfile=os.path.join(paths.codePath(), 'sslkeys', 'cert.pem'), + server_side=not self.initiatedConnection, + ssl_version=ssl.PROTOCOL_TLSv1, + do_handshake_on_connect=False, + ciphers='AECDH-AES256-SHA') +""" class HTTPSClient(HTTPClient, TLSHandshake): @@ -12,7 +20,15 @@ def __init__(self, host, path): if not hasattr(self, '_map'): asyncore.dispatcher.__init__(self) self.tlsDone = False -# TLSHandshake.__init__(self, address=(host, 443), certfile='/home/shurdeek/src/PyBitmessage/sslsrc/keys/cert.pem', keyfile='/home/shurdeek/src/PyBitmessage/src/sslkeys/key.pem', server_side=False, ciphers='AECDH-AES256-SHA') + """ + TLSHandshake.__init__( + self, + address=(host, 443), + certfile='/home/shurdeek/src/PyBitmessage/sslsrc/keys/cert.pem', + keyfile='/home/shurdeek/src/PyBitmessage/src/sslkeys/key.pem', + server_side=False, + ciphers='AECDH-AES256-SHA') + """ HTTPClient.__init__(self, host, path, connect=False) TLSHandshake.__init__(self, address=(host, 443), server_side=False) @@ -49,6 +65,7 @@ def handle_write(self): else: TLSHandshake.handle_write(self) + if __name__ == "__main__": client = HTTPSClient('anarchy.economicsofbitcoin.com', '/') asyncore.loop() diff --git a/src/network/invthread.py b/src/network/invthread.py index d0d758fbfe..503eefa16a 100644 --- a/src/network/invthread.py +++ b/src/network/invthread.py @@ -1,37 +1,57 @@ +""" +Thread to send inv annoucements +""" import Queue -from random import randint, shuffle -import threading +import random from time import time import addresses -from bmconfigparser import BMConfigParser -from helper_threading import StoppableThread -from network.connectionpool import BMConnectionPool -from network.dandelion import Dandelion -from queues import invQueue import protocol import state +import connectionpool +from network import dandelion_ins, invQueue +from threads import StoppableThread -class InvThread(threading.Thread, StoppableThread): - def __init__(self): - threading.Thread.__init__(self, name="InvBroadcaster") - self.initStop() - self.name = "InvBroadcaster" - def handleLocallyGenerated(self, stream, hashId): - Dandelion().addHash(hashId, stream=stream) - for connection in BMConnectionPool().inboundConnections.values() + \ - BMConnectionPool().outboundConnections.values(): - if state.dandelion and connection != Dandelion().objectChildStem(hashId): - continue - connection.objectsNewToThem[hashId] = time() +def handleExpiredDandelion(expired): + """For expired dandelion objects, mark all remotes as not having + the object""" + if not expired: + return + for i in connectionpool.pool.connections(): + if not i.fullyEstablished: + continue + for x in expired: + streamNumber, hashid, _ = x + try: + del i.objectsNewToMe[hashid] + except KeyError: + if streamNumber in i.streams: + with i.objectsNewToThemLock: + i.objectsNewToThem[hashid] = time() - def run(self): - while not state.shutdown: + +class InvThread(StoppableThread): + """Main thread that sends inv annoucements""" + + name = "InvBroadcaster" + + @staticmethod + def handleLocallyGenerated(stream, hashId): + """Locally generated inventory items require special handling""" + dandelion_ins.addHash(hashId, stream=stream) + for connection in connectionpool.pool.connections(): + if dandelion_ins.enabled and connection != \ + dandelion_ins.objectChildStem(hashId): + continue + connection.objectsNewToThem[hashId] = time() + + def run(self): # pylint: disable=too-many-branches + while not state.shutdown: # pylint: disable=too-many-nested-blocks chunk = [] while True: # Dandelion fluff trigger by expiration - Dandelion().expire() + handleExpiredDandelion(dandelion_ins.expire(invQueue)) try: data = invQueue.get(False) chunk.append((data[0], data[1])) @@ -42,8 +62,7 @@ def run(self): break if chunk: - for connection in BMConnectionPool().inboundConnections.values() + \ - BMConnectionPool().outboundConnections.values(): + for connection in connectionpool.pool.connections(): fluffs = [] stems = [] for inv in chunk: @@ -55,10 +74,10 @@ def run(self): except KeyError: continue try: - if connection == Dandelion().objectChildStem(inv[1]): + if connection == dandelion_ins.objectChildStem(inv[1]): # Fluff trigger by RNG # auto-ignore if config set to 0, i.e. dandelion is off - if randint(1, 100) >= state.dandelion: + if random.randint(1, 100) >= dandelion_ins.enabled: # nosec B311 fluffs.append(inv[1]) # send a dinv only if the stem node supports dandelion elif connection.services & protocol.NODE_DANDELION > 0: @@ -69,19 +88,22 @@ def run(self): fluffs.append(inv[1]) if fluffs: - shuffle(fluffs) - connection.append_write_buf(protocol.CreatePacket('inv', \ - addresses.encodeVarint(len(fluffs)) + "".join(fluffs))) + random.shuffle(fluffs) + connection.append_write_buf(protocol.CreatePacket( + 'inv', + addresses.encodeVarint( + len(fluffs)) + ''.join(fluffs))) if stems: - shuffle(stems) - connection.append_write_buf(protocol.CreatePacket('dinv', \ - addresses.encodeVarint(len(stems)) + "".join(stems))) + random.shuffle(stems) + connection.append_write_buf(protocol.CreatePacket( + 'dinv', + addresses.encodeVarint( + len(stems)) + ''.join(stems))) invQueue.iterate() - for i in range(len(chunk)): + for _ in range(len(chunk)): invQueue.task_done() - if Dandelion().refresh < time(): - Dandelion().reRandomiseStems() + dandelion_ins.reRandomiseStems() self.stop.wait(1) diff --git a/src/network/knownnodes.py b/src/network/knownnodes.py new file mode 100644 index 0000000000..c53be2cdd0 --- /dev/null +++ b/src/network/knownnodes.py @@ -0,0 +1,267 @@ +""" +Manipulations with knownNodes dictionary. +""" +# TODO: knownnodes object maybe? +# pylint: disable=global-statement + +import json +import logging +import os +import pickle # nosec B403 +import threading +import time +try: + from collections.abc import Iterable +except ImportError: + from collections import Iterable + +import state +from bmconfigparser import config +from network.node import Peer + +state.Peer = Peer + +knownNodesLock = threading.RLock() +"""Thread lock for knownnodes modification""" +knownNodes = {stream: {} for stream in range(1, 4)} +"""The dict of known nodes for each stream""" + +knownNodesTrimAmount = 2000 +"""trim stream knownnodes dict to this length""" + +knownNodesForgetRating = -0.5 +"""forget a node after rating is this low""" + +knownNodesActual = False + +logger = logging.getLogger('default') + +DEFAULT_NODES = ( + Peer('5.45.99.75', 8444), + Peer('75.167.159.54', 8444), + Peer('95.165.168.168', 8444), + Peer('85.180.139.241', 8444), + Peer('158.222.217.190', 8080), + Peer('178.62.12.187', 8448), + Peer('24.188.198.204', 8111), + Peer('109.147.204.113', 1195), + Peer('178.11.46.221', 8444) +) + + +def json_serialize_knownnodes(output): + """ + Reorganize knownnodes dict and write it as JSON to output + """ + _serialized = [] + for stream, peers in knownNodes.iteritems(): + for peer, info in peers.iteritems(): + info.update(rating=round(info.get('rating', 0), 2)) + _serialized.append({ + 'stream': stream, 'peer': peer._asdict(), 'info': info + }) + json.dump(_serialized, output, indent=4) + + +def json_deserialize_knownnodes(source): + """ + Read JSON from source and make knownnodes dict + """ + global knownNodesActual + for node in json.load(source): + peer = node['peer'] + info = node['info'] + peer = Peer(str(peer['host']), peer.get('port', 8444)) + knownNodes[node['stream']][peer] = info + if not (knownNodesActual + or info.get('self')) and peer not in DEFAULT_NODES: + knownNodesActual = True + + +def pickle_deserialize_old_knownnodes(source): + """ + Unpickle source and reorganize knownnodes dict if it has old format + the old format was {Peer:lastseen, ...} + the new format is {Peer:{"lastseen":i, "rating":f}} + """ + global knownNodes + knownNodes = pickle.load(source) # nosec B301 + for stream in knownNodes.keys(): + for node, params in knownNodes[stream].iteritems(): + if isinstance(params, (float, int)): + addKnownNode(stream, node, params) + + +def saveKnownNodes(dirName=None): + """Save knownnodes to filesystem""" + if dirName is None: + dirName = state.appdata + with knownNodesLock: + with open(os.path.join(dirName, 'knownnodes.dat'), 'wb') as output: + json_serialize_knownnodes(output) + + +def addKnownNode(stream, peer, lastseen=None, is_self=False): + """ + Add a new node to the dict or update lastseen if it already exists. + Do it for each stream number if *stream* is `Iterable`. + Returns True if added a new node. + """ + # pylint: disable=too-many-branches + if isinstance(stream, Iterable): + with knownNodesLock: + for s in stream: + addKnownNode(s, peer, lastseen, is_self) + return + + rating = 0.0 + if not lastseen: + # FIXME: maybe about 28 days? + lastseen = int(time.time()) + else: + lastseen = int(lastseen) + try: + info = knownNodes[stream].get(peer) + if lastseen > info['lastseen']: + info['lastseen'] = lastseen + except (KeyError, TypeError): + pass + else: + return + + if not is_self: + if len(knownNodes[stream]) > config.safeGetInt( + "knownnodes", "maxnodes"): + return + + knownNodes[stream][peer] = { + 'lastseen': lastseen, + 'rating': rating or 1 if is_self else 0, + 'self': is_self, + } + return True + + +def createDefaultKnownNodes(): + """Creating default Knownnodes""" + past = time.time() - 2418600 # 28 days - 10 min + for peer in DEFAULT_NODES: + addKnownNode(1, peer, past) + saveKnownNodes() + + +def readKnownNodes(): + """Load knownnodes from filesystem""" + try: + with open(state.appdata + 'knownnodes.dat', 'rb') as source: + with knownNodesLock: + try: + json_deserialize_knownnodes(source) + except ValueError: + source.seek(0) + pickle_deserialize_old_knownnodes(source) + except (IOError, OSError, KeyError, EOFError): + logger.debug( + 'Failed to read nodes from knownnodes.dat', exc_info=True) + createDefaultKnownNodes() + + # your own onion address, if setup + onionhostname = config.safeGet('bitmessagesettings', 'onionhostname') + if onionhostname and ".onion" in onionhostname: + onionport = config.safeGetInt('bitmessagesettings', 'onionport') + if onionport: + self_peer = Peer(onionhostname, onionport) + addKnownNode(1, self_peer, is_self=True) + state.ownAddresses[self_peer] = True + + +def increaseRating(peer): + """Increase rating of a peer node""" + increaseAmount = 0.1 + maxRating = 1 + with knownNodesLock: + for stream in knownNodes.keys(): + try: + knownNodes[stream][peer]["rating"] = min( + knownNodes[stream][peer]["rating"] + increaseAmount, + maxRating + ) + except KeyError: + pass + + +def decreaseRating(peer): + """Decrease rating of a peer node""" + decreaseAmount = 0.1 + minRating = -1 + with knownNodesLock: + for stream in knownNodes.keys(): + try: + knownNodes[stream][peer]["rating"] = max( + knownNodes[stream][peer]["rating"] - decreaseAmount, + minRating + ) + except KeyError: + pass + + +def trimKnownNodes(recAddrStream=1): + """Triming Knownnodes""" + if len(knownNodes[recAddrStream]) < \ + config.safeGetInt("knownnodes", "maxnodes"): + return + with knownNodesLock: + oldestList = sorted( + knownNodes[recAddrStream], + key=lambda x: x['lastseen'] + )[:knownNodesTrimAmount] + for oldest in oldestList: + del knownNodes[recAddrStream][oldest] + + +def dns(): + """Add DNS names to knownnodes""" + for port in [8080, 8444]: + addKnownNode( + 1, Peer('bootstrap%s.bitmessage.org' % port, port)) + + +def cleanupKnownNodes(pool): + """ + Cleanup knownnodes: remove old nodes and nodes with low rating + """ + global knownNodesActual + now = int(time.time()) + needToWriteKnownNodesToDisk = False + + with knownNodesLock: + for stream in knownNodes: + if stream not in pool.streams: + continue + keys = knownNodes[stream].keys() + for node in keys: + if len(knownNodes[stream]) <= 1: # leave at least one node + if stream == 1: + knownNodesActual = False + break + try: + age = now - knownNodes[stream][node]["lastseen"] + # scrap old nodes (age > 28 days) + if age > 2419200: + needToWriteKnownNodesToDisk = True + del knownNodes[stream][node] + continue + # scrap old nodes (age > 3 hours) with low rating + if (age > 10800 and knownNodes[stream][node]["rating"] + <= knownNodesForgetRating): + needToWriteKnownNodesToDisk = True + del knownNodes[stream][node] + continue + except TypeError: + logger.warning('Error in %s', node) + keys = [] + + # Let us write out the knowNodes to disk + # if there is anything new to write out. + if needToWriteKnownNodesToDisk: + saveKnownNodes() diff --git a/src/multiqueue.py b/src/network/multiqueue.py similarity index 56% rename from src/multiqueue.py rename to src/network/multiqueue.py index be5ce44ffa..3fad4e3423 100644 --- a/src/multiqueue.py +++ b/src/network/multiqueue.py @@ -1,22 +1,30 @@ -from collections import deque -import Queue +""" +A queue with multiple internal subqueues. +Elements are added into a random subqueue, and retrieval rotates +""" import random -import helper_random +from collections import deque + +from six.moves import queue -class MultiQueue(Queue.Queue): + +class MultiQueue(queue.Queue): + """A base queue class""" + # pylint: disable=redefined-builtin,attribute-defined-outside-init defaultQueueCount = 10 + def __init__(self, maxsize=0, count=0): if not count: self.queueCount = MultiQueue.defaultQueueCount else: self.queueCount = count - Queue.Queue.__init__(self, maxsize) + queue.Queue.__init__(self, maxsize) # Initialize the queue representation def _init(self, maxsize): self.iter = 0 self.queues = [] - for i in range(self.queueCount): + for _ in range(self.queueCount): self.queues.append(deque()) def _qsize(self, len=len): @@ -24,15 +32,18 @@ def _qsize(self, len=len): # Put a new item in the queue def _put(self, item): - #self.queue.append(item) - self.queues[helper_random.randomrandrange(self.queueCount)].append((item)) + # self.queue.append(item) + self.queues[random.randrange(self.queueCount)].append( # nosec B311 + (item)) # Get an item from the queue def _get(self): return self.queues[self.iter].popleft() def iterate(self): + """Increment the iteration counter""" self.iter = (self.iter + 1) % self.queueCount def totalSize(self): + """Return the total number of items in all the queues""" return sum(len(x) for x in self.queues) diff --git a/src/network/networkthread.py b/src/network/networkthread.py index 5a709c8b5f..640d47a1f3 100644 --- a/src/network/networkthread.py +++ b/src/network/networkthread.py @@ -1,39 +1,40 @@ -import threading - -from bmconfigparser import BMConfigParser -from debug import logger -from helper_threading import StoppableThread +""" +A thread to handle network concerns +""" import network.asyncore_pollchoose as asyncore -from network.connectionpool import BMConnectionPool -import state +import connectionpool +from queues import excQueue +from threads import StoppableThread + -class BMNetworkThread(threading.Thread, StoppableThread): - def __init__(self): - threading.Thread.__init__(self, name="Asyncore") - self.initStop() - self.name = "Asyncore" - logger.info("init asyncore thread") +class BMNetworkThread(StoppableThread): + """Main network thread""" + name = "Asyncore" def run(self): - while not self._stopped and state.shutdown == 0: - BMConnectionPool().loop() + try: + 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/node.py b/src/network/node.py index ab9f5fbeef..4c532b8195 100644 --- a/src/network/node.py +++ b/src/network/node.py @@ -1,3 +1,7 @@ +""" +Named tuples representing the network peers +""" import collections +Peer = collections.namedtuple('Peer', ['host', 'port']) Node = collections.namedtuple('Node', ['services', 'host', 'port']) diff --git a/src/network/objectracker.py b/src/network/objectracker.py index b511021260..91bb0552ae 100644 --- a/src/network/objectracker.py +++ b/src/network/objectracker.py @@ -1,11 +1,12 @@ +""" +Module for tracking objects +""" import time from threading import RLock -from inventory import Inventory -import network.connectionpool -from network.dandelion import Dandelion +import connectionpool +from network import dandelion_ins from randomtrackingdict import RandomTrackingDict -from state import missingObjects haveBloom = False @@ -24,7 +25,12 @@ # it isn't actually implemented yet so no point in turning it on haveBloom = False +# tracking pending downloads globally, for stats +missingObjects = {} + + class ObjectTracker(object): + """Object tracker mixin""" invCleanPeriod = 300 invInitialCapacity = 50000 invErrorRate = 0.03 @@ -40,37 +46,47 @@ def __init__(self): self.lastCleaned = time.time() def initInvBloom(self): + """Init bloom filter for tracking. WIP.""" if haveBloom: # lock? - self.invBloom = BloomFilter(capacity=ObjectTracker.invInitialCapacity, - error_rate=ObjectTracker.invErrorRate) + self.invBloom = BloomFilter( + capacity=ObjectTracker.invInitialCapacity, + error_rate=ObjectTracker.invErrorRate) def initAddrBloom(self): + """Init bloom filter for tracking addrs, WIP. + This either needs to be moved to addrthread.py or removed.""" if haveBloom: # lock? - self.addrBloom = BloomFilter(capacity=ObjectTracker.invInitialCapacity, - error_rate=ObjectTracker.invErrorRate) + self.addrBloom = BloomFilter( + capacity=ObjectTracker.invInitialCapacity, + error_rate=ObjectTracker.invErrorRate) def clean(self): + """Clean up tracking to prevent memory bloat""" if self.lastCleaned < time.time() - ObjectTracker.invCleanPeriod: if haveBloom: - if len(missingObjects) == 0: + if missingObjects == 0: self.initInvBloom() self.initAddrBloom() else: # release memory deadline = time.time() - ObjectTracker.trackingExpires with self.objectsNewToThemLock: - self.objectsNewToThem = {k: v for k, v in self.objectsNewToThem.iteritems() if v >= deadline} + self.objectsNewToThem = { + k: v + for k, v in self.objectsNewToThem.iteritems() + if v >= deadline} self.lastCleaned = time.time() def hasObj(self, hashid): + """Do we already have object?""" if haveBloom: return hashid in self.invBloom - else: - return hashid in self.objectsNewToMe + return hashid in self.objectsNewToMe def handleReceivedInventory(self, hashId): + """Handling received inventory""" if haveBloom: self.invBloom.add(hashId) try: @@ -83,20 +99,22 @@ def handleReceivedInventory(self, hashId): self.objectsNewToMe[hashId] = True def handleReceivedObject(self, streamNumber, hashid): - for i in network.connectionpool.BMConnectionPool().inboundConnections.values() + network.connectionpool.BMConnectionPool().outboundConnections.values(): + """Handling received object""" + 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): + if streamNumber in i.streams and ( + not dandelion_ins.hasHash(hashid) + or dandelion_ins.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 + # 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) + dandelion_ins.setHashStream(hashid, streamNumber) if i == self: try: @@ -107,23 +125,12 @@ def handleReceivedObject(self, streamNumber, hashid): self.objectsNewToMe.setLastObject() def hasAddr(self, addr): + """WIP, should be moved to addrthread.py or removed""" if haveBloom: return addr in self.invBloom + return None def addAddr(self, hashid): + """WIP, should be moved to addrthread.py or removed""" if haveBloom: self.addrBloom.add(hashid) - -# addr sending -> per node upload queue, and flush every minute or so -# inv sending -> if not in bloom, inv immediately, otherwise put into a per node upload queue and flush every minute or so -# data sending -> a simple queue - -# no bloom -# - if inv arrives -# - if we don't have it, add tracking and download queue -# - if we do have it, remove from tracking -# tracking downloads -# - per node hash of items the node has but we don't -# tracking inv -# - per node hash of items that neither the remote node nor we have -# diff --git a/src/network/proxy.py b/src/network/proxy.py index 1d7ca357cf..ed1af127b2 100644 --- a/src/network/proxy.py +++ b/src/network/proxy.py @@ -1,27 +1,36 @@ +""" +Set proxy if avaiable otherwise exception +""" +# pylint: disable=protected-access +import logging import socket import time -from advanceddispatcher import AdvancedDispatcher import asyncore_pollchoose as asyncore -from bmconfigparser import BMConfigParser -from debug import logger -import network.connectionpool -import state +from advanceddispatcher import AdvancedDispatcher +from bmconfigparser import config +from node import Peer + +logger = logging.getLogger('default') + class ProxyError(Exception): - errorCodes = ("UnknownError") + """Base proxy exception class""" + errorCodes = ("Unknown error",) def __init__(self, code=-1): self.code = code try: - self.message = self.__class__.errorCodes[self.code] + self.message = self.errorCodes[code] except IndexError: - self.message = self.__class__.errorCodes[-1] + self.message = self.errorCodes[-1] super(ProxyError, self).__init__(self.message) class GeneralProxyError(ProxyError): - errorCodes = ("Success", + """General proxy error class (not specfic to an implementation)""" + errorCodes = ( + "Success", "Invalid data", "Not connected", "Not available", @@ -30,12 +39,14 @@ class GeneralProxyError(ProxyError): "Timed out", "Network unreachable", "Connection refused", - "Host unreachable") + "Host unreachable" + ) class Proxy(AdvancedDispatcher): - # these are global, and if you change config during runtime, all active/new - # instances should change too + """Base proxy class""" + # these are global, and if you change config during runtime, + # all active/new instances should change too _proxy = ("127.0.0.1", 9050) _auth = None _onion_proxy = None @@ -44,70 +55,94 @@ class Proxy(AdvancedDispatcher): @property def proxy(self): + """Return proxy IP and port""" return self.__class__._proxy @proxy.setter def proxy(self, address): - if not isinstance(address, tuple) or (len(address) < 2) or \ - (not isinstance(address[0], str) or not isinstance(address[1], int)): + """Set proxy IP and port""" + if (not isinstance(address, tuple) or len(address) < 2 + or not isinstance(address[0], str) + or not isinstance(address[1], int)): raise ValueError self.__class__._proxy = address @property def auth(self): + """Return proxy authentication settings""" return self.__class__._auth @auth.setter def auth(self, authTuple): + """Set proxy authentication (username and password)""" self.__class__._auth = authTuple @property def onion_proxy(self): + """ + Return separate proxy IP and port for use only with onion + addresses. Untested. + """ return self.__class__._onion_proxy @onion_proxy.setter def onion_proxy(self, address): - if address is not None and (not isinstance(address, tuple) or (len(address) < 2) or \ - (not isinstance(address[0], str) or not isinstance(address[1], int))): + """Set onion proxy address""" + if address is not None and ( + not isinstance(address, tuple) or len(address) < 2 + or not isinstance(address[0], str) + or not isinstance(address[1], int) + ): raise ValueError self.__class__._onion_proxy = address @property def onion_auth(self): + """Return proxy authentication settings for onion hosts only""" return self.__class__._onion_auth @onion_auth.setter def onion_auth(self, authTuple): + """Set proxy authentication for onion hosts only. Untested.""" self.__class__._onion_auth = authTuple def __init__(self, address): - if not isinstance(address, state.Peer): + if not isinstance(address, Peer): raise ValueError AdvancedDispatcher.__init__(self) self.destination = address self.isOutbound = True self.fullyEstablished = False self.create_socket(socket.AF_INET, socket.SOCK_STREAM) - if BMConfigParser().safeGetBoolean("bitmessagesettings", "socksauthentication"): - self.auth = (BMConfigParser().safeGet("bitmessagesettings", "socksusername"), - BMConfigParser().safeGet("bitmessagesettings", "sockspassword")) + if config.safeGetBoolean( + "bitmessagesettings", "socksauthentication"): + self.auth = ( + config.safeGet( + "bitmessagesettings", "socksusername"), + config.safeGet( + "bitmessagesettings", "sockspassword")) else: self.auth = None - if address.host.endswith(".onion") and self.onion_proxy is not None: - self.connect(self.onion_proxy) - else: - self.connect(self.proxy) + self.connect( + self.onion_proxy + if address.host.endswith(".onion") and self.onion_proxy else + self.proxy + ) def handle_connect(self): + """Handle connection event (to the proxy)""" self.set_state("init") try: AdvancedDispatcher.handle_connect(self) except socket.error as e: if e.errno in asyncore._DISCONNECTED: - logger.debug("%s:%i: Connection failed: %s", self.destination.host, self.destination.port, str(e)) + logger.debug( + "%s:%i: Connection failed: %s", + self.destination.host, self.destination.port, e) return self.state_init() def state_proxy_handshake_done(self): + """Handshake is complete at this point""" self.connectedAt = time.time() return False diff --git a/src/network/receivequeuethread.py b/src/network/receivequeuethread.py index 0a7562cb44..88d3b740ce 100644 --- a/src/network/receivequeuethread.py +++ b/src/network/receivequeuethread.py @@ -1,61 +1,55 @@ +""" +Process data incoming from network +""" import errno import Queue import socket -import sys -import threading -import time - -import addresses -from bmconfigparser import BMConfigParser -from debug import logger -from helper_threading import StoppableThread -from inventory import Inventory -from network.connectionpool import BMConnectionPool -from network.bmproto import BMProto + +import connectionpool from network.advanceddispatcher import UnknownStateError -from queues import receiveDataQueue -import protocol -import state +from network import receiveDataQueue +from threads import StoppableThread + -class ReceiveQueueThread(threading.Thread, StoppableThread): +class ReceiveQueueThread(StoppableThread): + """This thread processes data received from the network + (which is done by the asyncore thread)""" def __init__(self, num=0): - threading.Thread.__init__(self, name="ReceiveQueue_%i" %(num)) - self.initStop() - self.name = "ReceiveQueue_%i" % (num) - logger.info("init receive queue thread %i", num) + 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 - # methods should return False if there isn't enough data, or the connection is to be aborted - - # state_* methods should return False if there isn't enough data, + # methods should return False if there isn't enough data, # or the connection is to be aborted + # state_* methods should return False if there isn't + # enough data, or the connection is to be aborted + try: - connection = BMConnectionPool().getConnectionByAddr(dest) - # KeyError = connection object not found + connection = connectionpool.pool.getConnectionByAddr(dest) + # connection object not found except KeyError: receiveDataQueue.task_done() continue try: connection.process() - # UnknownStateError = state isn't implemented - except (UnknownStateError): + # state isn't implemented + except UnknownStateError: pass except socket.error as err: if err.errno == errno.EBADF: connection.set_state("close", 0) else: - logger.error("Socket error: %s", str(err)) - except: - logger.error("Error processing", exc_info=True) + self.logger.error('Socket error: %s', err) + except: # noqa:E722 + self.logger.error('Error processing', exc_info=True) receiveDataQueue.task_done() diff --git a/src/network/socks4a.py b/src/network/socks4a.py index 978ede0405..e978616840 100644 --- a/src/network/socks4a.py +++ b/src/network/socks4a.py @@ -1,27 +1,43 @@ +""" +SOCKS4a proxy module +""" +# pylint: disable=attribute-defined-outside-init +import logging import socket import struct -from proxy import Proxy, ProxyError, GeneralProxyError +from proxy import GeneralProxyError, Proxy, ProxyError + +logger = logging.getLogger('default') + class Socks4aError(ProxyError): - errorCodes = ("Request granted", + """SOCKS4a error base class""" + errorCodes = ( + "Request granted", "Request rejected or failed", - "Request rejected because SOCKS server cannot connect to identd on the client", - "Request rejected because the client program and identd report different user-ids", - "Unknown error") + "Request rejected because SOCKS server cannot connect to identd" + " on the client", + "Request rejected because the client program and identd report" + " different user-ids", + "Unknown error" + ) class Socks4a(Proxy): + """SOCKS4a proxy class""" def __init__(self, address=None): Proxy.__init__(self, address) self.ipaddr = None self.destport = address[1] def state_init(self): + """Protocol initialisation (before connection is established)""" self.set_state("auth_done", 0) return True def state_pre_connect(self): + """Handle feedback from SOCKS4a while it is connecting on our behalf""" # Get the response if self.read_buf[0:1] != chr(0x00).encode(): # bad data @@ -40,24 +56,32 @@ def state_pre_connect(self): self.boundaddr = self.read_buf[4:] self.__proxysockname = (self.boundaddr, self.boundport) if self.ipaddr: - self.__proxypeername = (socket.inet_ntoa(self.ipaddr), self.destination[1]) + self.__proxypeername = ( + socket.inet_ntoa(self.ipaddr), self.destination[1]) else: self.__proxypeername = (self.destination[0], self.destport) self.set_state("proxy_handshake_done", length=8) return True def proxy_sock_name(self): - return socket.inet_ntoa(self.__proxysockname[0]) + """ + Handle return value when using SOCKS4a for DNS resolving + instead of connecting. + """ + return socket.inet_ntoa(self.__proxysockname[0]) class Socks4aConnection(Socks4a): + """Child SOCKS4a class used for making outbound connections.""" def __init__(self, address): Socks4a.__init__(self, address=address) def state_auth_done(self): + """Request connection to be made""" # Now we can request the actual connection rmtrslv = False - self.append_write_buf(struct.pack('>BBH', 0x04, 0x01, self.destination[1])) + self.append_write_buf( + struct.pack('>BBH', 0x04, 0x01, self.destination[1])) # If the given destination address is an IP address, we'll # use the IPv4 address request even if remote resolving was specified. try: @@ -65,14 +89,16 @@ def state_auth_done(self): self.append_write_buf(self.ipaddr) except socket.error: # Well it's not an IP number, so it's probably a DNS name. - if Proxy._remote_dns: + if self._remote_dns: # Resolve remotely rmtrslv = True self.ipaddr = None - self.append_write_buf(struct.pack("BBBB", 0x00, 0x00, 0x00, 0x01)) + self.append_write_buf( + struct.pack("BBBB", 0x00, 0x00, 0x00, 0x01)) else: # Resolve locally - self.ipaddr = socket.inet_aton(socket.gethostbyname(self.destination[0])) + self.ipaddr = socket.inet_aton( + socket.gethostbyname(self.destination[0])) self.append_write_buf(self.ipaddr) if self._auth: self.append_write_buf(self._auth[0]) @@ -83,6 +109,7 @@ def state_auth_done(self): return True def state_pre_connect(self): + """Tell SOCKS4a to initiate a connection""" try: return Socks4a.state_pre_connect(self) except Socks4aError as e: @@ -91,14 +118,17 @@ def state_pre_connect(self): class Socks4aResolver(Socks4a): + """DNS resolver class using SOCKS4a""" def __init__(self, host): self.host = host self.port = 8444 Socks4a.__init__(self, address=(self.host, self.port)) def state_auth_done(self): + """Request connection to be made""" # Now we can request the actual connection - self.append_write_buf(struct.pack('>BBH', 0x04, 0xF0, self.destination[1])) + self.append_write_buf( + struct.pack('>BBH', 0x04, 0xF0, self.destination[1])) self.append_write_buf(struct.pack("BBBB", 0x00, 0x00, 0x00, 0x01)) if self._auth: self.append_write_buf(self._auth[0]) @@ -108,4 +138,10 @@ def state_auth_done(self): return True def resolved(self): - print "Resolved %s as %s" % (self.host, self.proxy_sock_name()) + """ + Resolving is done, process the return value. To use this within + PyBitmessage, a callback needs to be implemented which hasn't + been done yet. + """ + logger.debug( + 'Resolved %s as %s', self.host, self.proxy_sock_name()) diff --git a/src/network/socks5.py b/src/network/socks5.py index 52136bdcfd..d1daae4226 100644 --- a/src/network/socks5.py +++ b/src/network/socks5.py @@ -1,18 +1,33 @@ +""" +SOCKS5 proxy module +""" +# pylint: disable=attribute-defined-outside-init + +import logging import socket import struct -from proxy import Proxy, ProxyError, GeneralProxyError +from node import Peer +from proxy import GeneralProxyError, Proxy, ProxyError + +logger = logging.getLogger('default') + class Socks5AuthError(ProxyError): - errorCodes = ("Succeeded", + """Rised when the socks5 protocol encounters an authentication error""" + errorCodes = ( + "Succeeded", "Authentication is required", "All offered authentication methods were rejected", "Unknown username or invalid password", - "Unknown error") + "Unknown error" + ) class Socks5Error(ProxyError): - errorCodes = ("Succeeded", + """Rised when socks5 protocol encounters an error""" + errorCodes = ( + "Succeeded", "General SOCKS server failure", "Connection not allowed by ruleset", "Network unreachable", @@ -21,16 +36,19 @@ class Socks5Error(ProxyError): "TTL expired", "Command not supported", "Address type not supported", - "Unknown error") + "Unknown error" + ) class Socks5(Proxy): + """A socks5 proxy base class""" def __init__(self, address=None): Proxy.__init__(self, address) self.ipaddr = None self.destport = address[1] def state_init(self): + """Protocol initialization (before connection is established)""" if self._auth: self.append_write_buf(struct.pack('BBBB', 0x05, 0x02, 0x00, 0x02)) else: @@ -39,6 +57,7 @@ def state_init(self): return True def state_auth_1(self): + """Perform authentication if peer is requesting it.""" ret = struct.unpack('BB', self.read_buf[:2]) if ret[0] != 5: # general error @@ -48,9 +67,10 @@ def state_auth_1(self): self.set_state("auth_done", length=2) elif ret[1] == 2: # username/password - self.append_write_buf(struct.pack('BB', 1, len(self._auth[0])) + \ - self._auth[0] + struct.pack('B', len(self._auth[1])) + \ - self._auth[1]) + self.append_write_buf( + struct.pack( + 'BB', 1, len(self._auth[0])) + self._auth[0] + struct.pack( + 'B', len(self._auth[1])) + self._auth[1]) self.set_state("auth_needed", length=2, expectBytes=2) else: if ret[1] == 0xff: @@ -62,6 +82,7 @@ def state_auth_1(self): return True def state_auth_needed(self): + """Handle response to authentication attempt""" ret = struct.unpack('BB', self.read_buf[0:2]) if ret[0] != 1: # general error @@ -74,6 +95,7 @@ def state_auth_needed(self): return True def state_pre_connect(self): + """Handle feedback from socks5 while it is connecting on our behalf.""" # Get the response if self.read_buf[0:1] != chr(0x05).encode(): self.close() @@ -81,7 +103,7 @@ def state_pre_connect(self): elif self.read_buf[1:2] != chr(0x00).encode(): # Connection failed self.close() - if ord(self.read_buf[1:2])<=8: + if ord(self.read_buf[1:2]) <= 8: raise Socks5Error(ord(self.read_buf[1:2])) else: raise Socks5Error(9) @@ -96,39 +118,53 @@ def state_pre_connect(self): return True def state_proxy_addr_1(self): + """Handle IPv4 address returned for peer""" self.boundaddr = self.read_buf[0:4] self.set_state("proxy_port", length=4, expectBytes=2) return True def state_proxy_addr_2_1(self): + """ + Handle other addresses than IPv4 returned for peer + (e.g. IPv6, onion, ...). This is part 1 which retrieves the + length of the data. + """ self.address_length = ord(self.read_buf[0:1]) - self.set_state("proxy_addr_2_2", length=1, expectBytes=self.address_length) + self.set_state( + "proxy_addr_2_2", length=1, expectBytes=self.address_length) return True def state_proxy_addr_2_2(self): + """ + Handle other addresses than IPv4 returned for peer + (e.g. IPv6, onion, ...). This is part 2 which retrieves the data. + """ self.boundaddr = self.read_buf[0:self.address_length] self.set_state("proxy_port", length=self.address_length, expectBytes=2) return True def state_proxy_port(self): + """Handle peer's port being returned.""" self.boundport = struct.unpack(">H", self.read_buf[0:2])[0] self.__proxysockname = (self.boundaddr, self.boundport) if self.ipaddr is not None: - self.__proxypeername = (socket.inet_ntoa(self.ipaddr), self.destination[1]) + self.__proxypeername = ( + socket.inet_ntoa(self.ipaddr), self.destination[1]) else: self.__proxypeername = (self.destination[0], self.destport) self.set_state("proxy_handshake_done", length=2) return True def proxy_sock_name(self): + """Handle return value when using SOCKS5 + for DNS resolving instead of connecting.""" return socket.inet_ntoa(self.__proxysockname[0]) class Socks5Connection(Socks5): - def __init__(self, address): - Socks5.__init__(self, address=address) - + """Child socks5 class used for making outbound connections.""" def state_auth_done(self): + """Request connection to be made""" # Now we can request the actual connection self.append_write_buf(struct.pack('BBB', 0x05, 0x01, 0x00)) # If the given destination address is an IP address, we'll @@ -136,21 +172,24 @@ def state_auth_done(self): try: self.ipaddr = socket.inet_aton(self.destination[0]) self.append_write_buf(chr(0x01).encode() + self.ipaddr) - except socket.error: + except socket.error: # may be IPv6! # Well it's not an IP number, so it's probably a DNS name. - if Proxy._remote_dns: + if self._remote_dns: # Resolve remotely self.ipaddr = None - self.append_write_buf(chr(0x03).encode() + chr(len(self.destination[0])).encode() + self.destination[0]) + self.append_write_buf(chr(0x03).encode() + chr( + len(self.destination[0])).encode() + self.destination[0]) else: # Resolve locally - self.ipaddr = socket.inet_aton(socket.gethostbyname(self.destination[0])) + self.ipaddr = socket.inet_aton( + socket.gethostbyname(self.destination[0])) self.append_write_buf(chr(0x01).encode() + self.ipaddr) self.append_write_buf(struct.pack(">H", self.destination[1])) self.set_state("pre_connect", length=0, expectBytes=4) return True def state_pre_connect(self): + """Tell socks5 to initiate a connection""" try: return Socks5.state_pre_connect(self) except Socks5Error as e: @@ -159,18 +198,27 @@ def state_pre_connect(self): class Socks5Resolver(Socks5): + """DNS resolver class using socks5""" def __init__(self, host): self.host = host self.port = 8444 - Socks5.__init__(self, address=(self.host, self.port)) + Socks5.__init__(self, address=Peer(self.host, self.port)) def state_auth_done(self): + """Perform resolving""" # Now we can request the actual connection self.append_write_buf(struct.pack('BBB', 0x05, 0xF0, 0x00)) - self.append_write_buf(chr(0x03).encode() + chr(len(self.host)).encode() + str(self.host)) + self.append_write_buf(chr(0x03).encode() + chr( + len(self.host)).encode() + str(self.host)) self.append_write_buf(struct.pack(">H", self.port)) self.set_state("pre_connect", length=0, expectBytes=4) return True def resolved(self): - print "Resolved %s as %s" % (self.host, self.proxy_sock_name()) + """ + Resolving is done, process the return value. + To use this within PyBitmessage, a callback needs to be + implemented which hasn't been done yet. + """ + logger.debug( + 'Resolved %s as %s', self.host, self.proxy_sock_name()) diff --git a/src/network/stats.py b/src/network/stats.py index 80925f7c9c..0ab1ae0fd4 100644 --- a/src/network/stats.py +++ b/src/network/stats.py @@ -1,8 +1,12 @@ +""" +Network statistics +""" import time -from network.connectionpool import BMConnectionPool import asyncore_pollchoose as asyncore -from state import missingObjects +import connectionpool +from objectracker import missingObjects + lastReceivedTimestamp = time.time() lastReceivedBytes = 0 @@ -11,60 +15,64 @@ lastSentBytes = 0 currentSentSpeed = 0 + def connectedHostsList(): - retval = [] - for i in BMConnectionPool().inboundConnections.values() + \ - BMConnectionPool().outboundConnections.values(): - if not i.fullyEstablished: - continue - try: - retval.append(i) - except AttributeError: - pass - return retval + """List of all the connected hosts""" + return connectionpool.pool.establishedConnections() + def sentBytes(): + """Sending Bytes""" return asyncore.sentBytes + def uploadSpeed(): + """Getting upload speed""" + # pylint: disable=global-statement global lastSentTimestamp, lastSentBytes, currentSentSpeed currentTimestamp = time.time() if int(lastSentTimestamp) < int(currentTimestamp): currentSentBytes = asyncore.sentBytes - currentSentSpeed = int((currentSentBytes - lastSentBytes) / (currentTimestamp - lastSentTimestamp)) + currentSentSpeed = int( + (currentSentBytes - lastSentBytes) / ( + currentTimestamp - lastSentTimestamp)) lastSentBytes = currentSentBytes lastSentTimestamp = currentTimestamp return currentSentSpeed + def receivedBytes(): + """Receiving Bytes""" return asyncore.receivedBytes + def downloadSpeed(): + """Getting download speed""" + # pylint: disable=global-statement global lastReceivedTimestamp, lastReceivedBytes, currentReceivedSpeed currentTimestamp = time.time() if int(lastReceivedTimestamp) < int(currentTimestamp): currentReceivedBytes = asyncore.receivedBytes - currentReceivedSpeed = int((currentReceivedBytes - lastReceivedBytes) / - (currentTimestamp - lastReceivedTimestamp)) + currentReceivedSpeed = int( + (currentReceivedBytes - lastReceivedBytes) / ( + currentTimestamp - lastReceivedTimestamp)) lastReceivedBytes = currentReceivedBytes lastReceivedTimestamp = currentTimestamp return currentReceivedSpeed + def pendingDownload(): + """Getting pending downloads""" return len(missingObjects) - #tmp = {} - #for connection in BMConnectionPool().inboundConnections.values() + \ - # BMConnectionPool().outboundConnections.values(): - # for k in connection.objectsNewToMe.keys(): - # tmp[k] = True - #return len(tmp) + def pendingUpload(): - #tmp = {} - #for connection in BMConnectionPool().inboundConnections.values() + \ - # BMConnectionPool().outboundConnections.values(): - # for k in connection.objectsNewToThem.keys(): - # tmp[k] = True - #This probably isn't the correct logic so it's disabled - #return len(tmp) + """Getting pending uploads""" + # tmp = {} + # 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 + # return len(tmp) return 0 diff --git a/src/network/tcp.py b/src/network/tcp.py index 163cbd8521..e8df5b7285 100644 --- a/src/network/tcp.py +++ b/src/network/tcp.py @@ -1,84 +1,114 @@ -import base64 -from binascii import hexlify -import hashlib +""" +TCP protocol handler +""" +# pylint: disable=too-many-ancestors + +import logging import math -import time -from pprint import pprint -import socket -import struct import random -import traceback +import socket +import time -from addresses import calculateInventoryHash -from debug import logger -from helper_random import randomBytes -import helper_random -from inventory import Inventory +# magic imports! +import addresses +import l10n +import protocol +import state +import connectionpool +from bmconfigparser import config +from highlevelcrypto import randomBytes +from network import dandelion_ins, invQueue, receiveDataQueue +from queues import UISignalQueue +from tr import _translate + +import asyncore_pollchoose as asyncore import knownnodes from network.advanceddispatcher import AdvancedDispatcher -from network.bmproto import BMProtoError, BMProtoInsufficientDataError, BMProtoExcessiveDataError, BMProto -from network.bmobject import BMObject, BMObjectInsufficientPOWError, BMObjectInvalidDataError, BMObjectExpiredError, BMObjectUnwantedStreamError, BMObjectInvalidError, BMObjectAlreadyHaveError -import network.connectionpool -from network.dandelion import Dandelion -from network.node import Node -import network.asyncore_pollchoose as asyncore -from network.proxy import Proxy, ProxyError, GeneralProxyError +from network.bmproto import BMProto from network.objectracker import ObjectTracker -from network.socks5 import Socks5Connection, Socks5Resolver, Socks5AuthError, Socks5Error -from network.socks4a import Socks4aConnection, Socks4aResolver, Socks4aError +from network.socks4a import Socks4aConnection +from network.socks5 import Socks5Connection from network.tls import TLSDispatcher +from node import Peer + + +logger = logging.getLogger('default') + + +maximumAgeOfNodesThatIAdvertiseToOthers = 10800 #: Equals three hours +maximumTimeOffsetWrongCount = 3 #: Connections with wrong time offset -import addresses -from bmconfigparser import BMConfigParser -from queues import invQueue, objectProcessorQueue, portCheckerQueue, UISignalQueue, receiveDataQueue -import shared -import state -import protocol class TCPConnection(BMProto, TLSDispatcher): + # pylint: disable=too-many-instance-attributes + """ + .. todo:: Look to understand and/or fix the non-parent-init-called + """ + def __init__(self, address=None, sock=None): BMProto.__init__(self, address=address, sock=sock) self.verackReceived = False self.verackSent = False self.streams = [0] self.fullyEstablished = False - self.connectedAt = 0 self.skipUntil = 0 if address is None and sock is not None: - self.destination = state.Peer(sock.getpeername()[0], sock.getpeername()[1]) + self.destination = Peer(*sock.getpeername()) self.isOutbound = False TLSDispatcher.__init__(self, sock, server_side=True) self.connectedAt = time.time() - logger.debug("Received connection from %s:%i", self.destination.host, self.destination.port) + logger.debug( + 'Received connection from %s:%i', + self.destination.host, self.destination.port) self.nodeid = randomBytes(8) elif address is not None and sock is not None: TLSDispatcher.__init__(self, sock, server_side=False) self.isOutbound = True - logger.debug("Outbound proxy connection to %s:%i", self.destination.host, self.destination.port) + logger.debug( + 'Outbound proxy connection to %s:%i', + self.destination.host, self.destination.port) else: self.destination = address self.isOutbound = True - if ":" in address.host: - self.create_socket(socket.AF_INET6, socket.SOCK_STREAM) - else: - self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + self.create_socket( + socket.AF_INET6 if ":" in address.host else socket.AF_INET, + socket.SOCK_STREAM) self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) TLSDispatcher.__init__(self, sock, server_side=False) self.connect(self.destination) - logger.debug("Connecting to %s:%i", self.destination.host, self.destination.port) - encodedAddr = protocol.encodeHost(self.destination.host) - if protocol.checkIPAddress(encodedAddr, True) and not protocol.checkSocksIP(self.destination.host): - self.local = True - else: - self.local = False - #shared.connectedHostsList[self.destination] = 0 - ObjectTracker.__init__(self) + logger.debug( + 'Connecting to %s:%i', + self.destination.host, self.destination.port) + try: + self.local = ( + protocol.checkIPAddress( + protocol.encodeHost(self.destination.host), True) + and not protocol.checkSocksIP(self.destination.host) + ) + except socket.error: + # it's probably a hostname + pass + self.network_group = protocol.network_group(self.destination.host) + ObjectTracker.__init__(self) # pylint: disable=non-parent-init-called self.bm_proto_reset() self.set_state("bm_header", expectBytes=protocol.Header.size) - def antiIntersectionDelay(self, initial = False): - # estimated time for a small object to propagate across the whole network - delay = math.ceil(math.log(max(len(knownnodes.knownNodes[x]) for x in knownnodes.knownNodes) + 2, 20)) * (0.2 + invQueue.queueCount/2.0) + def antiIntersectionDelay(self, initial=False): + """ + This is a defense against the so called intersection attacks. + + It is called when you notice peer is requesting non-existing + objects, or right after the connection is established. It will + estimate how long an object will take to propagate across the + network, and skip processing "getdata" requests until then. This + means an attacker only has one shot per IP to perform the attack. + """ + # estimated time for a small object to propagate across the + # whole network + max_known_nodes = max( + len(knownnodes.knownNodes[x]) for x in knownnodes.knownNodes) + delay = math.ceil(math.log(max_known_nodes + 2, 20)) * ( + 0.2 + invQueue.queueCount / 2.0) # take the stream with maximum amount of nodes # +2 is to avoid problems with log(0) and log(1) # 20 is avg connected nodes count @@ -87,100 +117,135 @@ def antiIntersectionDelay(self, initial = False): if initial: self.skipUntil = self.connectedAt + delay if self.skipUntil > time.time(): - logger.debug("Initial skipping processing getdata for %.2fs", self.skipUntil - time.time()) + logger.debug( + 'Initial skipping processing getdata for %.2fs', + self.skipUntil - time.time()) else: - logger.debug("Skipping processing getdata due to missing object for %.2fs", delay) + logger.debug( + 'Skipping processing getdata due to missing object' + ' for %.2fs', delay) self.skipUntil = time.time() + delay + def checkTimeOffsetNotification(self): + """ + Check if we have connected to too many nodes which have too high + time offset from us + """ + if BMProto.timeOffsetWrongCount > \ + maximumTimeOffsetWrongCount and \ + not self.fullyEstablished: + UISignalQueue.put(( + 'updateStatusBar', + _translate( + "MainWindow", + "The time on your computer, %1, may be wrong. " + "Please verify your settings." + ).arg(l10n.formatTimestamp()))) + def state_connection_fully_established(self): + """ + State after the bitmessage protocol handshake is completed + (version/verack exchange, and if both side support TLS, + the TLS handshake as well). + """ self.set_connection_fully_established() self.set_state("bm_header") self.bm_proto_reset() return True def set_connection_fully_established(self): + """Initiate inventory synchronisation.""" if not self.isOutbound and not self.local: - shared.clientHasReceivedIncomingConnections = True + state.clientHasReceivedIncomingConnections = True UISignalQueue.put(('setStatusIcon', 'green')) - UISignalQueue.put(('updateNetworkStatusTab', (self.isOutbound, True, self.destination))) + UISignalQueue.put(( + 'updateNetworkStatusTab', (self.isOutbound, True, self.destination) + )) self.antiIntersectionDelay(True) self.fullyEstablished = True - if self.isOutbound: + # The connection having host suitable for knownnodes + if self.isOutbound or not self.local and not state.socksIP: knownnodes.increaseRating(self.destination) - if self.isOutbound: - Dandelion().maybeAddStem(self) + knownnodes.addKnownNode( + self.streams, self.destination, time.time()) + dandelion_ins.maybeAddStem(self, invQueue) self.sendAddr() self.sendBigInv() def sendAddr(self): + """Send a partial list of known addresses to peer.""" # 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("bitmessagesettings", "maxaddrperstreamsend", 500) - - # init - addressCount = 0 - payload = b'' + maxAddrCount = config.safeGetInt( + "bitmessagesettings", "maxaddrperstreamsend", 500) templist = [] addrs = {} for stream in self.streams: with knownnodes.knownNodesLock: - if len(knownnodes.knownNodes[stream]) > 0: - filtered = {k: v for k, v in knownnodes.knownNodes[stream].items() - if v["lastseen"] > (int(time.time()) - shared.maximumAgeOfNodesThatIAdvertiseToOthers)} - elemCount = len(filtered) - if elemCount > maxAddrCount: - elemCount = maxAddrCount + for n, s in enumerate((stream, stream * 2, stream * 2 + 1)): + nodes = knownnodes.knownNodes.get(s) + if not nodes: + continue # only if more recent than 3 hours - addrs[stream] = helper_random.randomsample(filtered.items(), elemCount) - # sent 250 only if the remote isn't interested in it - if len(knownnodes.knownNodes[stream * 2]) > 0 and stream not in self.streams: - filtered = {k: v for k, v in knownnodes.knownNodes[stream*2].items() - if v["lastseen"] > (int(time.time()) - shared.maximumAgeOfNodesThatIAdvertiseToOthers)} - elemCount = len(filtered) - if elemCount > maxAddrCount / 2: - elemCount = int(maxAddrCount / 2) - addrs[stream * 2] = helper_random.randomsample(filtered.items(), elemCount) - if len(knownnodes.knownNodes[(stream * 2) + 1]) > 0 and stream not in self.streams: - filtered = {k: v for k, v in knownnodes.knownNodes[stream*2+1].items() - if v["lastseen"] > (int(time.time()) - shared.maximumAgeOfNodesThatIAdvertiseToOthers)} - elemCount = len(filtered) - if elemCount > maxAddrCount / 2: - elemCount = int(maxAddrCount / 2) - addrs[stream * 2 + 1] = helper_random.randomsample(filtered.items(), elemCount) - for substream in addrs.keys(): + # and having positive or neutral rating + filtered = [ + (k, v) for k, v in nodes.iteritems() + if v["lastseen"] > int(time.time()) + - maximumAgeOfNodesThatIAdvertiseToOthers + and v["rating"] >= 0 and not k.host.endswith('.onion') + ] + # sent 250 only if the remote isn't interested in it + elemCount = min( + len(filtered), + maxAddrCount / 2 if n else maxAddrCount) + addrs[s] = random.sample(filtered, + elemCount) # nosec B311 + for substream in addrs: for peer, params in addrs[substream]: templist.append((substream, peer, params["lastseen"])) - if len(templist) > 0: - self.append_write_buf(BMProto.assembleAddr(templist)) + if templist: + self.append_write_buf(protocol.assembleAddrMessage(templist)) def sendBigInv(self): + """ + Send hashes of all inventory objects, chunked as the protocol has + a per-command limit. + """ def sendChunk(): + """Send one chunk of inv entries in one command""" if objectCount == 0: return - logger.debug('Sending huge inv message with %i objects to just this one peer', objectCount) - self.append_write_buf(protocol.CreatePacket('inv', addresses.encodeVarint(objectCount) + payload)) + logger.debug( + 'Sending huge inv message with %i objects to just this' + ' one peer', objectCount) + self.append_write_buf(protocol.CreatePacket( + 'inv', addresses.encodeVarint(objectCount) + payload)) # Select all hashes for objects in this stream. bigInvList = {} for stream in self.streams: - # may lock for a long time, but I think it's better than thousands of small locks + # 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 dandelion_ins.hasHash(objHash): continue bigInvList[objHash] = 0 - #self.objectsNewToThem[objHash] = time.time() objectCount = 0 payload = b'' - # Now let us start appending all of these hashes together. They will be - # sent out in a big inv message to our new peer. - for hash, storedValue in bigInvList.items(): - payload += hash + # Now let us start appending all of these hashes together. + # They will be sent out in a big inv message to our new peer. + for obj_hash, _ in bigInvList.items(): + payload += obj_hash objectCount += 1 - if objectCount >= BMProto.maxObjectCount: + + # 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 >= protocol.MAX_OBJECT_COUNT - 1: sendChunk() payload = b'' objectCount = 0 @@ -189,74 +254,142 @@ def sendChunk(): sendChunk() def handle_connect(self): + """Callback for TCP connection being established.""" try: AdvancedDispatcher.handle_connect(self) except socket.error as e: + # pylint: disable=protected-access if e.errno in asyncore._DISCONNECTED: - logger.debug("%s:%i: Connection failed: %s" % (self.destination.host, self.destination.port, str(e))) + logger.debug( + '%s:%i: Connection failed: %s', + self.destination.host, self.destination.port, e) return self.nodeid = randomBytes(8) - self.append_write_buf(protocol.assembleVersionMessage(self.destination.host, self.destination.port, \ - network.connectionpool.BMConnectionPool().streams, False, nodeid=self.nodeid)) - #print "%s:%i: Sending version" % (self.destination.host, self.destination.port) + self.append_write_buf( + protocol.assembleVersionMessage( + self.destination.host, self.destination.port, + connectionpool.pool.streams, dandelion_ins.enabled, + False, nodeid=self.nodeid)) self.connectedAt = time.time() receiveDataQueue.put(self.destination) def handle_read(self): + """Callback for reading from a socket""" TLSDispatcher.handle_read(self) - if self.isOutbound and self.fullyEstablished: - for s in self.streams: - try: - with knownnodes.knownNodesLock: - knownnodes.knownNodes[s][self.destination]["lastseen"] = time.time() - except KeyError: - pass receiveDataQueue.put(self.destination) def handle_write(self): + """Callback for writing to a socket""" TLSDispatcher.handle_write(self) def handle_close(self): - if self.isOutbound and not self.fullyEstablished: - knownnodes.decreaseRating(self.destination) + """Callback for connection being closed.""" + host_is_global = self.isOutbound or not self.local and not state.socksIP if self.fullyEstablished: - UISignalQueue.put(('updateNetworkStatusTab', (self.isOutbound, False, self.destination))) - if self.isOutbound: - Dandelion().maybeRemoveStem(self) + UISignalQueue.put(( + 'updateNetworkStatusTab', + (self.isOutbound, False, self.destination) + )) + if host_is_global: + knownnodes.addKnownNode( + self.streams, self.destination, time.time()) + dandelion_ins.maybeRemoveStem(self) + else: + self.checkTimeOffsetNotification() + if host_is_global: + knownnodes.decreaseRating(self.destination) BMProto.handle_close(self) class Socks5BMConnection(Socks5Connection, TCPConnection): + """SOCKS5 wrapper for TCP connections""" + def __init__(self, address): Socks5Connection.__init__(self, address=address) TCPConnection.__init__(self, address=address, sock=self.socket) self.set_state("init") def state_proxy_handshake_done(self): + """ + State when SOCKS5 connection succeeds, we need to send a + Bitmessage handshake to peer. + """ Socks5Connection.state_proxy_handshake_done(self) self.nodeid = randomBytes(8) - self.append_write_buf(protocol.assembleVersionMessage(self.destination.host, self.destination.port, \ - network.connectionpool.BMConnectionPool().streams, False, nodeid=self.nodeid)) + self.append_write_buf( + protocol.assembleVersionMessage( + self.destination.host, self.destination.port, + connectionpool.pool.streams, dandelion_ins.enabled, + False, nodeid=self.nodeid)) self.set_state("bm_header", expectBytes=protocol.Header.size) return True class Socks4aBMConnection(Socks4aConnection, TCPConnection): + """SOCKS4a wrapper for TCP connections""" + def __init__(self, address): Socks4aConnection.__init__(self, address=address) TCPConnection.__init__(self, address=address, sock=self.socket) self.set_state("init") def state_proxy_handshake_done(self): + """ + State when SOCKS4a connection succeeds, we need to send a + Bitmessage handshake to peer. + """ Socks4aConnection.state_proxy_handshake_done(self) self.nodeid = randomBytes(8) - self.append_write_buf(protocol.assembleVersionMessage(self.destination.host, self.destination.port, \ - network.connectionpool.BMConnectionPool().streams, False, nodeid=self.nodeid)) + self.append_write_buf( + protocol.assembleVersionMessage( + self.destination.host, self.destination.port, + connectionpool.pool.streams, dandelion_ins.enabled, + False, nodeid=self.nodeid)) self.set_state("bm_header", expectBytes=protocol.Header.size) return True +def bootstrap(connection_class): + """Make bootstrapper class for connection type (connection_class)""" + class Bootstrapper(connection_class): + """Base class for bootstrappers""" + _connection_base = connection_class + + def __init__(self, host, port): + self._connection_base.__init__(self, Peer(host, port)) + self.close_reason = self._succeed = False + + def bm_command_addr(self): + """ + Got addr message - the bootstrap succeed. + Let BMProto process the addr message and switch state to 'close' + """ + BMProto.bm_command_addr(self) + self._succeed = True + self.close_reason = "Thanks for bootstrapping!" + self.set_state("close") + + def set_connection_fully_established(self): + """Only send addr here""" + # pylint: disable=attribute-defined-outside-init + self.fullyEstablished = True + self.sendAddr() + + def handle_close(self): + """ + After closing the connection switch knownnodes.knownNodesActual + back to False if the bootstrapper failed. + """ + BMProto.handle_close(self) + if not self._succeed: + knownnodes.knownNodesActual = False + + return Bootstrapper + + class TCPServer(AdvancedDispatcher): + """TCP connection server for Bitmessage protocol""" + def __init__(self, host='127.0.0.1', port=8444): if not hasattr(self, '_map'): AdvancedDispatcher.__init__(self) @@ -265,62 +398,52 @@ def __init__(self, host='127.0.0.1', port=8444): for attempt in range(50): try: if attempt > 0: - port = random.randint(32767, 65535) + logger.warning('Failed to bind on port %s', port) + port = random.randint(32767, 65535) # nosec B311 self.bind((host, port)) except socket.error as e: if e.errno in (asyncore.EADDRINUSE, asyncore.WSAEADDRINUSE): continue else: if attempt > 0: - BMConfigParser().set("bitmessagesettings", "port", str(port)) - BMConfigParser().save() + logger.warning('Setting port to %s', port) + config.set( + 'bitmessagesettings', 'port', str(port)) + config.save() break - self.destination = state.Peer(host, port) + self.destination = Peer(host, port) self.bound = True self.listen(5) def is_bound(self): + """Is the socket bound?""" try: return self.bound except AttributeError: return False def handle_accept(self): - pair = self.accept() - if pair is not None: - sock, addr = pair - state.ownAddresses[state.Peer(sock.getsockname()[0], sock.getsockname()[1])] = True - if len(network.connectionpool.BMConnectionPool().inboundConnections) + \ - len(network.connectionpool.BMConnectionPool().outboundConnections) > \ - BMConfigParser().safeGetInt("bitmessagesettings", "maxtotalconnections") + \ - BMConfigParser().safeGetInt("bitmessagesettings", "maxbootstrapconnections") + 10: - # 10 is a sort of buffer, in between it will go through the version handshake - # and return an error to the peer - logger.warning("Server full, dropping connection") - sock.close() - return - try: - network.connectionpool.BMConnectionPool().addConnection(TCPConnection(sock=sock)) - except socket.error: - pass - - -if __name__ == "__main__": - # initial fill - - for host in (("127.0.0.1", 8448),): - direct = TCPConnection(host) - while len(asyncore.socket_map) > 0: - print "loop, state = %s" % (direct.state) - asyncore.loop(timeout=10, count=1) - continue - - proxy = Socks5BMConnection(host) - while len(asyncore.socket_map) > 0: -# print "loop, state = %s" % (proxy.state) - asyncore.loop(timeout=10, count=1) - - proxy = Socks4aBMConnection(host) - while len(asyncore.socket_map) > 0: -# print "loop, state = %s" % (proxy.state) - asyncore.loop(timeout=10, count=1) + """Incoming connection callback""" + try: + sock = self.accept()[0] + except (TypeError, IndexError): + return + + state.ownAddresses[Peer(*sock.getsockname())] = True + if ( + len(connectionpool.pool) + > config.safeGetInt( + 'bitmessagesettings', 'maxtotalconnections') + + config.safeGetInt( + 'bitmessagesettings', 'maxbootstrapconnections') + 10 + ): + # 10 is a sort of buffer, in between it will go through + # the version handshake and return an error to the peer + logger.warning("Server full, dropping connection") + sock.close() + return + try: + connectionpool.pool.addConnection( + TCPConnection(sock=sock)) + except socket.error: + pass diff --git a/src/network/threads.py b/src/network/threads.py new file mode 100644 index 0000000000..9bdaa85d84 --- /dev/null +++ b/src/network/threads.py @@ -0,0 +1,49 @@ +"""Threading primitives for the network package""" + +import logging +import random +import threading +from contextlib import contextmanager + + +class StoppableThread(threading.Thread): + """Base class for application threads with stopThread method""" + name = None + logger = logging.getLogger('default') + + def __init__(self, name=None): + if name: + self.name = name + super(StoppableThread, self).__init__(name=self.name) + self.stop = threading.Event() + self._stopped = False + random.seed() + self.logger.info('Init thread %s', self.name) + + def stopThread(self): + """Stop the thread""" + self._stopped = True + self.stop.set() + + +class BusyError(threading.ThreadError): + """ + Thread error raised when another connection holds the lock + we are trying to acquire. + """ + pass + + +@contextmanager +def nonBlocking(lock): + """ + A context manager which acquires given lock non-blocking + and raises BusyError if failed to acquire. + """ + locked = lock.acquire(False) + if not locked: + raise BusyError + try: + yield + finally: + lock.release() diff --git a/src/network/tls.py b/src/network/tls.py index 379dae990e..994cbc5f1a 100644 --- a/src/network/tls.py +++ b/src/network/tls.py @@ -1,33 +1,56 @@ """ SSL/TLS negotiation. """ - +import logging import os import socket import ssl import sys -from debug import logger -from network.advanceddispatcher import AdvancedDispatcher import network.asyncore_pollchoose as asyncore -from queues import receiveDataQueue import paths -import protocol +from network.advanceddispatcher import AdvancedDispatcher +from network import receiveDataQueue + +logger = logging.getLogger('default') _DISCONNECTED_SSL = frozenset((ssl.SSL_ERROR_EOF,)) +if sys.version_info >= (2, 7, 13): + # this means TLSv1 or higher + # in the future change to + # ssl.PROTOCOL_TLS1.2 + sslProtocolVersion = ssl.PROTOCOL_TLS # pylint: disable=no-member +elif sys.version_info >= (2, 7, 9): + # this means any SSL/TLS. + # SSLv2 and 3 are excluded with an option after context is created + sslProtocolVersion = ssl.PROTOCOL_SSLv23 +else: + # this means TLSv1, there is no way to set "TLSv1 or higher" + # or "TLSv1.2" in < 2.7.9 + sslProtocolVersion = ssl.PROTOCOL_TLSv1 + + +# ciphers +if ( + ssl.OPENSSL_VERSION_NUMBER >= 0x10100000 + and not ssl.OPENSSL_VERSION.startswith(b"LibreSSL") +): + sslProtocolCiphers = "AECDH-AES256-SHA@SECLEVEL=0" +else: + sslProtocolCiphers = "AECDH-AES256-SHA" + + class TLSDispatcher(AdvancedDispatcher): - def __init__(self, address=None, sock=None, - certfile=None, keyfile=None, server_side=False, ciphers=protocol.sslProtocolCiphers): + """TLS functionality for classes derived from AdvancedDispatcher""" + # pylint: disable=too-many-instance-attributes,super-init-not-called + def __init__(self, _=None, sock=None, certfile=None, keyfile=None, + server_side=False, ciphers=sslProtocolCiphers): self.want_read = self.want_write = True - if certfile is None: - self.certfile = os.path.join(paths.codePath(), 'sslkeys', 'cert.pem') - else: - self.certfile = certfile - if keyfile is None: - self.keyfile = os.path.join(paths.codePath(), 'sslkeys', 'key.pem') - else: - self.keyfile = keyfile + self.certfile = certfile or os.path.join( + paths.codePath(), 'sslkeys', 'cert.pem') + self.keyfile = keyfile or os.path.join( + paths.codePath(), 'sslkeys', 'key.pem') self.server_side = server_side self.ciphers = ciphers self.tlsStarted = False @@ -36,132 +59,155 @@ def __init__(self, address=None, sock=None, self.isSSL = False def state_tls_init(self): + """Prepare sockets for TLS handshake""" self.isSSL = True self.tlsStarted = True - # Once the connection has been established, it's safe to wrap the - # socket. - if sys.version_info >= (2,7,9): - context = ssl.create_default_context(purpose = ssl.Purpose.SERVER_AUTH if self.server_side else ssl.Purpose.CLIENT_AUTH) + # Once the connection has been established, + # it's safe to wrap the socket. + if sys.version_info >= (2, 7, 9): + context = ssl.create_default_context( + purpose=ssl.Purpose.SERVER_AUTH + if self.server_side else ssl.Purpose.CLIENT_AUTH) context.set_ciphers(self.ciphers) context.set_ecdh_curve("secp256k1") context.check_hostname = False context.verify_mode = ssl.CERT_NONE # also exclude TLSv1 and TLSv1.1 in the future - context.options = ssl.OP_ALL | ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_SINGLE_ECDH_USE | ssl.OP_CIPHER_SERVER_PREFERENCE - self.sslSocket = context.wrap_socket(self.socket, server_side = self.server_side, do_handshake_on_connect=False) + context.options = ssl.OP_ALL | ssl.OP_NO_SSLv2 |\ + ssl.OP_NO_SSLv3 | ssl.OP_SINGLE_ECDH_USE |\ + ssl.OP_CIPHER_SERVER_PREFERENCE + self.sslSocket = context.wrap_socket( + self.socket, server_side=self.server_side, + do_handshake_on_connect=False) else: - self.sslSocket = ssl.wrap_socket(self.socket, - server_side=self.server_side, - ssl_version=protocol.sslProtocolVersion, - certfile=self.certfile, - keyfile=self.keyfile, - ciphers=self.ciphers, - do_handshake_on_connect=False) + self.sslSocket = ssl.wrap_socket( + self.socket, server_side=self.server_side, + ssl_version=sslProtocolVersion, + certfile=self.certfile, keyfile=self.keyfile, + ciphers=self.ciphers, do_handshake_on_connect=False) self.sslSocket.setblocking(0) self.want_read = self.want_write = True self.set_state("tls_handshake") return False -# if hasattr(self.socket, "context"): -# self.socket.context.set_ecdh_curve("secp256k1") - def state_tls_handshake(self): + @staticmethod + def state_tls_handshake(): + """ + Do nothing while TLS handshake is pending, as during this phase + we need to react to callbacks instead + """ return False def writable(self): + """Handle writable checks for TLS-enabled sockets""" try: if self.tlsStarted and not self.tlsDone and not self.write_buf: return self.want_write - return AdvancedDispatcher.writable(self) except AttributeError: - return AdvancedDispatcher.writable(self) + pass + return AdvancedDispatcher.writable(self) def readable(self): + """Handle readable check for TLS-enabled sockets""" try: - # during TLS handshake, and after flushing write buffer, return status of last handshake attempt + # during TLS handshake, and after flushing write buffer, + # return status of last handshake attempt if self.tlsStarted and not self.tlsDone and not self.write_buf: - #print "tls readable, %r" % (self.want_read) + logger.debug('tls readable, %r', self.want_read) return self.want_read - # prior to TLS handshake, receiveDataThread should emulate synchronous behaviour - elif not self.fullyEstablished and (self.expectBytes == 0 or not self.write_buf_empty()): + # prior to TLS handshake, + # receiveDataThread should emulate synchronous behaviour + if not self.fullyEstablished and ( + self.expectBytes == 0 or not self.write_buf_empty()): return False - return AdvancedDispatcher.readable(self) except AttributeError: - return AdvancedDispatcher.readable(self) + pass + return AdvancedDispatcher.readable(self) def handle_read(self): + """ + Handle reads for sockets during TLS handshake. Requires special + treatment as during the handshake, buffers must remain empty + and normal reads must be ignored. + """ try: # wait for write buffer flush if self.tlsStarted and not self.tlsDone and not self.write_buf: - #logger.debug("%s:%i TLS handshaking (read)", self.destination.host, self.destination.port) self.tls_handshake() else: - #logger.debug("%s:%i Not TLS handshaking (read)", self.destination.host, self.destination.port) - return AdvancedDispatcher.handle_read(self) + AdvancedDispatcher.handle_read(self) except AttributeError: - return AdvancedDispatcher.handle_read(self) + AdvancedDispatcher.handle_read(self) except ssl.SSLError as err: if err.errno == ssl.SSL_ERROR_WANT_READ: return - elif err.errno in _DISCONNECTED_SSL: - self.handle_close() - return - logger.info("SSL Error: %s", str(err)) + if err.errno not in _DISCONNECTED_SSL: + logger.info("SSL Error: %s", err) + self.close_reason = "SSL Error in handle_read" self.handle_close() - return def handle_write(self): + """ + Handle writes for sockets during TLS handshake. Requires special + treatment as during the handshake, buffers must remain empty + and normal writes must be ignored. + """ try: # wait for write buffer flush if self.tlsStarted and not self.tlsDone and not self.write_buf: - #logger.debug("%s:%i TLS handshaking (write)", self.destination.host, self.destination.port) self.tls_handshake() else: - #logger.debug("%s:%i Not TLS handshaking (write)", self.destination.host, self.destination.port) - return AdvancedDispatcher.handle_write(self) + AdvancedDispatcher.handle_write(self) except AttributeError: - return AdvancedDispatcher.handle_write(self) + AdvancedDispatcher.handle_write(self) except ssl.SSLError as err: if err.errno == ssl.SSL_ERROR_WANT_WRITE: - return 0 - elif err.errno in _DISCONNECTED_SSL: - self.handle_close() - return 0 - logger.info("SSL Error: %s", str(err)) + return + if err.errno not in _DISCONNECTED_SSL: + logger.info("SSL Error: %s", err) + self.close_reason = "SSL Error in handle_write" self.handle_close() - return def tls_handshake(self): + """Perform TLS handshake and handle its stages""" # wait for flush if self.write_buf: return False # Perform the handshake. try: - #print "handshaking (internal)" + logger.debug("handshaking (internal)") self.sslSocket.do_handshake() except ssl.SSLError as err: - #print "%s:%i: handshake fail" % (self.destination.host, self.destination.port) + self.close_reason = "SSL Error in tls_handshake" + logger.info("%s:%i: handshake fail", *self.destination) self.want_read = self.want_write = False if err.args[0] == ssl.SSL_ERROR_WANT_READ: - #print "want read" + logger.debug("want read") self.want_read = True if err.args[0] == ssl.SSL_ERROR_WANT_WRITE: - #print "want write" + logger.debug("want write") self.want_write = True if not (self.want_write or self.want_read): raise except socket.error as err: + # pylint: disable=protected-access if err.errno in asyncore._DISCONNECTED: + self.close_reason = "socket.error in tls_handshake" self.handle_close() else: raise else: if sys.version_info >= (2, 7, 9): self.tlsVersion = self.sslSocket.version() - logger.debug("%s:%i: TLS handshake success, TLS protocol version: %s", - self.destination.host, self.destination.port, self.sslSocket.version()) + logger.debug( + '%s:%i: TLS handshake success, TLS protocol version: %s', + self.destination.host, self.destination.port, + self.tlsVersion) else: self.tlsVersion = "TLSv1" - logger.debug("%s:%i: TLS handshake success", self.destination.host, self.destination.port) + logger.debug( + '%s:%i: TLS handshake success', + self.destination.host, self.destination.port) # The handshake has completed, so remove this channel and... self.del_channel() self.set_socket(self.sslSocket) diff --git a/src/network/udp.py b/src/network/udp.py index 0dba5a3ffa..30643d40c9 100644 --- a/src/network/udp.py +++ b/src/network/udp.py @@ -1,51 +1,52 @@ -import time -import Queue +""" +UDP protocol handler +""" +import logging import socket +import time -from debug import logger -from network.advanceddispatcher import AdvancedDispatcher -from network.bmproto import BMProtoError, BMProtoInsufficientDataError, BMProto -from network.bmobject import BMObject, BMObjectInsufficientPOWError, BMObjectInvalidDataError, BMObjectExpiredError, BMObjectInvalidError, BMObjectAlreadyHaveError -import network.asyncore_pollchoose as asyncore -from network.objectracker import ObjectTracker - -from queues import objectProcessorQueue, UISignalQueue, receiveDataQueue -import state +# magic imports! import protocol +import state +import connectionpool + +from network import receiveDataQueue +from bmproto import BMProto +from node import Peer +from objectracker import ObjectTracker + + +logger = logging.getLogger('default') + -class UDPSocket(BMProto): +class UDPSocket(BMProto): # pylint: disable=too-many-instance-attributes + """Bitmessage protocol over UDP (class)""" port = 8444 - announceInterval = 60 def __init__(self, host=None, sock=None, announcing=False): + # pylint: disable=bad-super-call super(BMProto, self).__init__(sock=sock) self.verackReceived = True self.verackSent = True - # TODO sort out streams + # .. todo:: sort out streams self.streams = [1] self.fullyEstablished = True - self.connectedAt = 0 self.skipUntil = 0 if sock is None: if host is None: host = '' - if ":" in host: - self.create_socket(socket.AF_INET6, socket.SOCK_DGRAM) - else: - self.create_socket(socket.AF_INET, socket.SOCK_DGRAM) + self.create_socket( + socket.AF_INET6 if ":" in host else socket.AF_INET, + socket.SOCK_DGRAM + ) self.set_socket_reuse() - logger.info("Binding UDP socket to %s:%i", host, UDPSocket.port) - self.socket.bind((host, UDPSocket.port)) - #BINDTODEVICE is only available on linux and requires root - #try: - #print "binding to %s" % (host) - #self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, host) - #except AttributeError: + logger.info("Binding UDP socket to %s:%i", host, self.port) + self.socket.bind((host, self.port)) else: self.socket = sock self.set_socket_reuse() - self.listening = state.Peer(self.socket.getsockname()[0], self.socket.getsockname()[1]) - self.destination = state.Peer(self.socket.getsockname()[0], self.socket.getsockname()[1]) + self.listening = Peer(*self.socket.getsockname()) + self.destination = Peer(*self.socket.getsockname()) ObjectTracker.__init__(self) self.connecting = False self.connected = True @@ -53,6 +54,7 @@ def __init__(self, host=None, sock=None, announcing=False): self.set_state("bm_header", expectBytes=protocol.Header.size) def set_socket_reuse(self): + """Set socket reuse option""" self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: @@ -60,48 +62,42 @@ def set_socket_reuse(self): except AttributeError: pass - def state_bm_command(self): - return BMProto.state_bm_command(self) - # disable most commands before doing research / testing # only addr (peer discovery), error and object are implemented - def bm_command_error(self): - return BMProto.bm_command_error(self) - def bm_command_getdata(self): + # return BMProto.bm_command_getdata(self) return True -# return BMProto.bm_command_getdata(self) def bm_command_inv(self): + # return BMProto.bm_command_inv(self) return True -# return BMProto.bm_command_inv(self) - - def bm_command_object(self): - return BMProto.bm_command_object(self) def bm_command_addr(self): -# BMProto.bm_command_object(self) addresses = self._decode_addr() - # only allow peer discovery from private IPs in order to avoid attacks from random IPs on the internet + # only allow peer discovery from private IPs in order to avoid + # attacks from random IPs on the internet if not self.local: return True remoteport = False - for i in addresses: - seenTime, stream, services, ip, port = i + 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() - BMProto.maxTimeOffset or seenTime > time.time() + BMProto.maxTimeOffset: + 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 the hosts' own announcement + # if the address isn't local, interpret it as + # the host's own announcement remoteport = port if remoteport is False: return True - logger.debug("received peer discovery from %s:%i (port %i):", self.destination.host, self.destination.port, remoteport) - if self.local: - state.discoveredPeers[state.Peer(self.destination.host, remoteport)] = time.time() + logger.debug( + "received peer discovery from %s:%i (port %i):", + self.destination.host, self.destination.port, remoteport) + state.discoveredPeers[Peer(self.destination.host, remoteport)] = \ + time.time() return True def bm_command_portcheck(self): @@ -126,51 +122,29 @@ def writable(self): return self.write_buf def readable(self): - return len(self.read_buf) < AdvancedDispatcher._buf_len + return len(self.read_buf) < self._buf_len def handle_read(self): try: - (recdata, addr) = self.socket.recvfrom(AdvancedDispatcher._buf_len) - except socket.error as e: - logger.error("socket error: %s", str(e)) + recdata, addr = self.socket.recvfrom(self._buf_len) + except socket.error: + logger.error("socket error on recvfrom:", exc_info=True) return - self.destination = state.Peer(addr[0], addr[1]) + self.destination = Peer(*addr) encodedAddr = protocol.encodeHost(addr[0]) - if protocol.checkIPAddress(encodedAddr, True): - self.local = True - else: - self.local = False - # overwrite the old buffer to avoid mixing data and so that self.local works correctly + self.local = bool(protocol.checkIPAddress(encodedAddr, True)) + # overwrite the old buffer to avoid mixing data and so that + # self.local works correctly self.read_buf[0:] = recdata self.bm_proto_reset() receiveDataQueue.put(self.listening) def handle_write(self): try: - retval = self.socket.sendto(self.write_buf, ('', UDPSocket.port)) - except socket.error as e: - logger.error("socket error on sendato: %s", str(e)) - retval = 0 + retval = self.socket.sendto( + self.write_buf, ('', self.port)) + except socket.error: + logger.error("socket error on sendto:", exc_info=True) + retval = len(self.write_buf) self.slice_write_buf(retval) - - -if __name__ == "__main__": - # initial fill - - for host in (("127.0.0.1", 8448),): - direct = BMConnection(host) - while len(asyncore.socket_map) > 0: - print "loop, state = %s" % (direct.state) - asyncore.loop(timeout=10, count=1) - continue - - proxy = Socks5BMConnection(host) - while len(asyncore.socket_map) > 0: -# print "loop, state = %s" % (proxy.state) - asyncore.loop(timeout=10, count=1) - - proxy = Socks4aBMConnection(host) - while len(asyncore.socket_map) > 0: -# print "loop, state = %s" % (proxy.state) - asyncore.loop(timeout=10, count=1) diff --git a/src/network/uploadthread.py b/src/network/uploadthread.py new file mode 100644 index 0000000000..6020983288 --- /dev/null +++ b/src/network/uploadthread.py @@ -0,0 +1,69 @@ +""" +`UploadThread` class definition +""" +import time + +import random +import protocol +import state +import connectionpool +from randomtrackingdict import RandomTrackingDict +from network import dandelion_ins +from threads import StoppableThread + + +class UploadThread(StoppableThread): + """ + This is a thread that uploads the objects that the peers requested from me + """ + maxBufSize = 2097152 # 2MB + name = "Uploader" + + def run(self): + while not self._stopped: + uploaded = 0 + # Choose uploading peers randomly + connections = connectionpool.pool.establishedConnections() + random.shuffle(connections) + for i in connections: + now = time.time() + # avoid unnecessary delay + if i.skipUntil >= now: + continue + if len(i.write_buf) > self.maxBufSize: + continue + try: + request = i.pendingUpload.randomKeys( + RandomTrackingDict.maxPending) + except KeyError: + continue + payload = bytearray() + chunk_count = 0 + for chunk in request: + del i.pendingUpload[chunk] + if dandelion_ins.hasHash(chunk) and \ + i != dandelion_ins.objectChildStem(chunk): + i.antiIntersectionDelay() + self.logger.info( + '%s asked for a stem object we didn\'t offer to it.', + i.destination) + break + try: + payload.extend(protocol.CreatePacket( + 'object', state.Inventory[chunk].payload)) + chunk_count += 1 + except KeyError: + i.antiIntersectionDelay() + self.logger.info( + '%s asked for an object we don\'t have.', + i.destination) + break + if not chunk_count: + continue + i.append_write_buf(payload) + self.logger.debug( + '%s:%i Uploading %i objects', + i.destination.host, i.destination.port, chunk_count) + uploaded += chunk_count + if not uploaded: + self.stop.wait(1) diff --git a/src/openclpow.py b/src/openclpow.py index 894a5b7760..5391590c58 100644 --- a/src/openclpow.py +++ b/src/openclpow.py @@ -1,16 +1,24 @@ -#!/usr/bin/env python2.7 -from struct import pack, unpack -import time -import hashlib -import random +""" +Module for Proof of Work using OpenCL +""" +import logging import os +from struct import pack -from bmconfigparser import BMConfigParser import paths +from bmconfigparser import config from state import shutdown -from debug import logger -libAvailable = True +try: + import numpy + import pyopencl as cl + libAvailable = True +except ImportError: + libAvailable = False + + +logger = logging.getLogger('default') + ctx = False queue = False program = False @@ -19,14 +27,10 @@ vendors = [] hash_dt = None -try: - import numpy - import pyopencl as cl -except: - libAvailable = False def initCL(): - global ctx, queue, program, hash_dt, libAvailable + """Initlialise OpenCL engine""" + global ctx, queue, program, hash_dt # pylint: disable=global-statement if libAvailable is False: return del enabledGpus[:] @@ -38,13 +42,14 @@ 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: - enabledGpus.extend(platform.get_devices(device_type=cl.device_type.GPU)) + 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: + except: # nosec B110 # noqa:E722 # pylint:disable=bare-except pass - if (len(enabledGpus) > 0): + if enabledGpus: ctx = cl.Context(devices=enabledGpus) queue = cl.CommandQueue(ctx) f = open(os.path.join(paths.codePath(), "bitmsghash", 'bitmsghash.cl'), 'r') @@ -54,23 +59,29 @@ def initCL(): else: logger.info("No OpenCL GPUs found") del enabledGpus[:] - except Exception as e: + except Exception: logger.error("OpenCL fail: ", exc_info=True) del enabledGpus[:] + def openclAvailable(): - return (len(gpus) > 0) + """Are there any OpenCL GPUs available?""" + return bool(gpus) + def openclEnabled(): - return (len(enabledGpus) > 0) + """Is OpenCL enabled (and available)?""" + return bool(enabledGpus) + -def do_opencl_pow(hash, target): +def do_opencl_pow(hash_, target): + """Perform PoW using OpenCL""" output = numpy.zeros(1, dtype=[('v', numpy.uint64, 1)]) - if (len(enabledGpus) == 0): + if not enabledGpus: return output[0][0] data = numpy.zeros(1, dtype=hash_dt, order='C') - data[0]['v'] = ("0000000000000000" + hash).decode("hex") + data[0]['v'] = ("0000000000000000" + hash_).decode("hex") data[0]['target'] = target hash_buf = cl.Buffer(ctx, cl.mem_flags.READ_ONLY | cl.mem_flags.COPY_HOST_PTR, hostbuf=data) @@ -82,30 +93,19 @@ def do_opencl_pow(hash, target): kernel.set_arg(0, hash_buf) kernel.set_arg(1, dest_buf) - start = time.time() progress = 0 - globamt = worksize*2000 + globamt = worksize * 2000 while output[0][0] == 0 and shutdown == 0: kernel.set_arg(2, pack("Q',hashlib.sha512(hashlib.sha512(pack('>Q',nonce) + initialHash).digest()).digest()[0:8]) - print "{} - value {} < {}".format(nonce, trialValue, target) - diff --git a/src/pathmagic.py b/src/pathmagic.py new file mode 100644 index 0000000000..3f32c0c1ef --- /dev/null +++ b/src/pathmagic.py @@ -0,0 +1,10 @@ +import os +import sys + + +def setup(): + """Add path to this file to sys.path""" + app_dir = os.path.dirname(os.path.abspath(__file__)) + os.chdir(app_dir) + sys.path.insert(0, app_dir) + return app_dir diff --git a/src/paths.py b/src/paths.py index 325fcd8b99..e0d4333407 100644 --- a/src/paths.py +++ b/src/paths.py @@ -1,76 +1,87 @@ -from os import environ, path -import sys +""" +Path related functions +""" +import logging +import os import re +import sys from datetime import datetime +from shutil import move + +logger = logging.getLogger('default') # When using py2exe or py2app, the variable frozen is added to the sys -# namespace. This can be used to setup a different code path for +# namespace. This can be used to setup a different code path for # binary distributions vs source distributions. -frozen = getattr(sys,'frozen', None) +frozen = getattr(sys, 'frozen', None) + def lookupExeFolder(): + """Returns executable folder path""" if frozen: - if frozen == "macosx_app": + exeFolder = ( # targetdir/Bitmessage.app/Contents/MacOS/Bitmessage - exeFolder = path.dirname(path.dirname(path.dirname(path.dirname(sys.executable)))) + path.sep - else: - exeFolder = path.dirname(sys.executable) + 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 = path.dirname(__file__) + path.sep + exeFolder = os.path.dirname(__file__) else: - exeFolder = '' - return exeFolder + return '' + return exeFolder + os.path.sep + def lookupAppdataFolder(): + """Returns path of the folder where application data is stored""" APPNAME = "PyBitmessage" - if "BITMESSAGE_HOME" in environ: - dataFolder = environ["BITMESSAGE_HOME"] - if dataFolder[-1] not in [path.sep, path.altsep]: - dataFolder += path.sep + dataFolder = os.environ.get('BITMESSAGE_HOME') + if dataFolder: + if dataFolder[-1] not in (os.path.sep, os.path.altsep): + dataFolder += os.path.sep elif sys.platform == 'darwin': - if "HOME" in environ: - dataFolder = path.join(environ["HOME"], "Library/Application Support/", APPNAME) + '/' - else: - stringToLog = 'Could not find home folder, please report this message and your OS X version to the BitMessage Github.' - if 'logger' in globals(): - logger.critical(stringToLog) - else: - print stringToLog - sys.exit() + try: + dataFolder = os.path.join( + os.environ['HOME'], + 'Library/Application Support/', APPNAME + ) + '/' - elif 'win32' in sys.platform or 'win64' in sys.platform: - dataFolder = path.join(environ['APPDATA'].decode(sys.getfilesystemencoding(), 'ignore'), APPNAME) + path.sep + except KeyError: + sys.exit( + 'Could not find home folder, please report this message' + ' and your OS X version to the BitMessage Github.') + elif sys.platform.startswith('win'): + dataFolder = os.path.join(os.environ['APPDATA'], APPNAME) + os.path.sep else: - from shutil import move try: - dataFolder = path.join(environ["XDG_CONFIG_HOME"], APPNAME) + dataFolder = os.path.join(os.environ['XDG_CONFIG_HOME'], APPNAME) except KeyError: - dataFolder = path.join(environ["HOME"], ".config", APPNAME) + dataFolder = os.path.join(os.environ['HOME'], '.config', APPNAME) - # Migrate existing data to the proper location if this is an existing install + # Migrate existing data to the proper location + # if this is an existing install try: - move(path.join(environ["HOME"], ".%s" % APPNAME), dataFolder) - stringToLog = "Moving data folder to %s" % (dataFolder) - if 'logger' in globals(): - logger.info(stringToLog) - else: - print stringToLog + move(os.path.join(os.environ['HOME'], '.%s' % APPNAME), dataFolder) + logger.info('Moving data folder to %s', dataFolder) except IOError: # Old directory may not exist. pass - dataFolder = dataFolder + '/' + dataFolder = dataFolder + os.path.sep return dataFolder - + + def codePath(): - if frozen == "macosx_app": - codePath = environ.get("RESOURCEPATH") - elif frozen: # windows - codePath = sys._MEIPASS - else: - codePath = path.dirname(__file__) - return codePath + """Returns path to the program sources""" + if not frozen: + return os.path.dirname(__file__) + return ( + os.environ.get('RESOURCEPATH') + # pylint: disable=protected-access + if frozen == "macosx_app" else sys._MEIPASS) + def tail(f, lines=20): + """Returns last lines in the f file object""" total_lines_wanted = lines BLOCK_SIZE = 1024 @@ -78,16 +89,17 @@ def tail(f, lines=20): block_end_byte = f.tell() lines_to_go = total_lines_wanted block_number = -1 - blocks = [] # blocks of size BLOCK_SIZE, in reverse order starting - # from the end of the file + # blocks of size BLOCK_SIZE, in reverse order starting + # from the end of the file + blocks = [] while lines_to_go > 0 and block_end_byte > 0: - if (block_end_byte - BLOCK_SIZE > 0): + if block_end_byte - BLOCK_SIZE > 0: # read the last block we haven't yet read - f.seek(block_number*BLOCK_SIZE, 2) + f.seek(block_number * BLOCK_SIZE, 2) blocks.append(f.read(BLOCK_SIZE)) else: # file too small, start from begining - f.seek(0,0) + f.seek(0, 0) # only read what was not read blocks.append(f.read(block_end_byte)) lines_found = blocks[-1].count('\n') @@ -99,9 +111,12 @@ def tail(f, lines=20): def lastCommit(): - githeadfile = path.join(codePath(), '..', '.git', 'logs', 'HEAD') + """ + Returns last commit information as dict with 'commit' and 'time' keys + """ + githeadfile = os.path.join(codePath(), '..', '.git', 'logs', 'HEAD') result = {} - if path.isfile(githeadfile): + if os.path.isfile(githeadfile): try: with open(githeadfile, 'rt') as githead: line = tail(githead, 1) diff --git a/src/plugins/__init__.py b/src/plugins/__init__.py index e69de29bb2..285009dff7 100644 --- a/src/plugins/__init__.py +++ b/src/plugins/__init__.py @@ -0,0 +1,7 @@ +""" +Simple plugin system based on setuptools +---------------------------------------- + + +""" +# .. include:: pybitmessage.plugins.plugin.rst diff --git a/src/plugins/desktop_xdg.py b/src/plugins/desktop_xdg.py new file mode 100644 index 0000000000..0b551e1c85 --- /dev/null +++ b/src/plugins/desktop_xdg.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- + +import os + +from xdg import BaseDirectory, Menu, Exceptions + + +class DesktopXDG(object): + """pyxdg Freedesktop desktop implementation""" + def __init__(self): + 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""" + autostart_path = os.path.join( + BaseDirectory.xdg_config_home, 'autostart', 'pybitmessage.desktop') + if autostart: + self.desktop.write(autostart_path) + else: + try: + os.remove(autostart_path) + except OSError: + pass + + +connect_plugin = DesktopXDG diff --git a/src/plugins/indicator_libmessaging.py b/src/plugins/indicator_libmessaging.py index 3617866346..b471d2efc2 100644 --- a/src/plugins/indicator_libmessaging.py +++ b/src/plugins/indicator_libmessaging.py @@ -1,4 +1,7 @@ # -*- coding: utf-8 -*- +""" +Indicator plugin using libmessaging +""" import gi gi.require_version('MessagingMenu', '1.0') # noqa:E402 @@ -9,12 +12,13 @@ class IndicatorLibmessaging(object): + """Plugin for libmessage indicator""" def __init__(self, form): try: self.app = MessagingMenu.App(desktop_id='pybitmessage.desktop') self.app.register() self.app.connect('activate-source', self.activate) - except: + except: # noqa:E722 self.app = None return @@ -32,15 +36,18 @@ def __del__(self): if self.app: self.app.unregister() - def activate(self, app, source): + def activate(self, app, source): # pylint: disable=unused-argument + """Activate the libmessaging indicator plugin""" self.form.appIndicatorInbox( self.new_message_item if source == 'messages' else self.new_broadcast_item ) - # show the number of unread messages and subscriptions - # on the messaging menu def show_unread(self, draw_attention=False): + """ + show the number of unread messages and subscriptions + on the messaging menu + """ for source, count in zip( ('messages', 'subscriptions'), self.form.getUnread() diff --git a/src/plugins/menu_qrcode.py b/src/plugins/menu_qrcode.py index 3831e89bb2..ea322a4940 100644 --- a/src/plugins/menu_qrcode.py +++ b/src/plugins/menu_qrcode.py @@ -3,16 +3,20 @@ A menu plugin showing QR-Code for bitmessage address in modal dialog. """ -from PyQt4 import QtGui, QtCore +import urllib + import qrcode +from PyQt4 import QtCore, QtGui from pybitmessage.tr import _translate # http://stackoverflow.com/questions/20452486 -class Image(qrcode.image.base.BaseImage): +class Image(qrcode.image.base.BaseImage): # pylint: disable=abstract-method """Image output class for qrcode using QPainter""" + def __init__(self, border, width, box_size): + # pylint: disable=super-init-not-called self.border = border self.width = width self.box_size = box_size @@ -46,7 +50,7 @@ def __init__(self, parent): font.setWeight(75) self.label.setFont(font) self.label.setAlignment( - QtCore.Qt.AlignCenter | QtCore.Qt.AlignVCenter) + QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) buttonBox = QtGui.QDialogButtonBox(self) buttonBox.setOrientation(QtCore.Qt.Horizontal) buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Ok) @@ -63,9 +67,11 @@ def retranslateUi(self): def render(self, text): """Draw QR-code and address in labels""" + pixmap = qrcode.make(text, image_factory=Image).pixmap() + self.image.setPixmap(pixmap) self.label.setText(text) - self.image.setPixmap( - qrcode.make(text, image_factory=Image).pixmap()) + self.label.setToolTip(text) + self.label.setFixedWidth(pixmap.width()) self.setFixedSize(QtGui.QWidget.sizeHint(self)) @@ -77,7 +83,19 @@ def on_action_ShowQR(): dialog = form.qrcode_dialog except AttributeError: form.qrcode_dialog = dialog = QRCodeDialog(form) - dialog.render('bitmessage:' + str(form.getCurrentAccount())) + account = form.getContactSelected() + try: + label = account._getLabel() # pylint: disable=protected-access + except AttributeError: + try: + label = account.getLabel() + except AttributeError: + return + dialog.render( + 'bitmessage:%s' % account.address + ( + '?' + urllib.urlencode({'label': label.encode('utf-8')}) + if label != account.address else '') + ) dialog.exec_() return on_action_ShowQR, _translate("MainWindow", "Show QR-code") diff --git a/src/plugins/notification_notify2.py b/src/plugins/notification_notify2.py index 3fd935c47f..f851737da1 100644 --- a/src/plugins/notification_notify2.py +++ b/src/plugins/notification_notify2.py @@ -1,15 +1,21 @@ # -*- coding: utf-8 -*- +""" +Notification plugin using notify2 +""" import gi gi.require_version('Notify', '0.7') -from gi.repository import Notify +from gi.repository import Notify # noqa:E402 Notify.init('pybitmessage') -def connect_plugin(title, subtitle, category, label, icon): + +def connect_plugin(title, subtitle, category, _, icon): + """Plugin for notify2""" if not icon: icon = 'mail-message-new' if category == 2 else 'pybitmessage' connect_plugin.notification.update(title, subtitle, icon) connect_plugin.notification.show() + connect_plugin.notification = Notify.Notification.new("Init", "Init") diff --git a/src/plugins/plugin.py b/src/plugins/plugin.py index 6601adafb9..629de0a683 100644 --- a/src/plugins/plugin.py +++ b/src/plugins/plugin.py @@ -1,16 +1,28 @@ # -*- coding: utf-8 -*- +""" +Operating with plugins +""" +import logging import pkg_resources +logger = logging.getLogger('default') + + def get_plugins(group, point='', name=None, fallback=None): """ - Iterate through plugins (`connect_plugin` attribute of entry point) - which name starts with `point` or equals to `name`. - If `fallback` kwarg specified, plugin with that name yield last. + :param str group: plugin group + :param str point: plugin name prefix + :param name: exact plugin name + :param fallback: fallback plugin name + + Iterate through plugins (``connect_plugin`` attribute of entry point) + which name starts with ``point`` or equals to ``name``. + If ``fallback`` kwarg specified, plugin with that name yield last. """ for ep in pkg_resources.iter_entry_points('bitmessage.' + group): - if name and ep.name == name or ep.name.startswith(point): + if name and ep.name == name or not point or ep.name.startswith(point): try: plugin = ep.load().connect_plugin if ep.name == fallback: @@ -22,6 +34,8 @@ def get_plugins(group, point='', name=None, fallback=None): ValueError, pkg_resources.DistributionNotFound, pkg_resources.UnknownExtra): + logger.debug( + 'Problem while loading %s', ep.name, exc_info=True) continue try: yield _fallback @@ -30,6 +44,8 @@ def get_plugins(group, point='', name=None, fallback=None): def get_plugin(*args, **kwargs): - """Returns first available plugin `from get_plugins()` if any.""" + """ + :return: first available plugin from :func:`get_plugins` if any. + """ for plugin in get_plugins(*args, **kwargs): return plugin diff --git a/src/plugins/proxyconfig_stem.py b/src/plugins/proxyconfig_stem.py new file mode 100644 index 0000000000..25f75f697f --- /dev/null +++ b/src/plugins/proxyconfig_stem.py @@ -0,0 +1,157 @@ +# -*- coding: utf-8 -*- +""" +Configure tor proxy and hidden service with +`stem `_ depending on *bitmessagesettings*: + + * try to start own tor instance on *socksport* if *sockshostname* + is unset or set to localhost; + * if *socksport* is already in use that instance is used only for + hidden service (if *sockslisten* is also set True); + * create ephemeral hidden service v3 if there is already *onionhostname*; + * otherwise use stem's 'BEST' version and save onion keys to the new + section using *onionhostname* as name for future use. +""" +import logging +import os +import random +import sys +import tempfile + +import stem +import stem.control +import stem.process +import stem.version + + +class DebugLogger(object): # pylint: disable=too-few-public-methods + """Safe logger wrapper for tor and plugin's logs""" + def __init__(self): + self._logger = logging.getLogger('default') + self._levels = { + 'err': 40, + 'warn': 30, + 'notice': 20 + } + + def __call__(self, line): + try: + level, line = line.split('[', 1)[1].split(']', 1) + except IndexError: + # Plugin's debug or unexpected log line from tor + self._logger.debug(line) + except ValueError: # some error while splitting + self._logger.warning(line) + else: + self._logger.log(self._levels.get(level, 10), '(tor) %s', line) + + +# pylint: disable=too-many-branches,too-many-statements +def connect_plugin(config): + """ + Run stem proxy configurator + + :param config: current configuration instance + :type config: :class:`pybitmessage.bmconfigparser.BMConfigParser` + :return: True if configuration was done successfully + """ + logwrite = DebugLogger() + if config.safeGet('bitmessagesettings', 'sockshostname', '') not in ( + 'localhost', '127.0.0.1', '' + ): + # remote proxy is choosen for outbound connections, + # nothing to do here, but need to set socksproxytype to SOCKS5! + config.set('bitmessagesettings', 'socksproxytype', 'SOCKS5') + logwrite( + 'sockshostname is set to remote address,' + ' aborting stem proxy configuration') + return + + tor_config = {'SocksPort': '9050'} + + datadir = tempfile.mkdtemp() + if sys.platform.startswith('win'): + # no ControlSocket on windows because there is no Unix sockets + tor_config['DataDirectory'] = datadir + else: + control_socket = os.path.join(datadir, 'control') + tor_config['ControlSocket'] = control_socket + + port = config.safeGetInt('bitmessagesettings', 'socksport', 9050) + for attempt in range(50): + if attempt > 0: + port = random.randint(32767, 65535) # nosec B311 + tor_config['SocksPort'] = str(port) + if tor_config.get('DataDirectory'): + control_port = port + 1 + tor_config['ControlPort'] = str(control_port) + # It's recommended to use separate tor instance for hidden services. + # So if there is a system wide tor, use it for outbound connections. + try: + stem.process.launch_tor_with_config( + tor_config, take_ownership=True, + timeout=(None if sys.platform.startswith('win') else 20), + init_msg_handler=logwrite) + except OSError: + if not attempt: + try: + stem.version.get_system_tor_version() + except IOError: + return + continue + else: + logwrite('Started tor on port %s' % port) + break + else: + logwrite('Failed to start tor') + return + + config.setTemp('bitmessagesettings', 'socksproxytype', 'SOCKS5') + + if config.safeGetBoolean('bitmessagesettings', 'sockslisten'): + # need a hidden service for inbound connections + try: + controller = ( + stem.control.Controller.from_port(port=control_port) + if sys.platform.startswith('win') else + stem.control.Controller.from_socket_file(control_socket) + ) + controller.authenticate() + except stem.SocketError: + # something goes wrong way + logwrite('Failed to instantiate or authenticate on controller') + return + + onionhostname = config.safeGet('bitmessagesettings', 'onionhostname') + onionkey = config.safeGet(onionhostname, 'privsigningkey') + if onionhostname and not onionkey: + logwrite('The hidden service found in config ): %s' % + onionhostname) + onionkeytype = config.safeGet(onionhostname, 'keytype') + + response = controller.create_ephemeral_hidden_service( + {config.safeGetInt('bitmessagesettings', 'onionport', 8444): + config.safeGetInt('bitmessagesettings', 'port', 8444)}, + key_type=(onionkeytype or 'NEW'), + key_content=(onionkey or onionhostname and 'ED25519-V3' or 'BEST') + ) + + if not response.is_ok(): + logwrite('Bad response from controller ):') + return + + if not onionkey: + logwrite('Started hidden service %s.onion' % response.service_id) + # only save new service keys + # if onionhostname was not set previously + if not onionhostname: + onionhostname = response.service_id + '.onion' + config.set( + 'bitmessagesettings', 'onionhostname', onionhostname) + config.add_section(onionhostname) + config.set( + onionhostname, 'privsigningkey', response.private_key) + config.set( + onionhostname, 'keytype', response.private_key_type) + config.save() + + return True diff --git a/src/plugins/sound_canberra.py b/src/plugins/sound_canberra.py index 094901ed56..9fea8197c8 100644 --- a/src/plugins/sound_canberra.py +++ b/src/plugins/sound_canberra.py @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- - -from pybitmessage.bitmessageqt import sound +""" +Sound theme plugin using pycanberra +""" import pycanberra +from pybitmessage.bitmessageqt import sound _canberra = pycanberra.Canberra() @@ -14,7 +16,8 @@ } -def connect_plugin(category, label=None): +def connect_plugin(category, label=None): # pylint: disable=unused-argument + """This function implements the entry point.""" try: _canberra.play(0, pycanberra.CA_PROP_EVENT_ID, _theme[category], None) except (KeyError, pycanberra.CanberraException): diff --git a/src/plugins/sound_gstreamer.py b/src/plugins/sound_gstreamer.py index 062da3f9df..8f3606dd87 100644 --- a/src/plugins/sound_gstreamer.py +++ b/src/plugins/sound_gstreamer.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- - +""" +Sound notification plugin using gstreamer +""" import gi gi.require_version('Gst', '1.0') from gi.repository import Gst # noqa: E402 @@ -9,6 +11,7 @@ def connect_plugin(sound_file): + """Entry point for sound file""" _player.set_state(Gst.State.NULL) _player.set_property("uri", "file://" + sound_file) _player.set_state(Gst.State.PLAYING) diff --git a/src/plugins/sound_playfile.py b/src/plugins/sound_playfile.py index c8216d07d7..c6b70f66b1 100644 --- a/src/plugins/sound_playfile.py +++ b/src/plugins/sound_playfile.py @@ -1,24 +1,28 @@ # -*- coding: utf-8 -*- - +""" +Sound notification plugin using external executable or winsound (on Windows) +""" try: import winsound def connect_plugin(sound_file): + """Plugin's entry point""" 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): - global play_cmd + """This function implements the entry point.""" + global play_cmd # pylint: disable=global-statement ext = os.path.splitext(sound_file)[-1] try: diff --git a/src/proofofwork.py b/src/proofofwork.py index df6ed29571..539db710b2 100644 --- a/src/proofofwork.py +++ b/src/proofofwork.py @@ -1,85 +1,153 @@ -#import shared -#import time -#from multiprocessing import Pool, cpu_count +""" +Proof of work calculation +""" +# pylint: disable=import-outside-toplevel + +import ctypes import hashlib -from struct import unpack, pack -from subprocess import call +import os +import subprocess # nosec B404 import sys +import tempfile import time -from bmconfigparser import BMConfigParser -from debug import logger -import paths +from struct import pack, unpack + +import highlevelcrypto import openclpow +import paths import queues -import tr -import os -import ctypes - import state +from bmconfigparser import config +from debug import logger +from defaults import ( + networkDefaultProofOfWorkNonceTrialsPerByte, + networkDefaultPayloadLengthExtraBytes) +from tr import _translate -bitmsglib = 'bitmsghash.so' +bitmsglib = 'bitmsghash.so' bmpow = None + +class LogOutput(object): # pylint: disable=too-few-public-methods + """ + A context manager that block stdout for its scope + and appends it's content to log before exit. Usage:: + + with LogOutput(): + os.system('ls -l') + + https://stackoverflow.com/questions/5081657 + """ + + def __init__(self, prefix='PoW'): + self.prefix = prefix + try: + sys.stdout.flush() + self._stdout = sys.stdout + self._stdout_fno = os.dup(sys.stdout.fileno()) + except AttributeError: + # NullWriter instance has no attribute 'fileno' on Windows + self._stdout = None + else: + self._dst, self._filepath = tempfile.mkstemp() + + def __enter__(self): + if not self._stdout: + return + stdout = os.dup(1) + os.dup2(self._dst, 1) + os.close(self._dst) + sys.stdout = os.fdopen(stdout, 'w') + + def __exit__(self, exc_type, exc_val, exc_tb): + if not self._stdout: + return + sys.stdout.close() + sys.stdout = self._stdout + sys.stdout.flush() + os.dup2(self._stdout_fno, 1) + + with open(self._filepath) as out: + for line in out: + logger.info('%s: %s', self.prefix, line) + os.remove(self._filepath) + + def _set_idle(): if 'linux' in sys.platform: os.nice(20) else: try: + # pylint: disable=no-member,import-error sys.getwindowsversion() - import win32api,win32process,win32con # @UnresolvedImport - pid = win32api.GetCurrentProcessId() - handle = win32api.OpenProcess(win32con.PROCESS_ALL_ACCESS, True, pid) - win32process.SetPriorityClass(handle, win32process.IDLE_PRIORITY_CLASS) - except: - #Windows 64-bit + import win32api + import win32process + import win32con + + handle = win32api.OpenProcess( + win32con.PROCESS_ALL_ACCESS, True, + win32api.GetCurrentProcessId()) + win32process.SetPriorityClass( + handle, win32process.IDLE_PRIORITY_CLASS) + 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]) - return [trialValue, nonce] + trialValue = trial_value(nonce, initialHash) + return trialValue, nonce + def _doSafePoW(target, initialHash): - logger.debug("Safe PoW start") + logger.debug('Safe PoW start') nonce = 0 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") - logger.debug("Safe PoW done") - return [trialValue, nonce] + logger.debug('Safe PoW done') + return trialValue, nonce + def _doFastPoW(target, initialHash): - logger.debug("Fast PoW start") + # pylint:disable=bare-except + logger.debug('Fast PoW start') from multiprocessing import Pool, cpu_count try: pool_size = cpu_count() - except: + except: # noqa:E722 pool_size = 4 - try: - maxCores = BMConfigParser().getint('bitmessagesettings', 'maxcores') - except: - maxCores = 99999 - if pool_size > maxCores: - pool_size = maxCores + maxCores = config.safeGetInt('bitmessagesettings', 'maxcores', 99999) + pool_size = min(pool_size, maxCores) pool = Pool(processes=pool_size) result = [] for i in range(pool_size): - result.append(pool.apply_async(_pool_worker, args=(i, initialHash, target, pool_size))) + result.append(pool.apply_async( + _pool_worker, args=(i, initialHash, target, pool_size))) while True: - if state.shutdown > 0: + if state.shutdown != 0: try: pool.terminate() pool.join() - except: + except: # nosec B110 # noqa:E722 pass raise StopIteration("Interrupted") for i in range(pool_size): @@ -93,203 +161,245 @@ def _doFastPoW(target, initialHash): result = result[i].get() pool.terminate() pool.join() - logger.debug("Fast PoW done") + logger.debug('Fast PoW done') return result[0], result[1] time.sleep(0.2) - + + def _doCPoW(target, initialHash): - h = initialHash - m = target - out_h = ctypes.pointer(ctypes.create_string_buffer(h, 64)) - out_m = ctypes.c_ulonglong(m) - 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]) + with LogOutput(): + h = initialHash + m = target + out_h = ctypes.pointer(ctypes.create_string_buffer(h, 64)) + out_m = ctypes.c_ulonglong(m) + logger.debug('C PoW start') + nonce = bmpow(out_h, out_m) + + trialValue = trial_value(nonce, initialHash) if state.shutdown != 0: raise StopIteration("Interrupted") - logger.debug("C PoW done") - return [trialValue, nonce] + logger.debug('C PoW done') + return trialValue, nonce + def _doGPUPoW(target, initialHash): - logger.debug("GPU PoW start") + 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]) - #print "{} - value {} < {}".format(nonce, trialValue, target) + trialValue = trial_value(nonce, initialHash) if trialValue > target: deviceNames = ", ".join(gpu.name for gpu in openclpow.enabledGpus) - queues.UISignalQueue.put(('updateStatusBar', (tr._translate("MainWindow",'Your GPU(s) did not calculate correctly, disabling OpenCL. Please report to the developers.'), 1))) - logger.error("Your GPUs (%s) did not calculate correctly, disabling OpenCL. Please report to the developers.", deviceNames) + queues.UISignalQueue.put(( + 'updateStatusBar', ( + _translate( + "MainWindow", + "Your GPU(s) did not calculate correctly," + " disabling OpenCL. Please report to the developers." + ), 1) + )) + logger.error( + 'Your GPUs (%s) did not calculate correctly, disabling OpenCL.' + ' Please report to the developers.', deviceNames) openclpow.enabledGpus = [] raise Exception("GPU did not calculate correctly.") if state.shutdown != 0: raise StopIteration("Interrupted") - logger.debug("GPU PoW done") - return [trialValue, nonce] - -def estimate(difficulty, format = False): - ret = difficulty / 10 - if ret < 1: - ret = 1 - if format: - out = str(int(ret)) + " seconds" - if ret > 60: - ret /= 60 - out = str(int(ret)) + " minutes" - if ret > 60: - ret /= 60 - out = str(int(ret)) + " hours" - if ret > 24: - ret /= 24 - out = str(int(ret)) + " days" - if ret > 7: - out = str(int(ret)) + " weeks" - if ret > 31: - out = str(int(ret)) + " months" - if ret > 366: - ret /= 366 - out = str(int(ret)) + " years" - else: - return ret + logger.debug('GPU PoW done') + return trialValue, nonce + + +# def estimate(difficulty, fmt=False): +# ret = difficulty / 10 +# if ret < 1: +# ret = 1 +# +# if fmt: +# out = str(int(ret)) + " seconds" +# if ret > 60: +# ret /= 60 +# out = str(int(ret)) + " minutes" +# if ret > 60: +# ret /= 60 +# out = str(int(ret)) + " hours" +# if ret > 24: +# ret /= 24 +# out = str(int(ret)) + " days" +# if ret > 7: +# out = str(int(ret)) + " weeks" +# if ret > 31: +# out = str(int(ret)) + " months" +# if ret > 366: +# ret /= 366 +# out = str(int(ret)) + " years" +# ret = None # Ensure legacy behaviour +# +# return ret + def getPowType(): + """Get the proof of work implementation""" + if openclpow.openclEnabled(): return "OpenCL" if bmpow: return "C" return "python" + def notifyBuild(tried=False): + """ + Notify the user of the success or otherwise of building the PoW C module + """ + if bmpow: - queues.UISignalQueue.put(('updateStatusBar', (tr._translate("proofofwork", "C PoW module built successfully."), 1))) + queues.UISignalQueue.put(('updateStatusBar', (_translate( + "proofofwork", "C PoW module built successfully."), 1))) elif tried: - queues.UISignalQueue.put(('updateStatusBar', (tr._translate("proofofwork", "Failed to build C PoW module. Please build it manually."), 1))) + queues.UISignalQueue.put(('updateStatusBar', (_translate( + "proofofwork", + "Failed to build C PoW module. Please build it manually."), 1))) else: - queues.UISignalQueue.put(('updateStatusBar', (tr._translate("proofofwork", "C PoW module unavailable. Please build it."), 1))) + queues.UISignalQueue.put(('updateStatusBar', (_translate( + "proofofwork", "C PoW module unavailable. Please build it."), 1))) + def buildCPoW(): + """Attempt to build the PoW C module""" if bmpow is not None: return - if paths.frozen is not None: - notifyBuild(False) - return - if sys.platform in ["win32", "win64"]: + if paths.frozen or sys.platform.startswith('win'): notifyBuild(False) return + try: + # GNU make + make_cmd = ['make', '-C', os.path.join(paths.codePath(), 'bitmsghash')] if "bsd" in sys.platform: # BSD make - call(["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")): + make_cmd += ['-f', 'Makefile.bsd'] + + subprocess.check_call(make_cmd) # nosec B603 + if os.path.exists( + os.path.join(paths.codePath(), 'bitmsghash', 'bitmsghash.so') + ): init() - notifyBuild(True) - else: - notifyBuild(True) - except: - notifyBuild(True) + except (OSError, subprocess.CalledProcessError): + pass + except: # noqa:E722 + logger.warning( + 'Unexpected exception rised when tried to build bitmsghash lib', + exc_info=True) + notifyBuild(True) + def run(target, initialHash): + """Run the proof of work calculation""" + if state.shutdown != 0: - raise + raise StopIteration("Interrupted") target = int(target) if openclpow.openclEnabled(): -# trialvalue1, nonce1 = _doGPUPoW(target, initialHash) -# trialvalue, nonce = _doFastPoW(target, initialHash) -# print "GPU: %s, %s" % (trialvalue1, nonce1) -# print "Fast: %s, %s" % (trialvalue, nonce) -# return [trialvalue, nonce] - try: - return _doGPUPoW(target, initialHash) - except StopIteration: - raise - except: - pass # fallback + return _doGPUPoW(target, initialHash) if bmpow: - try: - return _doCPoW(target, initialHash) - except StopIteration: - raise - except: - pass # fallback + return _doCPoW(target, initialHash) if paths.frozen == "macosx_app" or not paths.frozen: # on my (Peter Surda) Windows 10, Windows Defender # does not like this and fights with PyBitmessage # over CPU, resulting in very slow PoW # added on 2015-11-29: multiprocesing.freeze_support() doesn't help - try: - return _doFastPoW(target, initialHash) - except StopIteration: - logger.error("Fast PoW got StopIteration") - raise - except: - logger.error("Fast PoW got exception:", exc_info=True) - pass #fallback - try: - return _doSafePoW(target, initialHash) - except StopIteration: - raise - except: - pass #fallback + return _doFastPoW(target, initialHash) + + return _doSafePoW(target, initialHash) + + +def getTarget(payloadLength, ttl, nonceTrialsPerByte, payloadLengthExtraBytes): + """Get PoW target for given length, ttl and difficulty params""" + return 2 ** 64 / ( + nonceTrialsPerByte * ( + payloadLength + 8 + payloadLengthExtraBytes + (( + ttl * ( + payloadLength + 8 + payloadLengthExtraBytes + )) / (2 ** 16)) + )) + + +def calculate( + payload, ttl, + nonceTrialsPerByte=networkDefaultProofOfWorkNonceTrialsPerByte, + payloadLengthExtraBytes=networkDefaultPayloadLengthExtraBytes +): + """Do the PoW for the payload and TTL with optional difficulty params""" + return run(getTarget( + len(payload), ttl, nonceTrialsPerByte, payloadLengthExtraBytes), + hashlib.sha512(payload).digest()) + def resetPoW(): + """Initialise the OpenCL PoW""" openclpow.initCL() + # init + + def init(): - global bitmsglib, bso, bmpow + """Initialise PoW""" + # pylint: disable=broad-exception-caught,global-statement + global bitmsglib, bmpow openclpow.initCL() - - if "win32" == sys.platform: - if ctypes.sizeof(ctypes.c_voidp) == 4: - bitmsglib = 'bitmsghash32.dll' - else: - bitmsglib = 'bitmsghash64.dll' + if sys.platform.startswith('win'): + bitmsglib = ( + 'bitmsghash32.dll' if ctypes.sizeof(ctypes.c_voidp) == 4 else + 'bitmsghash64.dll') + libfile = os.path.join(paths.codePath(), 'bitmsghash', bitmsglib) try: # MSVS - bso = ctypes.WinDLL(os.path.join(paths.codePath(), "bitmsghash", bitmsglib)) - logger.info("Loaded C PoW DLL (stdcall) %s", bitmsglib) + bso = ctypes.WinDLL( + os.path.join(paths.codePath(), 'bitmsghash', bitmsglib)) + logger.info('Loaded C PoW DLL (stdcall) %s', bitmsglib) bmpow = bso.BitmessagePOW bmpow.restype = ctypes.c_ulonglong _doCPoW(2**63, "") - logger.info("Successfully tested C PoW DLL (stdcall) %s", bitmsglib) - except: - logger.error("C PoW test fail.", exc_info=True) + logger.info( + 'Successfully tested C PoW DLL (stdcall) %s', bitmsglib) + except ValueError: try: # MinGW - bso = ctypes.CDLL(os.path.join(paths.codePath(), "bitmsghash", bitmsglib)) - logger.info("Loaded C PoW DLL (cdecl) %s", bitmsglib) + bso = ctypes.CDLL(libfile) + logger.info('Loaded C PoW DLL (cdecl) %s', bitmsglib) bmpow = bso.BitmessagePOW bmpow.restype = ctypes.c_ulonglong _doCPoW(2**63, "") - logger.info("Successfully tested C PoW DLL (cdecl) %s", bitmsglib) - except: - logger.error("C PoW test fail.", exc_info=True) - bso = None + logger.info( + 'Successfully tested C PoW DLL (cdecl) %s', bitmsglib) + except Exception as e: + logger.error('Error: %s', e, exc_info=True) + except Exception as e: + logger.error('Error: %s', e, exc_info=True) else: try: - bso = ctypes.CDLL(os.path.join(paths.codePath(), "bitmsghash", bitmsglib)) + bso = ctypes.CDLL( + os.path.join(paths.codePath(), 'bitmsghash', bitmsglib)) except OSError: import glob try: bso = ctypes.CDLL(glob.glob(os.path.join( - paths.codePath(), "bitmsghash", "bitmsghash*.so" + paths.codePath(), 'bitmsghash', 'bitmsghash*.so' ))[0]) except (OSError, IndexError): bso = None - except: + except Exception: bso = None else: - logger.info("Loaded C PoW DLL %s", bitmsglib) - if bso: - try: - bmpow = bso.BitmessagePOW - bmpow.restype = ctypes.c_ulonglong - except: - bmpow = None - else: - bmpow = None + logger.info('Loaded C PoW DLL %s', bitmsglib) + if bso: + try: + bmpow = bso.BitmessagePOW + bmpow.restype = ctypes.c_ulonglong + except Exception: + logger.warning( + 'Failed to setup bmpow lib %s', bso, exc_info=True) + return + if bmpow is None: buildCPoW() diff --git a/src/protocol.py b/src/protocol.py index dca4c94276..257797bce8 100644 --- a/src/protocol.py +++ b/src/protocol.py @@ -1,99 +1,176 @@ +""" +Low-level protocol-related functions. +""" +# pylint: disable=too-many-boolean-expressions,too-many-return-statements +# pylint: disable=too-many-locals,too-many-statements + import base64 -from binascii import hexlify import hashlib import random import socket -import ssl -from struct import pack, unpack, Struct import sys import time -import traceback +from binascii import hexlify +from struct import Struct, pack, unpack -from addresses import calculateInventoryHash, encodeVarint, decodeVarint, decodeAddress, varintDecodeError -from bmconfigparser import BMConfigParser -from debug import logger import defaults -from helper_sql import sqlExecute import highlevelcrypto -from inventory import Inventory -from queues import objectProcessorQueue import state +from addresses import (decodeAddress, decodeVarint, encodeVarint, + varintDecodeError) +from bmconfigparser import config +from debug import logger +from helper_sql import sqlExecute +from network.node import Peer from version import softwareVersion -#Service flags +# 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 +#: This node supports SSL/TLS in the current connect (python < 2.7.9 +#: only supports an SSL client, so in that case it would only have this +#: on when the connection is a client). NODE_SSL = 2 +# (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) +# NODE_POW = 4 +#: Node supports dandelion NODE_DANDELION = 8 -#Bitfield flags +# Bitfield flags BITFIELD_DOESACK = 1 -#Error types +# Error types STATUS_WARNING = 0 STATUS_ERROR = 1 STATUS_FATAL = 2 -#Object types +# Object types OBJECT_GETPUBKEY = 0 OBJECT_PUBKEY = 1 OBJECT_MSG = 2 OBJECT_BROADCAST = 3 +OBJECT_ONIONPEER = 0x746f72 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 +# Compiled struct for packing/unpacking headers +# New code should use CreatePacket instead of Header.pack Header = Struct('!L12sL4s') VersionPacket = Struct('>LqQ20s4s36sH') # Bitfield + def getBitfield(address): + """Get a bitfield from an 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) + def checkBitfield(bitfieldBinary, flags): + """Check if a bitfield matches the given flags""" bitfield, = unpack('>I', bitfieldBinary) return (bitfield & flags) == flags + def isBitSetWithinBitfield(fourByteString, n): + """Check if a particular bit is set in a bitfeld""" # Uses MSB 0 bit numbering across 4 bytes of data n = 31 - n x, = unpack('>L', fourByteString) return x & 2**n != 0 -# ip addresses +# Streams + + +MIN_VALID_STREAM = 1 +MAX_VALID_STREAM = 2**63 - 1 + +# IP addresses + def encodeHost(host): - if host.find('.onion') > -1: - return '\xfd\x87\xd8\x7e\xeb\x43' + base64.b32decode(host.split(".")[0], True) + """Encode a given host to be used in low-level socket operations""" + if host.endswith('.onion'): + return b'\xfd\x87\xd8\x7e\xeb\x43' + base64.b32decode( + host.split(".")[0], True) elif host.find(':') == -1: - return '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFF' + \ + return b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFF' + \ socket.inet_aton(host) - else: - return socket.inet_pton(socket.AF_INET6, host) + return socket.inet_pton(socket.AF_INET6, host) + def networkType(host): - if host.find('.onion') > -1: + """Determine if a host is IPv4, IPv6 or an onion address""" + if host.endswith('.onion'): return 'onion' elif host.find(':') == -1: return 'IPv4' + return 'IPv6' + + +def network_group(host): + """Canonical identifier of network group + simplified, borrowed from + GetGroup() in src/netaddresses.cpp in bitcoin core""" + if not isinstance(host, str): + return None + network_type = networkType(host) + try: + raw_host = encodeHost(host) + except socket.error: + return host + if network_type == 'IPv4': + decoded_host = checkIPv4Address(raw_host[12:], True) + if decoded_host: + # /16 subnet + return raw_host[12:14] + elif network_type == 'IPv6': + decoded_host = checkIPv6Address(raw_host, True) + if decoded_host: + # /32 subnet + return raw_host[0:12] else: - return 'IPv6' + # just host, e.g. for tor + return host + # global network type group for local, private, unroutable + return network_type + def checkIPAddress(host, private=False): - if host[0:12] == '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFF': + """ + Returns hostStandardFormat if it is a valid IP address, + otherwise returns False + """ + if host[0:12] == b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFF': hostStandardFormat = socket.inet_ntop(socket.AF_INET, host[12:]) return checkIPv4Address(host[12:], hostStandardFormat, private) - elif host[0:6] == '\xfd\x87\xd8\x7e\xeb\x43': + elif host[0:6] == b'\xfd\x87\xd8\x7e\xeb\x43': # Onion, based on BMD/bitcoind hostStandardFormat = base64.b32encode(host[6:]).lower() + ".onion" if private: @@ -104,140 +181,227 @@ def checkIPAddress(host, private=False): hostStandardFormat = socket.inet_ntop(socket.AF_INET6, host) except ValueError: return False - if hostStandardFormat == "": - # This can happen on Windows systems which are not 64-bit compatible - # so let us drop the IPv6 address. + if len(hostStandardFormat) == 0: + # This can happen on Windows systems which are + # not 64-bit compatible so let us drop the IPv6 address. return False return checkIPv6Address(host, hostStandardFormat, private) + def checkIPv4Address(host, hostStandardFormat, private=False): - if host[0] == '\x7F': # 127/8 + """ + Returns hostStandardFormat if it is an IPv4 address, + otherwise returns False + """ + if host[0:1] == b'\x7F': # 127/8 if not private: - logger.debug('Ignoring IP address in loopback range: ' + hostStandardFormat) + logger.debug( + 'Ignoring IP address in loopback range: %s', + hostStandardFormat) return hostStandardFormat if private else False - if host[0] == '\x0A': # 10/8 + if host[0:1] == b'\x0A': # 10/8 if not private: - logger.debug('Ignoring IP address in private range: ' + hostStandardFormat) + logger.debug( + 'Ignoring IP address in private range: %s', hostStandardFormat) return hostStandardFormat if private else False - if host[0:2] == '\xC0\xA8': # 192.168/16 + if host[0:2] == b'\xC0\xA8': # 192.168/16 if not private: - logger.debug('Ignoring IP address in private range: ' + hostStandardFormat) + logger.debug( + 'Ignoring IP address in private range: %s', hostStandardFormat) return hostStandardFormat if private else False - if host[0:2] >= '\xAC\x10' and host[0:2] < '\xAC\x20': # 172.16/12 + if host[0:2] >= b'\xAC\x10' and host[0:2] < b'\xAC\x20': # 172.16/12 if not private: - logger.debug('Ignoring IP address in private range:' + hostStandardFormat) + logger.debug( + 'Ignoring IP address in private range: %s', hostStandardFormat) return hostStandardFormat if private else False return False if private else hostStandardFormat + def checkIPv6Address(host, hostStandardFormat, private=False): - if host == ('\x00' * 15) + '\x01': + """ + Returns hostStandardFormat if it is an IPv6 address, + otherwise returns False + """ + if host == b'\x00' * 15 + b'\x01': if not private: - logger.debug('Ignoring loopback address: ' + hostStandardFormat) + logger.debug('Ignoring loopback address: %s', hostStandardFormat) return False - if host[0] == '\xFE' and (ord(host[1]) & 0xc0) == 0x80: + try: + host = [ord(c) for c in host[:2]] + except TypeError: # python3 has ints already + pass + if host[0] == 0xfe and host[1] & 0xc0 == 0x80: if not private: - logger.debug ('Ignoring local address: ' + hostStandardFormat) + logger.debug('Ignoring local address: %s', hostStandardFormat) return hostStandardFormat if private else False - if (ord(host[0]) & 0xfe) == 0xfc: + if host[0] & 0xfe == 0xfc: if not private: - logger.debug ('Ignoring unique local address: ' + hostStandardFormat) + logger.debug( + 'Ignoring unique local address: %s', hostStandardFormat) return hostStandardFormat if private else False return False if private else hostStandardFormat -# checks -def haveSSL(server = False): - # python < 2.7.9's ssl library does not support ECDSA server due to missing initialisation of available curves, but client works ok - if server == False: +def haveSSL(server=False): + """ + Predicate to check if ECDSA server support is required and available + + python < 2.7.9's ssl library does not support ECDSA server due to + missing initialisation of available curves, but client works ok + """ + if not server: return True - elif sys.version_info >= (2,7,9): + elif sys.version_info >= (2, 7, 9): return True return False + def checkSocksIP(host): + """Predicate to check if we're using a SOCKS proxy""" + sockshostname = config.safeGet( + 'bitmessagesettings', 'sockshostname') try: - if state.socksIP is None or not state.socksIP: - state.socksIP = socket.gethostbyname(BMConfigParser().get("bitmessagesettings", "sockshostname")) - # uninitialised - except NameError: - state.socksIP = socket.gethostbyname(BMConfigParser().get("bitmessagesettings", "sockshostname")) - # resolving failure - except socket.gaierror: - state.socksIP = BMConfigParser().get("bitmessagesettings", "sockshostname") + if not state.socksIP: + state.socksIP = socket.gethostbyname(sockshostname) + except NameError: # uninitialised + state.socksIP = socket.gethostbyname(sockshostname) + except (TypeError, socket.gaierror): # None, resolving failure + state.socksIP = sockshostname return state.socksIP == host -def isProofOfWorkSufficient(data, - nonceTrialsPerByte=0, - payloadLengthExtraBytes=0): + +def isProofOfWorkSufficient( + data, nonceTrialsPerByte=0, payloadLengthExtraBytes=0, recvTime=0): + """ + Validate an object's Proof of Work using method described + :doc:`here ` + + Arguments: + int nonceTrialsPerByte (default: from `.defaults`) + int payloadLengthExtraBytes (default: from `.defaults`) + float recvTime (optional) UNIX epoch time when object was + received from the network (default: current system time) + Returns: + True if PoW valid and sufficient, False in all other cases + """ if nonceTrialsPerByte < defaults.networkDefaultProofOfWorkNonceTrialsPerByte: nonceTrialsPerByte = defaults.networkDefaultProofOfWorkNonceTrialsPerByte if payloadLengthExtraBytes < defaults.networkDefaultPayloadLengthExtraBytes: payloadLengthExtraBytes = defaults.networkDefaultPayloadLengthExtraBytes endOfLifeTime, = unpack('>Q', data[8:16]) - TTL = endOfLifeTime - 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]) - return POW <= 2 ** 64 / (nonceTrialsPerByte*(len(data) + payloadLengthExtraBytes + ((TTL*(len(data)+payloadLengthExtraBytes))/(2 ** 16)))) + POW, = unpack('>Q', highlevelcrypto.double_sha512( + data[:8] + hashlib.sha512(data[8:]).digest())[0:8]) + return POW <= 2 ** 64 / ( + nonceTrialsPerByte * ( + len(data) + payloadLengthExtraBytes + + ((TTL * (len(data) + payloadLengthExtraBytes)) / (2 ** 16)))) + # Packet creation -def CreatePacket(command, payload=''): + +def CreatePacket(command, payload=b''): + """Construct and return a packet""" payload_length = len(payload) 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 assembleVersionMessage(remoteHost, remotePort, participatingStreams, server = False, nodeid = None): - payload = '' + +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, + dandelion_enabled=True, server=False, nodeid=None +): + """ + Construct the payload of a version message, + return the resulting bytes of running `CreatePacket` on it + """ + payload = b'' payload += pack('>L', 3) # protocol version. # bitflags of the services I offer. - payload += pack('>q', - NODE_NETWORK | - (NODE_SSL if haveSSL(server) else 0) | - (NODE_DANDELION if state.dandelion else 0) - ) + payload += pack( + '>q', + NODE_NETWORK + | (NODE_SSL if haveSSL(server) else 0) + | (NODE_DANDELION if dandelion_enabled else 0) + ) payload += pack('>q', int(time.time())) - payload += pack( - '>q', 1) # boolservices of remote connection; ignored by the remote host. - if checkSocksIP(remoteHost) and server: # prevent leaking of tor outbound IP + # boolservices of remote connection; ignored by the remote host. + payload += pack('>q', 1) + if checkSocksIP(remoteHost) and server: + # prevent leaking of tor outbound IP payload += encodeHost('127.0.0.1') payload += pack('>H', 8444) else: - payload += encodeHost(remoteHost) + # use first 16 bytes if host data is longer + # for example in case of onion v3 service + try: + payload += encodeHost(remoteHost)[:16] + except socket.error: + payload += encodeHost('127.0.0.1') payload += pack('>H', remotePort) # remote IPv6 and port # bitflags of the services I offer. - payload += pack('>q', - NODE_NETWORK | - (NODE_SSL if haveSSL(server) else 0) | - (NODE_DANDELION if state.dandelion else 0) - ) - payload += '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFF' + pack( - '>L', 2130706433) # = 127.0.0.1. This will be ignored by the remote host. The actual remote connected IP will be used. - # we have a separate extPort and - # incoming over clearnet or - # outgoing through clearnet - if BMConfigParser().safeGetBoolean('bitmessagesettings', 'upnp') and state.extPort \ - and ((server and not checkSocksIP(remoteHost)) or \ - (BMConfigParser().get("bitmessagesettings", "socksproxytype") == "none" and not server)): - payload += pack('>H', state.extPort) - elif checkSocksIP(remoteHost) and server: # incoming connection over Tor - payload += pack('>H', BMConfigParser().getint('bitmessagesettings', 'onionport')) - else: # no extPort and not incoming over Tor - payload += pack('>H', BMConfigParser().getint('bitmessagesettings', 'port')) - - random.seed() + payload += pack( + '>q', + NODE_NETWORK + | (NODE_SSL if haveSSL(server) else 0) + | (NODE_DANDELION if dandelion_enabled else 0) + ) + # = 127.0.0.1. This will be ignored by the remote host. + # The actual remote connected IP will be used. + payload += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFF' + pack( + '>L', 2130706433) + # we have a separate extPort and incoming over clearnet + # or outgoing through clearnet + extport = config.safeGetInt('bitmessagesettings', 'extport') + if ( + extport and ((server and not checkSocksIP(remoteHost)) or ( + config.get('bitmessagesettings', 'socksproxytype') + == 'none' and not server)) + ): + payload += pack('>H', extport) + elif checkSocksIP(remoteHost) and server: # incoming connection over Tor + payload += pack( + '>H', config.getint('bitmessagesettings', 'onionport')) + else: # no extport and not incoming over Tor + payload += pack( + '>H', config.getint('bitmessagesettings', 'port')) + if nodeid is not None: payload += nodeid[0:8] else: payload += eightBytesOfRandomDataUsedToDetectConnectionsToSelf - userAgent = '/PyBitmessage:' + softwareVersion + '/' + userAgent = ('/PyBitmessage:%s/' % softwareVersion).encode('utf-8') payload += encodeVarint(len(userAgent)) payload += userAgent @@ -251,75 +415,112 @@ def assembleVersionMessage(remoteHost, remotePort, participatingStreams, server if count >= 160000: break - return CreatePacket('version', payload) + return CreatePacket(b'version', payload) + def assembleErrorMessage(fatal=0, banTime=0, inventoryVector='', errorText=''): + """ + Construct the payload of an error message, + return the resulting bytes of running `CreatePacket` on it + """ payload = encodeVarint(fatal) payload += encodeVarint(banTime) payload += encodeVarint(len(inventoryVector)) payload += inventoryVector payload += encodeVarint(len(errorText)) payload += errorText - return CreatePacket('error', payload) + return CreatePacket(b'error', payload) + # 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 already have the - address to which we want to try to send a message. The 'data' may come either - off of the wire or we might have had it already in our inventory when we tried - to send a msg to this particular address. + Version 4 pubkeys are encrypted. This function is run when we + already have the address to which we want to try to send a message. + The 'data' may come either off of the wire or we might have had it + already in our inventory when we tried to send a msg to this + particular address. """ try: - status, addressVersion, streamNumber, ripe = decodeAddress(address) - + addressVersion, streamNumber, ripe = decodeAddress(address)[1:] + readPosition = 20 # bypass the nonce, time, and object type - embeddedAddressVersion, varintLength = decodeVarint(data[readPosition:readPosition + 10]) + embeddedAddressVersion, varintLength = decodeVarint( + data[readPosition:readPosition + 10]) readPosition += varintLength - embeddedStreamNumber, varintLength = decodeVarint(data[readPosition:readPosition + 10]) + embeddedStreamNumber, varintLength = decodeVarint( + data[readPosition:readPosition + 10]) readPosition += varintLength - storedData = data[20:readPosition] # We'll store the address version and stream number (and some more) in the pubkeys table. - + # We'll store the address version and stream number + # (and some more) in the pubkeys table. + storedData = data[20:readPosition] + if addressVersion != embeddedAddressVersion: - logger.info('Pubkey decryption was UNsuccessful due to address version mismatch.') + logger.info( + 'Pubkey decryption was UNsuccessful' + ' due to address version mismatch.') return 'failed' if streamNumber != embeddedStreamNumber: - logger.info('Pubkey decryption was UNsuccessful due to stream number mismatch.') + logger.info( + 'Pubkey decryption was UNsuccessful' + ' due to stream number mismatch.') return 'failed' - + tag = data[readPosition:readPosition + 32] readPosition += 32 - signedData = data[8:readPosition] # the time through the tag. More data is appended onto signedData below after the decryption. + # the time through the tag. More data is appended onto + # signedData below after the decryption. + signedData = data[8:readPosition] encryptedData = data[readPosition:] - + # Let us try to decrypt the pubkey toAddress, cryptorObject = state.neededPubkeys[tag] if toAddress != address: - logger.critical('decryptAndCheckPubkeyPayload failed due to toAddress mismatch. This is very peculiar. toAddress: %s, address %s', toAddress, address) - # the only way I can think that this could happen is if someone encodes their address data two different ways. - # That sort of address-malleability should have been caught by the UI or API and an error given to the user. + logger.critical( + 'decryptAndCheckPubkeyPayload failed due to toAddress' + ' mismatch. This is very peculiar.' + ' toAddress: %s, address %s', + toAddress, address + ) + # the only way I can think that this could happen + # is if someone encodes their address data two different ways. + # That sort of address-malleability should have been caught + # by the UI or API and an error given to the user. return 'failed' try: decryptedData = cryptorObject.decrypt(encryptedData) - except: + except: # noqa:E722 + # FIXME: use a proper exception after `pyelliptic.ecc` is refactored. # Someone must have encrypted some data with a different key # but tagged it with a tag for which we are watching. logger.info('Pubkey decryption was unsuccessful.') return 'failed' - + readPosition = 0 - bitfieldBehaviors = decryptedData[readPosition:readPosition + 4] + # 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 - specifiedNonceTrialsPerByte, specifiedNonceTrialsPerByteLength = decodeVarint( - decryptedData[readPosition:readPosition + 10]) + specifiedNonceTrialsPerByteLength = decodeVarint( + decryptedData[readPosition:readPosition + 10])[1] readPosition += specifiedNonceTrialsPerByteLength - specifiedPayloadLengthExtraBytes, specifiedPayloadLengthExtraBytesLength = decodeVarint( - decryptedData[readPosition:readPosition + 10]) + specifiedPayloadLengthExtraBytesLength = decodeVarint( + decryptedData[readPosition:readPosition + 10])[1] readPosition += specifiedPayloadLengthExtraBytesLength storedData += decryptedData[:readPosition] signedData += decryptedData[:readPosition] @@ -327,270 +528,48 @@ def decryptAndCheckPubkeyPayload(data, address): decryptedData[readPosition:readPosition + 10]) readPosition += signatureLengthLength signature = decryptedData[readPosition:readPosition + signatureLength] - - if highlevelcrypto.verify(signedData, signature, hexlify(publicSigningKey)): - logger.info('ECDSA verify passed (within decryptAndCheckPubkeyPayload)') - else: - logger.info('ECDSA verify failed (within decryptAndCheckPubkeyPayload)') + + if not highlevelcrypto.verify( + signedData, signature, hexlify(pubSigningKey)): + logger.info( + 'ECDSA verify failed (within decryptAndCheckPubkeyPayload)') return 'failed' - - sha = hashlib.new('sha512') - sha.update(publicSigningKey + publicEncryptionKey) - ripeHasher = hashlib.new('ripemd160') - ripeHasher.update(sha.digest()) - embeddedRipe = ripeHasher.digest() - + + logger.info( + 'ECDSA verify passed (within decryptAndCheckPubkeyPayload)') + + embeddedRipe = highlevelcrypto.to_ripe(pubSigningKey, pubEncryptionKey) + if embeddedRipe != ripe: - # Although this pubkey object had the tag were were looking for and was - # encrypted with the correct encryption key, it doesn't contain the - # correct pubkeys. Someone is either being malicious or using buggy software. - logger.info('Pubkey decryption was UNsuccessful due to RIPE mismatch.') + # Although this pubkey object had the tag were were looking for + # and was encrypted with the correct encryption key, + # it doesn't contain the correct pubkeys. Someone is + # either being malicious or using buggy software. + logger.info( + 'Pubkey decryption was UNsuccessful due to RIPE mismatch.') return 'failed' - + # Everything checked out. Insert it into the pubkeys table. - - logger.info('within decryptAndCheckPubkeyPayload, addressVersion: %s, streamNumber: %s \n\ - ripe %s\n\ - publicSigningKey in hex: %s\n\ - publicEncryptionKey in hex: %s', addressVersion, - streamNumber, - hexlify(ripe), - hexlify(publicSigningKey), - hexlify(publicEncryptionKey) - ) - + + logger.info( + 'within decryptAndCheckPubkeyPayload, ' + 'addressVersion: %s, streamNumber: %s\nripe %s\n' + 'publicSigningKey in hex: %s\npublicEncryptionKey in hex: %s', + addressVersion, streamNumber, hexlify(ripe), + hexlify(pubSigningKey), hexlify(pubEncryptionKey) + ) + t = (address, addressVersion, storedData, int(time.time()), 'yes') sqlExecute('''INSERT INTO pubkeys VALUES (?,?,?,?,?)''', *t) return 'successful' - except varintDecodeError as e: - logger.info('Pubkey decryption was UNsuccessful due to a malformed varint.') + except varintDecodeError: + logger.info( + 'Pubkey decryption was UNsuccessful due to a malformed varint.') return 'failed' - except Exception as e: - logger.critical('Pubkey decryption was UNsuccessful because of an unhandled exception! This is definitely a bug! \n%s', traceback.format_exc()) + except Exception: + logger.critical( + 'Pubkey decryption was UNsuccessful because of' + ' an unhandled exception! This is definitely a bug!', + exc_info=True + ) return 'failed' - -def checkAndShareObjectWithPeers(data): - """ - This function is called after either receiving an object off of the wire - or after receiving one as ackdata. - Returns the length of time that we should reserve to process this message - if we are receiving it off of the wire. - """ - if len(data) > 2 ** 18: - logger.info('The payload length of this object is too large (%s bytes). Ignoring it.', len(data)) - return 0 - # Let us check to make sure that the proof of work is sufficient. - if not isProofOfWorkSufficient(data): - logger.info('Proof of work is insufficient.') - return 0 - - endOfLifeTime, = unpack('>Q', data[8:16]) - if endOfLifeTime - int(time.time()) > 28 * 24 * 60 * 60 + 10800: # The TTL may not be larger than 28 days + 3 hours of wiggle room - logger.info('This object\'s End of Life time is too far in the future. Ignoring it. Time is %s', endOfLifeTime) - return 0 - if endOfLifeTime - int(time.time()) < - 3600: # The EOL time was more than an hour ago. That's too much. - logger.info('This object\'s End of Life time was more than an hour ago. Ignoring the object. Time is %s', endOfLifeTime) - return 0 - intObjectType, = unpack('>I', data[16:20]) - try: - if intObjectType == 0: - _checkAndShareGetpubkeyWithPeers(data) - return 0.1 - elif intObjectType == 1: - _checkAndSharePubkeyWithPeers(data) - return 0.1 - elif intObjectType == 2: - _checkAndShareMsgWithPeers(data) - return 0.6 - elif intObjectType == 3: - _checkAndShareBroadcastWithPeers(data) - return 0.6 - else: - _checkAndShareUndefinedObjectWithPeers(data) - return 0.6 - except varintDecodeError as e: - logger.debug("There was a problem with a varint while checking to see whether it was appropriate to share an object with peers. Some details: %s", e) - except Exception as e: - logger.critical('There was a problem while checking to see whether it was appropriate to share an object with peers. This is definitely a bug! \n%s', traceback.format_exc()) - return 0 - - -def _checkAndShareUndefinedObjectWithPeers(data): - embeddedTime, = unpack('>Q', data[8:16]) - readPosition = 20 # bypass nonce, time, and object type - objectVersion, objectVersionLength = decodeVarint( - data[readPosition:readPosition + 9]) - readPosition += objectVersionLength - streamNumber, streamNumberLength = decodeVarint( - data[readPosition:readPosition + 9]) - if not streamNumber in state.streamsInWhichIAmParticipating: - logger.debug('The streamNumber %s isn\'t one we are interested in.', streamNumber) - return - - inventoryHash = calculateInventoryHash(data) - if inventoryHash in Inventory(): - logger.debug('We have already received this undefined object. Ignoring.') - return - objectType, = unpack('>I', data[16:20]) - Inventory()[inventoryHash] = ( - objectType, streamNumber, data, embeddedTime,'') - logger.debug('advertising inv with hash: %s', hexlify(inventoryHash)) - broadcastToSendDataQueues((streamNumber, 'advertiseobject', inventoryHash)) - - -def _checkAndShareMsgWithPeers(data): - embeddedTime, = unpack('>Q', data[8:16]) - readPosition = 20 # bypass nonce, time, and object type - objectVersion, objectVersionLength = decodeVarint( - data[readPosition:readPosition + 9]) - readPosition += objectVersionLength - streamNumber, streamNumberLength = decodeVarint( - data[readPosition:readPosition + 9]) - if not streamNumber in state.streamsInWhichIAmParticipating: - logger.debug('The streamNumber %s isn\'t one we are interested in.', streamNumber) - return - readPosition += streamNumberLength - inventoryHash = calculateInventoryHash(data) - if inventoryHash in Inventory(): - logger.debug('We have already received this msg message. Ignoring.') - return - # This msg message is valid. Let's let our peers know about it. - objectType = 2 - Inventory()[inventoryHash] = ( - objectType, streamNumber, data, embeddedTime,'') - logger.debug('advertising inv with hash: %s', hexlify(inventoryHash)) - broadcastToSendDataQueues((streamNumber, 'advertiseobject', inventoryHash)) - - # Now let's enqueue it to be processed ourselves. - objectProcessorQueue.put((objectType,data)) - -def _checkAndShareGetpubkeyWithPeers(data): - if len(data) < 42: - logger.info('getpubkey message doesn\'t contain enough data. Ignoring.') - return - if len(data) > 200: - logger.info('getpubkey is abnormally long. Sanity check failed. Ignoring object.') - embeddedTime, = unpack('>Q', data[8:16]) - readPosition = 20 # bypass the nonce, time, and object type - requestedAddressVersionNumber, addressVersionLength = decodeVarint( - data[readPosition:readPosition + 10]) - readPosition += addressVersionLength - streamNumber, streamNumberLength = decodeVarint( - data[readPosition:readPosition + 10]) - if not streamNumber in state.streamsInWhichIAmParticipating: - logger.debug('The streamNumber %s isn\'t one we are interested in.', streamNumber) - return - readPosition += streamNumberLength - - inventoryHash = calculateInventoryHash(data) - if inventoryHash in Inventory(): - logger.debug('We have already received this getpubkey request. Ignoring it.') - return - - objectType = 0 - Inventory()[inventoryHash] = ( - objectType, streamNumber, data, embeddedTime,'') - # This getpubkey request is valid. Forward to peers. - logger.debug('advertising inv with hash: %s', hexlify(inventoryHash)) - broadcastToSendDataQueues((streamNumber, 'advertiseobject', inventoryHash)) - - # Now let's queue it to be processed ourselves. - objectProcessorQueue.put((objectType,data)) - -def _checkAndSharePubkeyWithPeers(data): - if len(data) < 146 or len(data) > 440: # sanity check - return - embeddedTime, = unpack('>Q', data[8:16]) - readPosition = 20 # bypass the nonce, time, and object type - addressVersion, varintLength = decodeVarint( - data[readPosition:readPosition + 10]) - readPosition += varintLength - streamNumber, varintLength = decodeVarint( - data[readPosition:readPosition + 10]) - readPosition += varintLength - if not streamNumber in state.streamsInWhichIAmParticipating: - logger.debug('The streamNumber %s isn\'t one we are interested in.', streamNumber) - return - if addressVersion >= 4: - tag = data[readPosition:readPosition + 32] - logger.debug('tag in received pubkey is: %s', hexlify(tag)) - else: - tag = '' - - inventoryHash = calculateInventoryHash(data) - if inventoryHash in Inventory(): - logger.debug('We have already received this pubkey. Ignoring it.') - return - objectType = 1 - Inventory()[inventoryHash] = ( - objectType, streamNumber, data, embeddedTime, tag) - # This object is valid. Forward it to peers. - logger.debug('advertising inv with hash: %s', hexlify(inventoryHash)) - broadcastToSendDataQueues((streamNumber, 'advertiseobject', inventoryHash)) - - - # Now let's queue it to be processed ourselves. - objectProcessorQueue.put((objectType,data)) - - -def _checkAndShareBroadcastWithPeers(data): - if len(data) < 180: - logger.debug('The payload length of this broadcast packet is unreasonably low. Someone is probably trying funny business. Ignoring message.') - return - embeddedTime, = unpack('>Q', data[8:16]) - readPosition = 20 # bypass the nonce, time, and object type - broadcastVersion, broadcastVersionLength = decodeVarint( - data[readPosition:readPosition + 10]) - readPosition += broadcastVersionLength - if broadcastVersion >= 2: - streamNumber, streamNumberLength = decodeVarint(data[readPosition:readPosition + 10]) - readPosition += streamNumberLength - if not streamNumber in state.streamsInWhichIAmParticipating: - logger.debug('The streamNumber %s isn\'t one we are interested in.', streamNumber) - return - if broadcastVersion >= 3: - tag = data[readPosition:readPosition+32] - else: - tag = '' - inventoryHash = calculateInventoryHash(data) - if inventoryHash in Inventory(): - logger.debug('We have already received this broadcast object. Ignoring.') - return - # It is valid. Let's let our peers know about it. - objectType = 3 - Inventory()[inventoryHash] = ( - objectType, streamNumber, data, embeddedTime, tag) - # This object is valid. Forward it to peers. - logger.debug('advertising inv with hash: %s', hexlify(inventoryHash)) - broadcastToSendDataQueues((streamNumber, 'advertiseobject', inventoryHash)) - - # Now let's queue it to be processed ourselves. - objectProcessorQueue.put((objectType,data)) - -# If you want to command all of the sendDataThreads to do something, like shutdown or send some data, this -# function puts your data into the queues for each of the sendDataThreads. The sendDataThreads are -# responsible for putting their queue into (and out of) the sendDataQueues list. -def broadcastToSendDataQueues(data): - # logger.debug('running broadcastToSendDataQueues') - for q in state.sendDataQueues: - q.put(data) - -# sslProtocolVersion -if sys.version_info >= (2,7,13): - # this means TLSv1 or higher - # in the future change to - # ssl.PROTOCOL_TLS1.2 - sslProtocolVersion = ssl.PROTOCOL_TLS -elif sys.version_info >= (2,7,9): - # this means any SSL/TLS. SSLv2 and 3 are excluded with an option after context is created - sslProtocolVersion = ssl.PROTOCOL_SSLv23 -else: - # this means TLSv1, there is no way to set "TLSv1 or higher" or - # "TLSv1.2" in < 2.7.9 - sslProtocolVersion = ssl.PROTOCOL_TLSv1 - -# ciphers -if ssl.OPENSSL_VERSION_NUMBER >= 0x10100000 and not ssl.OPENSSL_VERSION.startswith("LibreSSL"): - sslProtocolCiphers = "AECDH-AES256-SHA@SECLEVEL=0" -else: - sslProtocolCiphers = "AECDH-AES256-SHA" diff --git a/src/pyelliptic/__init__.py b/src/pyelliptic/__init__.py index 761d08af7d..cafa89c9d0 100644 --- a/src/pyelliptic/__init__.py +++ b/src/pyelliptic/__init__.py @@ -1,19 +1,30 @@ -# Copyright (C) 2010 -# Author: Yann GUIBET -# Contact: +""" +Copyright (C) 2010 +Author: Yann GUIBET +Contact: + +Python OpenSSL wrapper. +For modern cryptography with ECC, AES, HMAC, Blowfish, ... + +This is an abandoned package maintained inside of the PyBitmessage. +""" + +from .cipher import Cipher +from .ecc import ECC +from .eccblind import ECCBlind +from .eccblindchain import ECCBlindChain +from .hash import hmac_sha256, hmac_sha512, pbkdf2 +from .openssl import OpenSSL __version__ = '1.3' __all__ = [ 'OpenSSL', 'ECC', + 'ECCBlind', + 'ECCBlindChain', 'Cipher', 'hmac_sha256', 'hmac_sha512', 'pbkdf2' ] - -from .openssl import OpenSSL -from .ecc import ECC -from .cipher import Cipher -from .hash import hmac_sha256, hmac_sha512, pbkdf2 diff --git a/src/pyelliptic/arithmetic.py b/src/pyelliptic/arithmetic.py index 1eec381a9f..23c24b5e0f 100644 --- a/src/pyelliptic/arithmetic.py +++ b/src/pyelliptic/arithmetic.py @@ -1,106 +1,166 @@ -import hashlib, re +""" +Arithmetic Expressions +""" +import hashlib +import re -P = 2**256-2**32-2**9-2**8-2**7-2**6-2**4-1 +P = 2**256 - 2**32 - 2**9 - 2**8 - 2**7 - 2**6 - 2**4 - 1 A = 0 Gx = 55066263022277343669578718895168534326250603453777594175500187360389116729240 Gy = 32670510020758816978083085130507043184471273380659243275938904335757337482424 -G = (Gx,Gy) +G = (Gx, Gy) + + +def inv(a, n): + """Inversion""" + lm, hm = 1, 0 + low, high = a % n, n + while low > 1: + r = high // low + nm, new = hm - lm * r, high - low * r + lm, low, hm, high = nm, new, lm, low + return lm % n -def inv(a,n): - lm, hm = 1,0 - low, high = a%n,n - while low > 1: - r = high/low - nm, new = hm-lm*r, high-low*r - lm, low, hm, high = nm, new, lm, low - return lm % n def get_code_string(base): - if base == 2: return '01' - elif base == 10: return '0123456789' - elif base == 16: return "0123456789abcdef" - elif base == 58: return "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" - elif base == 256: return ''.join([chr(x) for x in range(256)]) - else: raise ValueError("Invalid base!") - -def encode(val,base,minlen=0): - code_string = get_code_string(base) - result = "" - while val > 0: - result = code_string[val % base] + result - val /= base - if len(result) < minlen: - result = code_string[0]*(minlen-len(result))+result - return result - -def decode(string,base): - code_string = get_code_string(base) - result = 0 - if base == 16: string = string.lower() - while len(string) > 0: - result *= base - result += code_string.find(string[0]) - string = string[1:] - return result - -def changebase(string,frm,to,minlen=0): - return encode(decode(string,frm),to,minlen) - -def base10_add(a,b): - if a == None: return b[0],b[1] - if b == None: return a[0],a[1] - if a[0] == b[0]: - if a[1] == b[1]: return base10_double(a[0],a[1]) - else: return None - m = ((b[1]-a[1]) * inv(b[0]-a[0],P)) % P - x = (m*m-a[0]-b[0]) % P - y = (m*(a[0]-x)-a[1]) % P - return (x,y) - + """Returns string according to base value""" + if base == 2: + return b'01' + if base == 10: + return b'0123456789' + if base == 16: + return b'0123456789abcdef' + if base == 58: + return b'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' + if base == 256: + try: + return b''.join([chr(x) for x in range(256)]) + except TypeError: + return bytes([x for x in range(256)]) + + raise ValueError("Invalid base!") + + +def encode(val, base, minlen=0): + """Returns the encoded string""" + code_string = get_code_string(base) + result = b'' + while val > 0: + val, i = divmod(val, base) + result = code_string[i:i + 1] + result + if len(result) < minlen: + result = code_string[0:1] * (minlen - len(result)) + result + return result + + +def decode(string, base): + """Returns the decoded string""" + code_string = get_code_string(base) + result = 0 + if base == 16: + string = string.lower() + while string: + result *= base + result += code_string.find(string[0]) + string = string[1:] + return result + + +def changebase(string, frm, to, minlen=0): + """Change base of the string""" + return encode(decode(string, frm), to, minlen) + + +def base10_add(a, b): + """Adding the numbers that are of base10""" + # pylint: disable=too-many-function-args + if a is None: + return b[0], b[1] + if b is None: + return a[0], a[1] + if a[0] == b[0]: + if a[1] == b[1]: + return base10_double(a[0], a[1]) + return None + m = ((b[1] - a[1]) * inv(b[0] - a[0], P)) % P + x = (m * m - a[0] - b[0]) % P + y = (m * (a[0] - x) - a[1]) % P + return (x, y) + + def base10_double(a): - if a == None: return None - m = ((3*a[0]*a[0]+A)*inv(2*a[1],P)) % P - x = (m*m-2*a[0]) % P - y = (m*(a[0]-x)-a[1]) % P - return (x,y) + """Double the numbers that are of base10""" + if a is None: + return None + m = ((3 * a[0] * a[0] + A) * inv(2 * a[1], P)) % P + x = (m * m - 2 * a[0]) % P + y = (m * (a[0] - x) - a[1]) % P + return (x, y) + + +def base10_multiply(a, n): + """Multiply the numbers that are of base10""" + if n == 0: + return G + if n == 1: + return a + n, m = divmod(n, 2) + if m == 0: + return base10_double(base10_multiply(a, n)) + if m == 1: + return base10_add(base10_double(base10_multiply(a, n)), a) + return None + + +def hex_to_point(h): + """Converting hexadecimal to point value""" + return (decode(h[2:66], 16), decode(h[66:], 16)) -def base10_multiply(a,n): - if n == 0: return G - if n == 1: return a - if (n%2) == 0: return base10_double(base10_multiply(a,n/2)) - if (n%2) == 1: return base10_add(base10_double(base10_multiply(a,n/2)),a) -def hex_to_point(h): return (decode(h[2:66],16),decode(h[66:],16)) +def point_to_hex(p): + """Converting point value to hexadecimal""" + return b'04' + encode(p[0], 16, 64) + encode(p[1], 16, 64) -def point_to_hex(p): return '04'+encode(p[0],16,64)+encode(p[1],16,64) -def multiply(privkey,pubkey): - return point_to_hex(base10_multiply(hex_to_point(pubkey),decode(privkey,16))) +def multiply(privkey, pubkey): + """Multiplying keys""" + return point_to_hex(base10_multiply( + hex_to_point(pubkey), decode(privkey, 16))) + def privtopub(privkey): - return point_to_hex(base10_multiply(G,decode(privkey,16))) + """Converting key from private to public""" + return point_to_hex(base10_multiply(G, decode(privkey, 16))) + + +def add(p1, p2): + """Adding two public keys""" + if len(p1) == 32: + return encode(decode(p1, 16) + decode(p2, 16) % P, 16, 32) + return point_to_hex(base10_add(hex_to_point(p1), hex_to_point(p2))) -def add(p1,p2): - if (len(p1)==32): - return encode(decode(p1,16) + decode(p2,16) % P,16,32) - else: - return point_to_hex(base10_add(hex_to_point(p1),hex_to_point(p2))) def hash_160(string): - intermed = hashlib.sha256(string).digest() - ripemd160 = hashlib.new('ripemd160') - ripemd160.update(intermed) - return ripemd160.digest() + """Hashed version of public key""" + intermed = hashlib.sha256(string).digest() + ripemd160 = hashlib.new('ripemd160') + ripemd160.update(intermed) + return ripemd160.digest() + def dbl_sha256(string): - return hashlib.sha256(hashlib.sha256(string).digest()).digest() - + """Double hashing (SHA256)""" + return hashlib.sha256(hashlib.sha256(string).digest()).digest() + + def bin_to_b58check(inp): - inp_fmtd = '\x00' + inp - leadingzbytes = len(re.match('^\x00*',inp_fmtd).group(0)) - checksum = dbl_sha256(inp_fmtd)[:4] - return '1' * leadingzbytes + changebase(inp_fmtd+checksum,256,58) + """Convert binary to base58""" + inp_fmtd = '\x00' + inp + leadingzbytes = len(re.match('^\x00*', inp_fmtd).group(0)) + checksum = dbl_sha256(inp_fmtd)[:4] + return '1' * leadingzbytes + changebase(inp_fmtd + checksum, 256, 58) + -#Convert a public key (in hex) to a Bitcoin address def pubkey_to_address(pubkey): - return bin_to_b58check(hash_160(changebase(pubkey,16,256))) + """Convert a public key (in hex) to a Bitcoin address""" + return bin_to_b58check(hash_160(changebase(pubkey, 16, 256))) diff --git a/src/pyelliptic/cipher.py b/src/pyelliptic/cipher.py index b597cafa2b..af6c08ca81 100644 --- a/src/pyelliptic/cipher.py +++ b/src/pyelliptic/cipher.py @@ -1,15 +1,16 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - +""" +Symmetric Encryption +""" # Copyright (C) 2011 Yann GUIBET # See LICENSE for details. -from pyelliptic.openssl import OpenSSL +from .openssl import OpenSSL -class Cipher: +# pylint: disable=redefined-builtin +class Cipher(object): """ - Symmetric encryption + Main class for encryption import pyelliptic iv = pyelliptic.Cipher.gen_IV('aes-256-cfb') @@ -44,30 +45,34 @@ def get_all_cipher(): @staticmethod def get_blocksize(ciphername): + """This Method returns cipher blocksize""" cipher = OpenSSL.get_cipher(ciphername) return cipher.get_blocksize() @staticmethod def gen_IV(ciphername): + """Generate random initialization vector""" cipher = OpenSSL.get_cipher(ciphername) return OpenSSL.rand(cipher.get_blocksize()) def update(self, input): + """Update result with more data""" i = OpenSSL.c_int(0) buffer = OpenSSL.malloc(b"", len(input) + self.cipher.get_blocksize()) inp = OpenSSL.malloc(input, len(input)) if OpenSSL.EVP_CipherUpdate(self.ctx, OpenSSL.byref(buffer), OpenSSL.byref(i), inp, len(input)) == 0: raise Exception("[OpenSSL] EVP_CipherUpdate FAIL ...") - return buffer.raw[0:i.value] + return buffer.raw[0:i.value] # pylint: disable=invalid-slice-index def final(self): + """Returning the final value""" i = OpenSSL.c_int(0) buffer = OpenSSL.malloc(b"", self.cipher.get_blocksize()) if (OpenSSL.EVP_CipherFinal_ex(self.ctx, OpenSSL.byref(buffer), OpenSSL.byref(i))) == 0: raise Exception("[OpenSSL] EVP_CipherFinal_ex FAIL ...") - return buffer.raw[0:i.value] + return buffer.raw[0:i.value] # pylint: disable=invalid-slice-index def ciphering(self, input): """ @@ -77,6 +82,7 @@ def ciphering(self, input): return buff + self.final() def __del__(self): + # pylint: disable=protected-access if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: OpenSSL.EVP_CIPHER_CTX_reset(self.ctx) else: diff --git a/src/pyelliptic/ecc.py b/src/pyelliptic/ecc.py index bea645db17..bfdc2dd688 100644 --- a/src/pyelliptic/ecc.py +++ b/src/pyelliptic/ecc.py @@ -1,52 +1,65 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - +""" +Asymmetric cryptography using elliptic curves +""" +# pylint: disable=protected-access, too-many-branches, too-many-locals # Copyright (C) 2011 Yann GUIBET # See LICENSE for details. from hashlib import sha512 -from pyelliptic.openssl import OpenSSL -from pyelliptic.cipher import Cipher -from pyelliptic.hash import hmac_sha256, equals from struct import pack, unpack +from .cipher import Cipher +from .hash import equals, hmac_sha256 +from .openssl import OpenSSL + -class ECC: +class ECC(object): """ Asymmetric encryption with Elliptic Curve Cryptography (ECC) ECDH, ECDSA and ECIES - import pyelliptic + >>> from binascii import hexlify + >>> import pyelliptic - alice = pyelliptic.ECC() # default curve: sect283r1 - bob = pyelliptic.ECC(curve='sect571r1') + >>> alice = pyelliptic.ECC() # default curve: sect283r1 + >>> bob = pyelliptic.ECC(curve='sect571r1') - ciphertext = alice.encrypt("Hello Bob", bob.get_pubkey()) - print bob.decrypt(ciphertext) + >>> ciphertext = alice.encrypt("Hello Bob", bob.get_pubkey()) + >>> print(bob.decrypt(ciphertext)) - signature = bob.sign("Hello Alice") - # alice's job : - print pyelliptic.ECC( - pubkey=bob.get_pubkey()).verify(signature, "Hello Alice") + >>> signature = bob.sign("Hello Alice") + >>> # alice's job : + >>> 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 !") + >>> # 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!") - 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') + >>> alice = pyelliptic.ECC(curve='sect571r1') + >>> print(hexlify(alice.get_ecdh_key(bob.get_pubkey()))) + >>> print(hexlify(bob.get_ecdh_key(alice.get_pubkey()))) """ - def __init__(self, pubkey=None, privkey=None, pubkey_x=None, - pubkey_y=None, raw_privkey=None, curve='sect283r1'): + + def __init__( + self, + pubkey=None, + privkey=None, + pubkey_x=None, + pubkey_y=None, + raw_privkey=None, + curve='sect283r1', + ): """ - 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 type(curve) == str: + if isinstance(curve, str): self.curve = OpenSSL.get_curve(curve) else: self.curve = curve @@ -54,9 +67,9 @@ def __init__(self, pubkey=None, privkey=None, pubkey_x=None, if pubkey_x is not None and pubkey_y is not None: self._set_keys(pubkey_x, pubkey_y, raw_privkey) elif pubkey is not None: - curve, pubkey_x, pubkey_y, i = ECC._decode_pubkey(pubkey) + curve, pubkey_x, pubkey_y, _ = ECC._decode_pubkey(pubkey) if privkey is not None: - curve2, raw_privkey, i = ECC._decode_privkey(privkey) + curve2, raw_privkey, _ = ECC._decode_privkey(privkey) if curve != curve2: raise Exception("Bad ECC keys ...") self.curve = curve @@ -70,22 +83,23 @@ def _set_keys(self, pubkey_x, pubkey_y, privkey): 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): + """The name of currently used curve""" return OpenSSL.get_curve_by_id(self.curve) def get_curve_id(self): + """Currently used curve""" return self.curve def get_pubkey(self): @@ -93,22 +107,31 @@ def get_pubkey(self): High level function which returns : curve(2) + len_of_pubkeyX(2) + pubkeyX + len_of_pubkeyY + pubkeyY """ - 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 - )) + 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(pubkey_x)), + pubkey_x, + pack('!H', len(pubkey_y)), + pubkey_y, + )) def get_privkey(self): """ High level function which returns curve(2) + len_of_privkey(2) + privkey """ - return b''.join((pack('!H', self.curve), - pack('!H', len(self.privkey)), - self.privkey - )) + return b''.join(( + pack('!H', self.curve), + pack('!H', len(self.privkey)), + self.privkey, + )) @staticmethod def _decode_pubkey(pubkey): @@ -144,19 +167,17 @@ def _generate(self): 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) group = OpenSSL.EC_KEY_get0_group(key) pub_key = OpenSSL.EC_KEY_get0_public_key(key) - if (OpenSSL.EC_POINT_get_affine_coordinates_GFp(group, pub_key, - pub_key_x, - pub_key_y, 0 - )) == 0: + if OpenSSL.EC_POINT_get_affine_coordinates_GFp( + group, pub_key, pub_key_x, pub_key_y, 0) == 0: raise Exception( "[OpenSSL] EC_POINT_get_affine_coordinates_GFp FAIL ...") @@ -181,14 +202,15 @@ def _generate(self): 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, i = ECC._decode_pubkey(pubkey) + curve, pubkey_x, pubkey_y, _ = ECC._decode_pubkey(pubkey) if curve != self.curve: raise Exception("ECC keys must be from the same curve !") return sha512(self.raw_get_ecdh_key(pubkey_x, pubkey_y)).digest() def raw_get_ecdh_key(self, pubkey_x, pubkey_y): + """ECDH key as binary data""" try: ecdh_keybuffer = OpenSSL.malloc(0, 32) @@ -196,31 +218,31 @@ def raw_get_ecdh_key(self, pubkey_x, pubkey_y): 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: @@ -246,51 +268,50 @@ def raw_get_ecdh_key(self, pubkey_x, pubkey_y): 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, i = ECC._decode_pubkey(pubkey) + curve, pubkey_x, pubkey_y, _ = ECC._decode_pubkey(pubkey) if privkey is None: raw_privkey = None curve2 = curve else: - curve2, raw_privkey, i = ECC._decode_privkey(privkey) + curve2, raw_privkey, _ = ECC._decode_privkey(privkey) if curve != curve2: raise Exception("Bad public and private key") return self.raw_check_key(raw_privkey, pubkey_x, pubkey_y, curve) def raw_check_key(self, privkey, pubkey_x, pubkey_y, curve=None): + """Check key validity, key is supplied as binary data""" if curve is None: curve = self.curve - elif type(curve) == str: + 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 @@ -322,25 +343,27 @@ def sign(self, inputb, digest_alg=OpenSSL.digest_ecdsa_sha1): 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: @@ -349,12 +372,13 @@ def sign(self, inputb, digest_alg=OpenSSL.digest_ecdsa_sha1): 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] @@ -369,12 +393,11 @@ def sign(self, inputb, digest_alg=OpenSSL.digest_ecdsa_sha1): OpenSSL.EVP_MD_CTX_free(md_ctx) else: OpenSSL.EVP_MD_CTX_destroy(md_ctx) - pass 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)) @@ -390,27 +413,29 @@ def verify(self, sig, inputb, digest_alg=OpenSSL.digest_ecdsa_sha1): 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) @@ -418,13 +443,13 @@ def verify(self, sig, inputb, digest_alg=OpenSSL.digest_ecdsa_sha1): 0, digest, dgst_len.contents, bsig, len(sig), key) if ret == -1: - return False # Fail to Check - else: - if ret == 0: - return False # Bad signature ! - else: - return True # Good - return False + # Fail to Check + return False + if ret == 0: + # Bad signature ! + return False + # Good + return True finally: OpenSSL.EC_KEY_free(key) @@ -441,22 +466,30 @@ def encrypt(data, pubkey, ephemcurve=None, ciphername='aes-256-cbc'): """ Encrypt data with ECIES method using the public key of the recipient. """ - curve, pubkey_x, pubkey_y, i = ECC._decode_pubkey(pubkey) + curve, pubkey_x, pubkey_y, _ = ECC._decode_pubkey(pubkey) return ECC.raw_encrypt(data, pubkey_x, pubkey_y, curve=curve, ephemcurve=ephemcurve, ciphername=ciphername) @staticmethod - def raw_encrypt(data, pubkey_x, pubkey_y, curve='sect283r1', - ephemcurve=None, ciphername='aes-256-cbc'): + def raw_encrypt( + data, + pubkey_x, + pubkey_y, + curve='sect283r1', + ephemcurve=None, + ciphername='aes-256-cbc', + ): + """ECDH encryption, keys supplied in binary data format""" + if ephemcurve is None: ephemcurve = curve ephem = ECC(curve=ephemcurve) 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 @@ -465,16 +498,17 @@ def decrypt(self, data, ciphername='aes-256-cbc'): 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 - curve, pubkey_x, pubkey_y, i2 = ECC._decode_pubkey(data[i:]) - i += i2 - ciphertext = data[i:len(data)-32] + _, pubkey_x, pubkey_y, _i2 = ECC._decode_pubkey(data[i:]) + i += _i2 + ciphertext = data[i:len(data) - 32] i += len(ciphertext) mac = data[i:] key = sha512(self.raw_get_ecdh_key(pubkey_x, pubkey_y)).digest() 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 new file mode 100644 index 0000000000..f19a869f5f --- /dev/null +++ b/src/pyelliptic/eccblind.py @@ -0,0 +1,374 @@ +""" +ECC blind signature functionality based on +"An Efficient Blind Signature Scheme +Based on the Elliptic CurveDiscrete Logarithm Problem" by Morteza Nikooghadama + and Ali Zakerolhosseini , +http://www.isecure-journal.com/article_39171_47f9ec605dd3918c2793565ec21fcd7a.pdf +""" + +# variable names are based on the math in the paper, so they don't conform +# to PEP8 + +import time +from hashlib import sha256 +from struct import pack, unpack + +from .openssl import OpenSSL + +# first byte in serialisation can contain data +Y_BIT = 0x01 +COMPRESSED_BIT = 0x02 + +# formats +BIGNUM = '!32s' +EC = '!B32s' +PUBKEY = '!BB33s' + + +class Expiration(object): + """Expiration of pubkey""" + @staticmethod + def deserialize(val): + """Create an object out of int""" + year = ((val & 0xF0) >> 4) + 2020 + month = val & 0x0F + assert month < 12 + return Expiration(year, month) + + def __init__(self, year, month): + assert isinstance(year, int) + assert year > 2019 and year < 2036 + assert isinstance(month, int) + assert month < 12 + self.year = year + self.month = month + self.exp = year + month / 12.0 + + def serialize(self): + """Make int out of object""" + return ((self.year - 2020) << 4) + self.month + + def verify(self): + """Check if the pubkey has expired""" + now = time.gmtime() + return self.exp >= now.tm_year + (now.tm_mon - 1) / 12.0 + + +class Value(object): + """Value of a pubkey""" + @staticmethod + def deserialize(val): + """Make object out of int""" + return Value(val) + + def __init__(self, value=0xFF): + assert isinstance(value, int) + self.value = value + + def serialize(self): + """Make int out of object""" + return self.value & 0xFF + + def verify(self, value): + """Verify against supplied value""" + return value <= self.value + + +class ECCBlind(object): # pylint: disable=too-many-instance-attributes + """ + Class for ECC blind signature functionality + """ + + # init + k = None + R = None + F = None + d = None + Q = None + a = None + b = None + c = None + binv = None + r = None + m = None + m_ = None + s_ = None + signature = None + exp = None + val = None + + def ec_get_random(self): + """ + Random integer within the EC order + """ + randomnum = OpenSSL.BN_new() + OpenSSL.BN_rand(randomnum, OpenSSL.BN_num_bits(self.n), 0, 0) + return randomnum + + def ec_invert(self, a): + """ + ECC inversion + """ + inverse = OpenSSL.BN_mod_inverse(None, a, self.n, self.ctx) + return inverse + + def ec_gen_keypair(self): + """ + Generate an ECC keypair + We're using compressed keys + """ + d = self.ec_get_random() + Q = OpenSSL.EC_POINT_new(self.group) + OpenSSL.EC_POINT_mul(self.group, Q, d, None, None, None) + return (d, Q) + + def ec_Ftor(self, F): + """ + x0 coordinate of F + """ + # F = (x0, y0) + x0 = OpenSSL.BN_new() + y0 = OpenSSL.BN_new() + OpenSSL.EC_POINT_get_affine_coordinates(self.group, F, x0, y0, self.ctx) + OpenSSL.BN_free(y0) + return x0 + + def _ec_point_serialize(self, point): + """Make an EC point into a string""" + try: + x = OpenSSL.BN_new() + y = OpenSSL.BN_new() + OpenSSL.EC_POINT_get_affine_coordinates( + 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: + bx = OpenSSL.malloc(0, l_) + OpenSSL.BN_bn2binpad(x, bx, l_) + out = bx.raw + except AttributeError: + # padding manually + bx = OpenSSL.malloc(0, OpenSSL.BN_num_bytes(x)) + OpenSSL.BN_bn2bin(x, bx) + out = bx.raw.rjust(l_, b'\x00') + return pack(EC, y_byte, out) + + finally: + OpenSSL.BN_clear_free(x) + OpenSSL.BN_clear_free(y) + + 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), None) + y_bit &= Y_BIT + retval = OpenSSL.EC_POINT_new(self.group) + OpenSSL.EC_POINT_set_compressed_coordinates(self.group, + retval, + x, + y_bit, + self.ctx) + return retval + + def _bn_serialize(self, bn): + """Make a string out of BigNum""" + l_ = OpenSSL.BN_num_bytes(self.n) + try: + o = OpenSSL.malloc(0, l_) + OpenSSL.BN_bn2binpad(bn, o, l_) + return o.raw + except AttributeError: + o = OpenSSL.malloc(0, OpenSSL.BN_num_bytes(bn)) + OpenSSL.BN_bn2bin(bn, o) + return o.raw.rjust(l_, b'\x00') + + def _bn_deserialize(self, data): + """Make a BigNum out of string""" + x = OpenSSL.BN_bin2bn(data, OpenSSL.BN_num_bytes(self.n), None) + return x + + def _init_privkey(self, privkey): + """Initialise private key out of string/bytes""" + self.d = self._bn_deserialize(privkey) + + def privkey(self): + """Make a private key into a string""" + return pack(BIGNUM, self.d) + + def _init_pubkey(self, pubkey): + """Initialise pubkey out of string/bytes""" + unpacked = unpack(PUBKEY, pubkey) + self.expiration = Expiration.deserialize(unpacked[0]) + self.value = Value.deserialize(unpacked[1]) + self.Q = self._ec_point_deserialize(unpacked[2]) + + def pubkey(self): + """Make a pubkey into a string""" + return pack(PUBKEY, self.expiration.serialize(), + self.value.serialize(), + self._ec_point_serialize(self.Q)) + + def __init__(self, curve="secp256k1", pubkey=None, privkey=None, + year=2025, month=11, value=0xFF): + self.ctx = OpenSSL.BN_CTX_new() + + # ECC group + self.group = OpenSSL.EC_GROUP_new_by_curve_name( + OpenSSL.get_curve(curve)) + + # Order n + self.n = OpenSSL.BN_new() + OpenSSL.EC_GROUP_get_order(self.group, self.n, self.ctx) + + # Generator G + self.G = OpenSSL.EC_GROUP_get0_generator(self.group) + + # Identity O (infinity) + self.iO = OpenSSL.EC_POINT_new(self.group) + OpenSSL.EC_POINT_set_to_infinity(self.group, self.iO) + + if privkey: + assert pubkey + # load both pubkey and privkey from bytes + self._init_privkey(privkey) + self._init_pubkey(pubkey) + elif pubkey: + # load pubkey from bytes + self._init_pubkey(pubkey) + else: + # new keypair + self.d, self.Q = self.ec_gen_keypair() + if not year or not month: + now = time.gmtime() + if now.tm_mon == 12: + self.expiration = Expiration(now.tm_year + 1, 1) + else: + self.expiration = Expiration(now.tm_year, now.tm_mon + 1) + else: + self.expiration = Expiration(year, month) + self.value = Value(value) + + def __del__(self): + OpenSSL.BN_free(self.n) + OpenSSL.BN_CTX_free(self.ctx) + + def signer_init(self): + """ + Init signer + """ + # Signer: Random integer k + self.k = self.ec_get_random() + + # R = kG + self.R = OpenSSL.EC_POINT_new(self.group) + OpenSSL.EC_POINT_mul(self.group, self.R, self.k, None, None, None) + + return self._ec_point_serialize(self.R) + + def create_signing_request(self, R, msg): + """ + Requester creates a new signing request + """ + self.R = self._ec_point_deserialize(R) + msghash = sha256(msg).digest() + + # Requester: 3 random blinding factors + self.F = OpenSSL.EC_POINT_new(self.group) + OpenSSL.EC_POINT_set_to_infinity(self.group, self.F) + temp = OpenSSL.EC_POINT_new(self.group) + abinv = OpenSSL.BN_new() + + # F != O + while OpenSSL.EC_POINT_cmp(self.group, self.F, self.iO, self.ctx) == 0: + self.a = self.ec_get_random() + self.b = self.ec_get_random() + self.c = self.ec_get_random() + + # F = b^-1 * R... + self.binv = self.ec_invert(self.b) + 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, 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, 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) + + # Requester: Blinding (m' = br(m) + a) + self.m = OpenSSL.BN_new() + OpenSSL.BN_bin2bn(msghash, len(msghash), self.m) + + self.m_ = OpenSSL.BN_new() + OpenSSL.BN_mod_mul(self.m_, self.b, self.r, self.n, self.ctx) + OpenSSL.BN_mod_mul(self.m_, self.m_, self.m, self.n, self.ctx) + OpenSSL.BN_mod_add(self.m_, self.m_, self.a, self.n, self.ctx) + return self._bn_serialize(self.m_) + + def blind_sign(self, m_): + """ + Signer blind-signs the request + """ + self.m_ = self._bn_deserialize(m_) + self.s_ = OpenSSL.BN_new() + OpenSSL.BN_mod_mul(self.s_, self.d, self.m_, self.n, self.ctx) + OpenSSL.BN_mod_add(self.s_, self.s_, self.k, self.n, self.ctx) + OpenSSL.BN_free(self.k) + return self._bn_serialize(self.s_) + + def unblind(self, s_): + """ + Requester unblinds the signature + """ + self.s_ = self._bn_deserialize(s_) + s = OpenSSL.BN_new() + OpenSSL.BN_mod_mul(s, self.binv, self.s_, self.n, self.ctx) + OpenSSL.BN_mod_add(s, s, self.c, self.n, self.ctx) + OpenSSL.BN_free(self.a) + OpenSSL.BN_free(self.b) + OpenSSL.BN_free(self.c) + self.signature = (s, self.F) + return self._bn_serialize(s) + self._ec_point_serialize(self.F) + + def verify(self, msg, signature, value=1): + """ + Verify signature with certifier's pubkey + """ + + # convert msg to BIGNUM + self.m = OpenSSL.BN_new() + msghash = sha256(msg).digest() + OpenSSL.BN_bin2bn(msghash, len(msghash), self.m) + + # init + s, self.F = (self._bn_deserialize(signature[0:32]), + self._ec_point_deserialize(signature[32:])) + if self.r is None: + self.r = self.ec_Ftor(self.F) + + lhs = OpenSSL.EC_POINT_new(self.group) + rhs = OpenSSL.EC_POINT_new(self.group) + + OpenSSL.EC_POINT_mul(self.group, lhs, s, None, None, None) + + 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) + if retval == -1: + raise RuntimeError("EC_POINT_cmp returned an error") + elif not self.value.verify(value): + return False + elif not self.expiration.verify(): + return False + elif retval != 0: + return False + return True diff --git a/src/pyelliptic/eccblindchain.py b/src/pyelliptic/eccblindchain.py new file mode 100644 index 0000000000..56e8ce2a60 --- /dev/null +++ b/src/pyelliptic/eccblindchain.py @@ -0,0 +1,52 @@ +""" +Blind signature chain with a top level CA +""" + +from .eccblind import ECCBlind + + +class ECCBlindChain(object): # pylint: disable=too-few-public-methods + """ + # Class for ECC Blind Chain signature functionality + """ + + def __init__(self, ca=None, chain=None): + self.chain = [] + self.ca = [] + if ca: + for i in range(0, len(ca), 35): + self.ca.append(ca[i:i + 35]) + if chain: + self.chain.append(chain[0:35]) + for i in range(35, len(chain), 100): + if len(chain[i:]) == 65: + self.chain.append(chain[i:i + 65]) + else: + self.chain.append(chain[i:i + 100]) + + def verify(self, msg, value): + """Verify a chain provides supplied message and value""" + parent = None + l_ = 0 + for level in self.chain: + l_ += 1 + pubkey = None + signature = None + if len(level) == 100: + pubkey, signature = (level[0:35], level[35:]) + elif len(level) == 35: + if level not in self.ca: + return False + parent = level + continue + else: + signature = level + verifier_obj = ECCBlind(pubkey=parent) + if pubkey: + if not verifier_obj.verify(pubkey, signature, value): + return False + parent = pubkey + else: + return verifier_obj.verify(msg=msg, signature=signature, + value=value) + return None diff --git a/src/pyelliptic/hash.py b/src/pyelliptic/hash.py index fb910dd4e6..70c9a6ce8b 100644 --- a/src/pyelliptic/hash.py +++ b/src/pyelliptic/hash.py @@ -1,10 +1,10 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - +""" +Wrappers for hash functions from OpenSSL. +""" # Copyright (C) 2011 Yann GUIBET # See LICENSE for details. -from pyelliptic.openssl import OpenSSL +from .openssl import OpenSSL # For python3 @@ -27,10 +27,10 @@ def _equals_str(a, b): def equals(a, b): + """Compare two strings or bytearrays""" if isinstance(a, str): return _equals_str(a, b) - else: - return _equals_bytes(a, b) + return _equals_bytes(a, b) def hmac_sha256(k, m): @@ -58,6 +58,7 @@ def hmac_sha512(k, m): def pbkdf2(password, salt=None, i=10000, keylen=64): + """Key derivation function using SHA256""" if salt is None: salt = OpenSSL.rand(8) p_password = OpenSSL.malloc(password, len(password)) diff --git a/src/pyelliptic/openssl.py b/src/pyelliptic/openssl.py index 115bdc0848..851dfa1525 100644 --- a/src/pyelliptic/openssl.py +++ b/src/pyelliptic/openssl.py @@ -1,42 +1,52 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - # Copyright (C) 2011 Yann GUIBET # See LICENSE for details. # # Software slightly changed by Jonathan Warren - -import sys +""" +This module loads openssl libs with ctypes and incapsulates +needed openssl functionality in class _OpenSSL. +""" import ctypes +import sys + +# pylint: disable=protected-access OpenSSL = None -class CipherName: +class CipherName(object): + """Class returns cipher name, pointer and blocksize""" + def __init__(self, name, pointer, blocksize): self._name = name self._pointer = pointer self._blocksize = blocksize def __str__(self): - return "Cipher : " + self._name + " | Blocksize : " + str(self._blocksize) + " | Fonction pointer : " + str(self._pointer) + return "Cipher : " + self._name + \ + " | Blocksize : " + str(self._blocksize) + \ + " | Function pointer : " + str(self._pointer) def get_pointer(self): + """This method returns cipher pointer""" return self._pointer() def get_name(self): + """This method returns cipher name""" return self._name def get_blocksize(self): + """This method returns cipher blocksize""" return self._blocksize def get_version(library): + """This function return version, hexversion and cflages""" version = None hexversion = None cflags = None try: - #OpenSSL 1.1 + # OpenSSL 1.1 OPENSSL_VERSION = 0 OPENSSL_CFLAGS = 1 library.OpenSSL_version.argtypes = [ctypes.c_int] @@ -47,7 +57,7 @@ def get_version(library): hexversion = library.OpenSSL_version_num() except AttributeError: try: - #OpenSSL 1.0 + # OpenSSL 1.0 SSLEAY_VERSION = 0 SSLEAY_CFLAGS = 2 library.SSLeay.restype = ctypes.c_long @@ -57,22 +67,46 @@ def get_version(library): cflags = library.SSLeay_version(SSLEAY_CFLAGS) hexversion = library.SSLeay() except AttributeError: - #raise NotImplementedError('Cannot determine version of this OpenSSL library.') + # raise NotImplementedError('Cannot determine version of this OpenSSL library.') pass return (version, hexversion, cflags) -class _OpenSSL: +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 """ + # pylint: disable=too-many-statements, too-many-instance-attributes def __init__(self, library): """ Build the wrapper """ self._lib = ctypes.CDLL(library) self._version, self._hexversion, self._cflags = get_version(self._lib) - self._libreSSL = self._version.startswith("LibreSSL") + self._libreSSL = self._version.startswith(b"LibreSSL") self.pointer = ctypes.pointer self.c_int = ctypes.c_int @@ -80,25 +114,38 @@ def __init__(self, library): 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.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.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 @@ -117,68 +164,127 @@ def __init__(self, library): 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 self.EC_KEY_get0_group.restype = ctypes.c_void_p self.EC_KEY_get0_group.argtypes = [ctypes.c_void_p] - self.EC_POINT_get_affine_coordinates_GFp = self._lib.EC_POINT_get_affine_coordinates_GFp + self.EC_POINT_get_affine_coordinates_GFp = \ + 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.c_void_p] + self.EC_POINT_get_affine_coordinates_GFp.argtypes = [ctypes.c_void_p, + ctypes.POINTER(EC_POINT), + ctypes.POINTER(BIGNUM), + ctypes.POINTER(BIGNUM), + ctypes.c_void_p] + + try: + self.EC_POINT_get_affine_coordinates = \ + self._lib.EC_POINT_get_affine_coordinates + except AttributeError: + # OpenSSL docs say only use this for backwards compatibility + self.EC_POINT_get_affine_coordinates = \ + 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.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 - self.EC_KEY_set_group.argtypes = [ctypes.c_void_p, ctypes.c_void_p] + self.EC_KEY_set_group.argtypes = [ctypes.c_void_p, + ctypes.c_void_p] - self.EC_POINT_set_affine_coordinates_GFp = self._lib.EC_POINT_set_affine_coordinates_GFp + self.EC_POINT_set_affine_coordinates_GFp = \ + 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.c_void_p] + self.EC_POINT_set_affine_coordinates_GFp.argtypes = [ctypes.c_void_p, + ctypes.POINTER(EC_POINT), + ctypes.POINTER(BIGNUM), + ctypes.POINTER(BIGNUM), + ctypes.c_void_p] + + try: + self.EC_POINT_set_affine_coordinates = \ + self._lib.EC_POINT_set_affine_coordinates + except AttributeError: + # OpenSSL docs say only use this for backwards compatibility + self.EC_POINT_set_affine_coordinates = \ + 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.POINTER(EC_POINT), + ctypes.POINTER(BIGNUM), + ctypes.POINTER(BIGNUM), + ctypes.c_void_p] + + try: + self.EC_POINT_set_compressed_coordinates = \ + self._lib.EC_POINT_set_compressed_coordinates + except AttributeError: + # OpenSSL docs say only use this for backwards compatibility + self.EC_POINT_set_compressed_coordinates = \ + 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.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.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] + self.EC_POINT_mul.restype = ctypes.c_int + self.EC_POINT_mul.argtypes = [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 self._lib.EC_KEY_OpenSSL.restype = ctypes.c_void_p self._lib.EC_KEY_OpenSSL.argtypes = [] - + self.EC_KEY_set_method = self._lib.EC_KEY_set_method self._lib.EC_KEY_set_method.restype = ctypes.c_int - self._lib.EC_KEY_set_method.argtypes = [ctypes.c_void_p, ctypes.c_void_p] + self._lib.EC_KEY_set_method.argtypes = [ctypes.c_void_p, + ctypes.c_void_p] else: self.ECDH_OpenSSL = self._lib.ECDH_OpenSSL self._lib.ECDH_OpenSSL.restype = ctypes.c_void_p @@ -186,21 +292,21 @@ def __init__(self, library): self.ECDH_set_method = self._lib.ECDH_set_method self._lib.ECDH_set_method.restype = ctypes.c_int - self._lib.ECDH_set_method.argtypes = [ctypes.c_void_p, ctypes.c_void_p] - - self.BN_CTX_new = self._lib.BN_CTX_new - self._lib.BN_CTX_new.restype = ctypes.c_void_p - self._lib.BN_CTX_new.argtypes = [] + self._lib.ECDH_set_method.argtypes = [ctypes.c_void_p, + ctypes.c_void_p] self.ECDH_compute_key = self._lib.ECDH_compute_key self.ECDH_compute_key.restype = ctypes.c_int self.ECDH_compute_key.argtypes = [ctypes.c_void_p, - ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p] + ctypes.c_int, + ctypes.c_void_p, + ctypes.c_void_p] self.EVP_CipherInit_ex = self._lib.EVP_CipherInit_ex self.EVP_CipherInit_ex.restype = ctypes.c_int self.EVP_CipherInit_ex.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, ctypes.c_void_p] + ctypes.c_void_p, + ctypes.c_void_p] self.EVP_CIPHER_CTX_new = self._lib.EVP_CIPHER_CTX_new self.EVP_CIPHER_CTX_new.restype = ctypes.c_void_p @@ -223,13 +329,13 @@ def __init__(self, library): self.EVP_aes_256_cbc.restype = ctypes.c_void_p self.EVP_aes_256_cbc.argtypes = [] - #self.EVP_aes_128_ctr = self._lib.EVP_aes_128_ctr - #self.EVP_aes_128_ctr.restype = ctypes.c_void_p - #self.EVP_aes_128_ctr.argtypes = [] + # self.EVP_aes_128_ctr = self._lib.EVP_aes_128_ctr + # self.EVP_aes_128_ctr.restype = ctypes.c_void_p + # self.EVP_aes_128_ctr.argtypes = [] - #self.EVP_aes_256_ctr = self._lib.EVP_aes_256_ctr - #self.EVP_aes_256_ctr.restype = ctypes.c_void_p - #self.EVP_aes_256_ctr.argtypes = [] + # self.EVP_aes_256_ctr = self._lib.EVP_aes_256_ctr + # self.EVP_aes_256_ctr.restype = ctypes.c_void_p + # self.EVP_aes_256_ctr.argtypes = [] self.EVP_aes_128_ofb = self._lib.EVP_aes_128_ofb self.EVP_aes_128_ofb.restype = ctypes.c_void_p @@ -250,7 +356,7 @@ def __init__(self, library): self.EVP_rc4 = self._lib.EVP_rc4 self.EVP_rc4.restype = ctypes.c_void_p self.EVP_rc4.argtypes = [] - + if self._hexversion >= 0x10100000 and not self._libreSSL: self.EVP_CIPHER_CTX_reset = self._lib.EVP_CIPHER_CTX_reset self.EVP_CIPHER_CTX_reset.restype = ctypes.c_int @@ -267,7 +373,8 @@ def __init__(self, library): self.EVP_CipherUpdate = self._lib.EVP_CipherUpdate self.EVP_CipherUpdate.restype = ctypes.c_int self.EVP_CipherUpdate.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int] + ctypes.c_void_p, ctypes.c_void_p, + ctypes.c_void_p, ctypes.c_int] self.EVP_CipherFinal_ex = self._lib.EVP_CipherFinal_ex self.EVP_CipherFinal_ex.restype = ctypes.c_int @@ -281,11 +388,11 @@ def __init__(self, library): self.EVP_DigestInit_ex = self._lib.EVP_DigestInit_ex self.EVP_DigestInit_ex.restype = ctypes.c_int self._lib.EVP_DigestInit_ex.argtypes = 3 * [ctypes.c_void_p] - + 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 @@ -296,22 +403,24 @@ def __init__(self, library): self.EVP_DigestFinal_ex.restype = ctypes.c_int self.EVP_DigestFinal_ex.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] - + self.ECDSA_sign = self._lib.ECDSA_sign self.ECDSA_sign.restype = ctypes.c_int self.ECDSA_sign.argtypes = [ctypes.c_int, ctypes.c_void_p, - ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] + ctypes.c_int, ctypes.c_void_p, + ctypes.c_void_p, ctypes.c_void_p] self.ECDSA_verify = self._lib.ECDSA_verify self.ECDSA_verify.restype = ctypes.c_int self.ECDSA_verify.argtypes = [ctypes.c_int, ctypes.c_void_p, - ctypes.c_int, ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p] + ctypes.c_int, ctypes.c_void_p, + ctypes.c_int, ctypes.c_void_p] if self._hexversion >= 0x10100000 and not self._libreSSL: self.EVP_MD_CTX_new = self._lib.EVP_MD_CTX_new self.EVP_MD_CTX_new.restype = ctypes.c_void_p self.EVP_MD_CTX_new.argtypes = [] - + self.EVP_MD_CTX_reset = self._lib.EVP_MD_CTX_reset self.EVP_MD_CTX_reset.restype = None self.EVP_MD_CTX_reset.argtypes = [ctypes.c_void_p] @@ -329,11 +438,11 @@ def __init__(self, library): self.EVP_MD_CTX_create = self._lib.EVP_MD_CTX_create self.EVP_MD_CTX_create.restype = ctypes.c_void_p self.EVP_MD_CTX_create.argtypes = [] - + self.EVP_MD_CTX_init = self._lib.EVP_MD_CTX_init self.EVP_MD_CTX_init.restype = None self.EVP_MD_CTX_init.argtypes = [ctypes.c_void_p] - + self.EVP_MD_CTX_destroy = self._lib.EVP_MD_CTX_destroy self.EVP_MD_CTX_destroy.restype = None self.EVP_MD_CTX_destroy.argtypes = [ctypes.c_void_p] @@ -363,36 +472,174 @@ def __init__(self, library): 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_void_p] + ctypes.c_void_p, ctypes.c_size_t, + ctypes.c_void_p, ctypes.c_void_p] try: self.PKCS5_PBKDF2_HMAC = self._lib.PKCS5_PBKDF2_HMAC - except: + except Exception: # The above is not compatible with all versions of OSX. self.PKCS5_PBKDF2_HMAC = self._lib.PKCS5_PBKDF2_HMAC_SHA1 - + self.PKCS5_PBKDF2_HMAC.restype = ctypes.c_int self.PKCS5_PBKDF2_HMAC.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p, ctypes.c_int, ctypes.c_int, ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p] + # Blind signature requirements + self.BN_CTX_new = self._lib.BN_CTX_new + self.BN_CTX_new.restype = ctypes.c_void_p + self.BN_CTX_new.argtypes = [] + + self.BN_dup = self._lib.BN_dup + 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.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.POINTER(BIGNUM), + ctypes.c_ulong] + + self.BN_mul = self._lib.BN_mul + self.BN_mul.restype = ctypes.c_int + 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.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.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.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.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.POINTER(BIGNUM), + ctypes.c_ulong] + + self.BN_cmp = self._lib.BN_cmp + self.BN_cmp.restype = ctypes.c_int + 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.POINTER(BIGNUM)] + except AttributeError: + # OpenSSL 1.1.0 implements this as a function, but earlier + # versions as macro, so we need to workaround + self.BN_is_odd = self.BN_is_odd_compatible + + self.BN_bn2dec = self._lib.BN_bn2dec + self.BN_bn2dec.restype = ctypes.c_char_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 + self.EC_GROUP_new_by_curve_name.argtypes = [ctypes.c_int] + + 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.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.POINTER(BIGNUM), + ctypes.c_void_p] + + self.EC_GROUP_get0_generator = self._lib.EC_GROUP_get0_generator + 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.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.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.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.POINTER(EC_POINT)] + self._set_ciphers() self._set_curves() def _set_ciphers(self): self.cipher_algo = { - 'aes-128-cbc': CipherName('aes-128-cbc', self.EVP_aes_128_cbc, 16), - 'aes-256-cbc': CipherName('aes-256-cbc', self.EVP_aes_256_cbc, 16), - 'aes-128-cfb': CipherName('aes-128-cfb', self.EVP_aes_128_cfb128, 16), - 'aes-256-cfb': CipherName('aes-256-cfb', self.EVP_aes_256_cfb128, 16), - 'aes-128-ofb': CipherName('aes-128-ofb', self._lib.EVP_aes_128_ofb, 16), - 'aes-256-ofb': CipherName('aes-256-ofb', self._lib.EVP_aes_256_ofb, 16), - #'aes-128-ctr': CipherName('aes-128-ctr', self._lib.EVP_aes_128_ctr, 16), - #'aes-256-ctr': CipherName('aes-256-ctr', self._lib.EVP_aes_256_ctr, 16), - 'bf-cfb': CipherName('bf-cfb', self.EVP_bf_cfb64, 8), - 'bf-cbc': CipherName('bf-cbc', self.EVP_bf_cbc, 8), - 'rc4': CipherName('rc4', self.EVP_rc4, 128), # 128 is the initialisation size not block size + 'aes-128-cbc': CipherName( + 'aes-128-cbc', self.EVP_aes_128_cbc, 16), + 'aes-256-cbc': CipherName( + 'aes-256-cbc', self.EVP_aes_256_cbc, 16), + 'aes-128-cfb': CipherName( + 'aes-128-cfb', self.EVP_aes_128_cfb128, 16), + 'aes-256-cfb': CipherName( + 'aes-256-cfb', self.EVP_aes_256_cfb128, 16), + 'aes-128-ofb': CipherName( + 'aes-128-ofb', self._lib.EVP_aes_128_ofb, 16), + 'aes-256-ofb': CipherName( + 'aes-256-ofb', self._lib.EVP_aes_256_ofb, 16), + # 'aes-128-ctr': CipherName( + # 'aes-128-ctr', self._lib.EVP_aes_128_ctr, 16), + # 'aes-256-ctr': CipherName( + # 'aes-256-ctr', self._lib.EVP_aes_256_ctr, 16), + 'bf-cfb': CipherName( + 'bf-cfb', self.EVP_bf_cfb64, 8), + 'bf-cbc': CipherName( + 'bf-cbc', self.EVP_bf_cbc, 8), + # 128 is the initialisation size not block size + 'rc4': CipherName( + 'rc4', self.EVP_rc4, 128), } def _set_curves(self): @@ -436,6 +683,16 @@ def BN_num_bytes(self, x): """ return int((self.BN_num_bits(x) + 7) / 8) + def BN_is_odd_compatible(self, x): + """ + returns if BN is odd + we assume big endianness, and that BN is initialised + """ + length = self.BN_num_bytes(x) + data = self.malloc(0, length) + OpenSSL.BN_bn2bin(x, data) + return ord(data[length - 1]) & 1 + def get_cipher(self, name): """ returns the OpenSSL cipher instance @@ -452,13 +709,13 @@ def get_curve(self, name): raise Exception("Unknown curve") return self.curves[name] - def get_curve_by_id(self, id): + def get_curve_by_id(self, id_): """ returns the name of a elliptic curve with his id """ res = None for i in self.curves: - if self.curves[i] == id: + if self.curves[i] == id_: res = i break if res is None: @@ -469,47 +726,63 @@ def rand(self, size): """ OpenSSL random function """ - buffer = self.malloc(0, size) - # This pyelliptic library, by default, didn't check the return value of RAND_bytes. It is - # evidently possible that it returned an error and not-actually-random data. However, in - # tests on various operating systems, while generating hundreds of gigabytes of random - # strings of various sizes I could not get an error to occur. Also Bitcoin doesn't check - # the return value of RAND_bytes either. + buffer_ = self.malloc(0, size) + # This pyelliptic library, by default, didn't check the return value + # of RAND_bytes. It is evidently possible that it returned an error + # and not-actually-random data. However, in tests on various + # operating systems, while generating hundreds of gigabytes of random + # strings of various sizes I could not get an error to occur. + # Also Bitcoin doesn't check the return value of RAND_bytes either. # Fixed in Bitmessage version 0.4.2 (in source code on 2013-10-13) - while self.RAND_bytes(buffer, size) != 1: + while self.RAND_bytes(buffer_, size) != 1: import time time.sleep(1) - return buffer.raw + return buffer_.raw def malloc(self, data, size): """ returns a create_string_buffer (ctypes) """ - buffer = None + buffer_ = None if data != 0: if sys.version_info.major == 3 and isinstance(data, type('')): data = data.encode() - buffer = self.create_string_buffer(data, size) + buffer_ = self.create_string_buffer(data, size) else: - buffer = self.create_string_buffer(size) - return buffer + buffer_ = self.create_string_buffer(size) + return buffer_ + def loadOpenSSL(): + """This function finds and load the OpenSSL library""" + # pylint: disable=global-statement global OpenSSL from os import path, environ from ctypes.util import find_library - + libdir = [] - if getattr(sys,'frozen', None): + if getattr(sys, 'frozen', None): if 'darwin' in sys.platform: libdir.extend([ - path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.dylib'), - path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.1.1.0.dylib'), - path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.1.0.2.dylib'), - path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.1.0.1.dylib'), - path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.1.0.0.dylib'), - path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.0.9.8.dylib'), - ]) + path.join( + environ['RESOURCEPATH'], '..', + 'Frameworks', 'libcrypto.dylib'), + path.join( + environ['RESOURCEPATH'], '..', + 'Frameworks', 'libcrypto.1.1.0.dylib'), + path.join( + environ['RESOURCEPATH'], '..', + 'Frameworks', 'libcrypto.1.0.2.dylib'), + path.join( + environ['RESOURCEPATH'], '..', + 'Frameworks', 'libcrypto.1.0.1.dylib'), + path.join( + environ['RESOURCEPATH'], '..', + 'Frameworks', 'libcrypto.1.0.0.dylib'), + path.join( + environ['RESOURCEPATH'], '..', + 'Frameworks', 'libcrypto.0.9.8.dylib'), + ]) elif 'win32' in sys.platform or 'win64' in sys.platform: libdir.append(path.join(sys._MEIPASS, 'libeay32.dll')) else: @@ -528,15 +801,21 @@ def loadOpenSSL(): path.join(sys._MEIPASS, 'libssl.so.0.9.8'), ]) if 'darwin' in sys.platform: - libdir.extend(['libcrypto.dylib', '/usr/local/opt/openssl/lib/libcrypto.dylib']) + libdir.extend([ + 'libcrypto.dylib', '/usr/local/opt/openssl/lib/libcrypto.dylib']) elif 'win32' in sys.platform or 'win64' in sys.platform: libdir.append('libeay32.dll') + # kivy + elif 'ANDROID_ARGUMENT' in environ: + libdir.append('libcrypto1.1.so') + libdir.append('libssl1.1.so') else: libdir.append('libcrypto.so') libdir.append('libssl.so') libdir.append('libcrypto.so.1.0.0') libdir.append('libssl.so.1.0.0') - if 'linux' in sys.platform or 'darwin' in sys.platform or 'bsd' in sys.platform: + if 'linux' in sys.platform or 'darwin' in sys.platform \ + or 'bsd' in sys.platform: libdir.append(find_library('ssl')) elif 'win32' in sys.platform or 'win64' in sys.platform: libdir.append(find_library('libeay32')) @@ -544,8 +823,10 @@ def loadOpenSSL(): try: OpenSSL = _OpenSSL(library) return - except: + except Exception: # nosec B110 pass - raise Exception("Couldn't find and load the OpenSSL library. You must install it.") + raise Exception( + "Couldn't find and load the OpenSSL library. You must install it.") + loadOpenSSL() diff --git a/src/pyelliptic/tests/__init__.py b/src/pyelliptic/tests/__init__.py new file mode 100644 index 0000000000..b53ef88175 --- /dev/null +++ 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 0000000000..0348d3f0af --- /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 new file mode 100644 index 0000000000..1d1aecaf25 --- /dev/null +++ b/src/pyelliptic/tests/test_arithmetic.py @@ -0,0 +1,100 @@ +""" +Test the arithmetic functions +""" + +from binascii import unhexlify +import unittest + +try: + from pyelliptic import arithmetic +except ImportError: + from pybitmessage.pyelliptic import arithmetic + +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 +) + + +class TestArithmetic(unittest.TestCase): + """Test arithmetic functions""" + def test_base10_multiply(self): + """Test arithmetic.base10_multiply""" + self.assertEqual( + 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( + arithmetic.decode(sample_privsigningkey, 16), sample_factor) + + def test_encode(self): + """Encode sample factor into hex and compare to privsigningkey""" + self.assertEqual( + arithmetic.encode(sample_factor, 16), sample_privsigningkey) + + def test_changebase(self): + """Check the results of changebase()""" + self.assertEqual( + arithmetic.changebase(sample_privsigningkey, 16, 256, minlen=32), + unhexlify(sample_privsigningkey)) + self.assertEqual( + arithmetic.changebase(sample_pubsigningkey, 16, 256, minlen=64), + unhexlify(sample_pubsigningkey)) + self.assertEqual( + 32, # padding + len(arithmetic.changebase(sample_privsigningkey[:5], 16, 256, 32))) + + def test_hex_to_point(self): + """Check that sample_pubsigningkey is sample_point encoded in hex""" + self.assertEqual( + arithmetic.hex_to_point(sample_pubsigningkey), sample_point) + + def test_point_to_hex(self): + """Check that sample_point is sample_pubsigningkey decoded from hex""" + self.assertEqual( + arithmetic.point_to_hex(sample_point), sample_pubsigningkey) + + def test_privtopub(self): + """Generate public keys and check the result""" + self.assertEqual( + arithmetic.privtopub(sample_privsigningkey), + sample_pubsigningkey + ) + self.assertEqual( + arithmetic.privtopub(sample_privencryptionkey), + sample_pubencryptionkey + ) diff --git a/src/pyelliptic/tests/test_blindsig.py b/src/pyelliptic/tests/test_blindsig.py new file mode 100644 index 0000000000..8c4b2b9d87 --- /dev/null +++ b/src/pyelliptic/tests/test_blindsig.py @@ -0,0 +1,277 @@ +""" +Test for ECC blind signatures +""" +import os +import unittest +from hashlib import sha256 + +try: + from pyelliptic import ECCBlind, ECCBlindChain, OpenSSL +except ImportError: + from pybitmessage.pyelliptic import ECCBlind, ECCBlindChain, OpenSSL + +# pylint: disable=protected-access + + +class TestBlindSig(unittest.TestCase): + """ + Test case for ECC blind signature + """ + def test_blind_sig(self): + """Test full sequence using a random certifier key and a random message""" + # See page 127 of the paper + # (1) Initialization + signer_obj = ECCBlind() + point_r = signer_obj.signer_init() + self.assertEqual(len(signer_obj.pubkey()), 35) + + # (2) Request + requester_obj = ECCBlind(pubkey=signer_obj.pubkey()) + # only 64 byte messages are planned to be used in Bitmessage + msg = os.urandom(64) + msg_blinded = requester_obj.create_signing_request(point_r, msg) + self.assertEqual(len(msg_blinded), 32) + + # check + self.assertNotEqual(sha256(msg).digest(), msg_blinded) + + # (3) Signature Generation + signature_blinded = signer_obj.blind_sign(msg_blinded) + assert isinstance(signature_blinded, bytes) + self.assertEqual(len(signature_blinded), 32) + + # (4) Extraction + signature = requester_obj.unblind(signature_blinded) + assert isinstance(signature, bytes) + self.assertEqual(len(signature), 65) + + self.assertNotEqual(signature, signature_blinded) + + # (5) Verification + verifier_obj = ECCBlind(pubkey=signer_obj.pubkey()) + self.assertTrue(verifier_obj.verify(msg, signature)) + + def test_is_odd(self): + """Test our implementation of BN_is_odd""" + for _ in range(1024): + obj = ECCBlind() + x = OpenSSL.BN_new() + y = OpenSSL.BN_new() + OpenSSL.EC_POINT_get_affine_coordinates( + obj.group, obj.Q, x, y, None) + self.assertEqual(OpenSSL.BN_is_odd(y), + OpenSSL.BN_is_odd_compatible(y)) + + def test_serialize_ec_point(self): + """Test EC point serialization/deserialization""" + for _ in range(1024): + try: + obj = ECCBlind() + obj2 = ECCBlind() + randompoint = obj.Q + serialized = obj._ec_point_serialize(randompoint) + secondpoint = obj2._ec_point_deserialize(serialized) + x0 = OpenSSL.BN_new() + y0 = OpenSSL.BN_new() + OpenSSL.EC_POINT_get_affine_coordinates(obj.group, + randompoint, x0, + y0, obj.ctx) + x1 = OpenSSL.BN_new() + y1 = OpenSSL.BN_new() + OpenSSL.EC_POINT_get_affine_coordinates(obj2.group, + secondpoint, x1, + y1, obj2.ctx) + + 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, None), 0) + finally: + OpenSSL.BN_free(x0) + OpenSSL.BN_free(x1) + OpenSSL.BN_free(y0) + OpenSSL.BN_free(y1) + del obj + del obj2 + + def test_serialize_bn(self): + """Test Bignum serialization/deserialization""" + for _ in range(1024): + obj = ECCBlind() + obj2 = ECCBlind() + randomnum = obj.d + serialized = obj._bn_serialize(randomnum) + secondnum = obj2._bn_deserialize(serialized) + self.assertEqual(OpenSSL.BN_cmp(randomnum, secondnum), 0) + + def test_blind_sig_many(self): + """Test a lot of blind signatures""" + for _ in range(1024): + self.test_blind_sig() + + def test_blind_sig_value(self): + """Test blind signature value checking""" + signer_obj = ECCBlind(value=5) + point_r = signer_obj.signer_init() + requester_obj = ECCBlind(pubkey=signer_obj.pubkey()) + msg = os.urandom(64) + msg_blinded = requester_obj.create_signing_request(point_r, msg) + signature_blinded = signer_obj.blind_sign(msg_blinded) + signature = requester_obj.unblind(signature_blinded) + verifier_obj = ECCBlind(pubkey=signer_obj.pubkey()) + self.assertFalse(verifier_obj.verify(msg, signature, value=8)) + + def test_blind_sig_expiration(self): + """Test blind signature expiration checking""" + signer_obj = ECCBlind(year=2020, month=1) + point_r = signer_obj.signer_init() + requester_obj = ECCBlind(pubkey=signer_obj.pubkey()) + msg = os.urandom(64) + msg_blinded = requester_obj.create_signing_request(point_r, msg) + signature_blinded = signer_obj.blind_sign(msg_blinded) + signature = requester_obj.unblind(signature_blinded) + verifier_obj = ECCBlind(pubkey=signer_obj.pubkey()) + self.assertFalse(verifier_obj.verify(msg, signature)) + + def test_blind_sig_chain(self): # pylint: disable=too-many-locals + """Test blind signature chain using a random certifier key and a random message""" + + test_levels = 4 + msg = os.urandom(1024) + + ca = ECCBlind() + signer_obj = ca + + output = bytearray() + + for level in range(test_levels): + if not level: + output.extend(ca.pubkey()) + requester_obj = ECCBlind(pubkey=signer_obj.pubkey()) + child_obj = ECCBlind() + point_r = signer_obj.signer_init() + pubkey = child_obj.pubkey() + + if level == test_levels - 1: + msg_blinded = requester_obj.create_signing_request(point_r, + msg) + else: + msg_blinded = requester_obj.create_signing_request(point_r, + pubkey) + signature_blinded = signer_obj.blind_sign(msg_blinded) + signature = requester_obj.unblind(signature_blinded) + if level != test_levels - 1: + output.extend(pubkey) + output.extend(signature) + signer_obj = child_obj + verifychain = ECCBlindChain(ca=ca.pubkey(), chain=bytes(output)) + self.assertTrue(verifychain.verify(msg=msg, value=1)) + + def test_blind_sig_chain_wrong_ca(self): # pylint: disable=too-many-locals + """Test blind signature chain with an unlisted ca""" + + test_levels = 4 + msg = os.urandom(1024) + + ca = ECCBlind() + fake_ca = ECCBlind() + signer_obj = fake_ca + + output = bytearray() + + for level in range(test_levels): + requester_obj = ECCBlind(pubkey=signer_obj.pubkey()) + child_obj = ECCBlind() + if not level: + # unlisted CA, but a syntactically valid pubkey + output.extend(fake_ca.pubkey()) + point_r = signer_obj.signer_init() + pubkey = child_obj.pubkey() + + if level == test_levels - 1: + msg_blinded = requester_obj.create_signing_request(point_r, + msg) + else: + msg_blinded = requester_obj.create_signing_request(point_r, + pubkey) + signature_blinded = signer_obj.blind_sign(msg_blinded) + signature = requester_obj.unblind(signature_blinded) + if level != test_levels - 1: + output.extend(pubkey) + output.extend(signature) + signer_obj = child_obj + verifychain = ECCBlindChain(ca=ca.pubkey(), chain=bytes(output)) + self.assertFalse(verifychain.verify(msg, 1)) + + def test_blind_sig_chain_wrong_msg(self): # pylint: disable=too-many-locals + """Test blind signature chain with a fake message""" + + test_levels = 4 + msg = os.urandom(1024) + fake_msg = os.urandom(1024) + + ca = ECCBlind() + signer_obj = ca + + output = bytearray() + + for level in range(test_levels): + if not level: + output.extend(ca.pubkey()) + requester_obj = ECCBlind(pubkey=signer_obj.pubkey()) + child_obj = ECCBlind() + point_r = signer_obj.signer_init() + pubkey = child_obj.pubkey() + + if level == test_levels - 1: + msg_blinded = requester_obj.create_signing_request(point_r, + msg) + else: + msg_blinded = requester_obj.create_signing_request(point_r, + pubkey) + signature_blinded = signer_obj.blind_sign(msg_blinded) + signature = requester_obj.unblind(signature_blinded) + if level != test_levels - 1: + output.extend(pubkey) + output.extend(signature) + signer_obj = child_obj + verifychain = ECCBlindChain(ca=ca.pubkey(), chain=bytes(output)) + self.assertFalse(verifychain.verify(fake_msg, 1)) + + def test_blind_sig_chain_wrong_intermediary(self): # pylint: disable=too-many-locals + """Test blind signature chain using a fake intermediary pubkey""" + + test_levels = 4 + msg = os.urandom(1024) + wrong_level = 2 + + ca = ECCBlind() + signer_obj = ca + fake_intermediary = ECCBlind() + + output = bytearray() + + for level in range(test_levels): + if not level: + output.extend(ca.pubkey()) + requester_obj = ECCBlind(pubkey=signer_obj.pubkey()) + child_obj = ECCBlind() + point_r = signer_obj.signer_init() + pubkey = child_obj.pubkey() + + if level == test_levels - 1: + msg_blinded = requester_obj.create_signing_request(point_r, + msg) + else: + msg_blinded = requester_obj.create_signing_request(point_r, + pubkey) + signature_blinded = signer_obj.blind_sign(msg_blinded) + signature = requester_obj.unblind(signature_blinded) + if level == wrong_level: + output.extend(fake_intermediary.pubkey()) + elif level != test_levels - 1: + output.extend(pubkey) + output.extend(signature) + signer_obj = child_obj + verifychain = ECCBlindChain(ca=ca.pubkey(), chain=bytes(output)) + self.assertFalse(verifychain.verify(msg, 1)) diff --git a/src/pyelliptic/tests/test_ecc.py b/src/pyelliptic/tests/test_ecc.py new file mode 100644 index 0000000000..e87d1c2175 --- /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/pyelliptic/tests/test_openssl.py b/src/pyelliptic/tests/test_openssl.py new file mode 100644 index 0000000000..cb78927760 --- /dev/null +++ b/src/pyelliptic/tests/test_openssl.py @@ -0,0 +1,57 @@ +""" +Test if OpenSSL is working correctly +""" +import unittest + +try: + from pyelliptic.openssl import OpenSSL +except ImportError: + from pybitmessage.pyelliptic import OpenSSL + +try: + OpenSSL.BN_bn2binpad + have_pad = True +except AttributeError: + have_pad = None + + +class TestOpenSSL(unittest.TestCase): + """ + Test cases for OpenSSL + """ + def test_is_odd(self): + """Test BN_is_odd implementation""" + ctx = OpenSSL.BN_CTX_new() + a = OpenSSL.BN_new() + group = OpenSSL.EC_GROUP_new_by_curve_name( + OpenSSL.get_curve("secp256k1")) + OpenSSL.EC_GROUP_get_order(group, a, ctx) + + bad = 0 + for _ in range(1024): + OpenSSL.BN_rand(a, OpenSSL.BN_num_bits(a), 0, 0) + if not OpenSSL.BN_is_odd(a) == OpenSSL.BN_is_odd_compatible(a): + bad += 1 + self.assertEqual(bad, 0) + + @unittest.skipUnless(have_pad, 'Skipping OpenSSL pad test') + def test_padding(self): + """Test an alternative implementation of bn2binpad""" + + ctx = OpenSSL.BN_CTX_new() + a = OpenSSL.BN_new() + n = OpenSSL.BN_new() + group = OpenSSL.EC_GROUP_new_by_curve_name( + OpenSSL.get_curve("secp256k1")) + OpenSSL.EC_GROUP_get_order(group, n, ctx) + + bad = 0 + for _ in range(1024): + OpenSSL.BN_rand(a, OpenSSL.BN_num_bits(n), 0, 0) + b = OpenSSL.malloc(0, OpenSSL.BN_num_bytes(n)) + c = OpenSSL.malloc(0, OpenSSL.BN_num_bytes(a)) + OpenSSL.BN_bn2binpad(a, b, OpenSSL.BN_num_bytes(n)) + OpenSSL.BN_bn2bin(a, c) + if b.raw != c.raw.rjust(OpenSSL.BN_num_bytes(n), b'\x00'): + bad += 1 + self.assertEqual(bad, 0) diff --git a/src/qidenticon.py b/src/qidenticon.py index cc3af6b328..13be357806 100644 --- a/src/qidenticon.py +++ b/src/qidenticon.py @@ -1,255 +1,276 @@ -#!/usr/bin/env python -# -*- coding:utf-8 -*- - ### # qidenticon.py is Licesensed under FreeBSD License. # (http://www.freebsd.org/copyright/freebsd-license.html) # -# Copyright 2013 "Sendiulo". All rights reserved. -# -# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -### - -### -# identicon.py is Licesensed under FreeBSD License. -# (http://www.freebsd.org/copyright/freebsd-license.html) -# # Copyright 1994-2009 Shin Adachi. All rights reserved. +# Copyright 2013 "Sendiulo". All rights reserved. +# Copyright 2018-2021 The Bitmessage Developers. All rights reserved. # -# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: +# Redistribution and use in source and binary forms, +# with or without modification, are permitted provided that the following +# conditions are met: # -# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. # -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ``AS IS'' AND ANY EXPRESS +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ### +# pylint: disable=too-many-locals,too-many-arguments,too-many-function-args """ -qidenticon.py -identicon python implementation with QPixmap output -by sendiulo +Usage +----- -based on -identicon.py -identicon python implementation. -by Shin Adachi +>>> import qidenticon +>>> qidenticon.render_identicon(code, size) -= usage = - -== python == ->>> import qtidenticon ->>> qtidenticon.render_identicon(code, size) - -Return a PIL Image class instance which have generated identicon image. -```size``` specifies `patch size`. Generated image size is 3 * ```size```. +Returns an instance of :class:`QPixmap` which have generated identicon image. +``size`` specifies `patch size`. Generated image size is 3 * ``size``. """ -# we probably don't need all of them, but i don't want to check now -from PyQt4 import QtCore, QtGui -from PyQt4.QtCore import * -from PyQt4.QtGui import * +from six.moves import range + +try: + from PyQt5 import QtCore, QtGui +except (ImportError, RuntimeError): + from PyQt4 import QtCore, QtGui -__all__ = ['render_identicon', 'IdenticonRendererBase'] class IdenticonRendererBase(object): + """Encapsulate methods around rendering identicons""" + PATH_SET = [] - + def __init__(self, code): """ - @param code code for icon + :param code: code for icon """ if not isinstance(code, int): code = int(code) self.code = code - + def render(self, size, twoColor, opacity, penwidth): """ - render identicon to QPicture - - @param size identicon patchsize. (image size is 3 * [size]) - @return QPicture + render identicon to QPixmap + + :param size: identicon patchsize. (image size is 3 * [size]) + :returns: :class:`QPixmap` """ - + # decode the code - middle, corner, side, foreColor, secondColor, swap_cross = self.decode(self.code, twoColor) + middle, corner, side, foreColor, secondColor, swap_cross = \ + self.decode(self.code, twoColor) # make image - image = QPixmap(QSize(size * 3 +penwidth, size * 3 +penwidth)) - + image = QtGui.QPixmap( + QtCore.QSize(size * 3 + penwidth, size * 3 + penwidth)) + # fill background - backColor = QtGui.QColor(255,255,255,opacity) + backColor = QtGui.QColor(255, 255, 255, opacity) image.fill(backColor) - + kwds = { 'image': image, 'size': size, 'foreColor': foreColor if swap_cross else secondColor, 'penwidth': penwidth, 'backColor': backColor} - + # middle patch - image = self.drawPatchQt((1, 1), middle[2], middle[1], middle[0], **kwds) - + image = self.drawPatchQt( + (1, 1), middle[2], middle[1], middle[0], **kwds) + # side patch kwds['foreColor'] = foreColor - kwds['type'] = side[0] - for i in xrange(4): + kwds['patch_type'] = side[0] + for i in range(4): pos = [(1, 0), (2, 1), (1, 2), (0, 1)][i] image = self.drawPatchQt(pos, side[2] + 1 + i, side[1], **kwds) - + # corner patch kwds['foreColor'] = secondColor - kwds['type'] = corner[0] - for i in xrange(4): + kwds['patch_type'] = corner[0] + for i in range(4): pos = [(0, 0), (2, 0), (2, 2), (0, 2)][i] image = self.drawPatchQt(pos, corner[2] + 1 + i, corner[1], **kwds) - + return image - - def drawPatchQt(self, pos, turn, invert, type, image, size, foreColor, - backColor, penwidth): + def drawPatchQt( + self, pos, turn, invert, patch_type, image, size, foreColor, + backColor, penwidth): # pylint: disable=unused-argument """ - @param size patch size + :param size: patch size """ - path = self.PATH_SET[type] + path = self.PATH_SET[patch_type] if not path: # blank patch invert = not invert path = [(0., 0.), (1., 0.), (1., 1.), (0., 1.), (0., 0.)] - - polygon = QPolygonF([QPointF(x*size,y*size) for x,y in path]) - + polygon = QtGui.QPolygonF([ + QtCore.QPointF(x * size, y * size) for x, y in path]) + rot = turn % 4 - rect = [QPointF(0.,0.), QPointF(size, 0.), QPointF(size, size), QPointF(0., size)] - rotation = [0,90,180,270] - - nopen = QtGui.QPen(foreColor, Qt.NoPen) - foreBrush = QtGui.QBrush(foreColor, Qt.SolidPattern) + rect = [ + QtCore.QPointF(0., 0.), QtCore.QPointF(size, 0.), + QtCore.QPointF(size, size), QtCore.QPointF(0., size)] + rotation = [0, 90, 180, 270] + + nopen = QtGui.QPen(foreColor, QtCore.Qt.NoPen) + foreBrush = QtGui.QBrush(foreColor, QtCore.Qt.SolidPattern) if penwidth > 0: pen_color = QtGui.QColor(255, 255, 255) - pen = QtGui.QPen(pen_color, Qt.SolidPattern) + pen = QtGui.QPen(pen_color, QtCore.Qt.SolidPattern) pen.setWidth(penwidth) - - painter = QPainter() + + painter = QtGui.QPainter() painter.begin(image) painter.setPen(nopen) - - painter.translate(pos[0]*size +penwidth/2, pos[1]*size +penwidth/2) + + painter.translate( + pos[0] * size + penwidth / 2, pos[1] * size + penwidth / 2) painter.translate(rect[rot]) painter.rotate(rotation[rot]) - + if invert: # subtract the actual polygon from a rectangle to invert it - poly_rect = QPolygonF(rect) + poly_rect = QtGui.QPolygonF(rect) polygon = poly_rect.subtracted(polygon) painter.setBrush(foreBrush) if penwidth > 0: # draw the borders painter.setPen(pen) - painter.drawPolygon(polygon, Qt.WindingFill) + painter.drawPolygon(polygon, QtCore.Qt.WindingFill) # draw the fill painter.setPen(nopen) - painter.drawPolygon(polygon, Qt.WindingFill) - + painter.drawPolygon(polygon, QtCore.Qt.WindingFill) + painter.end() - + return image - ### virtual functions - def decode(self, code): + def decode(self, code, twoColor): + """virtual functions""" raise NotImplementedError - + + class DonRenderer(IdenticonRendererBase): """ - Don Park's implementation of identicon - see : http://www.docuverse.com/blog/donpark/2007/01/19/identicon-updated-and-source-released + Don Park's implementation of identicon, see: + https://blog.docuverse.com/2007/01/18/identicon-updated-and-source-released """ - + PATH_SET = [ - #[0] full square: + # [0] full square: [(0, 0), (4, 0), (4, 4), (0, 4)], - #[1] right-angled triangle pointing top-left: + # [1] right-angled triangle pointing top-left: [(0, 0), (4, 0), (0, 4)], - #[2] upwardy triangle: + # [2] upwardy triangle: [(2, 0), (4, 4), (0, 4)], - #[3] left half of square, standing rectangle: + # [3] left half of square, standing rectangle: [(0, 0), (2, 0), (2, 4), (0, 4)], - #[4] square standing on diagonale: + # [4] square standing on diagonale: [(2, 0), (4, 2), (2, 4), (0, 2)], - #[5] kite pointing topleft: + # [5] kite pointing topleft: [(0, 0), (4, 2), (4, 4), (2, 4)], - #[6] Sierpinski triangle, fractal triangles: + # [6] Sierpinski triangle, fractal triangles: [(2, 0), (4, 4), (2, 4), (3, 2), (1, 2), (2, 4), (0, 4)], - #[7] sharp angled lefttop pointing triangle: + # [7] sharp angled lefttop pointing triangle: [(0, 0), (4, 2), (2, 4)], - #[8] small centered square: + # [8] small centered square: [(1, 1), (3, 1), (3, 3), (1, 3)], - #[9] two small triangles: + # [9] two small triangles: [(2, 0), (4, 0), (0, 4), (0, 2), (2, 2)], - #[10] small topleft square: + # [10] small topleft square: [(0, 0), (2, 0), (2, 2), (0, 2)], - #[11] downpointing right-angled triangle on bottom: + # [11] downpointing right-angled triangle on bottom: [(0, 2), (4, 2), (2, 4)], - #[12] uppointing right-angled triangle on bottom: + # [12] uppointing right-angled triangle on bottom: [(2, 2), (4, 4), (0, 4)], - #[13] small rightbottom pointing right-angled triangle on topleft: + # [13] small rightbottom pointing right-angled triangle on topleft: [(2, 0), (2, 2), (0, 2)], - #[14] small lefttop pointing right-angled triangle on topleft: + # [14] small lefttop pointing right-angled triangle on topleft: [(0, 0), (2, 0), (0, 2)], - #[15] empty: + # [15] empty: []] - # get the [0] full square, [4] square standing on diagonale, [8] small centered square, or [15] empty tile: + # get the [0] full square, [4] square standing on diagonale, + # [8] small centered square, or [15] empty tile: MIDDLE_PATCH_SET = [0, 4, 8, 15] - + # modify path set - for idx in xrange(len(PATH_SET)): - if PATH_SET[idx]: - p = map(lambda vec: (vec[0] / 4.0, vec[1] / 4.0), PATH_SET[idx]) + for idx, path in enumerate(PATH_SET): + if path: + p = [(vec[0] / 4.0, vec[1] / 4.0) for vec in path] PATH_SET[idx] = p + p[:1] - + def decode(self, code, twoColor): - # decode the code - shift = 0; middleType = (code >> shift) & 0x03 - shift += 2; middleInvert= (code >> shift) & 0x01 - shift += 1; cornerType = (code >> shift) & 0x0F - shift += 4; cornerInvert= (code >> shift) & 0x01 - shift += 1; cornerTurn = (code >> shift) & 0x03 - shift += 2; sideType = (code >> shift) & 0x0F - shift += 4; sideInvert = (code >> shift) & 0x01 - shift += 1; sideTurn = (code >> shift) & 0x03 - shift += 2; blue = (code >> shift) & 0x1F - shift += 5; green = (code >> shift) & 0x1F - shift += 5; red = (code >> shift) & 0x1F - shift += 5; second_blue = (code >> shift) & 0x1F - shift += 5; second_green= (code >> shift) & 0x1F - shift += 5; second_red = (code >> shift) & 0x1F - shift += 1; swap_cross = (code >> shift) & 0x01 - + """decode the code""" + + shift = 0 + middleType = (code >> shift) & 0x03 + shift += 2 + middleInvert = (code >> shift) & 0x01 + shift += 1 + cornerType = (code >> shift) & 0x0F + shift += 4 + cornerInvert = (code >> shift) & 0x01 + shift += 1 + cornerTurn = (code >> shift) & 0x03 + shift += 2 + sideType = (code >> shift) & 0x0F + shift += 4 + sideInvert = (code >> shift) & 0x01 + shift += 1 + sideTurn = (code >> shift) & 0x03 + shift += 2 + blue = (code >> shift) & 0x1F + shift += 5 + green = (code >> shift) & 0x1F + shift += 5 + red = (code >> shift) & 0x1F + shift += 5 + second_blue = (code >> shift) & 0x1F + shift += 5 + second_green = (code >> shift) & 0x1F + shift += 5 + second_red = (code >> shift) & 0x1F + shift += 1 + swap_cross = (code >> shift) & 0x01 + middleType = self.MIDDLE_PATCH_SET[middleType] - + foreColor = (red << 3, green << 3, blue << 3) foreColor = QtGui.QColor(*foreColor) - + if twoColor: - secondColor = (second_blue << 3, second_green << 3, second_red << 3) + secondColor = ( + second_blue << 3, second_green << 3, second_red << 3) secondColor = QtGui.QColor(*secondColor) else: secondColor = foreColor - + return (middleType, middleInvert, 0),\ (cornerType, cornerInvert, cornerTurn),\ (sideType, sideInvert, sideTurn),\ - foreColor, secondColor, swap_cross + foreColor, secondColor, swap_cross -def render_identicon(code, size, twoColor=False, opacity=255, penwidth=0, renderer=None): +def render_identicon( + code, size, twoColor=False, opacity=255, penwidth=0, renderer=None): + """Render an image""" if not renderer: renderer = DonRenderer - return renderer(code).render(size, twoColor, opacity, penwidth) \ No newline at end of file + return renderer(code).render(size, twoColor, opacity, penwidth) diff --git a/src/queues.py b/src/queues.py index e8923dbd00..cee5ce8b9b 100644 --- a/src/queues.py +++ b/src/queues.py @@ -1,16 +1,46 @@ -import Queue +"""Most of the queues used by bitmessage threads are defined here.""" -from class_objectProcessorQueue import ObjectProcessorQueue -from multiqueue import MultiQueue +import threading +import time -workerQueue = Queue.Queue() -UISignalQueue = Queue.Queue() -addressGeneratorQueue = Queue.Queue() -# receiveDataThreads dump objects they hear on the network into this queue to be processed. +from six.moves import queue + + +class ObjectProcessorQueue(queue.Queue): + """Special queue class using lock for `.threads.objectProcessor`""" + + maxSize = 32000000 + + def __init__(self): + queue.Queue.__init__(self) + self.sizeLock = threading.Lock() + #: in Bytes. We maintain this to prevent nodes from flooding us + #: with objects which take up too much memory. If this gets + #: too big we'll sleep before asking for further objects. + self.curSize = 0 + + def put(self, item, block=True, timeout=None): + while self.curSize >= self.maxSize: + time.sleep(1) + with self.sizeLock: + self.curSize += len(item[1]) + queue.Queue.put(self, item, block, timeout) + + def get(self, block=True, timeout=None): + item = queue.Queue.get(self, block, timeout) + with self.sizeLock: + self.curSize -= len(item[1]) + return item + + +workerQueue = queue.Queue() +UISignalQueue = queue.Queue() +addressGeneratorQueue = queue.Queue() +#: `.network.ReceiveQueueThread` instances dump objects they hear +#: on the network into this queue to be processed. objectProcessorQueue = ObjectProcessorQueue() -invQueue = MultiQueue() -addrQueue = MultiQueue() -portCheckerQueue = Queue.Queue() -receiveDataQueue = Queue.Queue() -apiAddressGeneratorReturnQueue = Queue.Queue( - ) # The address generator thread uses this queue to get information back to the API thread. +#: The address generator thread uses this queue to get information back +#: to the API thread. +apiAddressGeneratorReturnQueue = queue.Queue() +#: for exceptions +excQueue = queue.Queue() diff --git a/src/randomtrackingdict.py b/src/randomtrackingdict.py index 2bd6e257ea..5bf1918192 100644 --- a/src/randomtrackingdict.py +++ b/src/randomtrackingdict.py @@ -1,12 +1,31 @@ -import random +""" +Track randomize ordered dict +""" from threading import RLock from time import time -import helper_random + +try: + import helper_random +except ImportError: + from . import helper_random + class RandomTrackingDict(object): + """ + Dict with randomised order and tracking. + + Keeps a track of how many items have been requested from the dict, + and timeouts. Resets after all objects have been retrieved and timed out. + The main purpose of this isn't as much putting related code together + as performance optimisation and anonymisation of downloading of objects + from other peers. If done using a standard dict or array, it takes + too much CPU (and looks convoluted). Randomisation helps with anonymity. + """ + # pylint: disable=too-many-instance-attributes maxPending = 10 pendingTimeout = 60 - def __init__(self): # O(1) + + def __init__(self): self.dictionary = {} self.indexDict = [] self.len = 0 @@ -46,7 +65,7 @@ def __setitem__(self, key, value): self.len += 1 def __delitem__(self, key): - if not key in self.dictionary: + if key not in self.dictionary: raise KeyError with self.lock: index = self.dictionary[key][0] @@ -67,9 +86,15 @@ def __delitem__(self, key): self.len -= 1 def setMaxPending(self, maxPending): + """ + Sets maximum number of objects that can be retrieved from the class + simultaneously as long as there is no timeout + """ self.maxPending = maxPending def setPendingTimeout(self, pendingTimeout): + """Sets how long to wait for a timeout if max pending is reached + (or all objects have been retrieved)""" self.pendingTimeout = pendingTimeout def setLastObject(self): @@ -77,19 +102,26 @@ def setLastObject(self): self.lastObject = time() def randomKeys(self, count=1): - if self.len == 0 or ((self.pendingLen >= self.maxPending or - self.pendingLen == self.len) and self.lastPoll + - self.pendingTimeout > time()): + """Retrieve count random keys from the dict + that haven't already been retrieved""" + if self.len == 0 or ( + (self.pendingLen >= self.maxPending or self.pendingLen == self.len) + and self.lastPoll + self.pendingTimeout > time()): raise KeyError + + # pylint: disable=redefined-outer-name with self.lock: # reset if we've requested all - # or if last object received too long time ago - if self.pendingLen == self.len or self.lastObject + self.pendingTimeout > time(): + # and if last object received too long time ago + if self.pendingLen == self.len and self.lastObject + \ + self.pendingTimeout < time(): self.pendingLen = 0 + self.setLastObject() available = self.len - self.pendingLen if count > available: count = available - randomIndex = helper_random.randomsample(range(self.len - self.pendingLen), count) + randomIndex = helper_random.randomsample( + range(self.len - self.pendingLen), count) retval = [self.indexDict[i] for i in randomIndex] for i in sorted(randomIndex, reverse=True): @@ -98,44 +130,3 @@ def randomKeys(self, count=1): self.pendingLen += 1 self.lastPoll = time() return retval - -if __name__ == '__main__': - def randString(): - retval = b'' - for _ in range(32): - retval += chr(random.randint(0,255)) - return retval - - a = [] - k = RandomTrackingDict() - d = {} - -# print "populating normal dict" -# a.append(time()) -# for i in range(50000): -# d[randString()] = True -# a.append(time()) - print "populating random tracking dict" - a.append(time()) - for i in range(50000): - k[randString()] = True - a.append(time()) - print "done" - while len(k) > 0: - retval = k.randomKeys(1000) - if not retval: - print "error getting random keys" - #a.append(time()) - try: - k.randomKeys(100) - print "bad" - except KeyError: - pass - #a.append(time()) - for i in retval: - del k[i] - #a.append(time()) - a.append(time()) - - for x in range(len(a) - 1): - print "%i: %.3f" % (x, a[x+1] - a[x]) diff --git a/src/shared.py b/src/shared.py index caf247696c..9357a4ee4f 100644 --- a/src/shared.py +++ b/src/shared.py @@ -1,48 +1,29 @@ -from __future__ import division +""" +Some shared functions + +.. deprecated:: 0.6.3 + Should be moved to different places and this file removed, + but it needs refactoring. +""" +from __future__ import division # Libraries. +import hashlib import os -import sys import stat -import time -import threading -import traceback -import hashlib -import subprocess -from struct import unpack +import subprocess # nosec B404 +import sys from binascii import hexlify -from pyelliptic import arithmetic + +from six.moves.reprlib import repr # Project imports. -import protocol -import state import highlevelcrypto -from bmconfigparser import BMConfigParser +import state +from addresses import decodeAddress, encodeVarint +from bmconfigparser import config from debug import logger -from addresses import ( - decodeAddress, encodeVarint, decodeVarint, varintDecodeError, - calculateInventoryHash -) -from helper_sql import sqlQuery, sqlExecute -from inventory import Inventory -from queues import objectProcessorQueue - - -verbose = 1 -# This is obsolete with the change to protocol v3 -# but the singleCleaner thread still hasn't been updated -# so we need this a little longer. -maximumAgeOfAnObjectThatIAmWillingToAccept = 216000 -# Equals 4 weeks. You could make this longer if you want -# but making it shorter would not be advisable because -# there is a very small possibility that it could keep you -# from obtaining a needed pubkey for a period of time. -lengthOfTimeToHoldOnToAllPubkeys = 2419200 -maximumAgeOfNodesThatIAdvertiseToOthers = 10800 # Equals three hours -# If you set this to True while on the normal network, -# you won't be able to send or sometimes receive messages. -useVeryEasyProofOfWorkForTesting = False - +from helper_sql import sqlQuery myECCryptorObjects = {} MyECSubscriptionCryptorObjects = {} @@ -51,38 +32,10 @@ myAddressesByHash = {} # The key in this dictionary is the tag generated from the address. myAddressesByTag = {} -broadcastSendersForWhichImWatching = {} -printLock = threading.Lock() -statusIconColor = 'red' -# List of hosts to which we are connected. Used to guarantee -# that the outgoingSynSender threads won't connect to the same -# remote node twice. -connectedHostsList = {} -thisapp = None # singleton lock instance -alreadyAttemptedConnectionsList = { -} # This is a list of nodes to which we have already attempted a connection -alreadyAttemptedConnectionsListLock = threading.Lock() -# used to clear out the alreadyAttemptedConnectionsList periodically -# so that we will retry connecting to hosts to which we have already -# tried to connect. -alreadyAttemptedConnectionsListResetTime = int(time.time()) -# A list of the amounts of time it took to successfully decrypt msg messages -successfullyDecryptMessageTimings = [] -ackdataForWhichImWatching = {} -# used by API command clientStatus -clientHasReceivedIncomingConnections = False -numberOfMessagesProcessed = 0 -numberOfBroadcastsProcessed = 0 -numberOfPubkeysProcessed = 0 - -# If True, the singleCleaner will write it to disk eventually. -needToWriteKnownNodesToDisk = False - -maximumLengthOfTimeToBotherResendingMessages = 0 -timeOffsetWrongCount = 0 def isAddressInMyAddressBook(address): + """Is address in my addressbook?""" queryreturn = sqlQuery( '''select address from addressbook where address=?''', address) @@ -91,6 +44,7 @@ def isAddressInMyAddressBook(address): # At this point we should really just have a isAddressInMy(book, address)... def isAddressInMySubscriptionsList(address): + """Am I subscribed to this address?""" queryreturn = sqlQuery( '''select * from subscriptions where address=?''', str(address)) @@ -98,6 +52,9 @@ def isAddressInMySubscriptionsList(address): def isAddressInMyAddressBookSubscriptionsListOrWhitelist(address): + """ + Am I subscribed to this address, is it in my addressbook or whitelist? + """ if isAddressInMyAddressBook(address): return True @@ -117,101 +74,74 @@ def isAddressInMyAddressBookSubscriptionsListOrWhitelist(address): return False -def decodeWalletImportFormat(WIFstring): - 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) - # 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) - - def reloadMyAddressHashes(): + """Reload keys for user's addresses from the config file""" logger.debug('reloading keys from keys.dat file') myECCryptorObjects.clear() myAddressesByHash.clear() myAddressesByTag.clear() # myPrivateKeys.clear() - keyfileSecure = checkSensitiveFilePermissions(state.appdata + 'keys.dat') + 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, hash = \ - decodeAddress(addressInKeysFile) - 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')) - ) + for addressInKeysFile in config.addresses(): + if not config.getboolean(addressInKeysFile, 'enabled'): + continue - # It is 32 bytes encoded as 64 hex characters - if len(privEncryptionKey) == 64: - myECCryptorObjects[hash] = \ - highlevelcrypto.makeCryptor(privEncryptionKey) - myAddressesByHash[hash] = addressInKeysFile - tag = hashlib.sha512(hashlib.sha512( - encodeVarint(addressVersionNumber) + - encodeVarint(streamNumber) + hash).digest() - ).digest()[32:] - myAddressesByTag[tag] = addressInKeysFile + hasEnabledKeys = True - else: - logger.error( - 'Error in reloadMyAddressHashes: Can\'t handle' - ' address versions other than 2, 3, or 4.\n' - ) + 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(state.appdata + 'keys.dat', hasEnabledKeys) + fixSensitiveFilePermissions(os.path.join( + state.appdata, 'keys.dat'), hasEnabledKeys) def reloadBroadcastSendersForWhichImWatching(): - broadcastSendersForWhichImWatching.clear() + """ + Reinitialize runtime data for the broadcasts I'm subscribed to + from the config file + """ MyECSubscriptionCryptorObjects.clear() queryreturn = sqlQuery('SELECT address FROM subscriptions where enabled=1') logger.debug('reloading subscriptions...') - for row in queryreturn: - address, = row - # status - _, addressVersionNumber, streamNumber, hash = decodeAddress(address) - if addressVersionNumber == 2: - broadcastSendersForWhichImWatching[hash] = 0 - # Now, for all addresses, even version 2 addresses, - # we should create Cryptor objects in a dictionary which we will - # use to attempt to decrypt encrypted broadcast messages. - - if addressVersionNumber <= 3: - privEncryptionKey = hashlib.sha512( - encodeVarint(addressVersionNumber) + - encodeVarint(streamNumber) + hash - ).digest()[:32] - MyECSubscriptionCryptorObjects[hash] = \ + for address, in queryreturn: + version, stream, ripe = decodeAddress(address)[1:] + data = encodeVarint(version) + encodeVarint(stream) + ripe + if version <= 3: + privEncryptionKey = hashlib.sha512(data).digest()[:32] + MyECSubscriptionCryptorObjects[ripe] = \ highlevelcrypto.makeCryptor(hexlify(privEncryptionKey)) else: - doubleHashOfAddressData = hashlib.sha512(hashlib.sha512( - encodeVarint(addressVersionNumber) + - encodeVarint(streamNumber) + hash - ).digest()).digest() + doubleHashOfAddressData = highlevelcrypto.double_sha512(data) tag = doubleHashOfAddressData[32:] privEncryptionKey = doubleHashOfAddressData[:32] MyECSubscriptionCryptorObjects[tag] = \ @@ -219,21 +149,22 @@ def reloadBroadcastSendersForWhichImWatching(): def fixPotentiallyInvalidUTF8Data(text): + """Sanitise invalid UTF-8 strings""" try: - unicode(text, 'utf-8') + text.decode('utf-8') return text - except: + except UnicodeDecodeError: return 'Part of the message is corrupt. The message cannot be' \ - ' displayed the normal way.\n\n' + repr(text) + ' displayed the normal way.\n\n' + repr(text) -# Checks sensitive file permissions for inappropriate umask -# during keys.dat creation. (Or unwise subsequent chmod.) -# -# Returns true iff file appears to have appropriate permissions. def checkSensitiveFilePermissions(filename): + """ + :param str filename: path to the file + :return: True if file appears to have appropriate permissions. + """ if sys.platform == 'win32': - # TODO: This might deserve extra checks by someone familiar with + # .. todo:: This might deserve extra checks by someone familiar with # Windows systems. return True elif sys.platform[:7] == 'freebsd': @@ -241,30 +172,29 @@ def checkSensitiveFilePermissions(filename): present_permissions = os.stat(filename)[0] disallowed_permissions = stat.S_IRWXG | stat.S_IRWXO return present_permissions & disallowed_permissions == 0 - else: - try: - # Skip known problems for non-Win32 filesystems - # without POSIX permissions. - fstype = subprocess.check_output( - 'stat -f -c "%%T" %s' % (filename), - shell=True, - stderr=subprocess.STDOUT - ) - if 'fuseblk' in fstype: - logger.info( - 'Skipping file permissions check for %s.' - ' Filesystem fuseblk detected.', filename) - return True - except: - # Swallow exception here, but we might run into trouble later! - logger.error('Could not determine filesystem type. %s', filename) - present_permissions = os.stat(filename)[0] - disallowed_permissions = stat.S_IRWXG | stat.S_IRWXO - return present_permissions & disallowed_permissions == 0 + try: + # Skip known problems for non-Win32 filesystems + # without POSIX permissions. + fstype = subprocess.check_output( + ['/usr/bin/stat', '-f', '-c', '%T', filename], + stderr=subprocess.STDOUT + ) # nosec B603 + if 'fuseblk' in fstype: + logger.info( + 'Skipping file permissions check for %s.' + ' Filesystem fuseblk detected.', filename) + return True + except: # noqa:E722 + # Swallow exception here, but we might run into trouble later! + logger.error('Could not determine filesystem type. %s', filename) + present_permissions = os.stat(filename)[0] + disallowed_permissions = stat.S_IRWXG | stat.S_IRWXO + return present_permissions & disallowed_permissions == 0 # Fixes permissions on a sensitive file. def fixSensitiveFilePermissions(filename, hasEnabledKeys): + """Try to change file permissions to be more restrictive""" if hasEnabledKeys: logger.warning( 'Keyfile had insecure permissions, and there were enabled' @@ -286,395 +216,3 @@ def fixSensitiveFilePermissions(filename, hasEnabledKeys): except Exception: logger.exception('Keyfile permissions could not be fixed.') raise - - -def isBitSetWithinBitfield(fourByteString, n): - # Uses MSB 0 bit numbering across 4 bytes of data - n = 31 - n - x, = unpack('>L', fourByteString) - return x & 2**n != 0 - - -def decryptAndCheckPubkeyPayload(data, address): - """ - Version 4 pubkeys are encrypted. This function is run when we - already have the address to which we want to try to send a message. - The 'data' may come either off of the wire or we might have had it - already in our inventory when we tried to send a msg to this - particular address. - """ - try: - # status - _, addressVersion, streamNumber, ripe = decodeAddress(address) - - readPosition = 20 # bypass the nonce, time, and object type - embeddedAddressVersion, varintLength = \ - decodeVarint(data[readPosition:readPosition + 10]) - readPosition += varintLength - embeddedStreamNumber, varintLength = \ - decodeVarint(data[readPosition:readPosition + 10]) - readPosition += varintLength - # We'll store the address version and stream number - # (and some more) in the pubkeys table. - storedData = data[20:readPosition] - - if addressVersion != embeddedAddressVersion: - logger.info( - 'Pubkey decryption was UNsuccessful' - ' due to address version mismatch.') - return 'failed' - if streamNumber != embeddedStreamNumber: - logger.info( - 'Pubkey decryption was UNsuccessful' - ' due to stream number mismatch.') - return 'failed' - - tag = data[readPosition:readPosition + 32] - readPosition += 32 - # the time through the tag. More data is appended onto - # signedData below after the decryption. - signedData = data[8:readPosition] - encryptedData = data[readPosition:] - - # Let us try to decrypt the pubkey - toAddress, cryptorObject = state.neededPubkeys[tag] - if toAddress != address: - logger.critical( - 'decryptAndCheckPubkeyPayload failed due to toAddress' - ' mismatch. This is very peculiar.' - ' toAddress: %s, address %s', - toAddress, address - ) - # the only way I can think that this could happen - # is if someone encodes their address data two different ways. - # That sort of address-malleability should have been caught - # by the UI or API and an error given to the user. - return 'failed' - try: - decryptedData = cryptorObject.decrypt(encryptedData) - except: - # Someone must have encrypted some data with a different key - # but tagged it with a tag for which we are watching. - logger.info('Pubkey decryption was unsuccessful.') - return 'failed' - - readPosition = 0 - # bitfieldBehaviors = decryptedData[readPosition:readPosition + 4] - readPosition += 4 - publicSigningKey = \ - '\x04' + decryptedData[readPosition:readPosition + 64] - readPosition += 64 - publicEncryptionKey = \ - '\x04' + decryptedData[readPosition:readPosition + 64] - readPosition += 64 - specifiedNonceTrialsPerByte, specifiedNonceTrialsPerByteLength = \ - decodeVarint(decryptedData[readPosition:readPosition + 10]) - readPosition += specifiedNonceTrialsPerByteLength - specifiedPayloadLengthExtraBytes, \ - specifiedPayloadLengthExtraBytesLength = \ - decodeVarint(decryptedData[readPosition:readPosition + 10]) - readPosition += specifiedPayloadLengthExtraBytesLength - storedData += decryptedData[:readPosition] - signedData += decryptedData[:readPosition] - signatureLength, signatureLengthLength = \ - decodeVarint(decryptedData[readPosition:readPosition + 10]) - readPosition += signatureLengthLength - signature = decryptedData[readPosition:readPosition + signatureLength] - - if not highlevelcrypto.verify( - signedData, signature, hexlify(publicSigningKey)): - logger.info( - 'ECDSA verify failed (within decryptAndCheckPubkeyPayload)') - return 'failed' - - logger.info( - 'ECDSA verify passed (within decryptAndCheckPubkeyPayload)') - - sha = hashlib.new('sha512') - sha.update(publicSigningKey + publicEncryptionKey) - ripeHasher = hashlib.new('ripemd160') - ripeHasher.update(sha.digest()) - embeddedRipe = ripeHasher.digest() - - if embeddedRipe != ripe: - # Although this pubkey object had the tag were were looking for - # and was encrypted with the correct encryption key, - # it doesn't contain the correct pubkeys. Someone is - # either being malicious or using buggy software. - logger.info( - 'Pubkey decryption was UNsuccessful due to RIPE mismatch.') - return 'failed' - - # Everything checked out. Insert it into the pubkeys table. - - logger.info( - 'within decryptAndCheckPubkeyPayload, ' - 'addressVersion: %s, streamNumber: %s\nripe %s\n' - 'publicSigningKey in hex: %s\npublicEncryptionKey in hex: %s', - addressVersion, streamNumber, hexlify(ripe), - hexlify(publicSigningKey), hexlify(publicEncryptionKey) - ) - - t = (address, addressVersion, storedData, int(time.time()), 'yes') - sqlExecute('''INSERT INTO pubkeys VALUES (?,?,?,?,?)''', *t) - return 'successful' - except varintDecodeError: - logger.info( - 'Pubkey decryption was UNsuccessful due to a malformed varint.') - return 'failed' - except Exception: - logger.critical( - 'Pubkey decryption was UNsuccessful because of' - ' an unhandled exception! This is definitely a bug! \n%s' % - traceback.format_exc() - ) - return 'failed' - - -def checkAndShareObjectWithPeers(data): - """ - This function is called after either receiving an object - off of the wire or after receiving one as ackdata. - Returns the length of time that we should reserve to process - this message if we are receiving it off of the wire. - """ - if len(data) > 2 ** 18: - logger.info( - 'The payload length of this object is too large (%i bytes).' - ' Ignoring it.', len(data) - ) - return 0 - # Let us check to make sure that the proof of work is sufficient. - if not protocol.isProofOfWorkSufficient(data): - logger.info('Proof of work is insufficient.') - return 0 - - endOfLifeTime, = unpack('>Q', data[8:16]) - # The TTL may not be larger than 28 days + 3 hours of wiggle room - if endOfLifeTime - int(time.time()) > 28 * 24 * 60 * 60 + 10800: - logger.info( - 'This object\'s End of Life time is too far in the future.' - ' Ignoring it. Time is %s', endOfLifeTime - ) - return 0 - # The EOL time was more than an hour ago. That's too much. - if endOfLifeTime - int(time.time()) < -3600: - logger.info( - 'This object\'s End of Life time was more than an hour ago.' - ' Ignoring the object. Time is %s' % endOfLifeTime - ) - return 0 - intObjectType, = unpack('>I', data[16:20]) - try: - if intObjectType == 0: - _checkAndShareGetpubkeyWithPeers(data) - return 0.1 - elif intObjectType == 1: - _checkAndSharePubkeyWithPeers(data) - return 0.1 - elif intObjectType == 2: - _checkAndShareMsgWithPeers(data) - return 0.6 - elif intObjectType == 3: - _checkAndShareBroadcastWithPeers(data) - return 0.6 - else: - _checkAndShareUndefinedObjectWithPeers(data) - return 0.6 - except varintDecodeError as e: - logger.debug( - 'There was a problem with a varint while checking' - ' to see whether it was appropriate to share an object' - ' with peers. Some details: %s' % e) - except Exception: - logger.critical( - 'There was a problem while checking to see whether it was' - ' appropriate to share an object with peers. This is' - ' definitely a bug! \n%s' % traceback.format_exc()) - return 0 - - -def _checkAndShareUndefinedObjectWithPeers(data): - embeddedTime, = unpack('>Q', data[8:16]) - readPosition = 20 # bypass nonce, time, and object type - objectVersion, objectVersionLength = decodeVarint( - data[readPosition:readPosition + 9]) - readPosition += objectVersionLength - streamNumber, streamNumberLength = decodeVarint( - data[readPosition:readPosition + 9]) - if streamNumber not in state.streamsInWhichIAmParticipating: - logger.debug( - 'The streamNumber %i isn\'t one we are interested in.', - streamNumber - ) - return - - inventoryHash = calculateInventoryHash(data) - if inventoryHash in Inventory(): - logger.debug( - 'We have already received this undefined object. Ignoring.') - return - objectType, = unpack('>I', data[16:20]) - Inventory()[inventoryHash] = ( - objectType, streamNumber, data, embeddedTime, '') - logger.debug('advertising inv with hash: %s', hexlify(inventoryHash)) - protocol.broadcastToSendDataQueues( - (streamNumber, 'advertiseobject', inventoryHash)) - - -def _checkAndShareMsgWithPeers(data): - embeddedTime, = unpack('>Q', data[8:16]) - readPosition = 20 # bypass nonce, time, and object type - objectVersion, objectVersionLength = \ - decodeVarint(data[readPosition:readPosition + 9]) - readPosition += objectVersionLength - streamNumber, streamNumberLength = \ - decodeVarint(data[readPosition:readPosition + 9]) - if streamNumber not in state.streamsInWhichIAmParticipating: - logger.debug( - 'The streamNumber %i isn\'t one we are interested in.', - streamNumber - ) - return - readPosition += streamNumberLength - inventoryHash = calculateInventoryHash(data) - if inventoryHash in Inventory(): - logger.debug('We have already received this msg message. Ignoring.') - return - # This msg message is valid. Let's let our peers know about it. - objectType = 2 - Inventory()[inventoryHash] = ( - objectType, streamNumber, data, embeddedTime, '') - logger.debug('advertising inv with hash: %s', hexlify(inventoryHash)) - protocol.broadcastToSendDataQueues( - (streamNumber, 'advertiseobject', inventoryHash)) - - # Now let's enqueue it to be processed ourselves. - objectProcessorQueue.put((objectType, data)) - - -def _checkAndShareGetpubkeyWithPeers(data): - if len(data) < 42: - logger.info( - 'getpubkey message doesn\'t contain enough data. Ignoring.') - return - embeddedTime, = unpack('>Q', data[8:16]) - readPosition = 20 # bypass the nonce, time, and object type - requestedAddressVersionNumber, addressVersionLength = \ - decodeVarint(data[readPosition:readPosition + 10]) - readPosition += addressVersionLength - streamNumber, streamNumberLength = \ - decodeVarint(data[readPosition:readPosition + 10]) - if streamNumber not in state.streamsInWhichIAmParticipating: - logger.debug( - 'The streamNumber %i isn\'t one we are interested in.', - streamNumber - ) - return - readPosition += streamNumberLength - - inventoryHash = calculateInventoryHash(data) - if inventoryHash in Inventory(): - logger.debug( - 'We have already received this getpubkey request. Ignoring it.') - return - - objectType = 0 - Inventory()[inventoryHash] = ( - objectType, streamNumber, data, embeddedTime, '') - # This getpubkey request is valid. Forward to peers. - logger.debug('advertising inv with hash: %s', hexlify(inventoryHash)) - protocol.broadcastToSendDataQueues( - (streamNumber, 'advertiseobject', inventoryHash)) - - # Now let's queue it to be processed ourselves. - objectProcessorQueue.put((objectType, data)) - - -def _checkAndSharePubkeyWithPeers(data): - if len(data) < 146 or len(data) > 440: # sanity check - return - embeddedTime, = unpack('>Q', data[8:16]) - readPosition = 20 # bypass the nonce, time, and object type - addressVersion, varintLength = \ - decodeVarint(data[readPosition:readPosition + 10]) - readPosition += varintLength - streamNumber, varintLength = \ - decodeVarint(data[readPosition:readPosition + 10]) - readPosition += varintLength - if streamNumber not in state.streamsInWhichIAmParticipating: - logger.debug( - 'The streamNumber %i isn\'t one we are interested in.', - streamNumber - ) - return - if addressVersion >= 4: - tag = data[readPosition:readPosition + 32] - logger.debug('tag in received pubkey is: %s', hexlify(tag)) - else: - tag = '' - - inventoryHash = calculateInventoryHash(data) - if inventoryHash in Inventory(): - logger.debug('We have already received this pubkey. Ignoring it.') - return - objectType = 1 - Inventory()[inventoryHash] = ( - objectType, streamNumber, data, embeddedTime, tag) - # This object is valid. Forward it to peers. - logger.debug('advertising inv with hash: %s', hexlify(inventoryHash)) - protocol.broadcastToSendDataQueues( - (streamNumber, 'advertiseobject', inventoryHash)) - - # Now let's queue it to be processed ourselves. - objectProcessorQueue.put((objectType, data)) - - -def _checkAndShareBroadcastWithPeers(data): - if len(data) < 180: - logger.debug( - 'The payload length of this broadcast packet is unreasonably low.' - ' Someone is probably trying funny business. Ignoring message.') - return - embeddedTime, = unpack('>Q', data[8:16]) - readPosition = 20 # bypass the nonce, time, and object type - broadcastVersion, broadcastVersionLength = \ - decodeVarint(data[readPosition:readPosition + 10]) - readPosition += broadcastVersionLength - if broadcastVersion >= 2: - streamNumber, streamNumberLength = \ - decodeVarint(data[readPosition:readPosition + 10]) - readPosition += streamNumberLength - if streamNumber not in state.streamsInWhichIAmParticipating: - logger.debug( - 'The streamNumber %i isn\'t one we are interested in.', - streamNumber - ) - return - if broadcastVersion >= 3: - tag = data[readPosition:readPosition+32] - else: - tag = '' - inventoryHash = calculateInventoryHash(data) - if inventoryHash in Inventory(): - logger.debug( - 'We have already received this broadcast object. Ignoring.') - return - # It is valid. Let's let our peers know about it. - objectType = 3 - Inventory()[inventoryHash] = ( - objectType, streamNumber, data, embeddedTime, tag) - # This object is valid. Forward it to peers. - logger.debug('advertising inv with hash: %s', hexlify(inventoryHash)) - protocol.broadcastToSendDataQueues( - (streamNumber, 'advertiseobject', inventoryHash)) - - # Now let's queue it to be processed ourselves. - objectProcessorQueue.put((objectType, data)) - - -def openKeysFile(): - if 'linux' in sys.platform: - subprocess.call(["xdg-open", state.appdata + 'keys.dat']) - else: - os.startfile(state.appdata + 'keys.dat') diff --git a/src/shutdown.py b/src/shutdown.py index ac96d2354c..441d655eef 100644 --- a/src/shutdown.py +++ b/src/shutdown.py @@ -1,71 +1,90 @@ +"""shutdown function""" + import os -import Queue import threading import time +from six.moves import queue + +import state from debug import logger from helper_sql import sqlQuery, sqlStoredProcedure -from helper_threading import StoppableThread -from knownnodes import saveKnownNodes -from inventory import Inventory -from queues import addressGeneratorQueue, objectProcessorQueue, UISignalQueue, workerQueue -import shared -import state +from network import StoppableThread +from network.knownnodes import saveKnownNodes +from queues import ( + addressGeneratorQueue, objectProcessorQueue, UISignalQueue, workerQueue) + def doCleanShutdown(): - state.shutdown = 1 #Used to tell proof of work worker threads and the objectProcessorThread to exit. + """ + Used to tell all the treads to finish work and exit. + """ + state.shutdown = 1 + objectProcessorQueue.put(('checkShutdownVariable', 'no data')) for thread in threading.enumerate(): if thread.isAlive() and isinstance(thread, StoppableThread): thread.stopThread() - - UISignalQueue.put(('updateStatusBar','Saving the knownNodes list of peers to disk...')) + + UISignalQueue.put(( + 'updateStatusBar', + 'Saving the knownNodes list of peers to disk...')) logger.info('Saving knownNodes list of peers to disk') saveKnownNodes() logger.info('Done saving knownNodes list of peers to disk') - UISignalQueue.put(('updateStatusBar','Done saving the knownNodes list of peers to disk.')) + UISignalQueue.put(( + 'updateStatusBar', + 'Done saving the knownNodes list of peers to disk.')) logger.info('Flushing inventory in memory out to disk...') UISignalQueue.put(( 'updateStatusBar', - 'Flushing inventory in memory out to disk. This should normally only take a second...')) - Inventory().flush() + 'Flushing inventory in memory out to disk.' + ' This should normally only take a second...')) + 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 we command the sqlThread to exit. + # Verify that the objectProcessor has finished exiting. It should have + # incremented the shutdown variable from 1 to 2. This must finish before + # we command the sqlThread to exit. while state.shutdown == 1: time.sleep(.1) - - # Wait long enough to guarantee that any running proof of work worker threads will check the - # shutdown variable and exit. If the main thread closes before they do then they won't stop. + + # Wait long enough to guarantee that any running proof of work worker + # threads will check the shutdown variable and exit. If the main thread + # closes before they do then they won't stop. time.sleep(.25) for thread in threading.enumerate(): - if (thread is not threading.currentThread() and - isinstance(thread, StoppableThread) and - thread.name != 'SQL'): + if ( + thread is not threading.currentThread() + and isinstance(thread, StoppableThread) + and thread.name != 'SQL' + ): logger.debug("Waiting for thread %s", thread.name) thread.join() - # This one last useless query will guarantee that the previous flush committed and that the + # This one last useless query will guarantee that the previous flush + # committed and that the # objectProcessorThread committed before we close the program. sqlQuery('SELECT address FROM subscriptions') logger.info('Finished flushing inventory.') sqlStoredProcedure('exit') # flush queues - for queue in (workerQueue, UISignalQueue, addressGeneratorQueue, objectProcessorQueue): + for q in ( + workerQueue, UISignalQueue, addressGeneratorQueue, + objectProcessorQueue): while True: try: - queue.get(False) - queue.task_done() - except Queue.Empty: + q.get(False) + q.task_done() + except queue.Empty: break - if shared.thisapp.daemon or not state.enableGUI: # FIXME redundant? + if state.thisapp.daemon or not state.enableGUI: logger.info('Clean shutdown complete.') - shared.thisapp.cleanup() - os._exit(0) + state.thisapp.cleanup() + os._exit(0) # pylint: disable=protected-access else: logger.info('Core shutdown complete.') for thread in threading.enumerate(): - logger.debug("Thread %s still running", thread.name) + logger.debug('Thread %s still running', thread.name) diff --git a/src/singleinstance.py b/src/singleinstance.py index ed1048bae8..cff9d7946c 100644 --- a/src/singleinstance.py +++ b/src/singleinstance.py @@ -1,32 +1,35 @@ -#! /usr/bin/env python +""" +This is based upon the singleton class from +`tendo `_ +which is under the Python Software Foundation License version 2 +""" import atexit -import errno -from multiprocessing import Process import os import sys + import state try: import fcntl # @UnresolvedImport -except: +except ImportError: pass -class singleinstance: - """ - Implements a single instance application by creating a lock file at appdata. - This is based upon the singleton class from tendo https://github.com/pycontribs/tendo - which is under the Python Software Foundation License version 2 +class singleinstance(object): + """ + Implements a single instance application by creating a lock file + at appdata. """ def __init__(self, flavor_id="", daemon=False): self.initialized = False self.counter = 0 self.daemon = daemon self.lockPid = None - self.lockfile = os.path.normpath(os.path.join(state.appdata, 'singleton%s.lock' % flavor_id)) + self.lockfile = os.path.normpath( + os.path.join(state.appdata, 'singleton%s.lock' % flavor_id)) - if not self.daemon and not state.curses: + if state.enableGUI and not self.daemon and not state.curses: # Tells the already running (if any) application to get focus. import bitmessageqt bitmessageqt.init() @@ -37,20 +40,24 @@ def __init__(self, flavor_id="", daemon=False): atexit.register(self.cleanup) def lock(self): + """Obtain single instance lock""" if self.lockPid is None: self.lockPid = os.getpid() if sys.platform == 'win32': try: - # file already exists, we try to remove (in case previous execution was interrupted) + # file already exists, we try to remove + # (in case previous execution was interrupted) if os.path.exists(self.lockfile): os.unlink(self.lockfile) - self.fd = os.open(self.lockfile, os.O_CREAT | os.O_EXCL | os.O_RDWR | os.O_TRUNC) - except OSError: - type, e, tb = sys.exc_info() + self.fd = os.open( + self.lockfile, + os.O_CREAT | os.O_EXCL | os.O_RDWR | os.O_TRUNC + ) + except OSError as e: if e.errno == 13: - print 'Another instance of this application is already running' - sys.exit(-1) - print(e.errno) + sys.exit( + 'Another instance of this application is' + ' already running') raise else: pidLine = "%i\n" % self.lockPid @@ -59,13 +66,15 @@ def lock(self): self.fp = open(self.lockfile, 'a+') try: if self.daemon and self.lockPid != os.getpid(): - fcntl.lockf(self.fp, fcntl.LOCK_EX) # wait for parent to finish + # wait for parent to finish + fcntl.lockf(self.fp, fcntl.LOCK_EX) else: fcntl.lockf(self.fp, fcntl.LOCK_EX | fcntl.LOCK_NB) self.lockPid = os.getpid() except IOError: - print 'Another instance of this application is already running' - sys.exit(-1) + sys.exit( + 'Another instance of this application is' + ' already running') else: pidLine = "%i\n" % self.lockPid self.fp.truncate(0) @@ -73,6 +82,7 @@ def lock(self): self.fp.flush() def cleanup(self): + """Release single instance lock""" if not self.initialized: return if self.daemon and self.lockPid == os.getpid(): @@ -83,11 +93,11 @@ def cleanup(self): os.close(self.fd) else: fcntl.lockf(self.fp, fcntl.LOCK_UN) - except Exception, e: + except (IOError, OSError): pass return - print "Cleaning up lockfile" + try: if sys.platform == 'win32': if hasattr(self, 'fd'): @@ -97,5 +107,5 @@ def cleanup(self): fcntl.lockf(self.fp, fcntl.LOCK_UN) if os.path.isfile(self.lockfile): os.unlink(self.lockfile) - except Exception, e: + except (IOError, OSError): pass diff --git a/src/singleton.py b/src/singleton.py index 1eef08e1bd..5c6c43be37 100644 --- a/src/singleton.py +++ b/src/singleton.py @@ -1,6 +1,21 @@ +""" +Singleton decorator definition +""" + +from functools import wraps + + def Singleton(cls): + """ + Decorator implementing the singleton pattern: + it restricts the instantiation of a class to one "single" instance. + """ instances = {} + + # https://github.com/sphinx-doc/sphinx/issues/3783 + @wraps(cls) def getinstance(): + """Find an instance or save newly created one""" if cls not in instances: instances[cls] = cls() return instances[cls] diff --git a/src/socks/BUGS b/src/socks/BUGS deleted file mode 100644 index fa8ccfad13..0000000000 --- a/src/socks/BUGS +++ /dev/null @@ -1,25 +0,0 @@ -SocksiPy version 1.00 -A Python SOCKS module. -(C) 2006 Dan-Haim. All rights reserved. -See LICENSE file for details. - - -KNOWN BUGS AND ISSUES ----------------------- - -There are no currently known bugs in this module. -There are some limits though: - -1) Only outgoing connections are supported - This module currently only supports -outgoing TCP connections, though some servers may support incoming connections -as well. UDP is not supported either. - -2) GSSAPI Socks5 authenticaion is not supported. - - -If you find any new bugs, please contact the author at: - -negativeiq@users.sourceforge.net - - -Thank you! diff --git a/src/socks/LICENSE b/src/socks/LICENSE deleted file mode 100644 index 04b6b1f37c..0000000000 --- a/src/socks/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -Copyright 2006 Dan-Haim. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. -3. Neither the name of Dan Haim nor the names of his contributors may be used - to endorse or promote products derived from this software without specific - prior written permission. - -THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED -WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO -EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA -OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT -OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE. diff --git a/src/socks/README b/src/socks/README deleted file mode 100644 index a52f55f37d..0000000000 --- a/src/socks/README +++ /dev/null @@ -1,201 +0,0 @@ -SocksiPy version 1.00 -A Python SOCKS module. -(C) 2006 Dan-Haim. All rights reserved. -See LICENSE file for details. - - -WHAT IS A SOCKS PROXY? -A SOCKS proxy is a proxy server at the TCP level. In other words, it acts as -a tunnel, relaying all traffic going through it without modifying it. -SOCKS proxies can be used to relay traffic using any network protocol that -uses TCP. - -WHAT IS SOCKSIPY? -This Python module allows you to create TCP connections through a SOCKS -proxy without any special effort. - -PROXY COMPATIBILITY -SocksiPy is compatible with three different types of proxies: -1. SOCKS Version 4 (Socks4), including the Socks4a extension. -2. SOCKS Version 5 (Socks5). -3. HTTP Proxies which support tunneling using the CONNECT method. - -SYSTEM REQUIREMENTS -Being written in Python, SocksiPy can run on any platform that has a Python -interpreter and TCP/IP support. -This module has been tested with Python 2.3 and should work with greater versions -just as well. - - -INSTALLATION -------------- - -Simply copy the file "socks.py" to your Python's lib/site-packages directory, -and you're ready to go. - - -USAGE ------- - -First load the socks module with the command: - ->>> import socks ->>> - -The socks module provides a class called "socksocket", which is the base to -all of the module's functionality. -The socksocket object has the same initialization parameters as the normal socket -object to ensure maximal compatibility, however it should be noted that socksocket -will only function with family being AF_INET and type being SOCK_STREAM. -Generally, it is best to initialize the socksocket object with no parameters - ->>> s = socks.socksocket() ->>> - -The socksocket object has an interface which is very similiar to socket's (in fact -the socksocket class is derived from socket) with a few extra methods. -To select the proxy server you would like to use, use the setproxy method, whose -syntax is: - -setproxy(proxytype, addr[, port[, rdns[, username[, password]]]]) - -Explaination of the parameters: - -proxytype - The type of the proxy server. This can be one of three possible -choices: PROXY_TYPE_SOCKS4, PROXY_TYPE_SOCKS5 and PROXY_TYPE_HTTP for Socks4, -Socks5 and HTTP servers respectively. - -addr - The IP address or DNS name of the proxy server. - -port - The port of the proxy server. Defaults to 1080 for socks and 8080 for http. - -rdns - This is a boolean flag than modifies the behavior regarding DNS resolving. -If it is set to True, DNS resolving will be preformed remotely, on the server. -If it is set to False, DNS resolving will be preformed locally. Please note that -setting this to True with Socks4 servers actually use an extension to the protocol, -called Socks4a, which may not be supported on all servers (Socks5 and http servers -always support DNS). The default is True. - -username - For Socks5 servers, this allows simple username / password authentication -with the server. For Socks4 servers, this parameter will be sent as the userid. -This parameter is ignored if an HTTP server is being used. If it is not provided, -authentication will not be used (servers may accept unauthentication requests). - -password - This parameter is valid only for Socks5 servers and specifies the -respective password for the username provided. - -Example of usage: - ->>> s.setproxy(socks.PROXY_TYPE_SOCKS5,"socks.example.com") ->>> - -After the setproxy method has been called, simply call the connect method with the -traditional parameters to establish a connection through the proxy: - ->>> s.connect(("www.sourceforge.net",80)) ->>> - -Connection will take a bit longer to allow negotiation with the proxy server. -Please note that calling connect without calling setproxy earlier will connect -without a proxy (just like a regular socket). - -Errors: Any errors in the connection process will trigger exceptions. The exception -may either be generated by the underlying socket layer or may be custom module -exceptions, whose details follow: - -class ProxyError - This is a base exception class. It is not raised directly but -rather all other exception classes raised by this module are derived from it. -This allows an easy way to catch all proxy-related errors. - -class GeneralProxyError - When thrown, it indicates a problem which does not fall -into another category. The parameter is a tuple containing an error code and a -description of the error, from the following list: -1 - invalid data - This error means that unexpected data has been received from -the server. The most common reason is that the server specified as the proxy is -not really a Socks4/Socks5/HTTP proxy, or maybe the proxy type specified is wrong. -4 - bad proxy type - This will be raised if the type of the proxy supplied to the -setproxy function was not PROXY_TYPE_SOCKS4/PROXY_TYPE_SOCKS5/PROXY_TYPE_HTTP. -5 - bad input - This will be raised if the connect method is called with bad input -parameters. - -class Socks5AuthError - This indicates that the connection through a Socks5 server -failed due to an authentication problem. The parameter is a tuple containing a -code and a description message according to the following list: - -1 - authentication is required - This will happen if you use a Socks5 server which -requires authentication without providing a username / password at all. -2 - all offered authentication methods were rejected - This will happen if the proxy -requires a special authentication method which is not supported by this module. -3 - unknown username or invalid password - Self descriptive. - -class Socks5Error - This will be raised for Socks5 errors which are not related to -authentication. The parameter is a tuple containing a code and a description of the -error, as given by the server. The possible errors, according to the RFC are: - -1 - General SOCKS server failure - If for any reason the proxy server is unable to -fulfill your request (internal server error). -2 - connection not allowed by ruleset - If the address you're trying to connect to -is blacklisted on the server or requires authentication. -3 - Network unreachable - The target could not be contacted. A router on the network -had replied with a destination net unreachable error. -4 - Host unreachable - The target could not be contacted. A router on the network -had replied with a destination host unreachable error. -5 - Connection refused - The target server has actively refused the connection -(the requested port is closed). -6 - TTL expired - The TTL value of the SYN packet from the proxy to the target server -has expired. This usually means that there are network problems causing the packet -to be caught in a router-to-router "ping-pong". -7 - Command not supported - The client has issued an invalid command. When using this -module, this error should not occur. -8 - Address type not supported - The client has provided an invalid address type. -When using this module, this error should not occur. - -class Socks4Error - This will be raised for Socks4 errors. The parameter is a tuple -containing a code and a description of the error, as given by the server. The -possible error, according to the specification are: - -1 - Request rejected or failed - Will be raised in the event of an failure for any -reason other then the two mentioned next. -2 - request rejected because SOCKS server cannot connect to identd on the client - -The Socks server had tried an ident lookup on your computer and has failed. In this -case you should run an identd server and/or configure your firewall to allow incoming -connections to local port 113 from the remote server. -3 - request rejected because the client program and identd report different user-ids - -The Socks server had performed an ident lookup on your computer and has received a -different userid than the one you have provided. Change your userid (through the -username parameter of the setproxy method) to match and try again. - -class HTTPError - This will be raised for HTTP errors. The parameter is a tuple -containing the HTTP status code and the description of the server. - - -After establishing the connection, the object behaves like a standard socket. -Call the close method to close the connection. - -In addition to the socksocket class, an additional function worth mentioning is the -setdefaultproxy function. The parameters are the same as the setproxy method. -This function will set default proxy settings for newly created socksocket objects, -in which the proxy settings haven't been changed via the setproxy method. -This is quite useful if you wish to force 3rd party modules to use a socks proxy, -by overriding the socket object. -For example: - ->>> socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5,"socks.example.com") ->>> socket.socket = socks.socksocket ->>> urllib.urlopen("http://www.sourceforge.net/") - - -PROBLEMS ---------- - -If you have any problems using this module, please first refer to the BUGS file -(containing current bugs and issues). If your problem is not mentioned you may -contact the author at the following E-Mail address: - -negativeiq@users.sourceforge.net - -Please allow some time for your question to be received and handled. - - -Dan-Haim, -Author. diff --git a/src/socks/__init__.py b/src/socks/__init__.py deleted file mode 100644 index 0bfa18f594..0000000000 --- a/src/socks/__init__.py +++ /dev/null @@ -1,462 +0,0 @@ -"""SocksiPy - Python SOCKS module. -Version 1.00 - -Copyright 2006 Dan-Haim. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. -3. Neither the name of Dan Haim nor the names of his contributors may be used - to endorse or promote products derived from this software without specific - prior written permission. - -THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED -WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO -EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA -OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT -OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE. - - -This module provides a standard socket-like interface for Python -for tunneling connections through SOCKS proxies. - -""" - -""" - -Minor modifications made by Christopher Gilbert (http://motomastyle.com/) -for use in PyLoris (http://pyloris.sourceforge.net/) - -Minor modifications made by Mario Vilas (http://breakingcode.wordpress.com/) -mainly to merge bug fixes found in Sourceforge - -""" - -import socket -import struct -import sys - -PROXY_TYPE_SOCKS4 = 1 -PROXY_TYPE_SOCKS5 = 2 -PROXY_TYPE_HTTP = 3 - -_defaultproxy = None -_orgsocket = socket.socket - -class ProxyError(Exception): pass -class GeneralProxyError(ProxyError): pass -class Socks5AuthError(ProxyError): pass -class Socks5Error(ProxyError): pass -class Socks4Error(ProxyError): pass -class HTTPError(ProxyError): pass - -_generalerrors = ("success", - "invalid data", - "not connected", - "not available", - "bad proxy type", - "bad input", - "timed out", - "network unreachable", - "connection refused", - "host unreachable") - -_socks5errors = ("succeeded", - "general SOCKS server failure", - "connection not allowed by ruleset", - "Network unreachable", - "Host unreachable", - "Connection refused", - "TTL expired", - "Command not supported", - "Address type not supported", - "Unknown error") - -_socks5autherrors = ("succeeded", - "authentication is required", - "all offered authentication methods were rejected", - "unknown username or invalid password", - "unknown error") - -_socks4errors = ("request granted", - "request rejected or failed", - "request rejected because SOCKS server cannot connect to identd on the client", - "request rejected because the client program and identd report different user-ids", - "unknown error") - -def setdefaultproxy(proxytype=None, addr=None, port=None, rdns=True, username=None, password=None): - """setdefaultproxy(proxytype, addr[, port[, rdns[, username[, password]]]]) - Sets a default proxy which all further socksocket objects will use, - unless explicitly changed. - """ - global _defaultproxy - _defaultproxy = (proxytype, addr, port, rdns, username, password) - -def wrapmodule(module): - """wrapmodule(module) - Attempts to replace a module's socket library with a SOCKS socket. Must set - a default proxy using setdefaultproxy(...) first. - This will only work on modules that import socket directly into the namespace; - most of the Python Standard Library falls into this category. - """ - if _defaultproxy != None: - module.socket.socket = socksocket - else: - raise GeneralProxyError((4, "no proxy specified")) - -class socksocket(socket.socket): - """socksocket([family[, type[, proto]]]) -> socket object - Open a SOCKS enabled socket. The parameters are the same as - those of the standard socket init. In order for SOCKS to work, - you must specify family=AF_INET, type=SOCK_STREAM and proto=0. - """ - - def __init__(self, family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0, _sock=None): - _orgsocket.__init__(self, family, type, proto, _sock) - if _defaultproxy != None: - self.__proxy = _defaultproxy - else: - self.__proxy = (None, None, None, None, None, None) - self.__proxysockname = None - self.__proxypeername = None - - def __recvall(self, count): - """__recvall(count) -> data - Receive EXACTLY the number of bytes requested from the socket. - Blocks until the required number of bytes have been received. - """ - try: - data = self.recv(count) - except socket.timeout: - raise GeneralProxyError((6, "timed out")) - while len(data) < count: - d = self.recv(count-len(data)) - if not d: raise GeneralProxyError((0, "connection closed unexpectedly")) - data = data + d - return data - - def setproxy(self, proxytype=None, addr=None, port=None, rdns=True, username=None, password=None): - """setproxy(proxytype, addr[, port[, rdns[, username[, password]]]]) - Sets the proxy to be used. - proxytype - The type of the proxy to be used. Three types - are supported: PROXY_TYPE_SOCKS4 (including socks4a), - PROXY_TYPE_SOCKS5 and PROXY_TYPE_HTTP - addr - The address of the server (IP or DNS). - port - The port of the server. Defaults to 1080 for SOCKS - servers and 8080 for HTTP proxy servers. - rdns - Should DNS queries be preformed on the remote side - (rather than the local side). The default is True. - Note: This has no effect with SOCKS4 servers. - username - Username to authenticate with to the server. - The default is no authentication. - password - Password to authenticate with to the server. - Only relevant when username is also provided. - """ - self.__proxy = (proxytype, addr, port, rdns, username, password) - - def __negotiatesocks5(self): - """__negotiatesocks5(self,destaddr,destport) - Negotiates a connection through a SOCKS5 server. - """ - # First we'll send the authentication packages we support. - if (self.__proxy[4]!=None) and (self.__proxy[5]!=None): - # The username/password details were supplied to the - # setproxy method so we support the USERNAME/PASSWORD - # authentication (in addition to the standard none). - self.sendall(struct.pack('BBBB', 0x05, 0x02, 0x00, 0x02)) - else: - # No username/password were entered, therefore we - # only support connections with no authentication. - self.sendall(struct.pack('BBB', 0x05, 0x01, 0x00)) - # We'll receive the server's response to determine which - # method was selected - chosenauth = self.__recvall(2) - if chosenauth[0:1] != chr(0x05).encode(): - self.close() - raise GeneralProxyError((1, _generalerrors[1])) - # Check the chosen authentication method - if chosenauth[1:2] == chr(0x00).encode(): - # No authentication is required - pass - elif chosenauth[1:2] == chr(0x02).encode(): - # Okay, we need to perform a basic username/password - # authentication. - self.sendall(chr(0x01).encode() + chr(len(self.__proxy[4])) + self.__proxy[4] + chr(len(self.__proxy[5])) + self.__proxy[5]) - authstat = self.__recvall(2) - if authstat[0:1] != chr(0x01).encode(): - # Bad response - self.close() - raise GeneralProxyError((1, _generalerrors[1])) - if authstat[1:2] != chr(0x00).encode(): - # Authentication failed - self.close() - raise Socks5AuthError((3, _socks5autherrors[3])) - # Authentication succeeded - else: - # Reaching here is always bad - self.close() - if chosenauth[1] == chr(0xFF).encode(): - raise Socks5AuthError((2, _socks5autherrors[2])) - else: - raise GeneralProxyError((1, _generalerrors[1])) - - def __connectsocks5(self, destaddr, destport): - # Now we can request the actual connection - req = struct.pack('BBB', 0x05, 0x01, 0x00) - # If the given destination address is an IP address, we'll - # use the IPv4 address request even if remote resolving was specified. - try: - ipaddr = socket.inet_aton(destaddr) - req = req + chr(0x01).encode() + ipaddr - except socket.error: - # Well it's not an IP number, so it's probably a DNS name. - if self.__proxy[3]: - # Resolve remotely - ipaddr = None - req = req + chr(0x03).encode() + chr(len(destaddr)).encode() + destaddr - else: - # Resolve locally - ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) - req = req + chr(0x01).encode() + ipaddr - req = req + struct.pack(">H", destport) - self.sendall(req) - # Get the response - resp = self.__recvall(4) - if resp[0:1] != chr(0x05).encode(): - self.close() - raise GeneralProxyError((1, _generalerrors[1])) - elif resp[1:2] != chr(0x00).encode(): - # Connection failed - self.close() - if ord(resp[1:2])<=8: - raise Socks5Error((ord(resp[1:2]), _socks5errors[ord(resp[1:2])])) - else: - raise Socks5Error((9, _socks5errors[9])) - # Get the bound address/port - elif resp[3:4] == chr(0x01).encode(): - boundaddr = self.__recvall(4) - elif resp[3:4] == chr(0x03).encode(): - resp = resp + self.recv(1) - boundaddr = self.__recvall(ord(resp[4:5])) - else: - self.close() - raise GeneralProxyError((1,_generalerrors[1])) - boundport = struct.unpack(">H", self.__recvall(2))[0] - self.__proxysockname = (boundaddr, boundport) - if ipaddr != None: - self.__proxypeername = (socket.inet_ntoa(ipaddr), destport) - else: - self.__proxypeername = (destaddr, destport) - - def __resolvesocks5(self, host): - # Now we can request the actual connection - req = struct.pack('BBB', 0x05, 0xF0, 0x00) - req += chr(0x03).encode() + chr(len(host)).encode() + host - req = req + struct.pack(">H", 8444) - self.sendall(req) - # Get the response - ip = "" - resp = self.__recvall(4) - if resp[0:1] != chr(0x05).encode(): - self.close() - raise GeneralProxyError((1, _generalerrors[1])) - elif resp[1:2] != chr(0x00).encode(): - # Connection failed - self.close() - if ord(resp[1:2])<=8: - raise Socks5Error((ord(resp[1:2]), _socks5errors[ord(resp[1:2])])) - else: - raise Socks5Error((9, _socks5errors[9])) - # Get the bound address/port - elif resp[3:4] == chr(0x01).encode(): - ip = socket.inet_ntoa(self.__recvall(4)) - elif resp[3:4] == chr(0x03).encode(): - resp = resp + self.recv(1) - ip = self.__recvall(ord(resp[4:5])) - else: - self.close() - raise GeneralProxyError((1,_generalerrors[1])) - boundport = struct.unpack(">H", self.__recvall(2))[0] - return ip - - def getproxysockname(self): - """getsockname() -> address info - Returns the bound IP address and port number at the proxy. - """ - return self.__proxysockname - - def getproxypeername(self): - """getproxypeername() -> address info - Returns the IP and port number of the proxy. - """ - return _orgsocket.getpeername(self) - - def getpeername(self): - """getpeername() -> address info - Returns the IP address and port number of the destination - machine (note: getproxypeername returns the proxy) - """ - return self.__proxypeername - - def getproxytype(self): - return self.__proxy[0] - - def __negotiatesocks4(self,destaddr,destport): - """__negotiatesocks4(self,destaddr,destport) - Negotiates a connection through a SOCKS4 server. - """ - # Check if the destination address provided is an IP address - rmtrslv = False - try: - ipaddr = socket.inet_aton(destaddr) - except socket.error: - # It's a DNS name. Check where it should be resolved. - if self.__proxy[3]: - ipaddr = struct.pack("BBBB", 0x00, 0x00, 0x00, 0x01) - rmtrslv = True - else: - ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) - # Construct the request packet - req = struct.pack(">BBH", 0x04, 0x01, destport) + ipaddr - # The username parameter is considered userid for SOCKS4 - if self.__proxy[4] != None: - req = req + self.__proxy[4] - req = req + chr(0x00).encode() - # DNS name if remote resolving is required - # NOTE: This is actually an extension to the SOCKS4 protocol - # called SOCKS4A and may not be supported in all cases. - if rmtrslv: - req = req + destaddr + chr(0x00).encode() - self.sendall(req) - # Get the response from the server - resp = self.__recvall(8) - if resp[0:1] != chr(0x00).encode(): - # Bad data - self.close() - raise GeneralProxyError((1,_generalerrors[1])) - if resp[1:2] != chr(0x5A).encode(): - # Server returned an error - self.close() - if ord(resp[1:2]) in (91, 92, 93): - self.close() - raise Socks4Error((ord(resp[1:2]), _socks4errors[ord(resp[1:2]) - 90])) - else: - raise Socks4Error((94, _socks4errors[4])) - # Get the bound address/port - self.__proxysockname = (socket.inet_ntoa(resp[4:]), struct.unpack(">H", resp[2:4])[0]) - if rmtrslv != None: - self.__proxypeername = (socket.inet_ntoa(ipaddr), destport) - else: - self.__proxypeername = (destaddr, destport) - - def __negotiatehttp(self, destaddr, destport): - """__negotiatehttp(self,destaddr,destport) - Negotiates a connection through an HTTP server. - """ - # If we need to resolve locally, we do this now - if not self.__proxy[3]: - addr = socket.gethostbyname(destaddr) - else: - addr = destaddr - self.sendall(("CONNECT " + addr + ":" + str(destport) + " HTTP/1.1\r\n" + "Host: " + destaddr + "\r\n\r\n").encode()) - # We read the response until we get the string "\r\n\r\n" - resp = self.recv(1) - while resp.find("\r\n\r\n".encode()) == -1: - resp = resp + self.recv(1) - # We just need the first line to check if the connection - # was successful - statusline = resp.splitlines()[0].split(" ".encode(), 2) - if statusline[0] not in ("HTTP/1.0".encode(), "HTTP/1.1".encode()): - self.close() - raise GeneralProxyError((1, _generalerrors[1])) - try: - statuscode = int(statusline[1]) - except ValueError: - self.close() - raise GeneralProxyError((1, _generalerrors[1])) - if statuscode != 200: - self.close() - raise HTTPError((statuscode, statusline[2])) - self.__proxysockname = ("0.0.0.0", 0) - self.__proxypeername = (addr, destport) - - def connect(self, destpair): - """connect(self, despair) - Connects to the specified destination through a proxy. - destpar - A tuple of the IP/DNS address and the port number. - (identical to socket's connect). - To select the proxy server use setproxy(). - """ - # Do a minimal input check first - if (not type(destpair) in (list,tuple)) or (len(destpair) < 2) or (type(destpair[0]) != type('')) or (type(destpair[1]) != int): - raise GeneralProxyError((5, _generalerrors[5])) - if self.__proxy[0] == PROXY_TYPE_SOCKS5: - if self.__proxy[2] != None: - portnum = self.__proxy[2] - else: - portnum = 1080 - try: - _orgsocket.connect(self, (self.__proxy[1], portnum)) - except socket.error as e: - # ENETUNREACH, WSAENETUNREACH - if e[0] in [101, 10051]: - raise GeneralProxyError((7, _generalerrors[7])) - # ECONNREFUSED, WSAECONNREFUSED - if e[0] in [111, 10061]: - raise GeneralProxyError((8, _generalerrors[8])) - # EHOSTUNREACH, WSAEHOSTUNREACH - if e[0] in [113, 10065]: - raise GeneralProxyError((9, _generalerrors[9])) - raise - self.__negotiatesocks5() - self.__connectsocks5(destpair[0], destpair[1]) - elif self.__proxy[0] == PROXY_TYPE_SOCKS4: - if self.__proxy[2] != None: - portnum = self.__proxy[2] - else: - portnum = 1080 - _orgsocket.connect(self,(self.__proxy[1], portnum)) - self.__negotiatesocks4(destpair[0], destpair[1]) - elif self.__proxy[0] == PROXY_TYPE_HTTP: - if self.__proxy[2] != None: - portnum = self.__proxy[2] - else: - portnum = 8080 - try: - _orgsocket.connect(self,(self.__proxy[1], portnum)) - except socket.error as e: - # ENETUNREACH, WSAENETUNREACH - if e[0] in [101, 10051]: - raise GeneralProxyError((7, _generalerrors[7])) - # ECONNREFUSED, WSAECONNREFUSED - if e[0] in [111, 10061]: - raise GeneralProxyError((8, _generalerrors[8])) - # EHOSTUNREACH, WSAEHOSTUNREACH - if e[0] in [113, 10065]: - raise GeneralProxyError((9, _generalerrors[9])) - raise - self.__negotiatehttp(destpair[0], destpair[1]) - elif self.__proxy[0] == None: - _orgsocket.connect(self, (destpair[0], destpair[1])) - else: - raise GeneralProxyError((4, _generalerrors[4])) - - def resolve(self, host): - if self.__proxy[0] == PROXY_TYPE_SOCKS5: - if self.__proxy[2] != None: - portnum = self.__proxy[2] - else: - portnum = 1080 - _orgsocket.connect(self, (self.__proxy[1], portnum)) - self.__negotiatesocks5() - return self.__resolvesocks5(host) - else: - return None diff --git a/src/sql/config_setting_ver_2.sql b/src/sql/config_setting_ver_2.sql new file mode 100644 index 0000000000..087d297a06 --- /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 0000000000..4bdcccc844 --- /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 0000000000..8bd8b0b311 --- /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 0000000000..ea42df4c87 --- /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 0000000000..9de784a5cb --- /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 0000000000..d2fd393d70 --- /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 0000000000..a13fa8cfc7 --- /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 0000000000..b9a036693e --- /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 0000000000..a2f6f6e34b --- /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 0000000000..0c1813d3e5 --- /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 0000000000..bc8296b90c --- /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 0000000000..8413aa0a4b --- /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 0000000000..18a5ecfc68 --- /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 0000000000..1fde0098e8 --- /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 50490423ea..90c9cf0d3a 100644 --- a/src/state.py +++ b/src/state.py @@ -1,67 +1,96 @@ -import collections +""" +Global runtime variables. +""" neededPubkeys = {} -streamsInWhichIAmParticipating = [] -sendDataQueues = [] #each sendData thread puts its queue in this list. -# For UPnP extPort = None +"""For UPnP""" -# for Tor hidden service socksIP = None +"""for Tor hidden service""" -# Network protocols availability, initialised below -networkProtocolAvailability = None - -appdata = '' #holds the location of the application data storage directory - -shutdown = 0 #Set to 1 by the doCleanShutdown function. Used to tell the proof of work worker threads to exit. +appdata = "" +"""holds the location of the application data storage directory""" +shutdown = 0 +""" +Set to 1 by the `.shutdown.doCleanShutdown` function. +Used to tell the threads to exit. +""" # Component control flags - set on startup, do not change during runtime # The defaults are for standalone GUI (default operating mode) -enableNetwork = True # enable network threads -enableObjProc = True # enable object processing threads -enableAPI = True # enable API (if configured) -enableGUI = True # enable GUI (QT or ncurses) -enableSTDIO = False # enable STDIO threads +enableNetwork = True +"""enable network threads""" +enableObjProc = True +"""enable object processing thread""" +enableAPI = True +"""enable API (if configured)""" +enableGUI = True +"""enable GUI (QT or ncurses)""" +enableSTDIO = False +"""enable STDIO threads""" +enableKivy = False +"""enable kivy app and test cases""" curses = False -sqlReady = False # set to true by sqlTread when ready for processing - maximumNumberOfHalfOpenConnections = 0 -invThread = None -addrThread = None -downloadThread = None +maximumLengthOfTimeToBotherResendingMessages = 0 ownAddresses = {} -# If the trustedpeer option is specified in keys.dat then this will -# contain a Peer which will be connected to instead of using the -# addresses advertised by other peers. The client will only connect to -# this peer and the timing attack mitigation will be disabled in order -# to download data faster. The expected use case is where the user has -# a fast connection to a trusted server where they run a BitMessage -# daemon permanently. If they then run a second instance of the client -# on a local machine periodically when they want to check for messages -# it will sync with the network a lot faster without compromising -# security. -trustedPeer = None - discoveredPeers = {} -# tracking pending downloads globally, for stats -missingObjects = {} +kivy = False -Peer = collections.namedtuple('Peer', ['host', 'port']) +kivyapp = None -def resetNetworkProtocolAvailability(): - global networkProtocolAvailability - networkProtocolAvailability = {'IPv4': None, 'IPv6': None, 'onion': None} +testmode = False -resetNetworkProtocolAvailability() +clientHasReceivedIncomingConnections = False +"""used by API command clientStatus""" -dandelion = 0 +numberOfMessagesProcessed = 0 +numberOfBroadcastsProcessed = 0 +numberOfPubkeysProcessed = 0 -testmode = False +statusIconColor = "red" +""" +GUI status icon color +.. note:: bad style, refactor it +""" + +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") diff --git a/src/storage/filesystem.py b/src/storage/filesystem.py index d64894a9c5..e756a82016 100644 --- a/src/storage/filesystem.py +++ b/src/storage/filesystem.py @@ -1,94 +1,149 @@ +""" +Module for using filesystem (directory with files) for inventory storage +""" +import logging +import os +import time from binascii import hexlify, unhexlify -from os import listdir, makedirs, path, remove, rmdir -import string from threading import RLock -import time -import traceback from paths import lookupAppdataFolder -from storage import InventoryStorage, InventoryItem +from .storage import InventoryItem, InventoryStorage + +logger = logging.getLogger('default') + class FilesystemInventory(InventoryStorage): + """Filesystem for inventory storage""" topDir = "inventory" objectDir = "objects" metadataFilename = "metadata" dataFilename = "data" def __init__(self): - super(self.__class__, self).__init__() - self.baseDir = path.join(lookupAppdataFolder(), FilesystemInventory.topDir) - for createDir in [self.baseDir, path.join(self.baseDir, "objects")]: - if path.exists(createDir): - if not path.isdir(createDir): - raise IOError("%s exists but it's not a directory" % (createDir)) + super(FilesystemInventory, self).__init__() + self.baseDir = os.path.join( + lookupAppdataFolder(), FilesystemInventory.topDir) + 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) - self.lock = RLock() # Guarantees that two receiveDataThreads don't receive and process the same message concurrently (probably sent by a malicious individual) + os.makedirs(createDir) + # Guarantees that two receiveDataThreads + # don't receive and process the same message + # concurrently (probably sent by a malicious individual) + self.lock = RLock() self._inventory = {} self._load() - def __contains__(self, hash): - retval = False + def __contains__(self, hashval): for streamDict in self._inventory.values(): - if hash in streamDict: + if hashval in streamDict: return True return False - def __getitem__(self, hash): + def __delitem__(self, hash_): + raise NotImplementedError + + def __getitem__(self, hashval): for streamDict in self._inventory.values(): try: - retval = streamDict[hash] + retval = streamDict[hashval] except KeyError: continue if retval.payload is None: - retval = InventoryItem(retval.type, retval.stream, self.getData(hash), retval.expires, retval.tag) + retval = InventoryItem( + retval.type, + retval.stream, + self.getData(hashval), + retval.expires, + retval.tag) return retval - raise KeyError(hash) + raise KeyError(hashval) - def __setitem__(self, hash, value): + def __setitem__(self, hashval, value): with self.lock: value = InventoryItem(*value) try: - makedirs(path.join(self.baseDir, FilesystemInventory.objectDir, hexlify(hash))) + os.makedirs(os.path.join( + self.baseDir, + FilesystemInventory.objectDir, + hexlify(hashval).decode())) except OSError: pass try: - with open(path.join(self.baseDir, FilesystemInventory.objectDir, hexlify(hash), FilesystemInventory.metadataFilename), 'w') as f: - f.write("%s,%s,%s,%s," % (value.type, value.stream, value.expires, hexlify(value.tag))) - with open(path.join(self.baseDir, FilesystemInventory.objectDir, hexlify(hash), FilesystemInventory.dataFilename), 'w') as f: + with open( + os.path.join( + self.baseDir, + FilesystemInventory.objectDir, + hexlify(hashval).decode(), + FilesystemInventory.metadataFilename, + ), + "w", + ) as f: + f.write("%s,%s,%s,%s," % ( + value.type, + value.stream, + value.expires, + hexlify(value.tag).decode())) + with open( + os.path.join( + self.baseDir, + FilesystemInventory.objectDir, + hexlify(hashval).decode(), + FilesystemInventory.dataFilename, + ), + "wb", + ) as f: f.write(value.payload) except IOError: raise KeyError try: - self._inventory[value.stream][hash] = value + self._inventory[value.stream][hashval] = value except KeyError: self._inventory[value.stream] = {} - self._inventory[value.stream][hash] = value + self._inventory[value.stream][hashval] = value - def delHashId(self, hash): - for stream in self._inventory.keys(): + def delHashId(self, hashval): + """Remove object from inventory""" + for stream in self._inventory: try: - del self._inventory[stream][hash] + del self._inventory[stream][hashval] except KeyError: pass with self.lock: try: - remove(path.join(self.baseDir, FilesystemInventory.objectDir, hexlify(hash), FilesystemInventory.metadataFilename)) + os.remove( + os.path.join( + self.baseDir, + FilesystemInventory.objectDir, + hexlify(hashval).decode(), + FilesystemInventory.metadataFilename)) except IOError: pass try: - remove(path.join(self.baseDir, FilesystemInventory.objectDir, hexlify(hash), FilesystemInventory.dataFilename)) + os.remove( + os.path.join( + self.baseDir, + FilesystemInventory.objectDir, + hexlify(hashval).decode(), + FilesystemInventory.dataFilename)) except IOError: pass try: - rmdir(path.join(self.baseDir, FilesystemInventory.objectDir, hexlify(hash))) + os.rmdir(os.path.join( + self.baseDir, + FilesystemInventory.objectDir, + hexlify(hashval).decode())) except IOError: pass def __iter__(self): elems = [] for streamDict in self._inventory.values(): - elems.extend (streamDict.keys()) + elems.extend(streamDict.keys()) return elems.__iter__() def __len__(self): @@ -101,73 +156,111 @@ def _load(self): newInventory = {} for hashId in self.object_list(): try: - objectType, streamNumber, expiresTime, tag = self.getMetadata(hashId) + objectType, streamNumber, expiresTime, tag = self.getMetadata( + hashId) try: - newInventory[streamNumber][hashId] = InventoryItem(objectType, streamNumber, None, expiresTime, tag) + newInventory[streamNumber][hashId] = InventoryItem( + objectType, streamNumber, None, expiresTime, tag) except KeyError: newInventory[streamNumber] = {} - newInventory[streamNumber][hashId] = InventoryItem(objectType, streamNumber, None, expiresTime, tag) + newInventory[streamNumber][hashId] = InventoryItem( + objectType, streamNumber, None, expiresTime, tag) except KeyError: - print "error loading %s" % (hexlify(hashId)) - pass + 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""" return self._inventory.keys() def object_list(self): - return [unhexlify(x) for x in listdir(path.join(self.baseDir, FilesystemInventory.objectDir))] + """Return inventory vectors (hashes) from a directory""" + 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(self.baseDir, FilesystemInventory.objectDir, hexlify(hashId), FilesystemInventory.dataFilename), 'r') as f: + with open( + os.path.join( + self.baseDir, + FilesystemInventory.objectDir, + hexlify(hashId).decode(), + FilesystemInventory.dataFilename, + ), + "r", + ) as f: return f.read() except IOError: raise AttributeError def getMetadata(self, hashId): + """Get object metadata""" try: - with open(path.join(self.baseDir, FilesystemInventory.objectDir, hexlify(hashId), FilesystemInventory.metadataFilename), 'r') as f: - objectType, streamNumber, expiresTime, tag, undef = string.split(f.read(), ",", 4) - return [int(objectType), int(streamNumber), int(expiresTime), unhexlify(tag)] + with open( + os.path.join( + self.baseDir, + FilesystemInventory.objectDir, + hexlify(hashId).decode(), + FilesystemInventory.metadataFilename, + ), + "r", + ) as f: + objectType, streamNumber, expiresTime, tag = f.read().split( + ",", 4)[:4] + return [ + int(objectType), + int(streamNumber), + int(expiresTime), + unhexlify(tag)] except IOError: raise KeyError def by_type_and_tag(self, objectType, tag): + """Get a list of objects filtered by object type and tag""" retval = [] - for stream, streamDict in self._inventory: + for streamDict in self._inventory.values(): for hashId, item in streamDict: if item.type == objectType and item.tag == tag: - try: + try: if item.payload is None: item.payload = self.getData(hashId) except IOError: continue - retval.append(InventoryItem(item.type, item.stream, item.payload, item.expires, item.tag)) + retval.append(InventoryItem( + item.type, + item.stream, + item.payload, + item.expires, + item.tag)) return retval def hashes_by_stream(self, stream): + """Return inventory vectors (hashes) for a stream""" try: return self._inventory[stream].keys() except KeyError: return [] def unexpired_hashes_by_stream(self, stream): - t = int(time.time()) + """Return unexpired hashes in the inventory for a particular stream""" 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 [] def flush(self): + """Flush the inventory and create a new, empty one""" self._load() def clean(self): - minTime = int(time.time()) - (60 * 60 * 30) + """Clean out old items from the inventory""" + minTime = int(time.time()) - 60 * 60 * 30 deletes = [] - for stream, streamDict in self._inventory.items(): + for streamDict in self._inventory.values(): for hashId, item in streamDict.items(): if item.expires < minTime: deletes.append(hashId) diff --git a/src/storage/sqlite.py b/src/storage/sqlite.py index 438cbdcb6d..eb5df098d8 100644 --- a/src/storage/sqlite.py +++ b/src/storage/sqlite.py @@ -1,45 +1,62 @@ -import collections -from threading import current_thread, enumerate as threadingEnumerate, RLock -import Queue +""" +Sqlite Inventory +""" import sqlite3 import time +from threading import RLock + +from helper_sql import SqlBulkExecute, sqlExecute, sqlQuery +from .storage import InventoryItem, InventoryStorage -from helper_sql import * -from storage import InventoryStorage, InventoryItem class SqliteInventory(InventoryStorage): + """Inventory using SQLite""" def __init__(self): - super(self.__class__, self).__init__() - self._inventory = {} #of objects (like msg payloads and pubkey payloads) Does not include protocol headers (the first 24 bytes of each packet). - self._objects = {} # cache for existing objects, used for quick lookups if we have an object. This is used for example whenever we receive an inv message from a peer to check to see what items are new to us. We don't delete things out of it; instead, the singleCleaner thread clears and refills it. - self.lock = RLock() # Guarantees that two receiveDataThreads don't receive and process the same message concurrently (probably sent by a malicious individual) + super(SqliteInventory, self).__init__() + # of objects (like msg payloads and pubkey payloads) + # Does not include protocol headers (the first 24 bytes of each packet). + self._inventory = {} + # cache for existing objects, used for quick lookups if we have an object. + # This is used for example whenever we receive an inv message from a peer + # to check to see what items are new to us. + # We don't delete things out of it; instead, + # the singleCleaner thread clears and refills it. + self._objects = {} + # Guarantees that two receiveDataThreads don't receive + # and process the same message concurrently + # (probably sent by a malicious individual) + self.lock = RLock() - def __contains__(self, hash): + def __contains__(self, hash_): with self.lock: - if hash in self._objects: + if hash_ in self._objects: return True - rows = sqlQuery('SELECT streamnumber FROM inventory WHERE hash=?', sqlite3.Binary(hash)) + rows = sqlQuery( + 'SELECT streamnumber FROM inventory WHERE hash=?', + sqlite3.Binary(hash_)) if not rows: return False - self._objects[hash] = rows[0][0] + self._objects[hash_] = rows[0][0] return True - def __getitem__(self, hash): + def __getitem__(self, hash_): with self.lock: - if hash in self._inventory: - return self._inventory[hash] - rows = sqlQuery('SELECT objecttype, streamnumber, payload, expirestime, tag FROM inventory WHERE hash=?', sqlite3.Binary(hash)) + if hash_ in self._inventory: + return self._inventory[hash_] + rows = sqlQuery( + 'SELECT objecttype, streamnumber, payload, expirestime, tag' + ' FROM inventory WHERE hash=?', sqlite3.Binary(hash_)) if not rows: - raise KeyError(hash) + raise KeyError(hash_) return InventoryItem(*rows[0]) - def __setitem__(self, hash, value): + def __setitem__(self, hash_, value): with self.lock: value = InventoryItem(*value) - self._inventory[hash] = value - self._objects[hash] = value.stream + self._inventory[hash_] = value + self._objects[hash_] = value.stream - def __delitem__(self, hash): + def __delitem__(self, hash_): raise NotImplementedError def __iter__(self): @@ -50,32 +67,57 @@ def __iter__(self): def __len__(self): with self.lock: - return len(self._inventory) + sqlQuery('SELECT count(*) FROM inventory')[0][0] + return len(self._inventory) + sqlQuery( + 'SELECT count(*) FROM inventory')[0][0] - def by_type_and_tag(self, objectType, tag): + def by_type_and_tag(self, objectType, tag=None): + """ + Get all inventory items of certain *objectType* + with *tag* if given. + """ + query = [ + 'SELECT objecttype, streamnumber, payload, expirestime, tag' + ' FROM inventory WHERE objecttype=?', objectType] + if tag: + query[0] += ' AND tag=?' + query.append(sqlite3.Binary(tag)) with self.lock: - values = [value for value in self._inventory.values() if value.type == objectType and value.tag == tag] - values += (InventoryItem(*value) for value in sqlQuery('SELECT objecttype, streamnumber, payload, expirestime, tag FROM inventory WHERE objecttype=? AND tag=?', objectType, sqlite3.Binary(tag))) + values = [ + value for value in self._inventory.values() + if value.type == objectType + and tag is None or value.tag == tag + ] + [InventoryItem(*value) for value in sqlQuery(*query)] return values def unexpired_hashes_by_stream(self, stream): + """Return unexpired inventory vectors filtered by stream""" with self.lock: t = int(time.time()) - hashes = [x for x, value in self._inventory.items() if value.stream == stream and value.expires > t] - hashes += (str(payload) for payload, in sqlQuery('SELECT hash FROM inventory WHERE streamnumber=? AND expirestime>?', stream, t)) + hashes = [x for x, value in self._inventory.items() + if value.stream == stream and value.expires > t] + hashes += (str(payload) for payload, in sqlQuery( + 'SELECT hash FROM inventory WHERE streamnumber=?' + ' AND expirestime>?', stream, t)) return hashes def flush(self): - with self.lock: # If you use both the inventoryLock and the sqlLock, always use the inventoryLock OUTSIDE of the sqlLock. + """Flush cache""" + with self.lock: + # If you use both the inventoryLock and the sqlLock, + # always use the inventoryLock OUTSIDE of the sqlLock. with SqlBulkExecute() as sql: for objectHash, value in self._inventory.items(): - sql.execute('INSERT INTO inventory VALUES (?, ?, ?, ?, ?, ?)', sqlite3.Binary(objectHash), *value) + sql.execute( + 'INSERT INTO inventory VALUES (?, ?, ?, ?, ?, ?)', + sqlite3.Binary(objectHash), *value) self._inventory.clear() def clean(self): + """Free memory / perform garbage collection""" with self.lock: - sqlExecute('DELETE FROM inventory WHERE expirestime= 0x3000000: + raise unittest.SkipTest('Module is not ported to python3') + + +def put_signal_file(path, filename): + """Creates file, presence of which is a signal about some event.""" + with open(os.path.join(path, filename), 'wb') as outfile: + outfile.write(b'%i' % time.time()) diff --git a/src/tests/core.py b/src/tests/core.py new file mode 100644 index 0000000000..6f9588906a --- /dev/null +++ b/src/tests/core.py @@ -0,0 +1,438 @@ +""" +Tests for core and those that do not work outside +(because of import error for example) +""" + +import atexit +import os +import pickle # nosec +import Queue +import random # nosec +import shutil +import socket +import string +import sys +import threading +import time +import unittest + +import protocol +import state +import helper_sent +import helper_addressbook + +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 +import network.connectionpool as connectionpool +from network.node import Node, Peer +from network.tcp import Socks4aBMConnection, Socks5BMConnection, TCPConnection +from queues import excQueue +from version import softwareVersion + +from common import cleanup + +try: + socket.socket().bind(('127.0.0.1', 9050)) + tor_port_free = True +except (OSError, socket.error): + tor_port_free = False + +frozen = getattr(sys, 'frozen', None) +knownnodes_file = os.path.join(state.appdata, 'knownnodes.dat') + + +def pickle_knownnodes(): + """Generate old style pickled knownnodes.dat""" + now = time.time() + with open(knownnodes_file, 'wb') as dst: + pickle.dump({ + stream: { + Peer( + '%i.%i.%i.%i' % tuple([ + random.randint(1, 255) for i in range(4)]), + 8444): {'lastseen': now, 'rating': 0.1} + for i in range(1, 4) # 3 test nodes + } + for stream in range(1, 4) # 3 test streams + }, dst) + + +class TestCore(unittest.TestCase): + """Test case, which runs in main pybitmessage thread""" + addr = 'BM-2cVvkzJuQDsQHLqxRXc6HZGPLZnkBLzEZY' + + def tearDown(self): + """Reset possible unexpected settings after test""" + knownnodes.addKnownNode(1, Peer('127.0.0.1', 8444), is_self=True) + 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)""" + msg_data = { + 'subject': ''.join( + random.choice(string.ascii_lowercase + string.digits) # nosec + for _ in range(40)), + 'body': ''.join( + random.choice(string.ascii_lowercase + string.digits) # nosec + for _ in range(10000)) + } + + obj1 = MsgEncode(msg_data, 1) + obj2 = MsgEncode(msg_data, 2) + obj3 = MsgEncode(msg_data, 3) + # print "1: %i 2: %i 3: %i" % ( + # len(obj1.data), len(obj2.data), len(obj3.data)) + + obj1e = MsgDecode(1, obj1.data) + # no subject in trivial encoding + self.assertEqual(msg_data['body'], obj1e.body) + + obj2e = MsgDecode(2, obj2.data) + self.assertEqual(msg_data['subject'], obj2e.subject) + self.assertEqual(msg_data['body'], obj2e.body) + + obj3e = MsgDecode(3, obj3.data) + self.assertEqual(msg_data['subject'], obj3e.subject) + self.assertEqual(msg_data['body'], obj3e.body) + + try: + MsgEncode({'body': 'A msg with no subject'}, 3) + except Exception as e: + self.fail( + 'Exception %s while trying to encode message' + ' with no subject!' % e + ) + + @unittest.skip('Bad environment for asyncore.loop') + def test_tcpconnection(self): + """initial fill script from network.tcp""" + config.set('bitmessagesettings', 'dontconnect', 'true') + try: + for peer in (Peer("127.0.0.1", 8448),): + direct = TCPConnection(peer) + while asyncore.socket_map: + print("loop, state = %s" % direct.state) + asyncore.loop(timeout=10, count=1) + except: # noqa:E722 + self.fail('Exception in test loop') + + def _load_knownnodes(self, filepath): + with knownnodes.knownNodesLock: + shutil.copyfile(filepath, knownnodes_file) + try: + knownnodes.readKnownNodes() + except AttributeError as e: + self.fail('Failed to load knownnodes: %s' % e) + + @staticmethod + def _wipe_knownnodes(): + with knownnodes.knownNodesLock: + knownnodes.knownNodes = {stream: {} for stream in range(1, 4)} + + @staticmethod + def _outdate_knownnodes(): + with knownnodes.knownNodesLock: + for nodes in knownnodes.knownNodes.itervalues(): + for node in nodes.itervalues(): + node['lastseen'] -= 2419205 # older than 28 days + + def test_knownnodes_pickle(self): + """ensure that 3 nodes was imported for each stream""" + pickle_knownnodes() + self._wipe_knownnodes() + knownnodes.readKnownNodes() + for nodes in knownnodes.knownNodes.itervalues(): + self_count = n = 0 + for n, node in enumerate(nodes.itervalues()): + if node.get('self'): + self_count += 1 + self.assertEqual(n - self_count, 2) + + def test_knownnodes_default(self): + """test adding default knownnodes if nothing loaded""" + cleanup(files=('knownnodes.dat',)) + self._wipe_knownnodes() + knownnodes.readKnownNodes() + self.assertGreaterEqual( + len(knownnodes.knownNodes[1]), len(knownnodes.DEFAULT_NODES)) + + def test_0_cleaner(self): + """test knownnodes starvation leading to IndexError in Asyncore""" + self._outdate_knownnodes() + # time.sleep(303) # singleCleaner wakes up every 5 min + knownnodes.cleanupKnownNodes(connectionpool.pool) + self.assertTrue(knownnodes.knownNodes[1]) + while True: + try: + thread, exc = excQueue.get(block=False) + except Queue.Empty: + return + if thread == 'Asyncore' and isinstance(exc, IndexError): + self.fail("IndexError because of empty knownNodes!") + + def _initiate_bootstrap(self): + config.set('bitmessagesettings', 'dontconnect', 'true') + self._wipe_knownnodes() + knownnodes.addKnownNode(1, Peer('127.0.0.1', 8444), is_self=True) + knownnodes.cleanupKnownNodes(connectionpool.pool) + time.sleep(5) + + def _check_connection(self, full=False): + """ + Check if there is at least one outbound connection to remote host + with name not starting with "bootstrap" in 6 minutes at most, + fail otherwise. + """ + _started = time.time() + config.remove_option('bitmessagesettings', 'dontconnect') + proxy_type = config.safeGet( + 'bitmessagesettings', 'socksproxytype') + if proxy_type == 'SOCKS5': + connection_base = Socks5BMConnection + elif proxy_type == 'SOCKS4a': + connection_base = Socks4aBMConnection + else: + connection_base = TCPConnection + c = 360 + while c > 0: + time.sleep(1) + c -= 2 + 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 %.2f' + ' seconds' % (peer, time.time() - _started)) + c += 1 + break + else: + self.assertIsInstance(con, connection_base) + self.assertNotEqual(peer.host, '127.0.0.1') + if full and not con.fullyEstablished: + continue + return + self.fail( + 'Failed to connect during %.2f sec' % (time.time() - _started)) + + def _check_knownnodes(self): + for stream in knownnodes.knownNodes.itervalues(): + for peer in stream: + if peer.host.startswith('bootstrap'): + self.fail( + 'Bootstrap server in knownnodes: %s' % peer.host) + + def test_dontconnect(self): + """all connections are closed 5 seconds after setting dontconnect""" + self._initiate_bootstrap() + self.assertEqual(len(connectionpool.pool.connections()), 0) + + def test_connection(self): + """test connection to bootstrap servers""" + self._initiate_bootstrap() + for port in [8080, 8444]: + for item in socket.getaddrinfo( + 'bootstrap%s.bitmessage.org' % port, 80): + try: + addr = item[4][0] + socket.inet_aton(item[4][0]) + except (TypeError, socket.error): + continue + else: + knownnodes.addKnownNode(1, Peer(addr, port)) + self._check_connection(True) + + def test_bootstrap(self): + """test bootstrapping""" + config.set('bitmessagesettings', 'socksproxytype', 'none') + self._initiate_bootstrap() + self._check_connection() + self._check_knownnodes() + # backup potentially enough knownnodes + knownnodes.saveKnownNodes() + with knownnodes.knownNodesLock: + shutil.copyfile(knownnodes_file, knownnodes_file + '.bak') + + @unittest.skipIf(tor_port_free, 'no running tor detected') + def test_bootstrap_tor(self): + """test bootstrapping with tor""" + config.set('bitmessagesettings', 'socksproxytype', 'SOCKS5') + self._initiate_bootstrap() + self._check_connection() + self._check_knownnodes() + + @unittest.skip('There are no onion bootstrap servers available') + @unittest.skipIf(tor_port_free, 'no running tor detected') + def test_onionservicesonly(self): + """ensure bitmessage doesn't try to connect to non-onion nodes + if onionservicesonly set, wait at least 3 onion nodes + """ + 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] + if node.host.endswith('.onion') + ]) < 3: # generate fake onion nodes if have not enough + with knownnodes.knownNodesLock: + for f in ('a', 'b', 'c', 'd'): + knownnodes.addKnownNode(1, Peer(f * 16 + '.onion', 8444)) + config.remove_option('bitmessagesettings', 'dontconnect') + tried_hosts = set() + for _ in range(360): + time.sleep(1) + for peer in connectionpool.pool.outboundConnections: + if peer.host.endswith('.onion'): + tried_hosts.add(peer.host) + else: + if not peer.host.startswith('bootstrap'): + self.fail( + 'Found non onion hostname %s in outbound' + 'connections!' % peer.host) + if len(tried_hosts) > 2: + 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( + 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') + + @staticmethod + def _decode_msg(data, pattern): + proto = BMProto() + proto.bm_proto_reset() + proto.payload = data[protocol.Header.size:] + return proto.decode_payload_content(pattern) + + def test_version(self): + """check encoding/decoding of the version message""" + dandelion_enabled = True + # with single stream + msg = protocol.assembleVersionMessage('127.0.0.1', 8444, [1], dandelion_enabled) + decoded = self._decode_msg(msg, "IQQiiQlsLv") + peer, _, ua, streams = self._decode_msg(msg, "IQQiiQlsLv")[4:] + self.assertEqual( + peer, Node(11 if dandelion_enabled else 3, '127.0.0.1', 8444)) + self.assertEqual(ua, '/PyBitmessage:' + softwareVersion + '/') + self.assertEqual(streams, [1]) + # with multiple streams + msg = protocol.assembleVersionMessage('127.0.0.1', 8444, [1, 2, 3], dandelion_enabled) + decoded = self._decode_msg(msg, "IQQiiQlslv") + peer, _, ua = decoded[4:7] + streams = decoded[7:] + self.assertEqual(streams, [1, 2, 3]) + + def test_insert_method_msgid(self): + """Test insert method of helper_sent module with message sending""" + fromAddress = 'BM-2cTrmD22fLRrumi3pPLg1ELJ6PdAaTRTdfg' + toAddress = 'BM-2cUGaEcGz9Zft1SPAo8FJtfzyADTpEgU9U' + message = 'test message' + subject = 'test subject' + result = helper_sent.insert( + toAddress=toAddress, fromAddress=fromAddress, + subject=subject, message=message + ) + queryreturn = sqlQuery( + '''select msgid from sent where ackdata=?''', result) + self.assertNotEqual(queryreturn[0][0] if queryreturn else '', '') + + column_type = sqlQuery( + '''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: + self._load_knownnodes( + os.path.join( + os.path.abspath(os.path.dirname(__file__)), + 'test_pattern', 'knownnodes.dat')) + except self.failureException: + raise + finally: + cleanup(files=('knownnodes.dat',)) + + @staticmethod + def delete_address_from_addressbook(address): + """Clean up addressbook""" + sqlQuery('''delete from addressbook where address=?''', address) + + 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.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) + self.assertEqual(queryreturn[0][0], 1) + self.delete_address_from_addressbook(self.addr) + + def test_adding_two_same_case_sensitive_addresses(self): + """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.delete_address_from_addressbook(address1) + self.delete_address_from_addressbook(address2) + + +def run(): + """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 + except ImportError: + Xvfb = None + else: + qt_tests = loader.loadTestsFromModule(bitmessageqt.tests) + suite.addTests(qt_tests) + + def keep_exc(ex_cls, exc, tb): # pylint: disable=unused-argument + """Own exception hook for test cases""" + excQueue.put(('tests', exc)) + + sys.excepthook = keep_exc + + if Xvfb: + vdisplay = Xvfb(width=1024, height=768) + vdisplay.start() + atexit.register(vdisplay.stop) + return unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/src/tests/mockbm/__init__.py b/src/tests/mockbm/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/tests/mockbm/bitmessagemock.py b/src/tests/mockbm/bitmessagemock.py new file mode 100644 index 0000000000..d9ee857b4f --- /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 0000000000..847b03ed05 --- /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 0000000000..79bb413ed5 --- /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 0000000000..e69de29bb2 diff --git a/src/tests/mockbm/pybitmessage/addresses.py b/src/tests/mockbm/pybitmessage/addresses.py new file mode 120000 index 0000000000..88fcee82a3 --- /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 0000000000..da05040eca --- /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 0000000000..34258bbc18 --- /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 0000000000..6173c3cdc6 --- /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 0000000000..e69de29bb2 diff --git a/src/tests/mockbm/pybitmessage/network/threads.py b/src/tests/mockbm/pybitmessage/network/threads.py new file mode 120000 index 0000000000..c95b4c3600 --- /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 0000000000..8c5560156e --- /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 0000000000..08c885d80e --- /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 0000000000..5e11256717 --- /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 0000000000..117203f5f8 --- /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 0000000000..f97dc41409 --- /dev/null +++ b/src/tests/partial.py @@ -0,0 +1,44 @@ +"""A test case for partial run class definition""" + +import os +import sys +import time +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]) + time.sleep(5) + cls.state.shutdown = 0 diff --git a/src/tests/samples.py b/src/tests/samples.py new file mode 100644 index 0000000000..b0a67dbf0f --- /dev/null +++ b/src/tests/samples.py @@ -0,0 +1,105 @@ +"""Various sample data""" + +from binascii import unhexlify + +# hello, page 1 of the Specification +sample_hash_data = b'hello' +sample_double_sha512 = unhexlify( + '0592a10584ffabf96539f3d780d776828c67da1ab5b169e9e8aed838aaecc9ed36d49ff14' + '23c55f019e050c66c6324f53588be88894fef4dcffdb74b98e2b200') + +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( + '044a367f049ec16cb6b6118eb734a9962d10b8db59c890cd08f210c43ff08bdf09' + 'd16f502ca26cd0713f38988a1237f1fc8fa07b15653c996dc4013af6d15505ce') +sample_pubencryptionkey = unhexlify( + '044597d59177fc1d89555d38915f581b5ff2286b39d022ca0283d2bdd5c36be5d3' + 'ce7b9b97792327851a562752e4b79475d1f51f5a71352482b241227f45ed36a9') +sample_privsigningkey = \ + b'93d0b61371a54b53df143b954035d612f8efa8a3ed1cf842c2186bfd8f876665' +sample_privencryptionkey = \ + b'4b0b73a54e19b059dc274ab69df095fe699f43b17397bca26fdf40f4d7400a3a' + +sample_ripe = b'003cd097eb7f35c87b5dc8b4538c22cb55312a9f' +# stream: 1, version: 2 +sample_address = 'BM-onkVu1KKL2UaUss5Upg9vXmqd3esTmV79' + +sample_factor = \ + 66858749573256452658262553961707680376751171096153613379801854825275240965733 +# G * sample_factor +sample_point = ( + 33567437183004486938355437500683826356288335339807546987348409590129959362313, + 94730058721143827257669456336351159718085716196507891067256111928318063085006 +) + +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_subscription_addresses is +# for the announcement broadcast, but is it matter? +sample_subscription_addresses = [ + 'BM-2cWQLCBGorT9pUGkYSuGGVr9LzE4mRnQaq', # version 4 + 'BM-GtovgYdgs7qXPkoYaRgrLFuFKz1SFpsw' # version 3 +] +sample_subscription_name = 'test sub' +sample_subscription_tag = unhexlify( + b'1a6db7c393f458c0e1efa791bbe354e3ab910006d6acd1da92fa3b0377f2dd67') + + +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' + + +# PoW + +sample_pow_target = 54227212183 +sample_pow_initial_hash = unhexlify( + '3758f55b5a8d902fd3597e4ce6a2d3f23daff735f65d9698c270987f4e67ad590' + 'b93f3ffeba0ef2fd08a8dc2f87b68ae5a0dc819ab57f22ad2c4c9c8618a43b3' +) diff --git a/src/tests/sql/init_version_10.sql b/src/tests/sql/init_version_10.sql new file mode 100644 index 0000000000..b1764e76c8 --- /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 0000000000..133284ecd9 --- /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 0000000000..875d859d45 --- /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 0000000000..ea3f17684f --- /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 0000000000..b894c038e4 --- /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 0000000000..7cd30571d0 --- /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 0000000000..bd87f8d857 --- /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 0000000000..9d9b6f3ae1 --- /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 0000000000..764634d28b --- /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 new file mode 100644 index 0000000000..dd98956271 --- /dev/null +++ b/src/tests/test_addresses.py @@ -0,0 +1,86 @@ + +import unittest +from binascii import unhexlify + +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_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] + + +class TestAddresses(unittest.TestCase): + """Test addresses manipulations""" + + def test_decode(self): + """Decode some well known addresses and check the result""" + self.assertEqual( + addresses.decodeAddress(sample_address), + ('success', 2, 1, unhexlify(sample_ripe))) + + status, version, stream, ripe1 = addresses.decodeAddress( + sample_deterministic_addr4) + self.assertEqual(status, 'success') + self.assertEqual(stream, 1) + self.assertEqual(version, 4) + status, version, stream, ripe2 = addresses.decodeAddress(sample_addr3) + self.assertEqual(status, 'success') + self.assertEqual(stream, 1) + self.assertEqual(version, 3) + self.assertEqual(ripe1, ripe2) + self.assertEqual(ripe1, unhexlify(sample_deterministic_ripe)) + + def test_encode(self): + """Encode sample ripe and compare the result to sample address""" + self.assertEqual( + sample_address, + addresses.encodeAddress(2, 1, unhexlify(sample_ripe))) + ripe = unhexlify(sample_deterministic_ripe) + self.assertEqual( + addresses.encodeAddress(3, 1, ripe), + 'BM-%s' % addresses.encodeBase58(sample_daddr3_512)) + + def test_base58(self): + """Check Base58 encoding and decoding""" + self.assertEqual(addresses.decodeBase58('1'), 0) + self.assertEqual(addresses.decodeBase58('!'), 0) + self.assertEqual( + addresses.decodeBase58(sample_addr4), sample_daddr4_512) + self.assertEqual( + addresses.decodeBase58(sample_addr3), sample_daddr3_512) + + self.assertEqual(addresses.encodeBase58(0), '1') + self.assertEqual(addresses.encodeBase58(-1), None) + self.assertEqual( + 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 0000000000..f97b24259f --- /dev/null +++ b/src/tests/test_addressgenerator.py @@ -0,0 +1,133 @@ +"""Tests for AddressGenerator (with thread or not)""" +# pylint: disable=import-error,no-member,import-outside-toplevel +import sys +import time +import unittest +from binascii import unhexlify + +import six +from six.moves import queue + +from .partial import TestPartialRun +from .samples import ( + sample_deterministic_addr3, sample_deterministic_addr4, + sample_deterministic_ripe, sample_subscription_addresses, sample_seed) + +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) + return None + + @unittest.skipIf( + sys.hexversion < 0x3000000, 'assertLogs is new in version 3.4') + def test_invalid_command(self): + """Test handling invalid commands""" + with self.assertLogs('default') as cm: # pylint: disable=no-member + self.command_queue.put(('wrong', 'command')) + self.command_queue.put(( + 'createRandomAddress', 2, 1, 'old_addr', 1, '', False, 0, 0)) + self.command_queue.put(( + 'createRandomAddress', 8, 1, 'new_addr', 1, '', False, 0, 0)) + time.sleep(2) + self.assertRegex(cm.output[0], r'Programming error:') + self.assertRegex(cm.output[1], r'Program error:.*version 2') + self.assertRegex(cm.output[2], r'Program error:.*version 8') + + 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)) + + 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.config.remove_section(sample_deterministic_addr3) + + 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')) + + self.assertEqual( + self._execute( + 'joinChan', sample_subscription_addresses[0], TEST_LABEL, + sample_seed, False), + 'chan name does not match address') + self.assertEqual( + self._execute( + 'joinChan', sample_deterministic_addr3, TEST_LABEL, + sample_seed, False), sample_deterministic_addr3) + + 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() + six.assertRegex(self, addr, r'^BM-') + six.assertRegex(self, 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 new file mode 100644 index 0000000000..82b115c3d3 --- /dev/null +++ b/src/tests/test_api.py @@ -0,0 +1,489 @@ +""" +Tests using API. +""" + +import base64 +import json +import time +from binascii import hexlify + +import psutil +import six +from six.moves import xmlrpc_client # nosec + +from .samples import ( + sample_deterministic_addr3, sample_deterministic_addr4, sample_seed, + sample_inbox_msg_ids, + sample_subscription_addresses, sample_subscription_name +) + +from .test_process import TestProcessProto + + +class TestAPIProto(TestProcessProto): + """Test case logic for testing API""" + _process_cmd = ['pybitmessage', '-t'] + + @classmethod + def setUpClass(cls): + """Setup XMLRPC proxy for pybitmessage API""" + super(TestAPIProto, cls).setUpClass() + cls.addresses = [] + cls.api = xmlrpc_client.ServerProxy( + "http://username:password@127.0.0.1:8442/") + for _ in range(5): + if cls._get_readline('.api_started'): + return + time.sleep(1) + + +class TestAPIShutdown(TestAPIProto): + """Separate test case for API command 'shutdown'""" + def test_shutdown(self): + """Shutdown the pybitmessage""" + self.assertEqual(self.api.shutdown(), 'done') + try: + self.process.wait(20) + except psutil.TimeoutExpired: + self.fail( + '%s has not stopped in 20 sec' % ' '.join(self._process_cmd)) + + +# TODO: uncovered API commands +# disseminatePreEncryptedMsg +# disseminatePubkey +# getMessageDataByDestinationHash + + +class TestAPI(TestAPIProto): + """Main API test case""" + _seed = base64.encodestring(sample_seed) + + def _add_random_address(self, label): + addr = self.api.createRandomAddress(base64.encodestring(label)) + return addr + + def test_user_password(self): + """Trying to connect with wrong username/password""" + api_wrong = xmlrpc_client.ServerProxy( + "http://test:wrong@127.0.0.1:8442/") + with self.assertRaises(xmlrpc_client.ProtocolError): + api_wrong.clientStatus() + + def test_connection(self): + """API command 'helloWorld'""" + self.assertEqual( + self.api.helloWorld('hello', 'world'), + 'hello-world' + ) + + def test_arithmetic(self): + """API command 'add'""" + self.assertEqual(self.api.add(69, 42), 111) + + def test_invalid_method(self): + """Issuing nonexistent command 'test'""" + self.assertEqual( + self.api.test(), + 'API Error 0020: Invalid method: test' + ) + + def test_message_inbox(self): + """Test message inbox methods""" + self.assertEqual( + len(json.loads( + self.api.getAllInboxMessages())["inboxMessages"]), + 4, + # Custom AssertError message for details + json.loads(self.api.getAllInboxMessages())["inboxMessages"] + ) + self.assertEqual( + len(json.loads( + self.api.getAllInboxMessageIds())["inboxMessageIds"]), + 4 + ) + self.assertEqual( + len(json.loads( + self.api.getInboxMessageById( + hexlify(sample_inbox_msg_ids[2])))["inboxMessage"]), + 1 + ) + self.assertEqual( + len(json.loads( + 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"]) + 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""" + status = json.loads(self.api.clientStatus()) + if status["networkStatus"] == "notConnected": + self.assertEqual(status["networkConnections"], 0) + 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( + json.loads(self.api.listAddresses()).get('addresses'), + self.addresses + ) + + def test_decode_address(self): + """Checking the return of API command 'decodeAddress'""" + result = json.loads( + self.api.decodeAddress(sample_deterministic_addr4)) + self.assertEqual(result.get('status'), 'success') + self.assertEqual(result['addressVersion'], 4) + self.assertEqual(result['streamNumber'], 1) + + def test_create_deterministic_addresses(self): + """Test creation of deterministic addresses""" + self.assertEqual( + self.api.getDeterministicAddress(self._seed, 4, 1), + sample_deterministic_addr4) + self.assertEqual( + self.api.getDeterministicAddress(self._seed, 3, 1), + sample_deterministic_addr3) + six.assertRegex( + self, self.api.getDeterministicAddress(self._seed, 2, 1), + r'^API Error 0002:') + + # This is here until the streams will be implemented + six.assertRegex( + self, self.api.getDeterministicAddress(self._seed, 3, 2), + r'API Error 0003:') + six.assertRegex( + self, self.api.createDeterministicAddresses(self._seed, 1, 4, 2), + r'API Error 0003:') + + six.assertRegex( + self, self.api.createDeterministicAddresses('', 1), + r'API Error 0001:') + six.assertRegex( + self, self.api.createDeterministicAddresses(self._seed, 1, 2), + r'API Error 0002:') + six.assertRegex( + self, self.api.createDeterministicAddresses(self._seed, 0), + r'API Error 0004:') + six.assertRegex( + self, self.api.createDeterministicAddresses(self._seed, 1000), + r'API Error 0005:') + + addresses = json.loads( + self.api.createDeterministicAddresses(self._seed, 2, 4) + )['addresses'] + self.assertEqual(len(addresses), 2) + self.assertEqual(addresses[0], sample_deterministic_addr4) + for addr in addresses: + self.assertEqual(self.api.deleteAddress(addr), 'success') + + def test_create_random_address(self): + """API command 'createRandomAddress': basic BM-address validation""" + addr = self._add_random_address('random_1') + six.assertRegex(self, addr, r'^BM-') + six.assertRegex(self, addr[3:], r'[a-zA-Z1-9]+$') + # Whitepaper says "around 36 character" + self.assertLessEqual(len(addr[3:]), 40) + self.assertEqual(self.api.deleteAddress(addr), 'success') + + def test_addressbook(self): + """Testing API commands for addressbook manipulations""" + # Initially it's empty + self.assertEqual( + json.loads(self.api.listAddressBookEntries()).get('addresses'), + [] + ) + # Add known address + self.api.addAddressBookEntry( + sample_deterministic_addr4, + base64.encodestring('tiger_4') + ) + # Check addressbook entry + entries = json.loads( + self.api.listAddressBookEntries()).get('addresses')[0] + self.assertEqual( + entries['address'], sample_deterministic_addr4) + self.assertEqual( + base64.decodestring(entries['label']), 'tiger_4') + # Try sending to this address (#1898) + addr = self._add_random_address('random_2') + # TODO: it was never deleted + msg = base64.encodestring('test message') + msg_subject = base64.encodestring('test_subject') + result = self.api.sendMessage( + sample_deterministic_addr4, addr, msg_subject, msg) + self.assertNotRegexpMatches(result, r'^API Error') + self.api.deleteAddress(addr) + # Remove known address + self.api.deleteAddressBookEntry(sample_deterministic_addr4) + # Addressbook should be empty again + self.assertEqual( + json.loads(self.api.listAddressBookEntries()).get('addresses'), + [] + ) + + def test_subscriptions(self): + """Testing the API commands related to subscriptions""" + + self.assertEqual( + self.api.addSubscription( + sample_subscription_addresses[0], + sample_subscription_name.encode('base64')), + 'Added subscription.' + ) + + added_subscription = {'label': None, 'enabled': False} + # check_address + for sub in json.loads(self.api.listSubscriptions())['subscriptions']: + # special address, added when sqlThread starts + 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, + 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_subscription_addresses[1]: + self.assertEqual( + base64.decodestring(s['label']), + 'Bitmessage new releases/announcements') + self.assertTrue(s['enabled']) + break + else: + self.fail( + 'Could not find Bitmessage new releases/announcements' + ' in subscriptions') + self.assertEqual( + self.api.deleteSubscription(sample_subscription_addresses[0]), + 'Deleted subscription if it existed.') + self.assertEqual( + self.api.deleteSubscription(sample_subscription_addresses[1]), + 'Deleted subscription if it existed.') + self.assertEqual( + json.loads(self.api.listSubscriptions())['subscriptions'], []) + + def test_blackwhitelist(self): + """Test API commands for managing the black/white list""" + # Initially it's black + self.assertEqual(self.api.getBlackWhitelistKind(), 'black') + # Initially they are empty + self.assertEqual( + json.loads(self.api.listBlacklistEntries()).get('addresses'), []) + self.assertEqual( + json.loads(self.api.listWhitelistEntries()).get('addresses'), []) + + # For the Blacklist: + # Add known address + self.api.addBlacklistEntry( + sample_deterministic_addr4, + base64.encodestring('tiger_4') + ) + # Check list entry + entry = json.loads(self.api.listBlacklistEntries()).get('addresses')[0] + self.assertEqual(entry['address'], sample_deterministic_addr4) + self.assertEqual(base64.decodestring(entry['label']), 'tiger_4') + # Remove known address + self.api.deleteBlacklistEntry(sample_deterministic_addr4) + self.assertEqual( + json.loads(self.api.listBlacklistEntries()).get('addresses'), []) + + # Only two kinds - black and white + six.assertRegex( + self, self.api.setBlackWhitelistKind('yellow'), + r'^API Error 0028:') + # Change kind + self.api.setBlackWhitelistKind('white') + self.assertEqual(self.api.getBlackWhitelistKind(), 'white') + + # For the Whitelist: + # Add known address + self.api.addWhitelistEntry( + sample_deterministic_addr4, + base64.encodestring('tiger_4') + ) + # Check list entry + entry = json.loads(self.api.listWhitelistEntries()).get('addresses')[0] + self.assertEqual(entry['address'], sample_deterministic_addr4) + self.assertEqual(base64.decodestring(entry['label']), 'tiger_4') + # Remove known address + self.api.deleteWhitelistEntry(sample_deterministic_addr4) + self.assertEqual( + json.loads(self.api.listWhitelistEntries()).get('addresses'), []) + + def test_send(self): + """Test message sending""" + addr = self._add_random_address('random_2') + msg = base64.encodestring('test message') + msg_subject = base64.encodestring('test_subject') + ackdata = self.api.sendMessage( + sample_deterministic_addr4, addr, msg_subject, msg) + try: + # Check ackdata and message status + int(ackdata, 16) + status = self.api.getStatus(ackdata) + if status == 'notfound': + raise KeyError + self.assertIn( + status, ( + 'msgqueued', 'awaitingpubkey', 'msgsent', 'ackreceived', + 'doingpubkeypow', 'doingmsgpow', 'msgsentnoackexpected' + )) + # Find the message in sent + for m in json.loads( + self.api.getSentMessagesByAddress(addr))['sentMessages']: + if m['ackData'] == ackdata: + sent_msg = m['message'] + break + else: + raise KeyError + except ValueError: + self.fail('sendMessage returned error or ackData is not hex') + except KeyError: + self.fail('Could not find sent message in sent messages') + else: + # Check found message + try: + self.assertEqual(sent_msg, msg.strip()) + except UnboundLocalError: + self.fail('Could not find sent message in sent messages') + # self.assertEqual(inbox_msg, msg.strip()) + self.assertEqual(json.loads( + self.api.getSentMessageByAckData(ackdata) + )['sentMessage'][0]['message'], sent_msg) + # Trash the message + self.assertEqual( + self.api.trashSentMessageByAckData(ackdata), + 'Trashed sent message (assuming message existed).') + # Empty trash + self.assertEqual(self.api.deleteAndVacuum(), 'done') + # The message should disappear + self.assertIsNone(json.loads( + self.api.getSentMessageByAckData(ackdata))) + finally: + self.assertEqual(self.api.deleteAddress(addr), 'success') + + def test_send_broadcast(self): + """Test broadcast sending""" + addr = self._add_random_address('random_2') + msg = base64.encodestring('test broadcast') + ackdata = self.api.sendBroadcast( + addr, base64.encodestring('test_subject'), msg) + + try: + int(ackdata, 16) + status = self.api.getStatus(ackdata) + if status == 'notfound': + raise KeyError + self.assertIn(status, ( + 'doingbroadcastpow', 'broadcastqueued', 'broadcastsent')) + + start = time.time() + while status != 'broadcastsent': + spent = int(time.time() - start) + if spent > 30: + self.fail('PoW is taking too much time: %ss' % spent) + time.sleep(1) # wait for PoW to get final msgid on next step + status = self.api.getStatus(ackdata) + + # Find the message and its ID in sent + for m in json.loads(self.api.getAllSentMessages())['sentMessages']: + if m['ackData'] == ackdata: + sent_msg = m['message'] + sent_msgid = m['msgid'] + break + else: + raise KeyError + except ValueError: + self.fail('sendBroadcast returned error or ackData is not hex') + except KeyError: + self.fail('Could not find sent broadcast in sent messages') + else: + # Check found message and its ID + try: + self.assertEqual(sent_msg, msg.strip()) + except UnboundLocalError: + self.fail('Could not find sent message in sent messages') + self.assertEqual(json.loads( + self.api.getSentMessageById(sent_msgid) + )['sentMessage'][0]['message'], sent_msg) + self.assertIn( + {'msgid': sent_msgid}, json.loads( + self.api.getAllSentMessageIds())['sentMessageIds']) + # Trash the message by ID + self.assertEqual( + self.api.trashSentMessage(sent_msgid), + 'Trashed sent message (assuming message existed).') + 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) + six.assertRegex(self, 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) + six.assertRegex(self, result, r'^API Error 0013:') + + def test_chan(self): + """Testing chan creation/joining""" + # Create chan with known address + self.assertEqual( + self.api.createChan(self._seed), sample_deterministic_addr4) + # cleanup + self.assertEqual( + self.api.leaveChan(sample_deterministic_addr4), 'success') + # Join chan with addresses of version 3 or 4 + for addr in (sample_deterministic_addr4, sample_deterministic_addr3): + self.assertEqual(self.api.joinChan(self._seed, addr), 'success') + self.assertEqual(self.api.leaveChan(addr), 'success') + # Joining with wrong address should fail + six.assertRegex( + self, self.api.joinChan(self._seed, 'BM-2cWzSnwjJ7yRP3nLEW'), + r'^API Error 0008:' + ) diff --git a/src/tests/test_api_thread.py b/src/tests/test_api_thread.py new file mode 100644 index 0000000000..6e453b1923 --- /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 new file mode 100644 index 0000000000..44db7c8a47 --- /dev/null +++ b/src/tests/test_config.py @@ -0,0 +1,135 @@ +""" +Various tests for config +""" +import unittest + +from six import StringIO +from pybitmessage.bmconfigparser import BMConfigParser + +test_config = """[bitmessagesettings] +maxaddrperstreamsend = 100 +maxbootstrapconnections = 10 +maxdownloadrate = 0 +maxoutboundconnections = 8 +maxtotalconnections = 100 +maxuploadrate = 0 +apiinterface = 127.0.0.1 +apiport = 8442 +udp = True + +[threads] +receive = 3 + +[network] +bind = None +dandelion = 90 + +[inventory] +storage = sqlite +acceptmismatch = False + +[knownnodes] +maxnodes = 15000 + +[zlib] +maxsize = 1048576""" + + +# pylint: disable=protected-access +class TestConfig(unittest.TestCase): + """A test case for bmconfigparser""" + + def setUp(self): + self.config = BMConfigParser() + self.config.add_section('bitmessagesettings') + + def test_safeGet(self): + """safeGet retuns provided default for nonexistent option or None""" + self.assertIs( + self.config.safeGet('nonexistent', 'nonexistent'), None) + self.assertEqual( + self.config.safeGet('nonexistent', 'nonexistent', 42), 42) + + def test_safeGetBoolean(self): + """safeGetBoolean returns False for nonexistent option, no default""" + self.assertIs( + self.config.safeGetBoolean('nonexistent', 'nonexistent'), False) + # no arg for default + # pylint: disable=too-many-function-args + with self.assertRaises(TypeError): + self.config.safeGetBoolean('nonexistent', 'nonexistent', True) + + def test_safeGetInt(self): + """safeGetInt retuns provided default for nonexistent option or 0""" + self.assertEqual( + self.config.safeGetInt('nonexistent', 'nonexistent'), 0) + self.assertEqual( + self.config.safeGetInt('nonexistent', 'nonexistent', 42), 42) + + def test_safeGetFloat(self): + """ + safeGetFloat retuns provided default for nonexistent option or 0.0 + """ + self.assertEqual( + self.config.safeGetFloat('nonexistent', 'nonexistent'), 0.0) + self.assertEqual( + 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): + """Some logic for testing _reset()""" + test_config_object = StringIO(test_config) + 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( + 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 0000000000..b76df7eccc --- /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 new file mode 100644 index 0000000000..9322a2f07f --- /dev/null +++ b/src/tests/test_config_process.py @@ -0,0 +1,39 @@ +""" +Various tests for config +""" + +import os +import tempfile +from pybitmessage.bmconfigparser import config +from .test_process import TestProcessProto +from .common import skip_python3 + +skip_python3() + + +class TestProcessConfig(TestProcessProto): + """A test case for keys.dat""" + home = tempfile.mkdtemp() + + def test_config_defaults(self): + """Test settings in the generated config""" + self._stop_process() + self._kill_process() + config.read(os.path.join(self.home, 'keys.dat')) + + self.assertEqual(config.safeGetInt( + 'bitmessagesettings', 'settingsversion'), 10) + self.assertEqual(config.safeGetInt( + 'bitmessagesettings', 'port'), 8444) + # don't connect + self.assertTrue(config.safeGetBoolean( + 'bitmessagesettings', 'dontconnect')) + # API disabled + self.assertFalse(config.safeGetBoolean( + 'bitmessagesettings', 'apienabled')) + + # extralowdifficulty is false + self.assertEqual(config.safeGetInt( + 'bitmessagesettings', 'defaultnoncetrialsperbyte'), 1000) + self.assertEqual(config.safeGetInt( + 'bitmessagesettings', 'defaultpayloadlengthextrabytes'), 1000) diff --git a/src/tests/test_crypto.py b/src/tests/test_crypto.py new file mode 100644 index 0000000000..6dbb2f314e --- /dev/null +++ b/src/tests/test_crypto.py @@ -0,0 +1,143 @@ +""" +Test the alternatives for crypto primitives +""" + +import hashlib +import ssl +import unittest +from abc import ABCMeta, abstractmethod +from binascii import hexlify + +from pybitmessage import highlevelcrypto + + +try: + from Crypto.Hash import RIPEMD160 +except ImportError: + RIPEMD160 = None + +from .samples import ( + 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 +) + + +_sha = hashlib.new('sha512') +_sha.update(sample_pubsigningkey + sample_pubencryptionkey) + +pubkey_sha = _sha.digest() + + +class RIPEMD160TestCase(object): + """Base class for RIPEMD160 test case""" + # pylint: disable=too-few-public-methods,no-member + __metaclass__ = ABCMeta + + @abstractmethod + def _hashdigest(self, data): + """RIPEMD160 digest implementation""" + pass + + def test_hash_string(self): + """Check RIPEMD160 hash function on string""" + 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 + def _hashdigest(data): + hasher = hashlib.new('ripemd160') + hasher.update(data) + return hasher.digest() + + +@unittest.skipUnless(RIPEMD160, 'pycrypto package not found') +class TestCrypto(RIPEMD160TestCase, unittest.TestCase): + """RIPEMD160 test case for Crypto""" + @staticmethod + def _hashdigest(data): + 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( + highlevelcrypto.privToPub(sample_privsigningkey), + hexlify(sample_pubsigningkey) + ) + self.assertEqual( + highlevelcrypto.privToPub(sample_privencryptionkey), + hexlify(sample_pubencryptionkey) + ) diff --git a/src/tests/test_helper_inbox.py b/src/tests/test_helper_inbox.py new file mode 100644 index 0000000000..a0b6de1bf5 --- /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 0000000000..9227e43a5d --- /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 0000000000..036bd2c945 --- /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_identicon.py b/src/tests/test_identicon.py new file mode 100644 index 0000000000..4c6be32d21 --- /dev/null +++ b/src/tests/test_identicon.py @@ -0,0 +1,49 @@ +"""Tests for qidenticon""" + +import atexit +import unittest + +try: + from PyQt5 import QtGui, QtWidgets + from xvfbwrapper import Xvfb + from pybitmessage import qidenticon +except ImportError: + Xvfb = None + # raise unittest.SkipTest( + # 'Skipping graphical test, because of no PyQt or xvfbwrapper') +else: + vdisplay = Xvfb(width=1024, height=768) + vdisplay.start() + atexit.register(vdisplay.stop) + + +sample_code = 0x3fd4bf901b9d4ea1394f0fb358725b28 +sample_size = 48 + + +@unittest.skipUnless( + Xvfb, 'Skipping graphical test, because of no PyQt or xvfbwrapper') +class TestIdenticon(unittest.TestCase): + """QIdenticon implementation test case""" + + @classmethod + def setUpClass(cls): + """Instantiate QtWidgets.QApplication""" + cls.app = QtWidgets.QApplication([]) + + def test_qidenticon_samples(self): + """Generate 4 qidenticon samples and check their properties""" + icon_simple = qidenticon.render_identicon(sample_code, sample_size) + self.assertIsInstance(icon_simple, QtGui.QPixmap) + self.assertEqual(icon_simple.height(), sample_size * 3) + self.assertEqual(icon_simple.width(), sample_size * 3) + self.assertFalse(icon_simple.hasAlphaChannel()) + + # icon_sample = QtGui.QPixmap() + # icon_sample.load('../images/qidenticon.png') + # self.assertFalse( + # icon_simple.toImage(), icon_sample.toImage()) + + icon_x = qidenticon.render_identicon( + sample_code, sample_size, opacity=0) + self.assertTrue(icon_x.hasAlphaChannel()) diff --git a/src/tests/test_inventory.py b/src/tests/test_inventory.py new file mode 100644 index 0000000000..d0b9ff6d4d --- /dev/null +++ b/src/tests/test_inventory.py @@ -0,0 +1,60 @@ +"""Tests for inventory""" + +import os +import shutil +import struct +import tempfile +import time +import unittest + +import six + +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 six.assertRaisesRegex( + self, 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_l10n.py b/src/tests/test_l10n.py new file mode 100644 index 0000000000..c698882765 --- /dev/null +++ b/src/tests/test_l10n.py @@ -0,0 +1,24 @@ +"""Tests for l10n module""" + +import re +import sys +import time +import unittest + +from pybitmessage import l10n + + +class TestL10n(unittest.TestCase): + """A test case for L10N""" + + def test_l10n_assumptions(self): + """Check the assumptions made while rewriting the l10n""" + self.assertFalse(re.search(r'\d', time.strftime("wrong"))) + timestring_type = type(time.strftime(l10n.DEFAULT_TIME_FORMAT)) + self.assertEqual(timestring_type, str) + if sys.version_info[0] == 2: + self.assertEqual(timestring_type, bytes) + + def test_getWindowsLocale(self): + """Check the getWindowsLocale() docstring example""" + self.assertEqual(l10n.getWindowsLocale("en_EN.UTF-8"), "english") diff --git a/src/tests/test_log.py b/src/tests/test_log.py new file mode 100644 index 0000000000..4e74e50d25 --- /dev/null +++ b/src/tests/test_log.py @@ -0,0 +1,21 @@ +"""Tests for logging""" + +import subprocess +import sys +import unittest + +from pybitmessage import proofofwork + + +class TestLog(unittest.TestCase): + """A test case for logging""" + + @unittest.skipIf( + sys.hexversion < 0x3000000, 'assertLogs is new in version 3.4') + def test_LogOutput(self): + """Use proofofwork.LogOutput to log output of a shell command""" + with self.assertLogs('default') as cm: # pylint: disable=no-member + with proofofwork.LogOutput('+'): + subprocess.call(['echo', 'HELLO']) + + self.assertEqual(cm.output, ['INFO:default:+: HELLO\n']) diff --git a/src/tests/test_logger.py b/src/tests/test_logger.py new file mode 100644 index 0000000000..7fbb91c8b9 --- /dev/null +++ b/src/tests/test_logger.py @@ -0,0 +1,58 @@ +""" +Testing the logger configuration +""" + +import os +import tempfile + +import six + +from .test_process import TestProcessProto + + +class TestLogger(TestProcessProto): + """A test case for logger configuration""" + + pattern = r' <===> ' + conf_template = ''' +[loggers] +keys=root + +[handlers] +keys=default + +[formatters] +keys=default + +[formatter_default] +format=%(asctime)s {1} %(message)s + +[handler_default] +class=FileHandler +level=NOTSET +formatter=default +args=({0!r}, 'w') + +[logger_root] +level=DEBUG +handlers=default +''' + + @classmethod + def setUpClass(cls): + cls.home = tempfile.mkdtemp() + cls._files = cls._files[2:] + ('logging.dat',) + cls.log_file = os.path.join(cls.home, 'debug.log') + + with open(os.path.join(cls.home, 'logging.dat'), 'wb') as dst: + dst.write(cls.conf_template.format(cls.log_file, cls.pattern)) + + super(TestLogger, cls).setUpClass() + + def test_fileConfig(self): + """Check that our logging.dat was used""" + + self._stop_process() + data = open(self.log_file).read() + six.assertRegex(self, data, self.pattern) + six.assertRegex(self, data, 'Loaded logger configuration') diff --git a/src/tests/test_msg.py b/src/tests/test_msg.py new file mode 100644 index 0000000000..cb586fa5bf --- /dev/null +++ b/src/tests/test_msg.py @@ -0,0 +1,39 @@ +"""Tests for messagetypes module""" +import unittest + +from six import text_type + +from pybitmessage import messagetypes + +sample_data = {"": "message", "subject": "subject", "body": "body"} +invalid_data = {"": "message", "subject": b"\x01\x02\x03", "body": b"\x01\x02\x03\x04"} + + +class TestMessageTypes(unittest.TestCase): + """A test case for messagetypes""" + + def test_msg_encode(self): + """Test msg encode""" + msgObj = messagetypes.message.Message() + encoded_message = msgObj.encode(sample_data) + self.assertEqual(type(encoded_message), dict) + self.assertEqual(encoded_message["subject"], sample_data["subject"]) + self.assertEqual(encoded_message["body"], sample_data["body"]) + + def test_msg_decode(self): + """Test msg decode""" + msgObj = messagetypes.constructObject(sample_data) + self.assertEqual(msgObj.subject, sample_data["subject"]) + self.assertEqual(msgObj.body, sample_data["body"]) + + def test_invalid_data_type(self): + """Test invalid data type""" + msgObj = messagetypes.constructObject(invalid_data) + self.assertTrue(isinstance(msgObj.subject, text_type)) + self.assertTrue(isinstance(msgObj.body, text_type)) + + def test_msg_process(self): + """Test msg process""" + msgObj = messagetypes.constructObject(sample_data) + self.assertTrue(isinstance(msgObj, messagetypes.message.Message)) + self.assertIsNone(msgObj.process()) diff --git a/src/tests/test_multiqueue.py b/src/tests/test_multiqueue.py new file mode 100644 index 0000000000..4b041f1ca8 --- /dev/null +++ b/src/tests/test_multiqueue.py @@ -0,0 +1,65 @@ +"""Test cases for multiqueue""" + +import unittest +from pybitmessage.network.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 0000000000..206117e035 --- /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 new file mode 100644 index 0000000000..d9ccbe2ed8 --- /dev/null +++ b/src/tests/test_openclpow.py @@ -0,0 +1,29 @@ +""" +Tests for openclpow module +""" + +import unittest +from binascii import hexlify + +from pybitmessage import openclpow, proofofwork + +from .samples import sample_pow_target, sample_pow_initial_hash + + +class TestOpenClPow(unittest.TestCase): + """ + Main opencl test case + """ + + @classmethod + def setUpClass(cls): + openclpow.initCL() + + @unittest.skipUnless(openclpow.enabledGpus, "No GPUs found / enabled") + def test_openclpow(self): + """Check the working of openclpow module""" + nonce = openclpow.do_opencl_pow( + hexlify(sample_pow_initial_hash), sample_pow_target) + self.assertLess( + nonce - proofofwork.trial_value(nonce, sample_pow_initial_hash), + sample_pow_target) diff --git a/src/tests/test_packets.py b/src/tests/test_packets.py new file mode 100644 index 0000000000..9dfb1d23a8 --- /dev/null +++ b/src/tests/test_packets.py @@ -0,0 +1,87 @@ +"""Test packets creation and parsing""" + +from binascii import unhexlify +from struct import pack + +from pybitmessage import addresses, protocol + +from .samples import ( + sample_addr_data, sample_object_data, sample_object_expires) +from .test_protocol import TestSocketInet + + +class TestSerialize(TestSocketInet): + """Test serializing and deserializing packet data""" + + def test_varint(self): + """Test varint encoding and decoding""" + data = addresses.encodeVarint(0) + self.assertEqual(data, b'\x00') + data = addresses.encodeVarint(42) + self.assertEqual(data, b'*') + data = addresses.encodeVarint(252) + self.assertEqual(data, unhexlify('fc')) + data = addresses.encodeVarint(253) + self.assertEqual(data, unhexlify('fd00fd')) + data = addresses.encodeVarint(100500) + self.assertEqual(data, unhexlify('fe00018894')) + data = addresses.encodeVarint(65535) + self.assertEqual(data, unhexlify('fdffff')) + data = addresses.encodeVarint(4294967295) + self.assertEqual(data, unhexlify('feffffffff')) + data = addresses.encodeVarint(4294967296) + self.assertEqual(data, unhexlify('ff0000000100000000')) + data = addresses.encodeVarint(18446744073709551615) + self.assertEqual(data, unhexlify('ffffffffffffffffff')) + + with self.assertRaises(addresses.varintEncodeError): + addresses.encodeVarint(18446744073709551616) + + value, length = addresses.decodeVarint(b'\xfeaddr') + self.assertEqual(value, protocol.OBJECT_ADDR) + self.assertEqual(length, 5) + value, length = addresses.decodeVarint(b'\xfe\x00tor') + self.assertEqual(value, protocol.OBJECT_ONIONPEER) + self.assertEqual(length, 5) + + def test_packet(self): + """Check the packet created by protocol.CreatePacket()""" + 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( + protocol.encodeHost('127.0.0.1'), + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFF' + + pack('>L', 2130706433)) + self.assertEqual( + protocol.encodeHost('191.168.1.1'), + unhexlify('00000000000000000000ffffbfa80101')) + self.assertEqual( + protocol.encodeHost('1.1.1.1'), + unhexlify('00000000000000000000ffff01010101')) + self.assertEqual( + protocol.encodeHost('0102:0304:0506:0708:090A:0B0C:0D0E:0F10'), + unhexlify('0102030405060708090a0b0c0d0e0f10')) + 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_pattern/knownnodes.dat b/src/tests/test_pattern/knownnodes.dat new file mode 100644 index 0000000000..a78a4434b5 --- /dev/null +++ b/src/tests/test_pattern/knownnodes.dat @@ -0,0 +1,104 @@ +(dp0 +I1 +(dp1 +ccopy_reg +_reconstructor +p2 +(cstate +Peer +p3 +c__builtin__ +tuple +p4 +(S'85.180.139.241' +p5 +I8444 +tp6 +tp7 +Rp8 +I1608398841 +sg2 +(g3 +g4 +(S'158.222.211.81' +p9 +I8080 +tp10 +tp11 +Rp12 +I1608398841 +sg2 +(g3 +g4 +(S'178.62.12.187' +p13 +I8448 +tp14 +tp15 +Rp16 +I1608398841 +sg2 +(g3 +g4 +(S'109.147.204.113' +p17 +I1195 +tp18 +tp19 +Rp20 +I1608398841 +sg2 +(g3 +g4 +(S'5.45.99.75' +p21 +I8444 +tp22 +tp23 +Rp24 +I1608398841 +sg2 +(g3 +g4 +(S'178.11.46.221' +p25 +I8444 +tp26 +tp27 +Rp28 +I1608398841 +sg2 +(g3 +g4 +(S'95.165.168.168' +p29 +I8444 +tp30 +tp31 +Rp32 +I1608398841 +sg2 +(g3 +g4 +(S'24.188.198.204' +p33 +I8111 +tp34 +tp35 +Rp36 +I1608398841 +sg2 +(g3 +g4 +(S'75.167.159.54' +p37 +I8444 +tp38 +tp39 +Rp40 +I1608398841 +ssI2 +(dp41 +sI3 +(dp42 +s. \ No newline at end of file diff --git a/src/tests/test_process.py b/src/tests/test_process.py new file mode 100644 index 0000000000..37b34541b2 --- /dev/null +++ b/src/tests/test_process.py @@ -0,0 +1,229 @@ +""" +Common reusable code for tests and tests for pybitmessage process. +""" + +import os +import signal +import subprocess # nosec +import sys +import tempfile +import time +import unittest + +import psutil + +from .common import cleanup, put_signal_file, skip_python3 + + +skip_python3() + + +class TestProcessProto(unittest.TestCase): + """Test case implementing common logic for external testing: + it starts pybitmessage in setUpClass() and stops it in tearDownClass() + """ + _process_cmd = ['pybitmessage', '-d'] + _threads_count_min = 15 + _threads_count_max = 16 + _threads_names = [ + 'PyBitmessage', + 'addressGenerato', + 'singleWorker', + 'SQL', + 'objectProcessor', + 'singleCleaner', + 'singleAPI', + 'Asyncore', + 'ReceiveQueue_0', + 'ReceiveQueue_1', + 'ReceiveQueue_2', + 'Announcer', + 'InvBroadcaster', + 'AddrBroadcaster', + 'Downloader', + 'Uploader' + ] + _files = ( + 'keys.dat', 'debug.log', 'messages.dat', 'knownnodes.dat', + '.api_started', 'unittest.lock' + ) + home = None + + @classmethod + def setUpClass(cls): + """Setup environment and start pybitmessage""" + cls.flag = False + if not cls.home: + cls.home = tempfile.gettempdir() + cls._cleanup_files() + os.environ['BITMESSAGE_HOME'] = cls.home + put_signal_file(cls.home, 'unittest.lock') + starttime = int(time.time()) - 0.5 + cls.process = psutil.Popen( + cls._process_cmd, stderr=subprocess.STDOUT) # nosec + + pidfile = os.path.join(cls.home, 'singleton.lock') + for _ in range(10): + time.sleep(1) + try: + pstat = os.stat(pidfile) + if starttime <= pstat.st_mtime and pstat.st_size > 0: + break # the pidfile is suitable + except OSError: + continue + + try: + pid = int(cls._get_readline('singleton.lock')) + cls.process = psutil.Process(pid) + time.sleep(5) + except (psutil.NoSuchProcess, TypeError): + cls.flag = True + + def setUp(self): + if self.flag: + self.fail("%s is not started ):" % self._process_cmd) + + @classmethod + def _get_readline(cls, pfile): + pfile = os.path.join(cls.home, pfile) + try: + with open(pfile, 'rb') as p: + return p.readline().strip() + except (OSError, IOError): + pass + + @classmethod + def _stop_process(cls, timeout=5): + cls.process.send_signal(signal.SIGTERM) + try: + cls.process.wait(timeout) + except psutil.TimeoutExpired: + return False + return True + + @classmethod + def _kill_process(cls, timeout=5): + try: + cls.process.send_signal(signal.SIGKILL) + cls.process.wait(timeout) + # Windows or already dead + except (AttributeError, psutil.NoSuchProcess): + return True + # except psutil.TimeoutExpired propagates, it means something is very + # wrong + return True + + @classmethod + def _cleanup_files(cls): + cleanup(cls.home, cls._files) + + @classmethod + def tearDownClass(cls): + """Ensures that pybitmessage stopped and removes files""" + try: + if not cls._stop_process(10): + processes = cls.process.children(recursive=True) + processes.append(cls.process) + for p in processes: + try: + p.kill() + except psutil.NoSuchProcess: + pass + except psutil.NoSuchProcess: + pass + finally: + cls._cleanup_files() + + def _test_threads(self): + """Test number and names of threads""" + + # pylint: disable=invalid-name + self.longMessage = True + + try: + # using ps for posix platforms + # because of https://github.com/giampaolo/psutil/issues/613 + thread_names = subprocess.check_output([ + "ps", "-L", "-o", "comm=", "--pid", + str(self.process.pid) + ]).split() + except subprocess.CalledProcessError: + thread_names = [] + except: # noqa:E722 + thread_names = [] + + running_threads = len(thread_names) + if 0 < running_threads < 30: # adequacy check + extra_threads = [] + missing_threads = [] + for thread_name in thread_names: + if thread_name not in self._threads_names: + extra_threads.append(thread_name) + for thread_name in self._threads_names: + if thread_name not in thread_names: + missing_threads.append(thread_name) + + msg = "Missing threads: {}, Extra threads: {}".format( + ",".join(missing_threads), ",".join(extra_threads)) + else: + running_threads = self.process.num_threads() + if sys.platform.startswith('win'): + running_threads -= 1 # one extra thread on Windows! + msg = "Unexpected running thread count" + + self.assertGreaterEqual( + running_threads, + self._threads_count_min, + msg) + + self.assertLessEqual( + running_threads, + self._threads_count_max, + msg) + + +class TestProcessShutdown(TestProcessProto): + """Separate test case for SIGTERM""" + def test_shutdown(self): + """Send to pybitmessage SIGTERM and ensure it stopped""" + # longer wait time because it's not a benchmark + self.assertTrue( + self._stop_process(20), + '%s has not stopped in 20 sec' % ' '.join(self._process_cmd)) + + +class TestProcess(TestProcessProto): + """A test case for pybitmessage process""" + @unittest.skipIf(sys.platform[:5] != 'linux', 'probably needs prctl') + def test_process_name(self): + """Check PyBitmessage process name""" + self.assertEqual(self.process.name(), 'PyBitmessage') + + @unittest.skipIf(psutil.version_info < (4, 0), 'psutil is too old') + def test_home(self): + """Ensure BITMESSAGE_HOME is used by process""" + 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(): + if c.status == 'LISTEN': + self.assertEqual(c.laddr[1], 8444) + break + + def test_files(self): + """Check existence of PyBitmessage files""" + for pfile in self._files: + if pfile.startswith('.'): + continue + self.assertIsNot( + self._get_readline(pfile), None, + 'Failed to read file %s' % pfile + ) + + def test_threads(self): + """Testing PyBitmessage threads""" + self._test_threads() diff --git a/src/tests/test_proofofwork.py b/src/tests/test_proofofwork.py new file mode 100644 index 0000000000..16ff649d9a --- /dev/null +++ b/src/tests/test_proofofwork.py @@ -0,0 +1,120 @@ +""" +Tests for proofofwork module +""" +# pylint: disable=protected-access + +import hashlib +import os +import time +import unittest +from struct import pack, unpack + +from pybitmessage import proofofwork, protocol +from pybitmessage.defaults import ( + networkDefaultProofOfWorkNonceTrialsPerByte, + networkDefaultPayloadLengthExtraBytes) + +from .partial import TestPartialRun +from .samples import sample_pow_target, sample_pow_initial_hash + +default_ttl = 7200 + + +class TestProofofworkBase(TestPartialRun): + """Basic test case for proofofwork""" + + @classmethod + def setUpClass(cls): + proofofwork.init() + super(TestProofofworkBase, cls).setUpClass() + + def setUp(self): + self.state.shutdown = 0 + + @staticmethod + def _make_sample_payload(TTL=default_ttl): + return pack('>Q', int(time.time() + TTL)) + os.urandom(166) + + def test_calculate(self): + """Ensure a calculated nonce has sufficient work for the protocol""" + payload = self._make_sample_payload() + nonce = proofofwork.calculate(payload, default_ttl)[1] + self.assertTrue( + protocol.isProofOfWorkSufficient(pack('>Q', nonce) + payload)) + + # pylint: disable=import-outside-toplevel + from class_singleWorker import singleWorker + + self.assertTrue(protocol.isProofOfWorkSufficient( + singleWorker._doPOWDefaults(payload, default_ttl))) + + +@unittest.skipUnless( + os.getenv('BITMESSAGE_TEST_POW'), "BITMESSAGE_TEST_POW is not set") +class TestProofofwork(TestProofofworkBase): + """The main test case for proofofwork""" + + def _make_sample_data(self): + payload = self._make_sample_payload() + return payload, proofofwork.getTarget( + len(payload), default_ttl, + networkDefaultProofOfWorkNonceTrialsPerByte, + networkDefaultPayloadLengthExtraBytes + ), hashlib.sha512(payload).digest() + + def test_calculate(self): + """Extended test for the main proofofwork call""" + # raise difficulty and TTL + TTL = 24 * 60 * 60 + payload = self._make_sample_payload(TTL) + nonce = proofofwork.calculate(payload, TTL, 2000, 2000)[1] + self.assertTrue( + protocol.isProofOfWorkSufficient( + pack('>Q', nonce) + payload, 2000, 2000, + int(time.time()) + TTL - 3600)) + + # pylint: disable=import-outside-toplevel + from class_singleWorker import singleWorker + + # pylint: disable=no-member + with self.assertLogs('default') as cm: + self.assertTrue(protocol.isProofOfWorkSufficient( + singleWorker._doPOWDefaults(payload, TTL, log_prefix='+'))) + self.assertEqual( + cm.output[0], + 'INFO:default:+ Doing proof of work... TTL set to %s' % TTL) + self.assertEqual( + cm.output[1][:34], 'INFO:default:+ Found proof of work') + + with self.assertLogs('default') as cm: + self.assertTrue(protocol.isProofOfWorkSufficient( + singleWorker._doPOWDefaults(payload, TTL, log_time=True))) + self.assertEqual(cm.output[2][:22], 'INFO:default:PoW took ') + + with self.assertRaises(StopIteration): + self.state.shutdown = 1 + proofofwork.calculate(payload, TTL) + + def test_CPoW(self): + """Do PoW with parameters from test_openclpow and check the result""" + nonce = proofofwork._doCPoW( + sample_pow_target, sample_pow_initial_hash)[0] + trial_value, = unpack( + '>Q', hashlib.sha512(hashlib.sha512( + pack('>Q', nonce) + sample_pow_initial_hash + ).digest()).digest()[0:8]) + self.assertLess((nonce - trial_value), sample_pow_target) + + def test_SafePoW(self): + """Do python PoW for a sample payload and check by protocol""" + payload, target, initial_hash = self._make_sample_data() + nonce = proofofwork._doSafePoW(target, initial_hash)[1] + self.assertTrue( + protocol.isProofOfWorkSufficient(pack('>Q', nonce) + payload)) + + def test_FastPoW(self): + """Do python multiprocessing PoW for a sample payload and check""" + payload, target, initial_hash = self._make_sample_data() + nonce = proofofwork._doFastPoW(target, initial_hash)[1] + self.assertTrue( + protocol.isProofOfWorkSufficient(pack('>Q', nonce) + payload)) diff --git a/src/tests/test_protocol.py b/src/tests/test_protocol.py new file mode 100644 index 0000000000..69e1e82f4e --- /dev/null +++ b/src/tests/test_protocol.py @@ -0,0 +1,115 @@ +""" +Tests for common protocol functions +""" + +import sys +import unittest + +from pybitmessage import protocol, state +from pybitmessage.helper_startup import fixSocket + + +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' + # checking protocol.encodeHost()[12:] + self.assertEqual( # 127.0.0.1 + token, protocol.checkIPv4Address(b'\x7f\x00\x00\x01', token, True)) + self.assertFalse( + protocol.checkIPv4Address(b'\x7f\x00\x00\x01', token)) + self.assertEqual( # 10.42.43.1 + token, protocol.checkIPv4Address(b'\n*+\x01', token, True)) + self.assertFalse( + protocol.checkIPv4Address(b'\n*+\x01', token, False)) + self.assertEqual( # 192.168.0.254 + token, protocol.checkIPv4Address(b'\xc0\xa8\x00\xfe', token, True)) + self.assertEqual( # 172.31.255.254 + token, protocol.checkIPv4Address(b'\xac\x1f\xff\xfe', token, True)) + # self.assertEqual( # 169.254.1.1 + # token, protocol.checkIPv4Address(b'\xa9\xfe\x01\x01', token, True)) + # self.assertEqual( # 254.128.1.1 + # token, protocol.checkIPv4Address(b'\xfe\x80\x01\x01', token, True)) + self.assertFalse( # 8.8.8.8 + protocol.checkIPv4Address(b'\x08\x08\x08\x08', token, True)) + + def test_checkIPv6Address(self): + """Check the results of protocol.checkIPv6Address()""" + test_ip = '2001:db8::ff00:42:8329' + self.assertEqual( + 'test', protocol.checkIPv6Address( + protocol.encodeHost(test_ip), 'test')) + self.assertFalse( + protocol.checkIPv6Address( + protocol.encodeHost(test_ip), 'test', True)) + for test_ip in ('fe80::200:5aee:feaa:20a2', 'fdf8:f53b:82e4::53'): + self.assertEqual( + 'test', protocol.checkIPv6Address( + protocol.encodeHost(test_ip), 'test', True)) + self.assertFalse( + protocol.checkIPv6Address( + protocol.encodeHost(test_ip), 'test')) + + def test_check_local(self): + """Check the logic of TCPConnection.local""" + self.assertFalse( + protocol.checkIPAddress(protocol.encodeHost('127.0.0.1'))) + self.assertTrue( + protocol.checkIPAddress(protocol.encodeHost('127.0.0.1'), True)) + self.assertTrue( + protocol.checkIPAddress(protocol.encodeHost('192.168.0.1'), True)) + self.assertTrue( + protocol.checkIPAddress(protocol.encodeHost('10.42.43.1'), True)) + self.assertTrue( + protocol.checkIPAddress(protocol.encodeHost('172.31.255.2'), True)) + self.assertFalse(protocol.checkIPAddress( + protocol.encodeHost('2001:db8::ff00:42:8329'), True)) + + globalhost = protocol.encodeHost('8.8.8.8') + self.assertFalse(protocol.checkIPAddress(globalhost, True)) + self.assertEqual(protocol.checkIPAddress(globalhost), '8.8.8.8') + + @unittest.skipIf( + sys.hexversion >= 0x3000000, 'this is still not working with python3') + def test_check_local_socks(self): + """The SOCKS part of the local check""" + self.assertTrue( + not protocol.checkSocksIP('127.0.0.1') + or state.socksIP) + + def test_network_group(self): + """Test various types of network groups""" + + test_ip = '1.2.3.4' + self.assertEqual(b'\x01\x02', protocol.network_group(test_ip)) + + test_ip = '127.0.0.1' + self.assertEqual('IPv4', protocol.network_group(test_ip)) + + self.assertEqual( + protocol.network_group('8.8.8.8'), + protocol.network_group('8.8.4.4')) + self.assertNotEqual( + protocol.network_group('1.1.1.1'), + protocol.network_group('8.8.8.8')) + + test_ip = '0102:0304:0506:0708:090A:0B0C:0D0E:0F10' + self.assertEqual( + b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C', + protocol.network_group(test_ip)) + + for test_ip in ( + 'bootstrap8444.bitmessage.org', 'quzwelsuziwqgpt2.onion', None): + self.assertEqual( + test_ip, protocol.network_group(test_ip)) diff --git a/src/tests/test_randomtrackingdict.py b/src/tests/test_randomtrackingdict.py new file mode 100644 index 0000000000..2db3c42379 --- /dev/null +++ b/src/tests/test_randomtrackingdict.py @@ -0,0 +1,49 @@ +""" +Tests for RandomTrackingDict Class +""" +import random +import unittest + +from time import time + + +class TestRandomTrackingDict(unittest.TestCase): + """ + Main protocol test case + """ + + @staticmethod + def randString(): + """helper function for tests, generates a random string""" + retval = '' + for _ in range(32): + retval += chr(random.randint(0, 255)) + return retval + + def test_check_randomtrackingdict(self): + """Check the logic of RandomTrackingDict class""" + from pybitmessage.randomtrackingdict import RandomTrackingDict + a = [] + k = RandomTrackingDict() + + a.append(time()) + for i in range(50000): + k[self.randString()] = True + a.append(time()) + + while k: + retval = k.randomKeys(1000) + if not retval: + self.fail("error getting random keys") + + try: + k.randomKeys(100) + self.fail("bad") + except KeyError: + pass + for i in retval: + del k[i] + a.append(time()) + + for x in range(len(a) - 1): + self.assertLess(a[x + 1] - a[x], 10) diff --git a/src/tests/test_shared.py b/src/tests/test_shared.py new file mode 100644 index 0000000000..ae90a9d441 --- /dev/null +++ b/src/tests/test_shared.py @@ -0,0 +1,200 @@ +"""Test cases for shared.py""" + +import unittest +from binascii import unhexlify + +from pybitmessage.highlevelcrypto import encodeWalletImportFormat +from pybitmessage.shared import ( + config, + isAddressInMyAddressBook, + isAddressInMySubscriptionsList, + checkSensitiveFilePermissions, + reloadBroadcastSendersForWhichImWatching, + reloadMyAddressHashes, + fixSensitiveFilePermissions, + myAddressesByHash, + myAddressesByTag, + myECCryptorObjects, + MyECSubscriptionCryptorObjects, + stat, + os, +) + +from .samples import ( + sample_address, sample_privencryptionkey, sample_ripe, + sample_subscription_addresses, sample_subscription_tag +) + +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 = [ + (addr,) for addr in sample_subscription_addresses + [sample_address] + ] + # before reload + self.assertEqual(len(MyECSubscriptionCryptorObjects), 0) + + reloadBroadcastSendersForWhichImWatching() + self.assertGreater(len(MyECSubscriptionCryptorObjects), 0) + self.assertTrue( + MyECSubscriptionCryptorObjects.get(unhexlify(sample_ripe)) + ) + self.assertTrue( + MyECSubscriptionCryptorObjects.get(sample_subscription_tag) + ) + + def test_reloadMyAddressHashes(self): + """Test for reloadMyAddressHashes""" + self.assertEqual(len(myAddressesByHash), 0) + self.assertEqual(len(myAddressesByTag), 0) + + config.add_section(sample_address) + config.set(sample_address, 'enabled', 'false') + config.set(sample_address, 'privencryptionkey', 'malformed') + config.save() + + reloadMyAddressHashes() + self.assertEqual(len(myAddressesByHash), 0) + + config.set(sample_address, 'enabled', 'true') + config.save() + + reloadMyAddressHashes() + self.assertEqual(len(myAddressesByHash), 0) + + config.set( + sample_address, 'privencryptionkey', + encodeWalletImportFormat( + unhexlify(sample_privencryptionkey)).decode() + ) # the key is not for the sample_address, but it doesn't matter + config.save() + + reloadMyAddressHashes() + ripe = unhexlify(sample_ripe) + self.assertEqual(len(myAddressesByTag), 1) + self.assertTrue(myECCryptorObjects.get(ripe)) + self.assertEqual(myAddressesByHash[ripe], sample_address) + + @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/tests/test_sqlthread.py b/src/tests/test_sqlthread.py new file mode 100644 index 0000000000..a612df3ae7 --- /dev/null +++ b/src/tests/test_sqlthread.py @@ -0,0 +1,44 @@ +"""Tests for SQL thread""" +# flake8: noqa:E402 +import os +import tempfile +import threading +import unittest + +from .common import skip_python3 + +skip_python3() + +os.environ['BITMESSAGE_HOME'] = tempfile.gettempdir() + +from pybitmessage.helper_sql import ( + sqlQuery, sql_ready, sqlStoredProcedure) # noqa:E402 +from pybitmessage.class_sqlThread import sqlThread # noqa:E402 +from pybitmessage.addresses import encodeAddress # noqa:E402 + + +class TestSqlThread(unittest.TestCase): + """Test case for SQL thread""" + + @classmethod + def setUpClass(cls): + # Start SQL thread + sqlLookup = sqlThread() + sqlLookup.daemon = True + sqlLookup.start() + sql_ready.wait() + + @classmethod + def tearDownClass(cls): + sqlStoredProcedure('exit') + for thread in threading.enumerate(): + if thread.name == "SQL": + thread.join() + + def test_create_function(self): + """Check the result of enaddr function""" + encoded_str = encodeAddress(4, 1, "21122112211221122112") + + query = sqlQuery('SELECT enaddr(4, 1, "21122112211221122112")') + self.assertEqual( + query[0][-1], encoded_str, "test case fail for create_function") diff --git a/src/threads.py b/src/threads.py new file mode 100644 index 0000000000..ac8bf7a6d5 --- /dev/null +++ b/src/threads.py @@ -0,0 +1,48 @@ +""" +PyBitmessage does various tasks in separate threads. Most of them inherit +from `.network.StoppableThread`. There are `addressGenerator` for +addresses generation, `objectProcessor` for processing the network objects +passed minimal validation, `singleCleaner` to periodically clean various +internal storages (like inventory and knownnodes) and do forced garbage +collection, `singleWorker` for doing PoW, `sqlThread` for querying sqlite +database. + +There are also other threads in the `.network` package. + +:func:`set_thread_name` is defined here for the threads that don't inherit from +:class:`.network.StoppableThread` +""" + +import threading + +from class_addressGenerator import addressGenerator +from class_objectProcessor import objectProcessor +from class_singleCleaner import singleCleaner +from class_singleWorker import singleWorker +from class_sqlThread import sqlThread + +try: + import prctl +except ImportError: + def set_thread_name(name): + """Set a name for the thread for python internal use.""" + threading.current_thread().name = name +else: + def set_thread_name(name): + """Set the thread name for external use (visible from the OS).""" + prctl.set_name(name) + + def _thread_name_hack(self): + set_thread_name(self.name) + threading.Thread.__bootstrap_original__(self) + # pylint: disable=protected-access + threading.Thread.__bootstrap_original__ = threading.Thread._Thread__bootstrap + threading.Thread._Thread__bootstrap = _thread_name_hack + + +printLock = threading.Lock() + +__all__ = [ + "addressGenerator", "objectProcessor", "singleCleaner", "singleWorker", + "sqlThread", "printLock" +] diff --git a/src/tr.py b/src/tr.py index cf7f16ac14..eec82c37d3 100644 --- a/src/tr.py +++ b/src/tr.py @@ -1,39 +1,59 @@ +""" +Translating text +""" import os -import shared +try: + import state +except ImportError: + from . import state + -# This is used so that the translateText function can be used when we are in daemon mode and not using any QT functions. class translateClass: + """ + This is used so that the translateText function can be used + when we are in daemon mode and not using any QT functions. + """ + # pylint: disable=old-style-class,too-few-public-methods def __init__(self, context, text): self.context = context self.text = text - def arg(self,argument): + + def arg(self, _): + """Replace argument placeholders""" if '%' in self.text: - return translateClass(self.context, self.text.replace('%','',1)) # This doesn't actually do anything with the arguments because we don't have a UI in which to display this information anyway. - else: - return self.text + # This doesn't actually do anything with the arguments + # because we don't have a UI in which to display this information anyway. + return translateClass(self.context, self.text.replace('%', '', 1)) + return self.text -def _translate(context, text, disambiguation = None, encoding = None, n = None): + +def _translate(context, text, disambiguation=None, encoding=None, n=None): + # pylint: disable=unused-argument return translateText(context, text, n) -def translateText(context, text, n = None): + +def translateText(context, text, n=None): + """Translate text in context""" try: - is_daemon = shared.thisapp.daemon + enableGUI = state.enableGUI except AttributeError: # inside the plugin - is_daemon = False - if not is_daemon: + enableGUI = True + if enableGUI: try: from PyQt4 import QtCore, QtGui except Exception as err: - print 'PyBitmessage requires PyQt unless you want to run it as a daemon and interact with it using the API. You can download PyQt from http://www.riverbankcomputing.com/software/pyqt/download or by searching Google for \'PyQt Download\'. If you want to run in daemon mode, see https://bitmessage.org/wiki/Daemon' - print 'Error message:', err - os._exit(0) + print('PyBitmessage requires PyQt unless you want to run it as a daemon' + ' and interact with it using the API.' + ' You can download PyQt from http://www.riverbankcomputing.com/software/pyqt/download' + ' or by searching Google for \'PyQt Download\'.' + ' If you want to run in daemon mode, see https://bitmessage.org/wiki/Daemon') + print('Error message:', err) + os._exit(0) # pylint: disable=protected-access if n is None: return QtGui.QApplication.translate(context, text) - else: - return QtGui.QApplication.translate(context, text, None, QtCore.QCoreApplication.CodecForTr, n) + return QtGui.QApplication.translate(context, text, None, QtCore.QCoreApplication.CodecForTr, n) else: if '%' in text: - return translateClass(context, text.replace('%','',1)) - else: - return text + return translateClass(context, text.replace('%', '', 1)) + return text diff --git a/src/translations/bitmessage.pro b/src/translations/bitmessage.pro index ed491b3d5d..b131d8e59e 100644 --- a/src/translations/bitmessage.pro +++ b/src/translations/bitmessage.pro @@ -5,20 +5,15 @@ SOURCES = ../addresses.py\ ../class_singleCleaner.py\ ../class_singleWorker.py\ ../class_sqlThread.py\ - ../helper_bitcoin.py\ - ../helper_bootstrap.py\ - ../helper_generic.py\ - ../helper_inbox.py\ ../helper_msgcoding.py\ - ../helper_sent.py\ - ../helper_startup.py\ + ../helper_search.py\ ../namecoin.py\ ../proofofwork.py\ - ../shared.py\ ../upnp.py\ ../bitmessageqt/__init__.py\ ../bitmessageqt/account.py\ - ../bitmessageqt/address_dialogs.py\ + ../bitmessageqt/address_dialogs.py\ + ../bitmessageqt/addressvalidator.py\ ../bitmessageqt/bitmessageui.py\ ../bitmessageqt/blacklist.py\ ../bitmessageqt/dialogs.py\ @@ -28,9 +23,10 @@ SOURCES = ../addresses.py\ ../bitmessageqt/messageview.py\ ../bitmessageqt/networkstatus.py\ ../bitmessageqt/newchandialog.py\ - ../bitmessageqt/safehtmlparser.py\ - ../bitmessageqt/settings.py\ - ../plugins/qrcodeui.py + ../bitmessageqt/settings.py\ + ../bitmessageqt/support.py\ + ../plugins/indicator_libmessaging.py\ + ../plugins/menu_qrcode.py FORMS = \ ../bitmessageqt/about.ui\ diff --git a/src/translations/bitmessage_eo.qm b/src/translations/bitmessage_eo.qm index fa6a1a96d7..77c20edfbc 100644 Binary files a/src/translations/bitmessage_eo.qm and b/src/translations/bitmessage_eo.qm differ diff --git a/src/translations/bitmessage_eo.ts b/src/translations/bitmessage_eo.ts index de853696ed..5707a39008 100644 --- a/src/translations/bitmessage_eo.ts +++ b/src/translations/bitmessage_eo.ts @@ -60,27 +60,27 @@ @mailchuck.com - + Registration failed: Registrado malsukcesis: - + The requested email address is not available, please try a new one. La dezirata retpoŝtadreso ne estas disponebla, bonvolu provi alian. - + Sending email gateway registration request Sendado de peto pri registrado ĉe retpoŝta kluzo - + Sending email gateway unregistration request Sendado de peto pri malregistrado de retpoŝta kluzo - + Sending email gateway status request Sendado de peto pri stato de retpoŝta kluzo @@ -112,7 +112,7 @@ Please type the desired email address (including @mailchuck.com) below: Mailchuck - + # You can use this to configure your email gateway account # Uncomment the setting you want to use # Here are the options: @@ -152,10 +152,54 @@ Please type the desired email address (including @mailchuck.com) below: # specified. As this scheme uses deterministic public keys, you will receive # the money directly. To turn it off again, set "feeamount" to 0. Requires # subscription. + + + + + + # You can use this to configure your email gateway account +# Uncomment the setting you want to use +# Here are the options: +# +# pgp: server +# The email gateway will create and maintain PGP keys for you and sign, verify, +# encrypt and decrypt on your behalf. When you want to use PGP but are lazy, +# use this. Requires subscription. +# +# pgp: local +# The email gateway will not conduct PGP operations on your behalf. You can +# either not use PGP at all, or use it locally. +# +# attachments: yes +# Incoming attachments in the email will be uploaded to MEGA.nz, and you can +# download them from there by following the link. Requires a subscription. +# +# attachments: no +# Attachments will be ignored. +# +# archive: yes +# Your incoming emails will be archived on the server. Use this if you need +# help with debugging problems or you need a third party proof of emails. This +# however means that the operator of the service will be able to read your +# emails even after they have been delivered to you. +# +# archive: no +# Incoming emails will be deleted from the server as soon as they are relayed +# to you. +# +# masterpubkey_btc: BIP44 xpub key or electrum v1 public seed +# offset_btc: integer (defaults to 0) +# feeamount: number with up to 8 decimal places +# feecurrency: BTC, XBT, USD, EUR or GBP +# Use these if you want to charge people who send you emails. If this is on and +# an unknown person sends you an email, they will be requested to pay the fee +# specified. As this scheme uses deterministic public keys, you will receive +# the money directly. To turn it off again, set "feeamount" to 0. Requires +# subscription. # Tie ĉi vi povas agordi vian konton ĉe retpoŝta kluzo # Malkomenti agordojn kiujn vi volas uzi -# Jenaj agordoj: +# Jen agordoj: # # pgp: server # La retpoŝta kluzo kreos kaj prizorgos PGP-ŝlosilojn por vi por subskribi, @@ -195,122 +239,122 @@ Please type the desired email address (including @mailchuck.com) below: MainWindow - + Reply to sender Respondi al sendinto - + Reply to channel Respondi al kanalo - + Add sender to your Address Book Aldoni sendinton al via adresaro - + Add sender to your Blacklist - Aldoni sendinton al via nigra listo + Aldoni sendinton al via blok-listo - + Move to Trash Movi al rubujo - + Undelete Malforigi - + View HTML code as formatted text Montri HTML-n kiel aranĝitan tekston - + Save message as... Konservi mesaĝon kiel… - + Mark Unread Marki kiel nelegitan - + New Nova - + Enable Ŝalti - + Disable Malŝalti - + Set avatar... Agordi avataron… - + Copy address to clipboard Kopii adreson al tondejo - + Special address behavior... Speciala sinteno de adreso… - + Email gateway Retpoŝta kluzo - + Delete Forigi - + Send message to this address Sendi mesaĝon al tiu adreso - + Subscribe to this address Aboni tiun adreson - + Add New Address Aldoni novan adreson - + Copy destination address to clipboard Kopii cel-adreson al tondejo - + Force send Devigi sendadon - + One of your addresses, %1, is an old version 1 address. Version 1 addresses are no longer supported. May we delete it now? Iu de viaj adresoj, %1, estas malnova versio 1 adreso. Versioj 1 adresoj ne estas jam subtenataj. Ĉu ni povas forigi ĝin? - + Waiting for their encryption key. Will request it again soon. Atendado je ilia ĉifroŝlosilo. Baldaŭ petos ĝin denove. @@ -320,17 +364,17 @@ Please type the desired email address (including @mailchuck.com) below: - + Queued. En atendovico. - + Message sent. Waiting for acknowledgement. Sent at %1 Mesaĝo sendita. Atendado je konfirmo. Sendita je %1 - + Message sent. Sent at %1 Mesaĝo sendita. Sendita je %1 @@ -340,7 +384,7 @@ Please type the desired email address (including @mailchuck.com) below: - + Acknowledgement of the message received %1 Ricevis konfirmon de la mesaĝo je %1 @@ -350,37 +394,37 @@ Please type the desired email address (including @mailchuck.com) below: Elsendo en atendovico. - + Broadcast on %1 Elsendo je %1 - + Problem: The work demanded by the recipient is more difficult than you are willing to do. %1 Problemo: la demandita laboro de la ricevonto estas pli malfacila ol vi pretas fari. %1 - + Problem: The recipient's encryption key is no good. Could not encrypt message. %1 Problemo: la ĉifroŝlosilo de la ricevonto estas rompita. Ne povis ĉifri la mesaĝon. %1 - + Forced difficulty override. Send should start soon. Devigita superado de limito de malfacilaĵo. Sendado devus baldaŭ komenci. - + Unknown status: %1 %2 Nekonata stato: %1 %2 - + Not Connected Ne konektita - + Show Bitmessage Montri Bitmesaĝon @@ -390,12 +434,12 @@ Please type the desired email address (including @mailchuck.com) below: Sendi - + Subscribe Aboni - + Channel Kanalo @@ -405,70 +449,70 @@ Please type the desired email address (including @mailchuck.com) below: Eliri - + You may manage your keys by editing the keys.dat file stored in the same directory as this program. It is important that you back up this file. - Vi povas administri viajn ŝlosilojn per redakti la dosieron keys.dat en la sama dosierujo kiel tiu programo. Estas grava, ke vi faru sekurkopion de tiu dosiero. + Vi povas administri viajn ŝlosilojn per redakti la dosieron “keys.dat” en la sama dosierujo kiel tiu programo. Estas grava, ke vi faru sekurkopion de tiu dosiero. - + You may manage your keys by editing the keys.dat file stored in %1 It is important that you back up this file. - Vi povas administri viajn ŝlosilojn per redakti la dosieron keys.dat en la dosierujo + Vi povas administri viajn ŝlosilojn per redakti la dosieron “keys.dat” en la dosierujo %1. Estas grava, ke vi faru sekurkopion de tiu dosiero. - + Open keys.dat? Ĉu malfermi keys.dat? - + You may manage your keys by editing the keys.dat file stored in the same directory as this program. It is important that you back up this file. Would you like to open the file now? (Be sure to close Bitmessage before making any changes.) - Vi povas administri viajn ŝlosilojn per redakti la dosieron keys.dat en la sama dosierujo kiel tiu programo. Estas grava ke vi faru sekurkopion de tiu dosiero. Ĉu vi volas malfermi la dosieron nun? (Bonvolu certigi ke Bitmesaĝo estas fermita antaŭ fari ŝanĝojn.) + Vi povas administri viajn ŝlosilojn per redakti la dosieron “keys.dat” en la sama dosierujo kiel tiu programo. Estas grava ke vi faru sekurkopion de tiu dosiero. Ĉu vi volas malfermi la dosieron nun? (Bonvolu certigi ke Bitmesaĝo estas fermita antaŭ fari ŝanĝojn.) - + You may manage your keys by editing the keys.dat file stored in %1 It is important that you back up this file. Would you like to open the file now? (Be sure to close Bitmessage before making any changes.) - Vi povas administri viajn ŝlosilojn per redakti la dosieron keys.dat en la dosierujo + Vi povas administri viajn ŝlosilojn per redakti la dosieron “keys.dat” en la dosierujo %1. Estas grava, ke vi faru sekurkopion de tiu dosiero. Ĉu vi volas malfermi la dosieron nun? (Bonvolu certigi ke Bitmesaĝo estas fermita antaŭ fari ŝanĝojn.) - + Delete trash? Ĉu malplenigi rubujon? - + Are you sure you want to delete all trashed messages? Ĉu vi certe volas forviŝi ĉiujn mesaĝojn el la rubujo? - + bad passphrase malprava pasvorto - + You must type your passphrase. If you don't have one then this is not the form for you. Vi devas tajpi vian pasvorton. Se vi ne havas pasvorton, tiu ĉi ne estas la prava formularo por vi. - + Bad address version number Erara numero de adresversio - + Your address version number must be a number: either 3 or 4. Via numero de adresversio devas esti: aŭ 3 aŭ 4. - + Your address version number must be either 3 or 4. Via numero de adresversio devas esti: aŭ 3 aŭ 4. @@ -538,12 +582,12 @@ Estas grava, ke vi faru sekurkopion de tiu dosiero. Ĉu vi volas malfermi la dos - + Connection lost Perdis konekton - + Connected Konektita @@ -573,7 +617,7 @@ Estas grava, ke vi faru sekurkopion de tiu dosiero. Ĉu vi volas malfermi la dos Error: Your account wasn't registered at an email gateway. Sending registration now as %1, please wait for the registration to be processed before retrying sending. - Eraro: Via konto ne estas registrita je retpoŝta kluzo. Registranta nun kiel %1, bonvolu atendi ĝis la registrado finos antaŭ vi reprovos sendi iun ajn. + Eraro: via konto ne estas registrita je retpoŝta kluzo. Registranta nun kiel %1, bonvolu atendi ĝis la registrado finos antaŭ vi reprovos sendi iun ajn. @@ -643,7 +687,7 @@ Estas grava, ke vi faru sekurkopion de tiu dosiero. Ĉu vi volas malfermi la dos Warning: You are currently not connected. Bitmessage will do the work necessary to send the message but it won't send until you connect. - Atentu: Vi ne estas nun konektita. Bitmesaĝo faros necesan laboron por sendi mesaĝon, tamen ĝi ne sendos ĝin antaŭ vi konektos. + Atentu: vi ne estas nun konektita. Bitmesaĝo faros necesan laboron por sendi mesaĝon, tamen ĝi ne sendos ĝin antaŭ vi konektos. @@ -661,12 +705,12 @@ Estas grava, ke vi faru sekurkopion de tiu dosiero. Ĉu vi volas malfermi la dos Dekstre alklaku kelka(j)n elemento(j)n en via adresaro kaj elektu 'Sendi mesaĝon al tiu adreso'. - + Fetched address from namecoin identity. Venigis adreson de namecoin-a identigo. - + New Message Nova mesaĝo @@ -681,47 +725,47 @@ Estas grava, ke vi faru sekurkopion de tiu dosiero. Ĉu vi volas malfermi la dos - + Address is valid. Adreso estas ĝusta. - + The address you entered was invalid. Ignoring it. La adreso kiun vi enmetis estas malĝusta. Ignoras ĝin. - + Error: You cannot add the same address to your address book twice. Try renaming the existing one if you want. Eraro: Vi ne povas duoble aldoni la saman adreson al via adresaro. Provu renomi la ekzistan se vi volas. - + Error: You cannot add the same address to your subscriptions twice. Perhaps rename the existing one if you want. Eraro: Vi ne povas aldoni duoble la saman adreson al viaj abonoj. Eble renomi la ekzistan se vi volas. - + Restart Restartigi - + You must restart Bitmessage for the port number change to take effect. - Vi devas restartigi Bitmesaĝon por ke la ŝanĝo de la numero de pordo (Port Number) efektivigu. + Vi devas restartigi Bitmesaĝon por ke ŝanĝo de numero de pordo efektivigu. - + Bitmessage will use your proxy from now on but you may want to manually restart Bitmessage now to close existing connections (if any). Bitmesaĝo uzos retperanton (proxy) ekde nun, sed eble vi volas permane restartigi Bitmesaĝon nun, por ke ĝi fermu eblajn ekzistajn konektojn. - + Number needed Numero bezonata - + Your maximum download and upload rate must be numbers. Ignoring what you typed. Maksimumaj elŝutrapido kaj alŝutrapido devas esti numeroj. Ignoras kion vi enmetis. @@ -733,7 +777,7 @@ Estas grava, ke vi faru sekurkopion de tiu dosiero. Ĉu vi volas malfermi la dos Note that the time limit you entered is less than the amount of time Bitmessage waits for the first resend attempt therefore your messages will never be resent. - Rigardu, ke la templimon vi enmetis estas pli malgrandan ol tempo dum kiu Bitmesaĝo atendas por resendi unuafoje, do viaj mesaĝoj estos senditaj neniam. + Rimarku, ke la templimon vi enmetis estas pli malgranda ol tempo dum kiu Bitmesaĝo atendas por resendi unuafoje, do viaj mesaĝoj estos senditaj neniam. @@ -746,44 +790,44 @@ Estas grava, ke vi faru sekurkopion de tiu dosiero. Ĉu vi volas malfermi la dos - + Passphrase mismatch Pasfrazoj malsamas - + The passphrase you entered twice doesn't match. Try again. Entajpitaj pasfrazoj malsamas. Provu denove. - + Choose a passphrase Elektu pasfrazon - + You really do need a passphrase. Vi ja vere bezonas pasfrazon. - + Address is gone Adreso foriris - + Bitmessage cannot find your address %1. Perhaps you removed it? Bitmesaĝo ne povas trovi vian adreson %1. Ĉu eble vi forviŝis ĝin? - + Address disabled Adreso malŝaltita - + Error: The address from which you are trying to send is disabled. You'll have to enable it on the 'Your Identities' tab before using it. - Eraro: La adreso kun kiu vi provas sendi estas malŝaltita. Vi devos ĝin ŝalti en la langeto 'Viaj identigoj' antaŭ uzi ĝin. + Eraro: la adreso kun kiu vi provas sendi estas malŝaltita. Vi devos ĝin ŝalti en la langeto 'Viaj identigoj' antaŭ uzi ĝin. @@ -791,42 +835,42 @@ Estas grava, ke vi faru sekurkopion de tiu dosiero. Ĉu vi volas malfermi la dos - + Entry added to the blacklist. Edit the label to your liking. - Aldonis elementon al la nigra listo. Redaktu la etikedon laŭvole. + Aldonis elementon al la listo de blokitoj. Redaktu la etikedon laŭvole. - + Error: You cannot add the same address to your blacklist twice. Try renaming the existing one if you want. - Eraro: Vi ne povas duoble aldoni la saman adreson al via nigra listo. Provu renomi la jaman se vi volas. + Eraro: vi ne povas duoble aldoni la saman adreson al la listo de blokitoj. Provu renomi la jaman se vi volas. - + Moved items to trash. Movis elementojn al rubujo. - + Undeleted item. Malforigis elementon. - + Save As... Konservi kiel… - + Write error. Skriberaro. - + No addresses selected. Neniu adreso elektita. - + If you delete the subscription, messages that you already received will become inaccessible. Maybe you can consider disabling the subscription instead. Disabled subscriptions will not receive new messages, but you can still view messages you already received. Are you sure you want to delete the subscription? @@ -835,7 +879,7 @@ Are you sure you want to delete the subscription? Ĉu vi certe volas forigi la abonon? - + If you delete the channel, messages that you already received will become inaccessible. Maybe you can consider disabling the channel instead. Disabled channels will not receive new messages, but you can still view messages you already received. Are you sure you want to delete the channel? @@ -844,32 +888,32 @@ Are you sure you want to delete the channel? Ĉu vi certe volas forigi la kanalon? - + Do you really want to remove this avatar? Ĉu vi certe volas forviŝi tiun ĉi avataron? - + You have already set an avatar for this address. Do you really want to overwrite it? Vi jam agordis avataron por tiu ĉi adreso. Ĉu vi vere volas superskribi ĝin? - + Start-on-login not yet supported on your OS. Starto-dum-ensaluto ne estas ankoraŭ ebla en via operaciumo. - + Minimize-to-tray not yet supported on your OS. Plejetigo al taskopleto ne estas ankoraŭ ebla en via operaciumo. - + Tray notifications not yet supported on your OS. Taskopletaj sciigoj ne estas ankoraŭ eblaj en via operaciumo. - + Testing... Testado… @@ -879,37 +923,37 @@ Are you sure you want to delete the channel? - + The address should start with ''BM-'' La adreso komencu kun “BM-” - + The address is not typed or copied correctly (the checksum failed). La adreso ne estis ĝuste tajpita aŭ kopiita (kontrolsumo malsukcesis). - + The version number of this address is higher than this software can support. Please upgrade Bitmessage. La numero de adresversio estas pli alta ol tiu, kiun la programo poveblas subteni. Bonvolu ĝisdatigi Bitmesaĝon. - + The address contains invalid characters. La adreso enhavas malpermesitajn simbolojn. - + Some data encoded in the address is too short. Iuj datumoj koditaj en la adreso estas tro mallongaj. - + Some data encoded in the address is too long. Iuj datumoj koditaj en la adreso estas tro longaj. - + Some data encoded in the address is malformed. Iuj datumoj koditaj en la adreso estas misformitaj. @@ -919,12 +963,12 @@ Are you sure you want to delete the channel? - + Address is an old type. We cannot display its past broadcasts. Malnova tipo de adreso. Ne povas montri ĝiajn antaŭajn elsendojn. - + There are no recent broadcasts from this address to display. Neniaj lastatempaj elsendoj de tiu ĉi adreso por montri. @@ -1119,22 +1163,22 @@ Are you sure you want to delete the channel? Anigi / krei kanalon - + All accounts Ĉiuj kontoj - + Zoom level %1% Pligrandigo: %1 - + Error: You cannot add the same address to your list twice. Perhaps rename the existing one if you want. Eraro: Vi ne povas aldoni duoble la saman adreson al via listo. Eble renomi la jaman se vi volas. - + Add new entry Aldoni novan elementon @@ -1149,37 +1193,37 @@ Are you sure you want to delete the channel? La nova versio de PyBitmessage estas disponebla: %1. Elŝutu ĝin de https://github.com/Bitmessage/PyBitmessage/releases/latest - + Waiting for PoW to finish... %1% Atendado ĝis laborpruvo finiĝos… %1% - + Shutting down Pybitmessage... %1% Fermado de PyBitmessage… %1% - + Waiting for objects to be sent... %1% Atendado ĝis objektoj estos senditaj… %1% - + Saving settings... %1% Konservado de agordoj… %1% - + Shutting down core... %1% Fermado de kerno… %1% - + Stopping notifications... %1% Haltigado de sciigoj… %1% - + Shutdown imminent... %1% Fermado tuj… %1% @@ -1189,42 +1233,42 @@ Are you sure you want to delete the channel? %n horo%n horoj - + %n day(s) %n tago%n tagoj - + Shutting down PyBitmessage... %1% Fermado de PyBitmessage… %1% - + Sent Senditaj - + Generating one new address Kreado de unu nova adreso - + Done generating address. Doing work necessary to broadcast it... Adreso kreita. Kalkulado de laborpruvo, kiu endas por elsendi ĝin… - + Generating %1 new addresses. Kreado de %1 novaj adresoj. - + %1 is already in 'Your Identities'. Not adding it again. %1 jam estas en ‘Viaj Identigoj’. Ĝi ne estos aldonita ree. - + Done generating address Ĉiuj adresoj estas kreitaj @@ -1234,111 +1278,111 @@ Are you sure you want to delete the channel? - + Disk full Disko plenplena - + Alert: Your disk or data storage volume is full. Bitmessage will now exit. Atentu: Via disko aŭ subdisko estas plenplena. Bitmesaĝo fermiĝos. - + Error! Could not find sender address (your address) in the keys.dat file. Eraro! Ne povas trovi adreson de sendanto (vian adreson) en la dosiero keys.dat. - + Doing work necessary to send broadcast... Kalkulado de laborpruvo, kiu endas por sendi elsendon… - + Broadcast sent on %1 Elsendo sendita je %1 - + Encryption key was requested earlier. Peto pri ĉifroŝlosilo jam sendita. - + Sending a request for the recipient's encryption key. Sendado de peto pri ĉifroŝlosilo de ricevonto. - + Looking up the receiver's public key Serĉado de publika ĉifroŝlosilo de ricevonto - + Problem: Destination is a mobile device who requests that the destination be included in the message but this is disallowed in your settings. %1 Eraro: celadreso estas portebla aparato kiu necesas, ke la celadreso estu enhavita en la mesaĝo, sed tio estas malpermesita ne viaj agordoj. %1 - + Doing work necessary to send message. There is no required difficulty for version 2 addresses like this. Kalkulado de laborpruvo, kiu endas por sendi mesaĝon. Malfacilaĵo ne estas bezonata por adresoj versioj 2, kiel tiu ĉi adreso. - + Doing work necessary to send message. Receiver's required difficulty: %1 and %2 Kalkulado de laborpruvo, kiu endas por sendi mesaĝon. Ricevonto postulas malfacilaĵon: %1 kaj %2 - + Problem: The work demanded by the recipient (%1 and %2) is more difficult than you are willing to do. %3 Eraro: la demandita laboro de la ricevonto (%1 kaj %2) estas pli malfacila ol vi pretas fari. %3 - + Problem: You are trying to send a message to yourself or a chan but your encryption key could not be found in the keys.dat file. Could not encrypt message. %1 Eraro: Vi provis sendi mesaĝon al vi mem aŭ al kanalo, tamen via ĉifroŝlosilo ne estas trovebla en la dosiero keys.dat. Mesaĝo ne povis esti ĉifrita. %1 - + Doing work necessary to send message. Kalkulado de laborpruvo, kiu endas por sendi mesaĝon. - + Message sent. Waiting for acknowledgement. Sent on %1 Mesaĝo sendita. Atendado je konfirmo. Sendita je %1 - + Doing work necessary to request encryption key. Kalkulado de laborpruvo, kiu endas por peti pri ĉifroŝlosilo. - + Broadcasting the public key request. This program will auto-retry if they are offline. Elsendado de peto pri publika ĉifroŝlosilo. La programo reprovos se ili estas eksterrete. - + Sending public key request. Waiting for reply. Requested at %1 Sendado de peto pri publika ĉifroŝlosilo. Atendado je respondo. Petis je %1 - + UPnP port mapping established on port %1 UPnP pord-mapigo farita je pordo %1 - + UPnP port mapping removed UPnP pord-mapigo forigita - + Mark all messages as read Marki ĉiujn mesaĝojn kiel legitajn @@ -1348,102 +1392,87 @@ Ricevonto postulas malfacilaĵon: %1 kaj %2 Ĉu vi certe volas marki ĉiujn mesaĝojn kiel legitajn? - + Doing work necessary to send broadcast. Kalkulado de laborpruvo, kiu endas por sendi elsendon. - + Proof of work pending Laborpruvo haltigita - + %n object(s) pending proof of work Haltigis laborpruvon por %n objektoHaltigis laborpruvon por %n objektoj - + %n object(s) waiting to be distributed %n objekto atendas je sendato%n objektoj atendas je sendato - + Wait until these tasks finish? Ĉu atendi ĝis tiujn taskojn finos? - - Problem communicating with proxy: %1. Please check your network settings. - Eraro dum komunikado kun retperanto: %1. Bonvolu kontroli viajn retajn agordojn. - - - - SOCKS5 Authentication problem: %1. Please check your SOCKS5 settings. - Eraro dum SOCKS5 aŭtentigado: %1. Bonvolu kontroli viajn SOCKS5-agordojn. - - - - The time on your computer, %1, may be wrong. Please verify your settings. - La horloĝo de via komputilo, %1, eble eraras. Bonvolu kontroli viajn agordojn. - - - + The name %1 was not found. La nomo %1 ne trovita. - + The namecoin query failed (%1) La namecoin-peto fiaskis (%1) - - The namecoin query failed. - La namecoin-peto fiaskis. + + Unknown namecoin interface type: %1 + Nekonata tipo de namecoin-fasado: %1 - - The name %1 has no valid JSON data. - La nomo %1 ne havas ĝustajn JSON-datumojn. + + The namecoin query failed. + La namecoin-peto fiaskis. - + The name %1 has no associated Bitmessage address. La nomo %1 ne estas atribuita kun bitmesaĝa adreso. - + Success! Namecoind version %1 running. Sukceso! Namecoind versio %1 funkcias. - + Success! NMControll is up and running. Sukceso! NMControl funkcias ĝuste. - + Couldn't understand NMControl. Ne povis kompreni NMControl. - + The connection to namecoin failed. Malsukcesis konekti al namecoin. - + Your GPU(s) did not calculate correctly, disabling OpenCL. Please report to the developers. Via(j) vidprocesoro(j) ne kalkulis senerare, malaktiviganta OpenCL. Bonvolu raporti tion al programistoj. - + Set notification sound... Agordi sciigan sonon… - + Welcome to easy and secure Bitmessage * send messages to other people @@ -1457,24 +1486,24 @@ Bonvenon al facila kaj sekura Bitmesaĝo * babili kun aliaj uloj en mesaĝ-kanaloj - + not recommended for chans malkonsilinda por kanaloj - + Quiet Mode Silenta reĝimo - + Problems connecting? Try enabling UPnP in the Network Settings Ĉu problemo kun konektado? Provu aktivigi UPnP en retaj agordoj. You are trying to send an email instead of a bitmessage. This requires registering with a gateway. Attempt to register? - Vi provas sendi retmesaĝon anstataŭ bitmesaĝ-mesaĝon. Tio ĉi postulas registri ĉe retpoŝta kluzo. Ĉu provi registri? + Vi provas sendi retmesaĝon anstataŭ bitmesaĝ-mesaĝon. Tio ĉi postulas registriĝi ĉe retpoŝta kluzo. Ĉu provi registriĝi? @@ -1517,57 +1546,77 @@ Bonvenon al facila kaj sekura Bitmesaĝo Eraro: io malĝustas kun la adreso de ricevonto %1. - + Error: %1 Eraro: %1 - + From %1 De %1 - + + Disconnecting + Malkonektado + + + + Connecting + Konektado + + + + Bitmessage will now drop all connections. Are you sure? + Bitmesaĝo ĉesos ĉiujn konektojn. Ĉu pluigi? + + + + Bitmessage will now start connecting to network. Are you sure? + Bitmesaĝo komencos konekti al la reto. Ĉu pluigi? + + + Synchronisation pending Samtempigado haltigita - + Bitmessage hasn't synchronised with the network, %n object(s) to be downloaded. If you quit now, it may cause delivery delays. Wait until the synchronisation finishes? Bitmesaĝo ne estas samtempigita kun la reto, %n objekto elŝutendas. Se vi eliros nun, tio povas igi malfruiĝojn de liveradoj. Ĉu atendi ĝis la samtempigado finiĝos?Bitmesaĝo ne estas samtempigita kun la reto, %n objektoj elŝutendas. Se vi eliros nun, tio povas igi malfruiĝojn de liveradoj. Ĉu atendi ĝis la samtempigado finiĝos? - + Not connected Nekonektita - + Bitmessage isn't connected to the network. If you quit now, it may cause delivery delays. Wait until connected and the synchronisation finishes? Bitmesaĝo ne estas konektita al la reto. Se vi eliros nun, tio povas igi malfruiĝojn de liveradoj. Ĉu atendi ĝis ĝi konektos kaj la samtempigado finiĝos? - + Waiting for network connection... Atendado je retkonekto… - + Waiting for finishing synchronisation... Atendado ĝis samtempigado finiĝos… - + You have already set a notification sound for this address book entry. Do you really want to overwrite it? Vi jam agordis sciigan sonon por tiu ĉi adreso. Ĉu vi volas anstataŭigi ĝin? - + Error occurred: could not load message from disk. Eraro okazis: ne povis legi mesaĝon el la disko. - + Display the %n recent broadcast(s) from this address. Montri %n lastan elsendon de tiu ĉi adreso.Montri %n lastajn elsendojn de tiu ĉi adreso. @@ -1587,22 +1636,22 @@ Bonvenon al facila kaj sekura Bitmesaĝo Forviŝi - + inbox - Ricevujo + ricevujo - + new novaj - + sent senditaj - + trash rubujo @@ -1610,22 +1659,22 @@ Bonvenon al facila kaj sekura Bitmesaĝo MessageView - + Follow external link Sekvi la eksteran ligilon - + The link "%1" will open in a browser. It may be a security risk, it could de-anonymise you or download malicious data. Are you sure? La ligilo "%1" estos malfermita per foliumilo. Tio povas esti malsekura, ĝi povos malanonimigi vin aŭ elŝuti malicajn datumojn. Ĉu vi certas? - + HTML detected, click here to display HTML detektita, alklaku ĉi tie por montri - + Click here to disable HTML Alklaku ĉi tie por malaktivigi HTML @@ -1633,14 +1682,14 @@ Bonvenon al facila kaj sekura Bitmesaĝo MsgDecode - + The message has an unknown encoding. Perhaps you should upgrade Bitmessage. La mesaĝo enhavas nekonatan kodoprezenton. Eble vi devas ĝisdatigi Bitmesaĝon. - + Unknown encoding Nekonata kodoprezento @@ -1796,7 +1845,7 @@ La “hazardnombra” adreso estas antaŭagordita, sed antaŭkalkuleblaj adresoj Nomo de kvazaŭ-dissendlisto: - + This is a chan address. You cannot use it as a pseudo-mailing list. Tio ĉi estas kanaladreso. Vi ne povas ĝin uzi kiel kvazaŭ-dissendolisto. @@ -1844,12 +1893,12 @@ La “hazardnombra” adreso estas antaŭagordita, sed antaŭkalkuleblaj adresoj Use a Blacklist (Allow all incoming messages except those on the Blacklist) - Uzi nigran liston (permesas ĉiujn alvenajn mesaĝojn escepte tiujn en la nigra listo) + Uzi liston de blokataj (permesi ĉiujn alvenajn mesaĝojn escepte tiujn en la listo) Use a Whitelist (Block all incoming messages except those on the Whitelist) - Uzi blankan liston (blokas ĉiujn alvenajn mesaĝojn escepte tiujn en la blanka listo) + Uzi liston de permesataj (bloki ĉiujn alvenajn mesaĝojn escepte tiujn en la listo) @@ -1867,14 +1916,14 @@ La “hazardnombra” adreso estas antaŭagordita, sed antaŭkalkuleblaj adresoj Adreso - + Blacklist - Nigra listo + Blokataj kontaktoj - + Whitelist - Blanka listo + Permesataj kontaktoj @@ -1951,7 +2000,7 @@ La “hazardnombra” adreso estas antaŭagordita, sed antaŭkalkuleblaj adresoj Vi havas konektojn al aliaj samtavolanoj kaj via fajroŝirmilo estas ĝuste agordita. - + You are using TCP port %1. (This can be changed in the settings). Vi uzas TCP-pordon %1 (tio ĉi estas ŝanĝebla en la agordoj). @@ -2004,27 +2053,27 @@ La “hazardnombra” adreso estas antaŭagordita, sed antaŭkalkuleblaj adresoj - + Since startup on %1 Ekde lanĉo de la programo je %1 - + Down: %1/s Total: %2 Elŝuto: %1/s Sume: %2 - + Up: %1/s Total: %2 Alŝuto: %1/s Sume: %2 - + Total Connections: %1 Ĉiuj konektoj: %1 - + Inventory lookups per second: %1 Petoj pri inventaro en sekundo: %1 @@ -2044,27 +2093,27 @@ La “hazardnombra” adreso estas antaŭagordita, sed antaŭkalkuleblaj adresoj Reto - + byte(s) bitokobitokoj - + Object(s) to be synced: %n Objekto por samtempigi: %nObjektoj por samtempigi: %n - + Processed %n person-to-person message(s). Pritraktis %n inter-personan mesaĝon.Pritraktis %n inter-personajn mesaĝojn. - + Processed %n broadcast message(s). Pritraktis %n elsendon.Pritraktis %n elsendojn. - + Processed %n public key(s). Pritraktis %n publikan ŝlosilon.Pritraktis %n publikajn ŝlosilojn. @@ -2190,17 +2239,17 @@ La “hazardnombra” adreso estas antaŭagordita, sed antaŭkalkuleblaj adresoj newchandialog - + Successfully created / joined chan %1 Sukcese kreis / anigis al la kanalo %1 - + Chan creation / joining failed Kreado / aniĝado al kanalo malsukcesis - + Chan creation / joining cancelled Kreado / aniĝado al kanalo nuligita @@ -2208,29 +2257,21 @@ La “hazardnombra” adreso estas antaŭagordita, sed antaŭkalkuleblaj adresoj proofofwork - + C PoW module built successfully. C PoW modulo konstruita sukcese. - + Failed to build C PoW module. Please build it manually. Malsukcesis konstrui C PoW modulon. Bonvolu konstrui ĝin permane. - + C PoW module unavailable. Please build it. C PoW modulo nedisponebla. Bonvolu konstrui ĝin. - - qrcodeDialog - - - QR-code - QR-kodo - - regenerateAddressesDialog @@ -2287,218 +2328,218 @@ La “hazardnombra” adreso estas antaŭagordita, sed antaŭkalkuleblaj adresoj settingsDialog - + Settings Agordoj - + Start Bitmessage on user login Startigi Bitmesaĝon dum ensaluto de uzanto - + Tray Taskopleto - + Start Bitmessage in the tray (don't show main window) - Startigi Bitmesaĝon en la taskopleto (tray) ne montrante tiun fenestron + Startigi Bitmesaĝon en la taskopleto ne montrante tiun ĉi fenestron - + Minimize to tray Plejetigi al taskopleto - + Close to tray Fermi al taskopleto - + Show notification when message received Montri sciigon kiam mesaĝo alvenas - + Run in Portable Mode Ekzekucii en Portebla Reĝimo - + In Portable Mode, messages and config files are stored in the same directory as the program rather than the normal application-data folder. This makes it convenient to run Bitmessage from a USB thumb drive. - En Portebla Reĝimo, mesaĝoj kaj agordoj estas enmemorigitaj en la sama dosierujo kiel la programo mem anstataŭ en la dosierujo por datumoj de aplikaĵoj. Tio igas ĝin komforta ekzekucii Bitmesaĝon el USB poŝmemorilo. + En la Portebla Reĝimo, mesaĝoj kaj agordoj estas enmemorigitaj en la sama dosierujo kiel la programo mem anstataŭ en la dosierujo por datumoj de aplikaĵoj. Tio igas ĝin komforta por ekzekucii Bitmesaĝon el USB poŝmemorilo. - + Willingly include unencrypted destination address when sending to a mobile device Volonte inkluzivi malĉifritan cel-adreson dum sendado al portebla aparato. - + Use Identicons Uzi ID-avatarojn - + Reply below Quote Respondi sub citaĵo - + Interface Language Fasada lingvo - + System Settings system Sistemaj agordoj - + User Interface Fasado - + Listening port - Aŭskultanta pordo (port) + Aŭskultanta pordo - + Listen for connections on port: Aŭskulti pri konektoj ĉe pordo: - + UPnP: UPnP: - + Bandwidth limit Rettrafika limo - + Maximum download rate (kB/s): [0: unlimited] Maksimuma rapido de elŝuto (kB/s): [0: senlima] - + Maximum upload rate (kB/s): [0: unlimited] Maksimuma rapido de alŝuto (kB/s): [0: senlima] - + Proxy server / Tor - Retperanta (proxy) servilo / Tor + Retperanta servilo / Tor - + Type: Speco: - + Server hostname: - Servilo gastiga nomo (hostname): + Servil-nomo: - + Port: - Pordo (port): + Pordo: - + Authentication Aŭtentigo - + Username: Uzantnomo: - + Pass: Pasvorto: - + Listen for incoming connections when using proxy Aŭskulti pri alvenaj konektoj kiam dum uzado de retperanto - + none neniu - + SOCKS4a SOCKS4a - + SOCKS5 SOCKS5 - + Network Settings Agordoj de reto - + Total difficulty: Tuta malfacilaĵo: - + The 'Total difficulty' affects the absolute amount of work the sender must complete. Doubling this value doubles the amount of work. La 'Tuta malfacilaĵo' efikas sur la tuta kvalito da laboro, kiun la sendonto devos fari. Duobligo de tiu valoro, duobligas la kvanton de laboro. - + Small message difficulty: Et-mesaĝa malfacilaĵo: - + When 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. - Kiam iu ajn sendas al vi mesaĝon, lia komputilo devas unue fari iom da laboro. La malfacilaĵo de tiu laboro implicite estas 1. Vi povas pligrandigi tiun valoron por novaj adresoj, kiujn vi generos per ŝanĝo de ĉi-tiaj valoroj. Ĉiuj novaj adresoj kreotaj de vi bezonos por ke sendontoj akceptu pli altan malfacilaĵon. Estas unu escepto: se vi aldonos kolegon al vi adresaro, Bitmesaĝo aŭtomate sciigos lin kiam vi sendos mesaĝon, ke li bezonos fari nur minimuman kvaliton da laboro: malfacilaĵo 1. + Kiam iu ajn sendas al vi mesaĝon, lia komputilo devas unue fari iom da laboro. La malfacilaĵo de tiu laboro implicite estas 1. Vi povas pligrandigi tiun valoron por novaj adresoj, kiujn vi generos per ŝanĝo de ĉi-tiaj valoroj. Ĉiuj novaj adresoj kreotaj de vi bezonos por ke sendontoj akceptu pli altan malfacilaĵon. Estas unu escepto: se vi aldonos amikon al via adresaro, Bitmesaĝo aŭtomate sciigos lin kiam vi sendos mesaĝon, ke li bezonos fari nur minimuman kvaliton da laboro: malfacilaĵo 1. - + 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. La 'Et-mesaĝa malfacilaĵo' ĉefe efikas malfacilaĵon por sendi malgrandajn mesaĝojn. Duobligo de tiu valoro, preskaŭ duobligas malfacilaĵon por sendi malgrandajn mesaĝojn, sed preskaŭ ne efikas grandajn mesaĝojn. - + Demanded difficulty Postulata malfacilaĵo - + Here 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. Tie ĉi vi povas agordi maksimuman kvanton da laboro kiun vi faru por sendi mesaĝon al alian persono. Se vi agordos ilin al 0, ĉiuj valoroj estos akceptitaj. - + Maximum acceptable total difficulty: Maksimuma akceptata tuta malfacilaĵo: - + Maximum acceptable small message difficulty: Maksimuma akceptata malfacilaĵo por et-mesaĝoj: - + Max acceptable difficulty Maksimuma akcepta malfacilaĵo @@ -2508,87 +2549,87 @@ La “hazardnombra” adreso estas antaŭagordita, sed antaŭkalkuleblaj adresoj - + <html><head/><body><p>Bitmessage 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 <span style=" font-style:italic;">test. </span></p><p>(Getting your own Bitmessage address into Namecoin is still rather difficult).</p><p>Bitmessage can use either namecoind directly or a running nmcontrol instance.</p></body></html> - <html><head/><body><p>Bitmesaĝo povas apliki alian Bitmono-bazitan programon - Namecoin - por fari adresojn hom-legeblajn. Ekzemple anstataŭ diri al via amiko longan Bitmesaĝan adreson, vi povas simple peti lin pri sendi mesaĝon al <span style=" font-style:italic;">id/kashnomo. </span></p><p>(Kreado de sia propra Bitmesaĝa adreso en Namecoin-on estas ankoraŭ ete malfacila).</p><p>Bitmesaĝo eblas uzi aŭ na namecoind rekte aŭ jaman aktivan aperon de nmcontrol.</p></body></html> + <html><head/><body><p>Bitmesaĝo povas apliki alian Bitmono-bazitan programon - Namecoin - por fari adresojn hom-legeblajn. Ekzemple anstataŭ diri al via amiko longan Bitmesaĝan adreson, vi povas simple peti lin pri sendi mesaĝon al <span style=" font-style:italic;">id/kashnomo. </span></p><p>(Kreado de sia propra Bitmesaĝa adreso en Namecoin estas ankoraŭ ete malfacila).</p><p>Bitmesaĝo eblas uzi aŭ na namecoind rekte aŭ jaman aktivan aperon de nmcontrol.</p></body></html> - + Host: Gastiga servilo: - + Password: Pasvorto: - + Test Testi - + Connect to: Konekti al: - + Namecoind Namecoind - + NMControl NMControl - + Namecoin integration Integrigo kun Namecoin - + <html><head/><body><p>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.</p><p>Leave these input fields blank for the default behavior. </p></body></html> <html><head/><body><p>Implicite se vi sendas mesaĝon al iu kaj li estos eksterrete por iomete da tempo, Bitmesaĝo provos resendi mesaĝon iam poste, kaj iam pli poste. La programo pluigos resendi mesaĝon ĝis sendonto konfirmos liveron. Tie ĉi vi povas ŝanĝi kiam Bitmesaĝo devos rezigni je sendado.</p><p>Lasu tiujn kampojn malplenaj por antaŭagordita sinteno.</p></body></html> - + Give up after Rezigni post - + and kaj - + days tagoj - + months. monatoj. - + Resends Expire Resenda fortempiĝo - + Hide connection notifications Ne montri sciigojn pri konekto - + Maximum outbound connections: [0: none] - Maksimumo de eligaj konektoj: [0: senlima] + Maksimume da eligaj konektoj: [0: senlima] - + Hardware GPU acceleration (OpenCL): Aparatara GPU-a plirapidigo (OpenCL): diff --git a/src/translations/bitmessage_fr.qm b/src/translations/bitmessage_fr.qm index e544a1c38c..8cb08a3ad7 100644 Binary files a/src/translations/bitmessage_fr.qm and b/src/translations/bitmessage_fr.qm differ diff --git a/src/translations/bitmessage_fr.ts b/src/translations/bitmessage_fr.ts index becfcbf8d6..149fd1efc4 100644 --- a/src/translations/bitmessage_fr.ts +++ b/src/translations/bitmessage_fr.ts @@ -112,7 +112,7 @@ Please type the desired email address (including @mailchuck.com) below: Mailchuck - + # You can use this to configure your email gateway account # Uncomment the setting you want to use # Here are the options: @@ -153,101 +153,102 @@ Please type the desired email address (including @mailchuck.com) below: # the money directly. To turn it off again, set "feeamount" to 0. Requires # subscription. - # Vous pouvez utiliser ceci pour configurer votre compte de passerelle de -# messagerie. -# Décommentez les paramètres que vous souhaitez utiliser. -# Les options se trouvent ci-dessous : -# + + + + + # You can use this to configure your email gateway account +# Uncomment the setting you want to use +# Here are the options: +# # pgp: server -# La passerelle de messagerie va créer et conserver pour vous les clefs PGP, -# et va signer, vérifier, chiffrer et déchiffrer en votre nom. Choisissez cela si -# vous voulez utilisez PGP mais que vous êtes paresseux. Exige une inscription. +# The email gateway will create and maintain PGP keys for you and sign, verify, +# encrypt and decrypt on your behalf. When you want to use PGP but are lazy, +# use this. Requires subscription. # # pgp: local -# La passerelle de messagerie ne va pas exécuter les commandes PGP en -# votre nom. Vous pouvez soit ne pas utiliser PGP du tout, soit l’utiliser -# localement. +# The email gateway will not conduct PGP operations on your behalf. You can +# either not use PGP at all, or use it locally. # # attachments: yes -# Les pièces-jointes reçues dans le courriel seront téléversées sur MEGA.nz, -# d’où vous pourrez les télécharger en cliquant sur le lien. Exige une -# inscription. +# Incoming attachments in the email will be uploaded to MEGA.nz, and you can +# download them from there by following the link. Requires a subscription. # # attachments: no -# Les pièces jointes seront ignorées. -# +# Attachments will be ignored. +# # archive: yes -# Les courriels que vous recevrez seront archivés sur le serveur. Utilisez -# ceci si vous avez besoin d’aide pour des problèmes de déboguage ou -# si vous avez besoin d’une preuve par un tiers des courriels. Cela signifie -# cependant que le fournisseur du service pourra lire vos courriels même -# après leur réception. +# Your incoming emails will be archived on the server. Use this if you need +# help with debugging problems or you need a third party proof of emails. This +# however means that the operator of the service will be able to read your +# emails even after they have been delivered to you. # # archive: no -# Les courriels reçus seront supprimés du serveur dès qu’ils vous auront été -# transmis. +# Incoming emails will be deleted from the server as soon as they are relayed +# to you. # -# masterpubkey_btc: clef xpub BIP44 ou graine publique electrum v1 -# offset_btc: entier (par défaut à 0) -# feeamount: nombre avec jusqu’à 8 décimales -# feecurrency: BTC, XBT, USD, EUR ou GBP -# Utilisez ceci si vous voulez faire payer ceux qui vous envoient des courriels. -# Si ceci est activé et qu’une personne inconnue vous envoie un courriel, il -# devra payer le tarif indiqué. Comme ce mécanisme emploie des clefs -# publiques déterministes, vous recevrez l’argent directement. Pour désactiver -# à nouveau ceci, réglez "feeamount" à 0. Exige une inscription. - +# masterpubkey_btc: BIP44 xpub key or electrum v1 public seed +# offset_btc: integer (defaults to 0) +# feeamount: number with up to 8 decimal places +# feecurrency: BTC, XBT, USD, EUR or GBP +# Use these if you want to charge people who send you emails. If this is on and +# an unknown person sends you an email, they will be requested to pay the fee +# specified. As this scheme uses deterministic public keys, you will receive +# the money directly. To turn it off again, set "feeamount" to 0. Requires +# subscription. + + MainWindow - + Reply to sender Répondre à l’expéditeur - + Reply to channel Répondre au canal - + Add sender to your Address Book Ajouter l’expéditeur au carnet d’adresses - + Add sender to your Blacklist Ajouter l’expéditeur à votre liste noire - + Move to Trash Envoyer à la Corbeille - + Undelete Restaurer - + View HTML code as formatted text Voir le code HTML comme du texte formaté - + Save message as... Enregistrer le message sous… - + Mark Unread Marquer comme non-lu - + New Nouvelle @@ -272,12 +273,12 @@ Please type the desired email address (including @mailchuck.com) below: Copier l’adresse dans le presse-papier - + Special address behavior... Comportement spécial de l’adresse… - + Email gateway Passerelle de courriel @@ -287,37 +288,37 @@ Please type the desired email address (including @mailchuck.com) below: Effacer - + Send message to this address Envoyer un message à cette adresse - + Subscribe to this address S’abonner à cette adresse - + Add New Address Ajouter une nouvelle adresse - + Copy destination address to clipboard Copier l’adresse de destination dans le presse-papier - + Force send Forcer l’envoi - + One of your addresses, %1, is an old version 1 address. Version 1 addresses are no longer supported. May we delete it now? Une de vos adresses, %1, est une vieille adresse de la version 1. Les adresses de la version 1 ne sont plus supportées. Nous pourrions la supprimer maintenant? - + Waiting for their encryption key. Will request it again soon. En attente de la clé de chiffrement. Une nouvelle requête sera bientôt lancée. @@ -327,17 +328,17 @@ Please type the desired email address (including @mailchuck.com) below: - + Queued. En attente. - + Message sent. Waiting for acknowledgement. Sent at %1 Message envoyé. En attente de l’accusé de réception. Envoyé %1 - + Message sent. Sent at %1 Message envoyé. Envoyé %1 @@ -347,47 +348,47 @@ Please type the desired email address (including @mailchuck.com) below: - + Acknowledgement of the message received %1 Accusé de réception reçu %1 - + Broadcast queued. Message de diffusion en attente. - + Broadcast on %1 Message de diffusion du %1 - + Problem: The work demanded by the recipient is more difficult than you are willing to do. %1 Problème : Le travail demandé par le destinataire est plus difficile que ce que vous avez paramétré. %1 - + Problem: The recipient's encryption key is no good. Could not encrypt message. %1 Problème : la clé de chiffrement du destinataire n’est pas bonne. Il n’a pas été possible de chiffrer le message. %1 - + Forced difficulty override. Send should start soon. Neutralisation forcée de la difficulté. L’envoi devrait bientôt commencer. - + Unknown status: %1 %2 Statut inconnu : %1 %2 - + Not Connected Déconnecté - + Show Bitmessage Afficher Bitmessage @@ -397,12 +398,12 @@ Please type the desired email address (including @mailchuck.com) below: Envoyer - + Subscribe S’abonner - + Channel Canal @@ -412,12 +413,12 @@ Please type the desired email address (including @mailchuck.com) below: Quitter - + You may manage your keys by editing the keys.dat file stored in the same directory as this program. It is important that you back up this file. Vous pouvez éditer vos clés en éditant le fichier keys.dat stocké dans le même répertoire que ce programme. Il est important de faire des sauvegardes de ce fichier. - + You may manage your keys by editing the keys.dat file stored in %1 It is important that you back up this file. @@ -425,54 +426,54 @@ It is important that you back up this file. Il est important de faire des sauvegardes de ce fichier. - + Open keys.dat? Ouvrir keys.dat ? - + You may manage your keys by editing the keys.dat file stored in the same directory as this program. It is important that you back up this file. Would you like to open the file now? (Be sure to close Bitmessage before making any changes.) Vous pouvez éditer vos clés en éditant le fichier keys.dat stocké dans le même répertoire que ce programme. Il est important de faire des sauvegardes de ce fichier. Souhaitez-vous l’ouvrir maintenant ? (Assurez-vous de fermer Bitmessage avant d’effectuer des changements.) - + You may manage your keys by editing the keys.dat file stored in %1 It is important that you back up this file. Would you like to open the file now? (Be sure to close Bitmessage before making any changes.) Vous pouvez éditer vos clés en éditant le fichier keys.dat stocké dans le répertoire %1. Il est important de faire des sauvegardes de ce fichier. Souhaitez-vous l’ouvrir maintenant? (Assurez-vous de fermer Bitmessage avant d’effectuer des changements.) - + Delete trash? Supprimer la corbeille ? - + Are you sure you want to delete all trashed messages? Êtes-vous sûr de vouloir supprimer tous les messages dans la corbeille ? - + bad passphrase Mauvaise phrase secrète - + You must type your passphrase. If you don't have one then this is not the form for you. Vous devez taper votre phrase secrète. Si vous n’en avez pas, ce formulaire n’est pas pour vous. - + Bad address version number Mauvais numéro de version d’adresse - + Your address version number must be a number: either 3 or 4. Votre numéro de version d’adresse doit être un nombre : soit 3 soit 4. - + Your address version number must be either 3 or 4. Votre numéro de version d’adresse doit être soit 3 soit 4. @@ -542,22 +543,22 @@ It is important that you back up this file. Would you like to open the file now? - + Connection lost Connexion perdue - + Connected Connecté - + Message trashed Message envoyé à la corbeille - + The TTL, or Time-To-Live is the length of time that the network will hold the message. The recipient must get it during this time. If your Bitmessage client does not hear an acknowledgement, it will resend the message automatically. The longer the Time-To-Live, the @@ -566,17 +567,17 @@ It is important that you back up this file. Would you like to open the file now? Le destinataire doit l’obtenir avant ce temps. Si votre client Bitmessage ne reçoit pas de confirmation de réception, il va le ré-envoyer automatiquement. Plus le Time-To-Live est long, plus grand est le travail que votre ordinateur doit effectuer pour envoyer le message. Un Time-To-Live de quatre ou cinq jours est souvent approprié. - + Message too long Message trop long - + The message that you are trying to send is too long by %1 bytes. (The maximum is 261644 bytes). Please cut it down before sending. Le message que vous essayez d’envoyer est trop long de %1 octets (le maximum est 261644 octets). Veuillez le réduire avant de l’envoyer. - + Error: Your account wasn't registered at an email gateway. Sending registration now as %1, please wait for the registration to be processed before retrying sending. Erreur : votre compte n’a pas été inscrit à une passerelle de courrier électronique. Envoi de l’inscription maintenant en tant que %1, veuillez patienter tandis que l’inscription est en cours de traitement, avant de retenter l’envoi. @@ -621,57 +622,57 @@ Le destinataire doit l’obtenir avant ce temps. Si votre client Bitmessage ne r - + Error: You must specify a From address. If you don't have one, go to the 'Your Identities' tab. - Erreur : Vous devez spécifier une adresse d’expéditeur. Si vous n’en avez pas, rendez-vous dans l’onglet 'Vos identités'. + Erreur : Vous devez spécifier une adresse d’expéditeur. Si vous n’en avez pas, rendez-vous dans l’onglet "Vos identités". - + Address version number Numéro de version de l’adresse - + Concerning the address %1, Bitmessage cannot understand address version numbers of %2. Perhaps upgrade Bitmessage to the latest version. Concernant l’adresse %1, Bitmessage ne peut pas comprendre les numéros de version de %2. Essayez de mettre à jour Bitmessage vers la dernière version. - + Stream number Numéro de flux - + Concerning the address %1, Bitmessage cannot handle stream numbers of %2. Perhaps upgrade Bitmessage to the latest version. Concernant l’adresse %1, Bitmessage ne peut pas supporter les nombres de flux de %2. Essayez de mettre à jour Bitmessage vers la dernière version. - + Warning: You are currently not connected. Bitmessage will do the work necessary to send the message but it won't send until you connect. Avertissement : Vous êtes actuellement déconnecté. Bitmessage fera le travail nécessaire pour envoyer le message mais il ne sera pas envoyé tant que vous ne vous connecterez pas. - + Message queued. Message mis en file d’attente. - + Your 'To' field is empty. - Votre champ 'Vers' est vide. + Votre champ "Vers" est vide. - + Right click one or more entries in your address book and select 'Send message to this address'. - Cliquez droit sur une ou plusieurs entrées dans votre carnet d’adresses et sélectionnez 'Envoyer un message à ces adresses'. + Cliquez droit sur une ou plusieurs entrées dans votre carnet d’adresses puis sélectionnez "Envoyer un message à ces adresses". - + Fetched address from namecoin identity. Récupération avec succès de l’adresse de l’identité Namecoin. - + New Message Nouveau message @@ -696,47 +697,47 @@ Le destinataire doit l’obtenir avant ce temps. Si votre client Bitmessage ne r L’adresse que vous avez entrée est invalide. Adresse ignorée. - + Error: You cannot add the same address to your address book twice. Try renaming the existing one if you want. Erreur : Vous ne pouvez pas ajouter une adresse déjà présente dans votre carnet d’adresses. Essayez de renommer l’adresse existante. - + Error: You cannot add the same address to your subscriptions twice. Perhaps rename the existing one if you want. Erreur : vous ne pouvez pas ajouter la même adresse deux fois à vos abonnements. Peut-être que vous pouvez renommer celle qui existe si vous le souhaitez. - + Restart Redémarrer - + You must restart Bitmessage for the port number change to take effect. Vous devez redémarrer Bitmessage pour que le changement de port prenne effet. - + Bitmessage will use your proxy from now on but you may want to manually restart Bitmessage now to close existing connections (if any). Bitmessage utilisera votre proxy dorénavant, mais vous pouvez redémarrer manuellement Bitmessage maintenant afin de fermer des connexions existantes (si il y en existe). - + Number needed Nombre requis - + Your maximum download and upload rate must be numbers. Ignoring what you typed. Vos taux maximum de téléchargement et de téléversement doivent être des nombres. Ce que vous avez tapé est ignoré. - + Will not resend ever Ne renverra jamais - + Note that the time limit you entered is less than the amount of time Bitmessage waits for the first resend attempt therefore your messages will never be resent. Notez que la limite de temps que vous avez entrée est plus courte que le temps d’attente respecté par Bitmessage avant le premier essai de renvoi, par conséquent votre message ne sera jamais renvoyé. @@ -771,24 +772,24 @@ Le destinataire doit l’obtenir avant ce temps. Si votre client Bitmessage ne r Vous devez vraiment utiliser une phrase secrète. - + Address is gone L’adresse a disparu - + Bitmessage cannot find your address %1. Perhaps you removed it? Bitmessage ne peut pas trouver votre adresse %1. Peut-être l’avez-vous supprimée? - + Address disabled Adresse désactivée - + Error: The address from which you are trying to send is disabled. You'll have to enable it on the 'Your Identities' tab before using it. - Erreur : L’adresse avec laquelle vous essayez de communiquer est désactivée. Vous devez d’abord l’activer dans l’onglet 'Vos identités' avant de l’utiliser. + Erreur : L’adresse avec laquelle vous essayez de communiquer est désactivée. Vous devez d’abord l’activer dans l’onglet "Vos identités" avant de l’utiliser. @@ -796,42 +797,42 @@ Le destinataire doit l’obtenir avant ce temps. Si votre client Bitmessage ne r - + Entry added to the blacklist. Edit the label to your liking. Entrée ajoutée à la liste noire. Éditez l’étiquette à votre convenance. - + Error: You cannot add the same address to your blacklist twice. Try renaming the existing one if you want. Erreur : vous ne pouvez pas ajouter la même adresse deux fois à votre liste noire. Essayez de renommer celle qui existe si vous le souhaitez. - + Moved items to trash. Messages déplacés dans la corbeille. - + Undeleted item. Articles restaurés. - + Save As... Enregistrer sous… - + Write error. Erreur d’écriture. - + No addresses selected. Aucune adresse sélectionnée. - + If you delete the subscription, messages that you already received will become inaccessible. Maybe you can consider disabling the subscription instead. Disabled subscriptions will not receive new messages, but you can still view messages you already received. Are you sure you want to delete the subscription? @@ -840,7 +841,7 @@ Are you sure you want to delete the subscription? Êtes-vous sur de vouloir supprimer cet abonnement ? - + If you delete the channel, messages that you already received will become inaccessible. Maybe you can consider disabling the channel instead. Disabled channels will not receive new messages, but you can still view messages you already received. Are you sure you want to delete the channel? @@ -849,32 +850,32 @@ Are you sure you want to delete the channel? Êtes-vous sûr de vouloir supprimer ce canal ? - + Do you really want to remove this avatar? Voulez-vous vraiment enlever cet avatar ? - + You have already set an avatar for this address. Do you really want to overwrite it? Vous avez déjà mis un avatar pour cette adresse. Voulez-vous vraiment l’écraser ? - + Start-on-login not yet supported on your OS. Le démarrage dès l’ouverture de session n’est pas encore supporté sur votre OS. - + Minimize-to-tray not yet supported on your OS. La minimisation en zone système n’est pas encore supportée sur votre OS. - + Tray notifications not yet supported on your OS. Les notifications en zone système ne sont pas encore supportées sur votre OS. - + Testing... Tester… @@ -1124,7 +1125,7 @@ Are you sure you want to delete the channel? Rejoindre / créer un canal - + All accounts Tous les comptes @@ -1149,42 +1150,42 @@ Are you sure you want to delete the channel? - + New version of PyBitmessage is available: %1. Download it from https://github.com/Bitmessage/PyBitmessage/releases/latest Une nouvelle version de PyBitmessage est disponible : %1. Veuillez la télécharger depuis https://github.com/Bitmessage/PyBitmessage/releases/latest - + Waiting for PoW to finish... %1% En attente de la fin de la PoW… %1% - + Shutting down Pybitmessage... %1% Pybitmessage en cours d’arrêt… %1% - + Waiting for objects to be sent... %1% En attente de l’envoi des objets… %1% - + Saving settings... %1% Enregistrement des paramètres… %1% - + Shutting down core... %1% Cœur en cours d’arrêt… %1% - + Stopping notifications... %1% Arrêt des notifications… %1% - + Shutdown imminent... %1% Arrêt imminent… %1% @@ -1194,42 +1195,42 @@ Are you sure you want to delete the channel? %n heure%n heures - + %n day(s) %n jour%n jours - + Shutting down PyBitmessage... %1% PyBitmessage en cours d’arrêt… %1% - + Sent Envoyé - + Generating one new address Production d’une nouvelle adresse - + Done generating address. Doing work necessary to broadcast it... La production de l’adresse a été effectuée. Travail en cours afin de l’émettre… - + Generating %1 new addresses. Production de %1 nouvelles adresses. - + %1 is already in 'Your Identities'. Not adding it again. %1 est déjà dans "Vos identités". Il ne sera pas ajouté de nouveau. - + Done generating address La production d’une adresse a été effectuée @@ -1239,96 +1240,96 @@ Are you sure you want to delete the channel? - + Disk full Disque plein - + Alert: Your disk or data storage volume is full. Bitmessage will now exit. Alerte : votre disque ou le volume de stockage de données est plein. Bitmessage va maintenant se fermer. - + Error! Could not find sender address (your address) in the keys.dat file. Erreur ! Il n’a pas été possible de trouver l’adresse d’expéditeur (votre adresse) dans le fichier keys.dat. - + Doing work necessary to send broadcast... Travail en cours afin d’envoyer le message de diffusion… - + Broadcast sent on %1 Message de diffusion envoyé %1 - + Encryption key was requested earlier. La clé de chiffrement a été demandée plus tôt. - + Sending a request for the recipient's encryption key. Envoi d’une demande de la clé de chiffrement du destinataire. - + Looking up the receiver's public key Recherche de la clé publique du récepteur - + Problem: Destination is a mobile device who requests that the destination be included in the message but this is disallowed in your settings. %1 Problème : la destination est un dispositif mobile qui nécessite que la destination soit incluse dans le message mais ceci n’est pas autorisé dans vos paramètres. %1 - + Doing work necessary to send message. There is no required difficulty for version 2 addresses like this. Travail en cours afin d’envoyer le message. Il n’y a pas de difficulté requise pour les adresses version 2 comme celle-ci. - + Doing work necessary to send message. Receiver's required difficulty: %1 and %2 Travail en cours afin d’envoyer le message. Difficulté requise du destinataire : %1 et %2 - + Problem: The work demanded by the recipient (%1 and %2) is more difficult than you are willing to do. %3 Problème : Le travail demandé par le destinataire (%1 and %2) est plus difficile que ce que vous avez paramétré. %3 - + Problem: You are trying to send a message to yourself or a chan but your encryption key could not be found in the keys.dat file. Could not encrypt message. %1 Problème : Vous essayez d’envoyer un message à un canal ou à vous-même mais votre clef de chiffrement n’a pas été trouvée dans le fichier keys.dat. Le message ne peut pas être chiffré. %1 - + Doing work necessary to send message. Travail en cours afin d’envoyer le message. - + Message sent. Waiting for acknowledgement. Sent on %1 Message envoyé. En attente de l’accusé de réception. Envoyé %1 - + Doing work necessary to request encryption key. Travail en cours afin d’obtenir la clé de chiffrement. - + Broadcasting the public key request. This program will auto-retry if they are offline. Diffusion de la demande de clef publique. Ce programme réessaiera automatiquement si ils sont déconnectés. - + Sending public key request. Waiting for reply. Requested at %1 Envoi d’une demande de clef publique. En attente d’une réponse. Demandée à %1 @@ -1343,97 +1344,82 @@ Difficulté requise du destinataire : %1 et %2 Transfert de port UPnP retiré - + Mark all messages as read Marquer tous les messages comme lus - + Are you sure you would like to mark all messages read? Êtes-vous sûr(e) de vouloir marquer tous les messages comme lus ? - + Doing work necessary to send broadcast. Travail en cours afin d’envoyer la diffusion. - + Proof of work pending En attente de preuve de fonctionnement - + %n object(s) pending proof of work %n objet en attente de preuve de fonctionnement%n objet(s) en attente de preuve de fonctionnement - + %n object(s) waiting to be distributed %n objet en attente d'être distribué%n objet(s) en attente d'être distribués - + Wait until these tasks finish? Attendre jusqu'à ce que ces tâches se terminent ? - - Problem communicating with proxy: %1. Please check your network settings. - Problème de communication avec le proxy : %1. Veuillez vérifier vos réglages réseau. - - - - SOCKS5 Authentication problem: %1. Please check your SOCKS5 settings. - Problème d’authentification SOCKS5 : %1. Veuillez vérifier vos réglages SOCKS5. - - - - The time on your computer, %1, may be wrong. Please verify your settings. - L'heure sur votre ordinateur, %1, pourrait être faussse. Veuillez vérifier vos paramètres. - - - + The name %1 was not found. Le nom %1 n'a pas été trouvé. - + The namecoin query failed (%1) La requête Namecoin a échouée (%1) - + The namecoin query failed. La requête Namecoin a échouée. - + The name %1 has no valid JSON data. Le nom %1 n'a aucune donnée JSON valide. - + The name %1 has no associated Bitmessage address. Le nom %1 n'a aucune adresse Bitmessage d'associée. - + Success! Namecoind version %1 running. Succès ! Namecoind version %1 en cours d'exécution. - + Success! NMControll is up and running. Succès ! NMControll est debout et en cours d'exécution. - + Couldn't understand NMControl. Ne pouvait pas comprendre NMControl. - + The connection to namecoin failed. La connexion à Namecoin a échouée. @@ -1443,12 +1429,12 @@ Difficulté requise du destinataire : %1 et %2 Votre GPU(s) n'a pas calculé correctement, mettant OpenCL hors service. Veuillez remonter ceci aux développeurs s'il vous plaît. - + Set notification sound... Mettre un son de notification ... - + Welcome to easy and secure Bitmessage * send messages to other people @@ -1460,122 +1446,142 @@ Bienvenue dans le facile et sécurisé Bitmessage * envoyer des messages à d'autres personnes * envoyer des messages par diffusion (broadcast) à la manière de Twitter ou * discuter dans des canaux (channels) avec d'autres personnes - + - + not recommended for chans pas recommandé pour les canaux - + Quiet Mode Mode tranquille - + Problems connecting? Try enabling UPnP in the Network Settings Des difficultés à se connecter ? Essayez de permettre UPnP dans les "Paramètres réseau" - + You are trying to send an email instead of a bitmessage. This requires registering with a gateway. Attempt to register? Vous essayez d'envoyer un courrier électronique au lieu d'un bitmessage. Ceci exige votre inscription à une passerelle. Essayer de vous inscrire ? - + Error: Bitmessage addresses start with BM- Please check the recipient address %1 Erreur : Les adresses Bitmessage commencent par BM- Veuillez vérifier l'adresse du destinataire %1 - + Error: The recipient address %1 is not typed or copied correctly. Please check it. Erreur : L’adresse du destinataire %1 n’est pas correctement tapée ou recopiée. Veuillez la vérifier. - + Error: The recipient address %1 contains invalid characters. Please check it. Erreur : L’adresse du destinataire %1 contient des caractères invalides. Veuillez la vérifier. - + Error: The version of the recipient address %1 is too high. Either you need to upgrade your Bitmessage software or your acquaintance is being clever. Erreur : la version de l’adresse destinataire %1 est trop élevée. Vous devez mettre à niveau votre logiciel Bitmessage ou alors celui de votre connaissance est plus intelligent. - + Error: Some data encoded in the recipient address %1 is too short. There might be something wrong with the software of your acquaintance. Erreur : quelques données codées dans l’adresse destinataire %1 sont trop courtes. Il pourrait y avoir un soucis avec le logiciel de votre connaissance. - + Error: Some data encoded in the recipient address %1 is too long. There might be something wrong with the software of your acquaintance. Erreur : quelques données codées dans l’adresse destinataire %1 sont trop longues. Il pourrait y avoir un soucis avec le logiciel de votre connaissance. - + Error: Some data encoded in the recipient address %1 is malformed. There might be something wrong with the software of your acquaintance. Erreur : quelques données codées dans l’adresse destinataire %1 sont mal formées. Il pourrait y avoir un soucis avec le logiciel de votre connaissance. - + Error: Something is wrong with the recipient address %1. Erreur : quelque chose ne va pas avec l'adresse de destinataire %1. - + Error: %1 Erreur : %1 - + From %1 De %1 - + + Disconnecting + Déconnexion + + + + Connecting + Connexion + + + + Bitmessage will now drop all connectins. Are you sure? + Bitmessage va maintenant laisser tomber toutes les connections. Êtes-vous sûr(e) ? + + + + Bitmessage will now start connecting to network. Are you sure? + + + + Synchronisation pending En attente de synchronisation - + Bitmessage hasn't synchronised with the network, %n object(s) to be downloaded. If you quit now, it may cause delivery delays. Wait until the synchronisation finishes? Bitmessage ne s'est pas synchronisé avec le réseau, %n objet(s) à télécharger. Si vous quittez maintenant, cela pourrait causer des retards de livraison. Attendre jusqu'à ce que la synchronisation aboutisse ?Bitmessage ne s'est pas synchronisé avec le réseau, %n objet(s) à télécharger. Si vous quittez maintenant, cela pourrait causer des retards de livraison. Attendre jusqu'à ce que la synchronisation aboutisse ? - + Not connected Non connecté - + Bitmessage isn't connected to the network. If you quit now, it may cause delivery delays. Wait until connected and the synchronisation finishes? Bitmessage n'est pas connecté au réseau. Si vous quittez maintenant, cela pourrait causer des retards de livraison. Attendre jusqu'à ce qu'il soit connecté et que la synchronisation se termine ? - + Waiting for network connection... En attente de connexion réseau... - + Waiting for finishing synchronisation... En attente d'achèvement de la synchronisation... - + You have already set a notification sound for this address book entry. Do you really want to overwrite it? Vous avez déjà mis un son de notification pour cette adresse. Voulez-vous vraiment l’écraser ? - + Error occurred: could not load message from disk. Une erreur a eu lieu : ne peut pas charger de message depuis le disque. Display the %n recent broadcast(s) from this address. - + Afficher le messages de diffusion le plus récent originaire de cette adresse.Afficher les %n messages de diffusion les plus récents originaires de cette adresse. @@ -1593,22 +1599,22 @@ Bienvenue dans le facile et sécurisé Bitmessage Vider - + inbox entrant - + new nouveau - + sent envoyé - + trash corbeille @@ -1639,14 +1645,14 @@ Bienvenue dans le facile et sécurisé Bitmessage MsgDecode - + The message has an unknown encoding. Perhaps you should upgrade Bitmessage. Le message est codé de façon inconnue. Peut-être que vous devriez mettre à niveau Bitmessage. - + Unknown encoding Encodage inconnu @@ -1662,7 +1668,8 @@ Peut-être que vous devriez mettre à niveau Bitmessage. Here you may generate as many addresses as you like. Indeed, creating and abandoning addresses is encouraged. 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: - Vous pouvez générer autant d’adresses que vous le souhaitez. En effet, nous vous encourageons à créer et à délaisser vos adresses. Vous pouvez générer des adresses en utilisant des nombres aléatoires ou en utilisant une phrase secrète. Si vous utilisez une phrase secrète, l’adresse sera une adresse "déterministe". L’option 'Nombre Aléatoire' est sélectionnée par défaut mais les adresses déterministes ont certains avantages et inconvénients : + Vous pouvez générer autant d’adresses que vous le souhaitez. En effet, nous vous encourageons à créer et à délaisser vos adresses. Vous pouvez générer des adresses en utilisant des nombres aléatoires ou en utilisant une phrase secrète. Si vous utilisez une phrase secrète, l’adresse sera une adresse "déterministe". +L’option "Nombre Aléatoire" est sélectionnée par défaut mais les adresses déterministes ont certains avantages et inconvénients : @@ -2174,7 +2181,7 @@ The 'Random Number' option is selected by default but deterministic ad Chan passphrase/name: - + Phrase passe ou nom : @@ -2228,14 +2235,6 @@ The 'Random Number' option is selected by default but deterministic ad Module PoW C non disponible. Veuillez le construire. - - qrcodeDialog - - - QR-code - QR-code - - regenerateAddressesDialog @@ -2292,218 +2291,218 @@ The 'Random Number' option is selected by default but deterministic ad settingsDialog - + Settings Paramètres - + Start Bitmessage on user login Démarrer Bitmessage à la connexion de l’utilisateur - + Tray Zone de notification - + Start Bitmessage in the tray (don't show main window) Démarrer Bitmessage dans la barre des tâches (ne pas montrer la fenêtre principale) - + Minimize to tray Minimiser dans la barre des tâches - + Close to tray Fermer vers la zone de notification - + Show notification when message received Montrer une notification lorsqu’un message est reçu - + Run in Portable Mode Lancer en Mode Portable - + In Portable Mode, messages and config files are stored in the same directory as the program rather than the normal application-data folder. This makes it convenient to run Bitmessage from a USB thumb drive. En Mode Portable, les messages et les fichiers de configuration sont stockés dans le même dossier que le programme plutôt que le dossier de l’application. Cela rend l’utilisation de Bitmessage plus facile depuis une clé USB. - + Willingly include unencrypted destination address when sending to a mobile device Inclure volontairement l’adresse de destination non chiffrée lors de l’envoi vers un dispositif mobile - + Use Identicons Utilise des Identicônes. - + Reply below Quote Réponse en dessous de la citation - + Interface Language Langue de l’interface - + System Settings system Paramètres système - + User Interface Interface utilisateur - + Listening port Port d’écoute - + Listen for connections on port: Écouter les connexions sur le port : - + UPnP: UPnP : - + Bandwidth limit Limite de bande passante - + Maximum download rate (kB/s): [0: unlimited] Taux de téléchargement maximal (kO/s) : [0 : illimité] - + Maximum upload rate (kB/s): [0: unlimited] Taux de téléversement maximal (kO/s) : [0 : illimité] - + Proxy server / Tor Serveur proxy / Tor - + Type: Type : - + Server hostname: Nom du serveur: - + Port: Port : - + Authentication Authentification - + Username: Utilisateur : - + Pass: Mot de passe : - + Listen for incoming connections when using proxy Écoute les connexions entrantes lors de l’utilisation du proxy - + none aucun - + SOCKS4a SOCKS4a - + SOCKS5 SOCKS5 - + Network Settings Paramètres réseau - + Total difficulty: Difficulté totale : - + The 'Total difficulty' affects the absolute amount of work the sender must complete. Doubling this value doubles the amount of work. - La 'difficulté totale' affecte le montant total de travail que l’envoyeur devra compléter. Doubler cette valeur double la charge de travail. + La "difficulté totale" affecte le montant total de travail que l’envoyeur devra accomplir. Doubler cette valeur double la charge de travail. - + Small message difficulty: Difficulté d’un message court : - + When 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. Lorsque quelqu’un vous envoie un message, son ordinateur doit d’abord effectuer un travail. La difficulté de ce travail, par défaut, est de 1. Vous pouvez augmenter cette valeur pour les adresses que vous créez en changeant la valeur ici. Chaque nouvelle adresse que vous créez requerra à l’envoyeur de faire face à une difficulté supérieure. Il existe une exception : si vous ajoutez un ami ou une connaissance à votre carnet d’adresses, Bitmessage les notifiera automatiquement lors du prochain message que vous leur envoyez qu’ils ne doivent compléter que la charge de travail minimale : difficulté 1. - + 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. - La 'difficulté d’un message court' affecte principalement la difficulté d’envoyer des messages courts. Doubler cette valeur rend la difficulté à envoyer un court message presque double, tandis qu’un message plus long ne sera pas réellement affecté. + La "difficulté d’un message court" affecte principalement la difficulté d’envoyer des messages courts. Doubler cette valeur rend la difficulté à envoyer un court message presque double, tandis qu’un message plus long ne sera pas réellement affecté. - + Demanded difficulty Difficulté exigée - + Here 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. Vous pouvez préciser quelle charge de travail vous êtes prêt à effectuer afin d’envoyer un message à une personne. Placer cette valeur à 0 signifie que n’importe quelle valeur est acceptée. - + Maximum acceptable total difficulty: Difficulté maximale acceptée : - + Maximum acceptable small message difficulty: Difficulté maximale acceptée pour les messages courts : - + Max acceptable difficulty Difficulté maximale acceptée @@ -2513,87 +2512,87 @@ The 'Random Number' option is selected by default but deterministic ad - + <html><head/><body><p>Bitmessage 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 <span style=" font-style:italic;">test. </span></p><p>(Getting your own Bitmessage address into Namecoin is still rather difficult).</p><p>Bitmessage can use either namecoind directly or a running nmcontrol instance.</p></body></html> <html><head/><body><p>Bitmessage peut utiliser Namecoin, un autre programme basé sur Bitcoin, pour avoir des adresses plus parlantes. Par exemple, plutôt que de donner à votre ami votre longue adresse Bitmessage, vous pouvez simplement lui dire d’envoyer un message à <span style=" font-style:italic;">test. </span></p><p>(Obtenir votre propre adresse Bitmessage au sein de Namecoin est encore assez difficile).</p><p>Bitmessage peut soit utiliser directement namecoind soit exécuter une instance de nmcontrol.</p></body></html> - + Host: Hôte : - + Password: Mot de passe : - + Test Test - + Connect to: Connexion à : - + Namecoind Namecoind - + NMControl NMControl - + Namecoin integration Intégration avec Namecoin - + <html><head/><body><p>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.</p><p>Leave these input fields blank for the default behavior. </p></body></html> <html><head/><body><p>Par défaut, si vous envoyez un message à quelqu’un et que cette personne est hors connexion pendant plus de deux jours, Bitmessage enverra le message de nouveau après des deux jours supplémentaires. Ceci sera continué avec reculement (backoff) exponentiel pour toujours; les messages seront réenvoyés après 5, 10, 20 jours etc. jusqu’à ce que le récepteur accuse leur réception. Ici vous pouvez changer ce comportement en faisant en sorte que Bitmessage renonce après un certain nombre de jours ou de mois.</p> <p>Si vous souhaitez obtenir le comportement par défaut alors laissez vides ces champs de saisie. </p></body></html> - + Give up after Abandonner après - + and et - + days jours - + months. mois. - + Resends Expire Expiration des renvois automatiques - + Hide connection notifications Cacher les notifications de connexion - + Maximum outbound connections: [0: none] Connexions sortantes maximum: [0: aucune] - + Hardware GPU acceleration (OpenCL): Accélération matérielle GPU (OpenCL) : diff --git a/src/translations/bitmessage_ja.qm b/src/translations/bitmessage_ja.qm index 9135be1a2e..77fa63d1a5 100644 Binary files a/src/translations/bitmessage_ja.qm and b/src/translations/bitmessage_ja.qm differ diff --git a/src/translations/bitmessage_ja.ts b/src/translations/bitmessage_ja.ts index 7f1e4515a8..f11289f54d 100644 --- a/src/translations/bitmessage_ja.ts +++ b/src/translations/bitmessage_ja.ts @@ -60,27 +60,27 @@ @mailchuck.com - + Registration failed: 登録に失敗しました: - + The requested email address is not available, please try a new one. リクエストしたメールアドレスは利用できません。新しいメールアドレスを試してください。 - + Sending email gateway registration request メールゲートウェイの登録リクエストを送信しています - + Sending email gateway unregistration request メールゲートウェイの登録抹消リクエストを送信しています - + Sending email gateway status request メールゲートウェイの状態リクエストを送信しています @@ -112,7 +112,7 @@ Please type the desired email address (including @mailchuck.com) below: Mailchuck - + # You can use this to configure your email gateway account # Uncomment the setting you want to use # Here are the options: @@ -152,6 +152,50 @@ Please type the desired email address (including @mailchuck.com) below: # specified. As this scheme uses deterministic public keys, you will receive # the money directly. To turn it off again, set "feeamount" to 0. Requires # subscription. + + + + + + # You can use this to configure your email gateway account +# Uncomment the setting you want to use +# Here are the options: +# +# pgp: server +# The email gateway will create and maintain PGP keys for you and sign, verify, +# encrypt and decrypt on your behalf. When you want to use PGP but are lazy, +# use this. Requires subscription. +# +# pgp: local +# The email gateway will not conduct PGP operations on your behalf. You can +# either not use PGP at all, or use it locally. +# +# attachments: yes +# Incoming attachments in the email will be uploaded to MEGA.nz, and you can +# download them from there by following the link. Requires a subscription. +# +# attachments: no +# Attachments will be ignored. +# +# archive: yes +# Your incoming emails will be archived on the server. Use this if you need +# help with debugging problems or you need a third party proof of emails. This +# however means that the operator of the service will be able to read your +# emails even after they have been delivered to you. +# +# archive: no +# Incoming emails will be deleted from the server as soon as they are relayed +# to you. +# +# masterpubkey_btc: BIP44 xpub key or electrum v1 public seed +# offset_btc: integer (defaults to 0) +# feeamount: number with up to 8 decimal places +# feecurrency: BTC, XBT, USD, EUR or GBP +# Use these if you want to charge people who send you emails. If this is on and +# an unknown person sends you an email, they will be requested to pay the fee +# specified. As this scheme uses deterministic public keys, you will receive +# the money directly. To turn it off again, set "feeamount" to 0. Requires +# subscription. # これを使用して、メールゲートウェイアカウントを設定できます # 使用する設定のコメントを外してください @@ -180,14 +224,14 @@ Please type the desired email address (including @mailchuck.com) below: # 読むことができるということを意味します。 # # archive: no -# 受信メールは、あなたに中継されるとすぐにサーバーから削除されます。 +# 受信メールは、あなたに転送されるとすぐにサーバーから削除されます。 # # masterpubkey_btc: BIP44 xpub key または electrum v1 public seed # offset_btc: 整数 (デフォルトは 0) # feeamount: 小数点以下 8 桁までの数字 # feecurrency: BTC, XBT, USD, EUR または GBP # メールを送信した人に請求したい場合に、これらを使用します。 これがオンで、 -# 未知の人があなたにメールを送信した場合、指定された料金を支払うように要求されます。 +# 不明な人があなたにメールを送信した場合、指定された手数料を支払うように要求されます。 # この方式は決定的な公開鍵を使用するため、直接お金を受け取ることになります。 # もう一度オフにするには、「feeamount」を 0 に設定します。 # サブスクリプションが必要です。 @@ -197,52 +241,52 @@ Please type the desired email address (including @mailchuck.com) below: MainWindow - + Reply to sender 送信元に返信 - + Reply to channel チャンネルに返信 - + Add sender to your Address Book 送信元をアドレス帳に追加 - + Add sender to your Blacklist 送信元をブラックリストに追加 - + Move to Trash ゴミ箱へ移動 - + Undelete 削除を元に戻す - + View HTML code as formatted text HTMLコードを整形したテキストで表示 - + Save message as... 形式を選択してメッセージを保存 - + Mark Unread 未読にする - + New 新規 @@ -267,12 +311,12 @@ Please type the desired email address (including @mailchuck.com) below: アドレスをコピー - + Special address behavior... アドレスの特別な動作 - + Email gateway メールゲートウェイ @@ -282,37 +326,37 @@ Please type the desired email address (including @mailchuck.com) below: 削除 - + Send message to this address このアドレスへ送信 - + Subscribe to this address このアドレスを購読 - + Add New Address アドレスを追加 - + Copy destination address to clipboard 宛先アドレスをコピー - + Force send 強制的に送信 - + One of your addresses, %1, is an old version 1 address. Version 1 addresses are no longer supported. May we delete it now? %1は古いバージョン1のアドレスです。バージョン1のアドレスはサポートが終了しています。すぐに削除しますか? - + Waiting for their encryption key. Will request it again soon. 暗号鍵を待っています。 すぐにもう一度リクエストします。 @@ -322,17 +366,17 @@ Please type the desired email address (including @mailchuck.com) below: - + Queued. キューに入りました。 - + Message sent. Waiting for acknowledgement. Sent at %1 メッセージを送信しました。 確認応答を待っています。 %1 で送信されました - + Message sent. Sent at %1 メッセージは送信されました。送信先: %1 @@ -342,7 +386,7 @@ Please type the desired email address (including @mailchuck.com) below: - + Acknowledgement of the message received %1 メッセージの確認を受け取りました %1 @@ -352,27 +396,27 @@ Please type the desired email address (including @mailchuck.com) below: 配信がキューに入りました。 - + Broadcast on %1 配信: %1 - + Problem: The work demanded by the recipient is more difficult than you are willing to do. %1 問題: 受信者が要求している処理は現在あなたが設定しているよりも高い難易度です。 %1 - + Problem: The recipient's encryption key is no good. Could not encrypt message. %1 問題: 受信者の暗号鍵は正当でない物です。メッセージを暗号化できません。 %1 - + Forced difficulty override. Send should start soon. 難易度を強制上書きしました。まもなく送信されます。 - + Unknown status: %1 %2 不明なステータス: %1 %2 @@ -382,7 +426,7 @@ Please type the desired email address (including @mailchuck.com) below: 未接続 - + Show Bitmessage Bitmessageを表示 @@ -392,12 +436,12 @@ Please type the desired email address (including @mailchuck.com) below: 送る - + Subscribe 購読 - + Channel チャンネル @@ -407,66 +451,70 @@ Please type the desired email address (including @mailchuck.com) below: 終了 - + You may manage your keys by editing the keys.dat file stored in the same directory as this program. It is important that you back up this file. - プログラムを同じディレクトリに保存されているkeys.datファイルを編集することで鍵を管理できます。ファイルをバックアップしておくことも重要です。 + プログラムと同じディレクトリに保存されているkeys.datファイルを編集することで鍵を管理できます。ファイルをバックアップしておくことは重要です。 - + You may manage your keys by editing the keys.dat file stored in %1 It is important that you back up this file. - %1に保存されているkeys.datファイルを編集することで鍵を管理できます。ファイルをバックアップしておくことも重要です。 + %1 +に保存されているkeys.datファイルを編集することで鍵を管理できます。 +このファイルをバックアップしておくことは重要です。 - + Open keys.dat? keys.datを開きますか? - + You may manage your keys by editing the keys.dat file stored in the same directory as this program. It is important that you back up this file. Would you like to open the file now? (Be sure to close Bitmessage before making any changes.) - プログラムを同じディレクトリに保存されているkeys.datファイルを編集することで鍵を管理できます。ファイルをバックアップしておくことも重要です。すぐにファイルを開きますか?(必ず編集する前にBitmessageを終了してください) + プログラムと同じディレクトリに保存されているkeys.datファイルを編集することで鍵を管理できます。このファイルをバックアップしておくことは重要です。すぐにファイルを開きますか?(必ず編集する前にBitmessageを終了してください) - + You may manage your keys by editing the keys.dat file stored in %1 It is important that you back up this file. Would you like to open the file now? (Be sure to close Bitmessage before making any changes.) - %1に保存されているkeys.datファイルを編集することで鍵を管理できます。ファイルをバックアップしておくことも重要です。すぐにファイルを開きますか?(必ず編集する前にBitmessageを終了してください) + %1 +に保存されているkeys.datファイルを編集することで鍵を管理できます。 +ファイルをバックアップしておくことは重要です。すぐにファイルを開きますか?(必ず編集する前にBitmessageを終了してください) - + Delete trash? ゴミ箱を空にしますか? - + Are you sure you want to delete all trashed messages? ゴミ箱内のメッセージを全て削除してもよろしいですか? - + bad passphrase 不正なパスフレーズ - + You must type your passphrase. If you don't have one then this is not the form for you. パスフレーズを入力してください。パスフレーズがない場合は入力する必要はありません。 - + Bad address version number 不正なアドレスのバージョン番号 - + Your address version number must be a number: either 3 or 4. アドレスのバージョン番号は数字にする必要があります: 3 または 4。 - + Your address version number must be either 3 or 4. アドレスのバージョン番号は、3 または 4 のどちらかにする必要があります。 @@ -574,7 +622,7 @@ It is important that you back up this file. Would you like to open the file now? Error: Your account wasn't registered at an email gateway. Sending registration now as %1, please wait for the registration to be processed before retrying sending. - エラー: アカウントがメールゲートウェイに登録されていません。 今 %1 として登録を送信しています。送信を再試行する前に、登録が処理されるまでお待ちください。 + エラー: アカウントがメールゲートウェイに登録されていません。 今 %1 として登録を送信しています。送信を再試行する前に、登録が処理されるのをお待ちください。 @@ -629,7 +677,7 @@ It is important that you back up this file. Would you like to open the file now? Concerning the address %1, Bitmessage cannot understand address version numbers of %2. Perhaps upgrade Bitmessage to the latest version. - アドレス %1 に接続。%2 のバージョン番号は処理できません。Bitmessageを最新のバージョンへアップデートしてください。 + アドレス %1 に接続しています。%2 のバージョン番号は処理できません。Bitmessageを最新のバージョンへアップデートしてください。 @@ -639,12 +687,12 @@ It is important that you back up this file. Would you like to open the file now? Concerning the address %1, Bitmessage cannot handle stream numbers of %2. Perhaps upgrade Bitmessage to the latest version. - アドレス %1 に接続。%2 のストリーム番号は処理できません。Bitmessageを最新のバージョンへアップデートしてください。 + アドレス %1 に接続しています。%2 のストリーム番号は処理できません。Bitmessageを最新のバージョンへアップデートしてください。 Warning: You are currently not connected. Bitmessage will do the work necessary to send the message but it won't send until you connect. - 警告: 接続されていません。Bitmessageはメッセージの処理を行いますが、ネットワークに接続するまで送信はされません。 + 警告: 接続されていません。Bitmessageはメッセージ送信の処理を行いますが、ネットワークに接続するまで送信はされません。 @@ -662,12 +710,12 @@ It is important that you back up this file. Would you like to open the file now? アドレス帳から一つ、または複数のアドレスを右クリックして「このアドレスへ送信」を選んでください。 - + Fetched address from namecoin identity. namecoin IDからアドレスを取得。 - + New Message 新規メッセージ @@ -692,37 +740,37 @@ It is important that you back up this file. Would you like to open the file now? 入力されたアドレスは不正です。無視されました。 - + Error: You cannot add the same address to your address book twice. Try renaming the existing one if you want. エラー: 同じアドレスを複数アドレス帳に追加する事はできません。既存の項目をリネームしてください。 - + Error: You cannot add the same address to your subscriptions twice. Perhaps rename the existing one if you want. エラー: 購読に、同じアドレスを2回追加することはできません。 必要に応じて、既存の名前を変更してください。 - + Restart 再開 - + You must restart Bitmessage for the port number change to take effect. ポート番号の変更を有効にするにはBitmessageを再起動してください。 - + Bitmessage will use your proxy from now on but you may want to manually restart Bitmessage now to close existing connections (if any). プロキシの設定を有効にするには手動でBitmessageを再起動してください。既に接続がある場合は切断されます。 - + Number needed 数字が必要です - + Your maximum download and upload rate must be numbers. Ignoring what you typed. 最大ダウンロード数とアップロード数は数字にする必要があります。 入力されたものを無視します。 @@ -747,42 +795,42 @@ It is important that you back up this file. Would you like to open the file now? - + Passphrase mismatch パスフレーズが一致しません - + The passphrase you entered twice doesn't match. Try again. 再度入力されたパスフレーズが一致しません。再入力してください。 - + Choose a passphrase パスフレーズを選択してください - + You really do need a passphrase. パスフレーズが必要です。 - + Address is gone アドレスが無効になりました - + Bitmessage cannot find your address %1. Perhaps you removed it? アドレス %1 が見つかりません。既に削除していませんか? - + Address disabled アドレスが無効になりました - + Error: The address from which you are trying to send is disabled. You'll have to enable it on the 'Your Identities' tab before using it. エラー: 送信しようとしたアドレスは無効になっています。使用する前に「アドレス一覧」で有効にしてください。 @@ -792,42 +840,42 @@ It is important that you back up this file. Would you like to open the file now? - + Entry added to the blacklist. Edit the label to your liking. ブラックリストに項目が追加されました。ラベルは自由に編集できます。 - + Error: You cannot add the same address to your blacklist twice. Try renaming the existing one if you want. エラー: ブラックリストに同じアドレスを2回追加することはできません。 必要に応じて既存の名前を変更してみてください。 - + Moved items to trash. アイテムをゴミ箱へ移動。 - + Undeleted item. アイテムの削除を元に戻します。 - + Save As... 形式を選択して保存 - + Write error. 書き込みエラー。 - + No addresses selected. アドレスが未選択です。 - + If you delete the subscription, messages that you already received will become inaccessible. Maybe you can consider disabling the subscription instead. Disabled subscriptions will not receive new messages, but you can still view messages you already received. Are you sure you want to delete the subscription? @@ -836,7 +884,7 @@ Are you sure you want to delete the subscription? 購読を削除してもよろしいですか? - + If you delete the channel, messages that you already received will become inaccessible. Maybe you can consider disabling the channel instead. Disabled channels will not receive new messages, but you can still view messages you already received. Are you sure you want to delete the channel? @@ -845,32 +893,32 @@ Are you sure you want to delete the channel? チャンネルを削除してもよろしいですか? - + Do you really want to remove this avatar? このアバターを削除してもよろしいですか? - + You have already set an avatar for this address. Do you really want to overwrite it? すでにこのアドレスのアバターを設定しています。 上書きしてもよろしいですか? - + Start-on-login not yet supported on your OS. ログイン時に開始は、まだお使いのOSでサポートされていません。 - + Minimize-to-tray not yet supported on your OS. トレイに最小化は、まだお使いのOSでサポートされていません。 - + Tray notifications not yet supported on your OS. トレイ通知は、まだお使いのOSでサポートされていません。 - + Testing... テスト中 @@ -880,37 +928,37 @@ Are you sure you want to delete the channel? - + The address should start with ''BM-'' アドレスは「BM-」から始まります - + The address is not typed or copied correctly (the checksum failed). このアドレスは正しく入力、またはコピーされていません。(チェックサムが一致しません)。 - + The version number of this address is higher than this software can support. Please upgrade Bitmessage. このアドレスのバージョン番号はこのプログラムのサポート範囲外です。Bitmessageをアップデートしてください。 - + The address contains invalid characters. 入力されたアドレスは不正な文字を含んでいます。 - + Some data encoded in the address is too short. このアドレスでエンコードされたデータが短すぎます。 - + Some data encoded in the address is too long. このアドレスでエンコードされたデータが長過ぎます。 - + Some data encoded in the address is malformed. このアドレスでエンコードされた一部のデータが不正です。 @@ -920,12 +968,12 @@ Are you sure you want to delete the channel? - + Address is an old type. We cannot display its past broadcasts. アドレスが古い形式です。 過去の配信は表示できません。 - + There are no recent broadcasts from this address to display. このアドレスから表示する最近の配信はありません。 @@ -1120,12 +1168,12 @@ Are you sure you want to delete the channel? チャンネルに参加 / 作成 - + All accounts すべてのアカウント - + Zoom level %1% ズーム レベル %1% @@ -1150,37 +1198,37 @@ Are you sure you want to delete the channel? 新しいバージョンの PyBitmessage が利用可能です: %1。 https://github.com/Bitmessage/PyBitmessage/releases/latest からダウンロードしてください - + Waiting for PoW to finish... %1% - PoW(証明)が完了するのを待っています... %1% + PoW(プルーフオブワーク)が完了するのを待っています... %1% - + Shutting down Pybitmessage... %1% Pybitmessageをシャットダウンしています... %1% - + Waiting for objects to be sent... %1% オブジェクトの送信待ち... %1% - + Saving settings... %1% 設定を保存しています... %1% - + Shutting down core... %1% コアをシャットダウンしています... %1% - + Stopping notifications... %1% 通知を停止しています... %1% - + Shutdown imminent... %1% すぐにシャットダウンします... %1% @@ -1190,42 +1238,42 @@ Are you sure you want to delete the channel? %n 時間 - + %n day(s) %n 日 - + Shutting down PyBitmessage... %1% PyBitmessageをシャットダウンしています... %1% - + Sent 送信済 - + Generating one new address 新しいアドレスを生成しています - + Done generating address. Doing work necessary to broadcast it... アドレスの生成を完了しました。 配信に必要な処理を行っています... - + Generating %1 new addresses. %1 の新しいアドレスを生成しています。 - + %1 is already in 'Your Identities'. Not adding it again. %1はすでに「アドレス一覧」にあります。 もう一度追加できません。 - + Done generating address アドレスの生成を完了しました @@ -1235,111 +1283,111 @@ Are you sure you want to delete the channel? - + Disk full ディスクがいっぱいです - + Alert: Your disk or data storage volume is full. Bitmessage will now exit. アラート: ディスクまたはデータストレージのボリュームがいっぱいです。 Bitmessageが終了します。 - + Error! Could not find sender address (your address) in the keys.dat file. エラー! keys.datファイルで送信元アドレス (あなたのアドレス) を見つけることができませんでした。 - + Doing work necessary to send broadcast... 配信に必要な処理を行っています... - + Broadcast sent on %1 配信が送信されました %1 - + Encryption key was requested earlier. 暗号鍵は以前にリクエストされました。 - + Sending a request for the recipient's encryption key. 受信者の暗号鍵のリクエストを送信します。 - + Looking up the receiver's public key 受信者の公開鍵を探しています - + Problem: Destination is a mobile device who requests that the destination be included in the message but this is disallowed in your settings. %1 問題: メッセージに含まれた宛先のリクエストはモバイルデバイスですが、設定では許可されていません。 %1 - + Doing work necessary to send message. There is no required difficulty for version 2 addresses like this. メッセージの送信に必要な処理を行っています。 このようなバージョン2のアドレスには、必要な難易度はありません。 - + Doing work necessary to send message. Receiver's required difficulty: %1 and %2 メッセージの送信に必要な処理を行っています。 受信者の必要な難易度: %1 および %2 - + Problem: The work demanded by the recipient (%1 and %2) is more difficult than you are willing to do. %3 問題: 受信者が要求している処理 (%1 および %2) は、現在あなたが設定しているよりも高い難易度です。 %3 - + Problem: You are trying to send a message to yourself or a chan but your encryption key could not be found in the keys.dat file. Could not encrypt message. %1 問題: あなた自身またはチャンネルにメッセージを送信しようとしていますが、暗号鍵がkeys.datファイルに見つかりませんでした。 メッセージを暗号化できませんでした。 %1 - + Doing work necessary to send message. メッセージの送信に必要な処理を行っています。 - + Message sent. Waiting for acknowledgement. Sent on %1 メッセージを送信しました。 確認応答を待っています。 %1 で送信しました - + Doing work necessary to request encryption key. 暗号鍵のリクエストに必要な処理を行っています。 - + Broadcasting the public key request. This program will auto-retry if they are offline. 公開鍵のリクエストを配信しています。 このプログラムがオフラインの場合、自動的に再試行されます。 - + Sending public key request. Waiting for reply. Requested at %1 公開鍵のリクエストを送信しています。 返信を待っています。 %1 でリクエストしました - + UPnP port mapping established on port %1 ポート%1でUPnPポートマッピングが確立しました - + UPnP port mapping removed UPnPポートマッピングを削除しました - + Mark all messages as read すべてのメッセージを既読にする @@ -1349,87 +1397,87 @@ Receiver's required difficulty: %1 and %2 すべてのメッセージを既読にしてもよろしいですか? - + Doing work necessary to send broadcast. 配信に必要な処理を行っています。 - + Proof of work pending PoW(証明)を待っています - + %n object(s) pending proof of work %n オブジェクトが証明待ち (PoW) - + %n object(s) waiting to be distributed %n オブジェクトが配布待ち - + Wait until these tasks finish? これらのタスクが完了するまで待ちますか? - + The name %1 was not found. 名前 %1 が見つかりませんでした。 - + The namecoin query failed (%1) namecoin のクエリに失敗しました (%1) - - The namecoin query failed. - namecoin のクエリに失敗しました。 + + Unknown namecoin interface type: %1 + 不明な namecoin インターフェースタイプ: %1 - - The name %1 has no valid JSON data. - 名前 %1 は有効な JSON データがありません。 + + The namecoin query failed. + namecoin のクエリに失敗しました。 - + The name %1 has no associated Bitmessage address. 名前 %1 は関連付けられた Bitmessage アドレスがありません。 - + Success! Namecoind version %1 running. 成功! Namecoind バージョン %1 が実行中。 - + Success! NMControll is up and running. 成功! NMControll が開始して実行中です。 - + Couldn't understand NMControl. NMControl を理解できませんでした。 - + The connection to namecoin failed. namecoin への接続に失敗しました。 - + Your GPU(s) did not calculate correctly, disabling OpenCL. Please report to the developers. GPUが正しく求められないため、OpenCLが無効になりました。 開発者に報告してください。 - + Set notification sound... 通知音を設定... - + Welcome to easy and secure Bitmessage * send messages to other people @@ -1444,12 +1492,12 @@ Receiver's required difficulty: %1 and %2 - + not recommended for chans チャンネルにはお勧めしません - + Quiet Mode マナーモード @@ -1504,57 +1552,77 @@ Receiver's required difficulty: %1 and %2 エラー: 受信者のアドレス %1 には何かしら誤りがあります。 - + Error: %1 エラー: %1 - + From %1 送信元 %1 - + + Disconnecting + 切断しています + + + + Connecting + 接続しています + + + + Bitmessage will now drop all connections. Are you sure? + Bitmessage はすべての接続を削除します。 よろしいですか? + + + + Bitmessage will now start connecting to network. Are you sure? + Bitmessage はネットワークへの接続を開始します。 よろしいですか? + + + Synchronisation pending 同期を保留しています - + Bitmessage hasn't synchronised with the network, %n object(s) to be downloaded. If you quit now, it may cause delivery delays. Wait until the synchronisation finishes? - Bitmessageはネットワークと同期していません。%n のオブジェクトをダウンロードする必要があります。 今、終了すると、配送が遅れることがあります。 同期が完了するまで待ちますか? + Bitmessageはネットワークと同期されていません。%n のオブジェクトをダウンロードする必要があります。 今、終了すると、配信が遅れる可能性があります。 同期が完了するまで待ちますか? - + Not connected 未接続 - + Bitmessage isn't connected to the network. If you quit now, it may cause delivery delays. Wait until connected and the synchronisation finishes? - Bitmessageはネットワークに接続していません。 今、終了すると、配送が遅れることがあります。 接続して、同期が完了するまで待ちますか? + Bitmessageはネットワークに接続していません。 今、終了すると、配信が遅れる可能性があります。 接続して、同期が完了するまで待ちますか? - + Waiting for network connection... ネットワーク接続を待っています... - + Waiting for finishing synchronisation... 同期の完了を待っています... - + You have already set a notification sound for this address book entry. Do you really want to overwrite it? すでにこのアドレス帳エントリの通知音を設定しています。 上書きしてもよろしいですか? - + Error occurred: could not load message from disk. エラーが発生しました: ディスクからメッセージを読み込みできません。 - + Display the %n recent broadcast(s) from this address. このアドレスから%nの最新の配信を表示します。 @@ -1574,22 +1642,22 @@ Receiver's required difficulty: %1 and %2 クリア - + inbox 受信トレイ - + new 新規 - + sent 送信済 - + trash ゴミ箱 @@ -1597,22 +1665,22 @@ Receiver's required difficulty: %1 and %2 MessageView - + Follow external link 外部リンクをフォロー - + The link "%1" will open in a browser. It may be a security risk, it could de-anonymise you or download malicious data. Are you sure? リンク "%1" はブラウザで開きます。 セキュリティリスクの可能性があります。匿名性がなくなったり、悪意のあるデータをダウンロードする可能性があります。 よろしいですか? - + HTML detected, click here to display HTMLが検出されました。ここをクリックすると表示します - + Click here to disable HTML ここをクリックするとHTMLを無効にします @@ -1620,14 +1688,14 @@ Receiver's required difficulty: %1 and %2 MsgDecode - + The message has an unknown encoding. Perhaps you should upgrade Bitmessage. メッセージのエンコードが不明です。 Bitmessageをアップグレードする必要があるかもしれません。 - + Unknown encoding 不明なエンコード @@ -1782,7 +1850,7 @@ The 'Random Number' option is selected by default but deterministic ad 仮想メーリングリストの名前: - + This is a chan address. You cannot use it as a pseudo-mailing list. chanアドレスは仮想メーリングリストのアドレスには使用できません。 @@ -1888,7 +1956,7 @@ The 'Random Number' option is selected by default but deterministic ad Work offline - + オフラインで動作 @@ -1937,7 +2005,7 @@ The 'Random Number' option is selected by default but deterministic ad ファイアーウォールを適切に設定し、他のpeerへ接続してください。 - + You are using TCP port %1. (This can be changed in the settings). 使用中のポート %1 (設定で変更できます)。 @@ -1990,27 +2058,27 @@ The 'Random Number' option is selected by default but deterministic ad - + Since startup on %1 起動日時 %1 - + Down: %1/s Total: %2 ダウン: %1/秒 合計: %2 - + Up: %1/s Total: %2 アップ: %1/秒 合計: %2 - + Total Connections: %1 接続数: %1 - + Inventory lookups per second: %1 毎秒のインベントリ検索: %1 @@ -2030,27 +2098,27 @@ The 'Random Number' option is selected by default but deterministic ad ネットワークの状態 - + byte(s) バイト - + Object(s) to be synced: %n 同期する必要のあるオブジェクト: %n - + Processed %n person-to-person message(s). %n 通の1対1のメッセージを処理しました。 - + Processed %n broadcast message(s). %n 件の配信を処理しました。 - + Processed %n public key(s). %n 件の公開鍵を処理しました。 @@ -2176,17 +2244,17 @@ The 'Random Number' option is selected by default but deterministic ad newchandialog - + Successfully created / joined chan %1 チャンネル %1 を正常に作成 / 参加しました - + Chan creation / joining failed チャンネルの作成 / 参加に失敗しました - + Chan creation / joining cancelled チャンネルの作成 / 参加をキャンセルしました @@ -2194,29 +2262,21 @@ The 'Random Number' option is selected by default but deterministic ad proofofwork - + C PoW module built successfully. C PoW モジュールのビルドに成功しました。 - + Failed to build C PoW module. Please build it manually. C PoW モジュールのビルドに失敗しました。手動でビルドしてください。 - + C PoW module unavailable. Please build it. C PoW モジュールが利用できません。ビルドしてください。 - - qrcodeDialog - - - QR-code - QR コード - - regenerateAddressesDialog @@ -2227,7 +2287,7 @@ The 'Random Number' option is selected by default but deterministic ad Regenerate existing addresses - + 既存のアドレスを再生成する @@ -2273,218 +2333,218 @@ The 'Random Number' option is selected by default but deterministic ad settingsDialog - + Settings 設定 - + Start Bitmessage on user login ユーザのログイン時にBitmessageを起動 - + Tray トレイ - + Start Bitmessage in the tray (don't show main window) Bitmessageをトレイ内で起動する(メインウィンドウを表示しない) - + Minimize to tray タスクトレイへ最小化 - + Close to tray トレイに閉じる - + Show notification when message received メッセージの受信時に通知する - + Run in Portable Mode ポータブルモードで実行 - + In Portable Mode, messages and config files are stored in the same directory as the program rather than the normal application-data folder. This makes it convenient to run Bitmessage from a USB thumb drive. ポータブルモード時、メッセージと設定ファイルは通常のアプリケーションデータのフォルダではなく同じディレクトリに保存されます。これによりBitmessageをUSBドライブから実行できます。 - + Willingly include unencrypted destination address when sending to a mobile device 携帯端末にメッセージを送る時は暗号化されていないアドレスを許可する - + Use Identicons Identiconsを使用する - + Reply below Quote 下に返信 - + Interface Language - インターフェイス言語 + インターフェース言語 - + System Settings system システム設定 - + User Interface ユーザインターフェース - + Listening port リスニングポート - + Listen for connections on port: 接続を待つポート: - + UPnP: UPnP: - + Bandwidth limit 帯域幅の制限 - + Maximum download rate (kB/s): [0: unlimited] 最大ダウンロード速度 (kB/秒): [0: 無制限] - + Maximum upload rate (kB/s): [0: unlimited] 最大アップロード速度 (kB/秒): [0: 無制限] - + Proxy server / Tor プロキシサーバー/Tor - + Type: タイプ: - + Server hostname: サーバーホスト名: - + Port: ポート: - + Authentication 認証 - + Username: ユーザー名: - + Pass: パス: - + Listen for incoming connections when using proxy プロキシ使用時に外部からの接続を待機する - + none 無し - + SOCKS4a SOCKS4a - + SOCKS5 SOCKS5 - + Network Settings ネットワーク設定 - + Total difficulty: 全体の難易度: - + The 'Total difficulty' affects the absolute amount of work the sender must complete. Doubling this value doubles the amount of work. 「全体の難易度」は完全に全てのメッセージに影響します。この値を二倍にすると処理量も二倍になります。 - + Small message difficulty: 小さいメッセージの難易度: - + When 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. 誰かがあなたにメッセージを送る時、相手のコンピューターはいくらか計算処理を行います。処理の難易度はデフォルトでは1です。この値を変更すると新しいアドレスではこのデフォルト値を引き上げることができます。その場合、新しいアドレスはメッセージの送信者により高い難易度を要求します。例外もあります: 友人や知り合いをアドレス帳に登録すると、Bitmessageは次にメッセージを送る際、自動的に要求される処理の難易度を最低限の1で済むように通知します。 - + 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. 「小さいメッセージの難易度」は小さいメッセージを行う時にだけ影響します。この値を二倍にすれば小さなメッセージに必要な処理の難易度は二倍になりますが、実際にはデータ量の多いメッセージには影響しません。 - + Demanded difficulty 要求される難易度 - + Here 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. ここでは他のユーザーへメッセージを送る際に行うことを許可する処理量の上限を設定します。0を設定するとどんな量でも許容します。 - + Maximum acceptable total difficulty: 許可する難易度の上限: - + Maximum acceptable small message difficulty: 小さなメッセージに許可する難易度の上限: - + Max acceptable difficulty 許可する最大の難易度 @@ -2494,87 +2554,87 @@ The 'Random Number' option is selected by default but deterministic ad - + <html><head/><body><p>Bitmessage 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 <span style=" font-style:italic;">test. </span></p><p>(Getting your own Bitmessage address into Namecoin is still rather difficult).</p><p>Bitmessage can use either namecoind directly or a running nmcontrol instance.</p></body></html> <html><head/><body><p>Bitmessageはアドレスを読みやすくするため、NamecoinというBitcoinベースの別のプログラムを利用できます。例えば、あなたの友人に長いBitmessageアドレスを伝える代わりに、単純に<span style=" font-style:italic;">テスト</span>でメッセージを送るよう伝えることができます。</p><p>(Bitmessageアドレスを独自にNamecoinにするのはかなり難しいです)。</p><p>Bitmessageは直接namecoindを使うか、nmcontrolインスタンスを使うことができます。</p></body></html> - + Host: ホスト: - + Password: パスワード: - + Test テスト - + Connect to: 接続先: - + Namecoind Namecoind - + NMControl NMControl - + Namecoin integration Namecoin連携 - + <html><head/><body><p>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.</p><p>Leave these input fields blank for the default behavior. </p></body></html> <html><head/><body><p>デフォルトでは、あなたが誰かにメッセージを送信して、相手が 2 日以上オフラインになっている場合、 Bitmessage はさらに 2 日後にメッセージを再送信します。 これは指数関数的後退で永遠に続きます。 受信者がそれらを確認するまで、メッセージは 5、10、20 日後に再送信されます。 ここで Bitmessage が一定の日数または月数後に諦める数を入力して、その動作を変更することができます。</p><p>デフォルトの動作は、この入力フィールドを空白のままにします。 </p></body></html> - + Give up after 次の期間後に諦める - + and - + days - + months. ヶ月。 - + Resends Expire 再送信の期限 - + Hide connection notifications 接続通知を非表示 - + Maximum outbound connections: [0: none] 最大アウトバウンド接続: [0: なし] - + Hardware GPU acceleration (OpenCL): ハードウェア GPU アクセラレーション (OpenCL): diff --git a/src/translations/bitmessage_pl.qm b/src/translations/bitmessage_pl.qm index 94e7b8cab8..fb31e8d552 100644 Binary files a/src/translations/bitmessage_pl.qm and b/src/translations/bitmessage_pl.qm differ diff --git a/src/translations/bitmessage_pl.ts b/src/translations/bitmessage_pl.ts index c10259d7e4..89c6162ec8 100644 --- a/src/translations/bitmessage_pl.ts +++ b/src/translations/bitmessage_pl.ts @@ -60,27 +60,27 @@ @mailchuck.com - + Registration failed: Rejestracja nie powiodła się: - + The requested email address is not available, please try a new one. Wybrany adres e-mail nie jest dostępny, proszę spróbować inny. - + Sending email gateway registration request Wysyłanie zapytania o rejestrację na bramce poczty - + Sending email gateway unregistration request Wysyłanie zapytania o wyrejestrowanie z bramki poczty - + Sending email gateway status request Wysyłanie zapytania o stan bramki poczty @@ -112,7 +112,7 @@ Please type the desired email address (including @mailchuck.com) below: Mailchuck - + # You can use this to configure your email gateway account # Uncomment the setting you want to use # Here are the options: @@ -152,9 +152,53 @@ Please type the desired email address (including @mailchuck.com) below: # specified. As this scheme uses deterministic public keys, you will receive # the money directly. To turn it off again, set "feeamount" to 0. Requires # subscription. + + + + + + # You can use this to configure your email gateway account +# Uncomment the setting you want to use +# Here are the options: +# +# pgp: server +# The email gateway will create and maintain PGP keys for you and sign, verify, +# encrypt and decrypt on your behalf. When you want to use PGP but are lazy, +# use this. Requires subscription. +# +# pgp: local +# The email gateway will not conduct PGP operations on your behalf. You can +# either not use PGP at all, or use it locally. +# +# attachments: yes +# Incoming attachments in the email will be uploaded to MEGA.nz, and you can +# download them from there by following the link. Requires a subscription. +# +# attachments: no +# Attachments will be ignored. +# +# archive: yes +# Your incoming emails will be archived on the server. Use this if you need +# help with debugging problems or you need a third party proof of emails. This +# however means that the operator of the service will be able to read your +# emails even after they have been delivered to you. +# +# archive: no +# Incoming emails will be deleted from the server as soon as they are relayed +# to you. +# +# masterpubkey_btc: BIP44 xpub key or electrum v1 public seed +# offset_btc: integer (defaults to 0) +# feeamount: number with up to 8 decimal places +# feecurrency: BTC, XBT, USD, EUR or GBP +# Use these if you want to charge people who send you emails. If this is on and +# an unknown person sends you an email, they will be requested to pay the fee +# specified. As this scheme uses deterministic public keys, you will receive +# the money directly. To turn it off again, set "feeamount" to 0. Requires +# subscription. # Tutaj możesz skonfigurować ustawienia bramki poczty e-mail -# Odkomentuj (usuń znak '#') opcje których chcesz użyć +# Odkomentuj (usuń znak „#”) opcje których chcesz użyć # Ustawienia: # # pgp: server @@ -175,8 +219,8 @@ Please type the desired email address (including @mailchuck.com) below: # # archive: yes # Przychodzące wiadomości zostaną zarchiwizowane na serwerze. Użyj tej -# opcji przy diagnozowaniu problemów, lub jeżeli potrzebujesz dowodu -# przesyłani wiadomości na zewnętrznym serwerze. Włączenie tej opcji +# opcji przy diagnozowaniu problemów lub jeżeli potrzebujesz dowodu +# przesyłania wiadomości na zewnętrznym serwerze. Włączenie tej opcji # spowoduje, że operator usługi będzie mógł czytać Twoje listy nawet po # przesłaniu ich do Ciebie. # @@ -192,129 +236,129 @@ Please type the desired email address (including @mailchuck.com) below: # Tobie wiadomość. Jeżeli ta opcja jest włączona i nieznana osoba wyśle # Ci wiadomość, będzie poproszona o wniesienie opłaty. Ta funkcja używa # deterministycznych kluczy publicznych, dostaniesz pieniądze -# bezpośrednio. Aby ją ponownie wyłączyć, ustaw 'feeamount' na 0. +# bezpośrednio. Aby ją ponownie wyłączyć, ustaw „feeamount” na 0. # Wymaga subskrypcji. MainWindow - + Reply to sender Odpowiedz do nadawcy - + Reply to channel Odpowiedz do kanału - + Add sender to your Address Book Dodaj nadawcę do Książki Adresowej - + Add sender to your Blacklist Dodaj nadawcę do Listy Blokowanych - + Move to Trash Przenieś do kosza - + Undelete Przywróć - + View HTML code as formatted text Wyświetl kod HTML w postaci sformatowanej - + Save message as... Zapisz wiadomość jako… - + Mark Unread Oznacz jako nieprzeczytane - + New Nowe - + Enable Aktywuj - + Disable Deaktywuj - + Set avatar... Ustaw awatar… - + Copy address to clipboard Kopiuj adres do schowka - + Special address behavior... Specjalne zachowanie adresu… - + Email gateway Przekaźnik e-mail - + Delete Usuń - + Send message to this address Wyślij wiadomość pod ten adres - + Subscribe to this address Subskrybuj ten adres - + Add New Address Dodaj nowy adres - + Copy destination address to clipboard Kopiuj adres odbiorcy do schowka - + Force send Wymuś wysłanie - + One of your addresses, %1, is an old version 1 address. Version 1 addresses are no longer supported. May we delete it now? Jeden z adresów, %1, jest starym adresem wersji 1. Adresy tej wersji nie są już wspierane. Usunąć go? - + Waiting for their encryption key. Will request it again soon. Oczekiwanie na klucz szyfrujący odbiorcy. Niedługo nastąpi ponowne wysłanie o niego prośby. @@ -324,17 +368,17 @@ Please type the desired email address (including @mailchuck.com) below: - + Queued. W kolejce do wysłania. - + Message sent. Waiting for acknowledgement. Sent at %1 Wiadomość wysłana. Oczekiwanie na potwierdzenie odbioru. Wysłano o %1 - + Message sent. Sent at %1 Wiadomość wysłana. Wysłano o %1 @@ -344,7 +388,7 @@ Please type the desired email address (including @mailchuck.com) below: - + Acknowledgement of the message received %1 Otrzymano potwierdzenie odbioru wiadomości %1 @@ -354,37 +398,37 @@ Please type the desired email address (including @mailchuck.com) below: Przekaz w kolejce do wysłania. - + Broadcast on %1 Wysłana o %1 - + Problem: The work demanded by the recipient is more difficult than you are willing to do. %1 Problem: dowód pracy wymagany przez odbiorcę jest trudniejszy niż zaakceptowany przez Ciebie. %1 - + Problem: The recipient's encryption key is no good. Could not encrypt message. %1 Problem: klucz szyfrujący odbiorcy jest nieprawidłowy. Nie można zaszyfrować wiadomości. %1 - + Forced difficulty override. Send should start soon. Wymuszono ominięcie trudności. Wysłanie zostanie wkrótce rozpoczęte. - + Unknown status: %1 %2 Nieznany status: %1 %2 - + Not Connected Brak połączenia - + Show Bitmessage Pokaż Bitmessage @@ -394,12 +438,12 @@ Please type the desired email address (including @mailchuck.com) below: Wyślij - + Subscribe Subskrybuj - + Channel Kanał @@ -409,12 +453,12 @@ Please type the desired email address (including @mailchuck.com) below: Zamknij - + You may manage your keys by editing the keys.dat file stored in the same directory as this program. It is important that you back up this file. Możesz zarządzać swoimi kluczami edytując plik keys.dat znajdujący się w tym samym katalogu co program. Zaleca się zrobienie kopii zapasowej tego pliku. - + You may manage your keys by editing the keys.dat file stored in %1 It is important that you back up this file. @@ -423,17 +467,17 @@ It is important that you back up this file. Zaleca się zrobienie kopii zapasowej tego pliku. - + Open keys.dat? Otworzyć plik keys.dat? - + You may manage your keys by editing the keys.dat file stored in the same directory as this program. It is important that you back up this file. Would you like to open the file now? (Be sure to close Bitmessage before making any changes.) Możesz zarządzać swoimi kluczami edytując plik keys.dat znajdujący się w tym samym katalogu co program. Zaleca się zrobienie kopii zapasowej tego pliku. Czy chcesz otworzyć ten plik teraz? (Zamknij Bitmessage, przed wprowadzeniem jakichkolwiek zmian.) - + You may manage your keys by editing the keys.dat file stored in %1 It is important that you back up this file. Would you like to open the file now? (Be sure to close Bitmessage before making any changes.) @@ -442,37 +486,37 @@ It is important that you back up this file. Would you like to open the file now? Zaleca się zrobienie kopii zapasowej tego pliku. Czy chcesz otworzyć ten plik teraz? (Zamknij Bitmessage przed wprowadzeniem jakichkolwiek zmian.) - + Delete trash? Opróżnić kosz? - + Are you sure you want to delete all trashed messages? Czy na pewno usunąć wszystkie wiadomości z kosza? - + bad passphrase nieprawidłowe hasło - + You must type your passphrase. If you don't have one then this is not the form for you. Musisz wpisać swoje hasło. Jeżeli go nie posiadasz, to ten formularz nie jest dla Ciebie. - + Bad address version number Nieprawidłowy numer wersji adresu - + Your address version number must be a number: either 3 or 4. Twój numer wersji adresu powinien wynosić: 3 lub 4. - + Your address version number must be either 3 or 4. Twój numer wersji adresu powinien wynosić: 3 lub 4. @@ -542,12 +586,12 @@ Zaleca się zrobienie kopii zapasowej tego pliku. Czy chcesz otworzyć ten plik - + Connection lost Połączenie utracone - + Connected Połączono @@ -668,12 +712,12 @@ Zwykle 4-5 dniowy TTL jest odpowiedni. Użyj prawego przycisku myszy na adresie z książki adresowej i wybierz opcję "Wyślij wiadomość do tego adresu". - + Fetched address from namecoin identity. Pobrano adres z identyfikatora Namecoin. - + New Message Nowa wiadomość @@ -688,47 +732,47 @@ Zwykle 4-5 dniowy TTL jest odpowiedni. - + Address is valid. Adres jest prawidłowy. - + The address you entered was invalid. Ignoring it. Wprowadzono niewłaściwy adres, który został zignorowany. - + Error: You cannot add the same address to your address book twice. Try renaming the existing one if you want. Błąd: Adres znajduje się już w książce adresowej. - + Error: You cannot add the same address to your subscriptions twice. Perhaps rename the existing one if you want. Błąd: Adres znajduje się już na liście subskrybcji. - + Restart Uruchom ponownie - + You must restart Bitmessage for the port number change to take effect. Musisz zrestartować Bitmessage, aby zmiana numeru portu weszła w życie. - + Bitmessage will use your proxy from now on but you may want to manually restart Bitmessage now to close existing connections (if any). Bitmessage będzie of teraz korzystał z serwera proxy, ale możesz ręcznie zrestartować Bitmessage, aby zamknąć obecne połączenia (jeżeli występują). - + Number needed Wymagany numer - + Your maximum download and upload rate must be numbers. Ignoring what you typed. Maksymalne prędkości wysyłania i pobierania powinny być liczbami. Zignorowano zmiany. @@ -753,44 +797,44 @@ Zwykle 4-5 dniowy TTL jest odpowiedni. - + Passphrase mismatch Hasła różnią się - + The passphrase you entered twice doesn't match. Try again. Hasła, które wpisałeś nie pasują. Spróbuj ponownie. - + Choose a passphrase Wpisz hasło - + You really do need a passphrase. Naprawdę musisz wpisać hasło. - + Address is gone Adres zniknął - + Bitmessage cannot find your address %1. Perhaps you removed it? Bitmessage nie może odnaleźć Twojego adresu %1. Może go usunąłeś? - + Address disabled Adres nieaktywny - + Error: The address from which you are trying to send is disabled. You'll have to enable it on the 'Your Identities' tab before using it. - Błąd: adres, z którego próbowałeś wysłać wiadomość jest nieaktywny. Włącz go w zakładce 'Twoje tożsamości' zanim go użyjesz. + Błąd: adres, z którego próbowałeś wysłać wiadomość jest nieaktywny. Włącz go w zakładce „Twoje tożsamości”, zanim go użyjesz. @@ -798,42 +842,42 @@ Zwykle 4-5 dniowy TTL jest odpowiedni. - + Entry added to the blacklist. Edit the label to your liking. Dodano wpis do listy blokowanych. Można teraz zmienić jego nazwę. - + Error: You cannot add the same address to your blacklist twice. Try renaming the existing one if you want. - Błąd: Adres znajduje się już na liście blokowanych. + Błąd: adres znajduje się już na liście blokowanych. - + Moved items to trash. Przeniesiono wiadomości do kosza. - + Undeleted item. Przywrócono wiadomość. - + Save As... Zapisz jako… - + Write error. Błąd zapisu. - + No addresses selected. Nie wybrano adresu. - + If you delete the subscription, messages that you already received will become inaccessible. Maybe you can consider disabling the subscription instead. Disabled subscriptions will not receive new messages, but you can still view messages you already received. Are you sure you want to delete the subscription? @@ -842,7 +886,7 @@ Are you sure you want to delete the subscription? Czy na pewno chcesz usunąć tę subskrypcję? - + If you delete the channel, messages that you already received will become inaccessible. Maybe you can consider disabling the channel instead. Disabled channels will not receive new messages, but you can still view messages you already received. Are you sure you want to delete the channel? @@ -851,32 +895,32 @@ Are you sure you want to delete the channel? Czy na pewno chcesz usunąć ten kanał? - + Do you really want to remove this avatar? Czy na pewno chcesz usunąć ten awatar? - + You have already set an avatar for this address. Do you really want to overwrite it? Już ustawiłeś awatar dla tego adresu. Czy na pewno chcesz go nadpisać? - + Start-on-login not yet supported on your OS. Start po zalogowaniu jeszcze nie jest wspierany pod Twoim systemem. - + Minimize-to-tray not yet supported on your OS. Minimalizacja do zasobnika nie jest jeszcze wspierana pod Twoim systemem. - + Tray notifications not yet supported on your OS. Powiadomienia w zasobniku nie są jeszcze wspierane pod Twoim systemem. - + Testing... Testowanie… @@ -886,37 +930,37 @@ Czy na pewno chcesz usunąć ten kanał? - + The address should start with ''BM-'' Adres powinien zaczynać się od „BM-” - + The address is not typed or copied correctly (the checksum failed). Adres nie został skopiowany lub przepisany poprawnie (błąd sumy kontrolnej). - + The version number of this address is higher than this software can support. Please upgrade Bitmessage. Numer wersji tego adresu jest wyższy niż ten program może obsłużyć. Proszę zaktualizować Bitmessage. - + The address contains invalid characters. Adres zawiera nieprawidłowe znaki. - + Some data encoded in the address is too short. Niektóre dane zakodowane w adresie są za krótkie. - + Some data encoded in the address is too long. Niektóre dane zakodowane w adresie są za długie. - + Some data encoded in the address is malformed. Niektóre dane zakodowane w adresie są uszkodzone. @@ -926,12 +970,12 @@ Czy na pewno chcesz usunąć ten kanał? - + Address is an old type. We cannot display its past broadcasts. Adres starego typu. Nie można wyświetlić jego wiadomości subskrypcji. - + There are no recent broadcasts from this address to display. Brak niedawnych wiadomości subskrypcji do wyświetlenia. @@ -1126,22 +1170,22 @@ Czy na pewno chcesz usunąć ten kanał? Dołącz / Utwórz kanał - + All accounts Wszystkie konta - + Zoom level %1% Poziom powiększenia %1% - + Error: You cannot add the same address to your list twice. Perhaps rename the existing one if you want. Błąd: Adres znajduje sie już na liście. - + Add new entry Dodaj nowy wpis @@ -1156,37 +1200,37 @@ Czy na pewno chcesz usunąć ten kanał? Nowa wersja Bitmessage jest dostępna: %1. Pobierz ją z https://github.com/Bitmessage/PyBitmessage/releases/latest - + Waiting for PoW to finish... %1% Oczekiwanie na wykonanie dowodu pracy… %1% - + Shutting down Pybitmessage... %1% Zamykanie PyBitmessage… %1% - + Waiting for objects to be sent... %1% Oczekiwanie na wysłanie obiektów… %1% - + Saving settings... %1% Zapisywanie ustawień… %1% - + Shutting down core... %1% Zamykanie rdzenia programu… %1% - + Stopping notifications... %1% Zatrzymywanie powiadomień… %1% - + Shutdown imminent... %1% Zaraz zamknę… %1% @@ -1196,42 +1240,42 @@ Czy na pewno chcesz usunąć ten kanał? %n godzina%n godziny%n godzin%n godzin - + %n day(s) %n dzień%n dni%n dni%n dni - + Shutting down PyBitmessage... %1% Zamykanie PyBitmessage… %1% - + Sent Wysłane - + Generating one new address Generowanie jednego nowego adresu - + Done generating address. Doing work necessary to broadcast it... Adresy wygenerowany. Wykonywanie dowodu pracy niezbędnego na jego rozesłanie… - + Generating %1 new addresses. Generowanie %1 nowych adresów. - + %1 is already in 'Your Identities'. Not adding it again. %1 jest już w 'Twoich tożsamościach'. Nie zostanie tu dodany. - + Done generating address Ukończono generowanie adresów @@ -1241,111 +1285,111 @@ Czy na pewno chcesz usunąć ten kanał? - + Disk full Dysk pełny - + Alert: Your disk or data storage volume is full. Bitmessage will now exit. Uwaga: Twój dysk lub partycja jest pełny. Bitmessage zamknie się. - + Error! Could not find sender address (your address) in the keys.dat file. Błąd! Nie można odnaleźć adresu nadawcy (Twojego adresu) w pliku keys.dat. - + Doing work necessary to send broadcast... Wykonywanie dowodu pracy niezbędnego do wysłania przekazu… - + Broadcast sent on %1 Wysłano: %1 - + Encryption key was requested earlier. Prośba o klucz szyfrujący została już wysłana. - + Sending a request for the recipient's encryption key. Wysyłanie zapytania o klucz szyfrujący odbiorcy. - + Looking up the receiver's public key Wyszukiwanie klucza publicznego odbiorcy - + Problem: Destination is a mobile device who requests that the destination be included in the message but this is disallowed in your settings. %1 Problem: adres docelowy jest urządzeniem przenośnym, które wymaga, aby adres docelowy był zawarty w wiadomości, ale jest to zabronione w Twoich ustawieniach. %1 - + Doing work necessary to send message. There is no required difficulty for version 2 addresses like this. Wykonywanie dowodu pracy niezbędnego do wysłania wiadomości. Nie ma wymaganej trudności dla adresów w wersji 2, takich jak ten adres. - + Doing work necessary to send message. Receiver's required difficulty: %1 and %2 Wykonywanie dowodu pracy niezbędnego do wysłania wiadomości. Odbiorca wymaga trudności: %1 i %2 - + Problem: The work demanded by the recipient (%1 and %2) is more difficult than you are willing to do. %3 Problem: dowód pracy wymagany przez odbiorcę (%1 i %2) jest trudniejszy niż chciałbyś wykonać. %3 - + Problem: You are trying to send a message to yourself or a chan but your encryption key could not be found in the keys.dat file. Could not encrypt message. %1 Problem: próbujesz wysłać wiadomość do siebie lub na kanał, ale Twój klucz szyfrujący nie został znaleziony w pliku keys.dat. Nie można zaszyfrować wiadomości. %1 - + Doing work necessary to send message. Wykonywanie pracy potrzebnej do wysłania wiadomości. - + Message sent. Waiting for acknowledgement. Sent on %1 Wiadomość wysłana. Oczekiwanie na potwierdzenie odbioru. Wysłano o %1 - + Doing work necessary to request encryption key. Wykonywanie pracy niezbędnej do prośby o klucz szyfrujący. - + Broadcasting the public key request. This program will auto-retry if they are offline. Rozsyłanie prośby o klucz publiczny. Program spróbuje ponownie, jeżeli jest on niepołączony. - + Sending public key request. Waiting for reply. Requested at %1 Wysyłanie prośby o klucz publiczny. Oczekiwanie na odpowiedź. Zapytano o %1 - + UPnP port mapping established on port %1 Mapowanie portów UPnP wykonano na porcie %1 - + UPnP port mapping removed Usunięto mapowanie portów UPnP - + Mark all messages as read Oznacz wszystkie jako przeczytane @@ -1355,102 +1399,87 @@ Odbiorca wymaga trudności: %1 i %2 Czy na pewno chcesz oznaczyć wszystkie wiadomości jako przeczytane? - + Doing work necessary to send broadcast. Wykonywanie dowodu pracy niezbędnego do wysłania przekazu. - + Proof of work pending Dowód pracy zawieszony - + %n object(s) pending proof of work Zawieszony dowód pracy %n obiektuZawieszony dowód pracy %n obiektówZawieszony dowód pracy %n obiektówZawieszony dowód pracy %n obiektów - + %n object(s) waiting to be distributed %n obiekt oczekuje na wysłanie%n obiektów oczekuje na wysłanie%n obiektów oczekuje na wysłanie%n obiektów oczekuje na wysłanie - + Wait until these tasks finish? Czy poczekać aż te zadania zostaną zakończone? - - Problem communicating with proxy: %1. Please check your network settings. - Błąd podczas komunikacji z proxy: %1. Proszę sprawdź swoje ustawienia sieci. - - - - SOCKS5 Authentication problem: %1. Please check your SOCKS5 settings. - Problem z autoryzacją SOCKS5: %1. Proszę sprawdź swoje ustawienia SOCKS5. - - - - The time on your computer, %1, may be wrong. Please verify your settings. - Czas na Twoim komputerze, %1, może być błędny. Proszę sprawdź swoje ustawienia. - - - + The name %1 was not found. Ksywka %1 nie została znaleziona. - + The namecoin query failed (%1) Zapytanie namecoin nie powiodło się (%1) - - The namecoin query failed. - Zapytanie namecoin nie powiodło się. + + Unknown namecoin interface type: %1 + Nieznany typ interfejsu namecoin: %1 - - The name %1 has no valid JSON data. - Ksywka %1 nie zawiera prawidłowych danych JSON. + + The namecoin query failed. + Zapytanie namecoin nie powiodło się. - + The name %1 has no associated Bitmessage address. Ksywka %1 nie ma powiązanego adresu Bitmessage. - + Success! Namecoind version %1 running. Namecoind wersja %1 działa poprawnie! - + Success! NMControll is up and running. NMControl działa poprawnie! - + Couldn't understand NMControl. Nie można zrozumieć NMControl. - + The connection to namecoin failed. Nie udało się połączyć z namecoin. - + Your GPU(s) did not calculate correctly, disabling OpenCL. Please report to the developers. Twoje procesory graficzne nie obliczyły poprawnie, wyłączam OpenCL. Prosimy zaraportować przypadek twórcom programu. - + Set notification sound... Ustaw dźwięk powiadomień… - + Welcome to easy and secure Bitmessage * send messages to other people @@ -1464,17 +1493,17 @@ Witamy w przyjaznym i bezpiecznym Bitmessage * dyskutuj na kanałach (chany) z innymi ludźmi - + not recommended for chans niezalecany dla kanałów - + Quiet Mode Tryb cichy - + Problems connecting? Try enabling UPnP in the Network Settings Problem z połączeniem? Spróbuj włączyć UPnP w ustawieniach sieci. @@ -1524,57 +1553,77 @@ Witamy w przyjaznym i bezpiecznym Bitmessage Błąd: coś jest nie tak z adresem odbiorcy %1. - + Error: %1 Błąd: %1 - + From %1 Od %1 - + + Disconnecting + Rozłączanie + + + + Connecting + Łączenie + + + + Bitmessage will now drop all connections. Are you sure? + Bitmessage zatrzyma wszystkie połączenia? Czy kontynuować? + + + + Bitmessage will now start connecting to network. Are you sure? + Bitmessage rozpocznie łączenie z siecią. Czy kontynuować? + + + Synchronisation pending Synchronizacja zawieszona - + Bitmessage hasn't synchronised with the network, %n object(s) to be downloaded. If you quit now, it may cause delivery delays. Wait until the synchronisation finishes? Bitmessage nie zsynchronizował się z siecią, %n obiekt oczekuje na pobranie. Jeżeli zamkniesz go teraz, może to spowodować opóźnienia dostarczeń. Czy poczekać na zakończenie synchronizacji?Bitmessage nie zsynchronizował się z siecią, %n obiekty oczekują na pobranie. Jeżeli zamkniesz go teraz, może to spowodować opóźnienia dostarczeń. Czy poczekać na zakończenie synchronizacji?Bitmessage nie zsynchronizował się z siecią, %n obiektów oczekuje na pobranie. Jeżeli zamkniesz go teraz, może to spowodować opóźnienia dostarczeń. Czy poczekać na zakończenie synchronizacji?Bitmessage nie zsynchronizował się z siecią, %n obiektów oczekuje na pobranie. Jeżeli zamkniesz go teraz, może to spowodować opóźnienia dostarczeń. Czy poczekać na zakończenie synchronizacji? - + Not connected Niepołączony - + Bitmessage isn't connected to the network. If you quit now, it may cause delivery delays. Wait until connected and the synchronisation finishes? Bitmessage nie połączył się z siecią. Jeżeli zamkniesz go teraz, może to spowodować opóźnienia dostarczeń. Czy poczekać na połączenie i zakończenie synchronizacji? - + Waiting for network connection... Oczekiwanie na połączenie sieciowe… - + Waiting for finishing synchronisation... Oczekiwanie na zakończenie synchronizacji… - + You have already set a notification sound for this address book entry. Do you really want to overwrite it? Już ustawiłeś dźwięk powiadomienia dla tego kontaktu. Czy chcesz go zastąpić? - + Error occurred: could not load message from disk. Wystąpił błąd: nie można załadować wiadomości z dysku. - + Display the %n recent broadcast(s) from this address. Wyświetl %n ostatnią wiadomość przekazu z tego adresu.Wyświetl %n ostatnią wiadomość przekazu z tego adresu.Wyświetl %n ostatnich wiadomości przekazu z tego adresu.Wyświetl %n ostatnich wiadomości przekazu z tego adresu. @@ -1594,22 +1643,22 @@ Witamy w przyjaznym i bezpiecznym Bitmessage Wymaż - + inbox odebrane - + new nowe - + sent wysłane - + trash kosz @@ -1617,22 +1666,22 @@ Witamy w przyjaznym i bezpiecznym Bitmessage MessageView - + Follow external link Otwórz zewnętrzne łącze - + The link "%1" will open in a browser. It may be a security risk, it could de-anonymise you or download malicious data. Are you sure? Odnośnik "%1" zostanie otwarty w przeglądarce. Może to spowodować zagrożenie bezpieczeństwa, może on ujawnić Twoją anonimowość lub pobrać złośliwe dane. Czy jesteś pewien? - + HTML detected, click here to display Wykryto HTML, kliknij tu, aby go wyświetlić - + Click here to disable HTML Kliknij tutaj aby wyłączyć HTML @@ -1640,14 +1689,14 @@ Witamy w przyjaznym i bezpiecznym Bitmessage MsgDecode - + The message has an unknown encoding. Perhaps you should upgrade Bitmessage. Wiadomość zawiera nierozpoznane kodowanie. Prawdopodobnie powinieneś zaktualizować Bitmessage. - + Unknown encoding Nierozpoznane kodowanie @@ -1803,7 +1852,7 @@ Generowanie adresów „losowych” jest wybrane domyślnie, jednak deterministy Nazwa pseudo-listy-dyskusyjnej: - + This is a chan address. You cannot use it as a pseudo-mailing list. To jest adres kanału. Nie możesz go użyć jako pseudo-listy-dyskusyjnej. @@ -1874,12 +1923,12 @@ Generowanie adresów „losowych” jest wybrane domyślnie, jednak deterministy Adres - + Blacklist Czarna lista - + Whitelist Biała lista @@ -1958,7 +2007,7 @@ Generowanie adresów „losowych” jest wybrane domyślnie, jednak deterministy Masz połączenia z innymi użytkownikami i twoja zapora sieciowa jest skonfigurowana poprawnie. - + You are using TCP port %1. (This can be changed in the settings). Btimessage używa portu TCP %1. (Można go zmienić w ustawieniach). @@ -2011,27 +2060,27 @@ Generowanie adresów „losowych” jest wybrane domyślnie, jednak deterministy - + Since startup on %1 Od startu programu o %1 - + Down: %1/s Total: %2 Pobieranie: %1/s W całości: %2 - + Up: %1/s Total: %2 Wysyłanie: %1/s W całości: %2 - + Total Connections: %1 Wszystkich połączeń: %1 - + Inventory lookups per second: %1 Zapytań o elementy na sekundę: %1 @@ -2051,27 +2100,27 @@ Generowanie adresów „losowych” jest wybrane domyślnie, jednak deterministy Stan sieci - + byte(s) bajtbajtówbajtówbajtów - + Object(s) to be synced: %n Jeden obiekt do zsynchronizowaniaObieków do zsynchronizowania: %nObieków do zsynchronizowania: %nObieków do zsynchronizowania: %n - + Processed %n person-to-person message(s). Przetworzono %n wiadomość zwykłą.Przetworzono %n wiadomości zwykłych.Przetworzono %n wiadomości zwykłych.Przetworzono %n wiadomości zwykłych. - + Processed %n broadcast message(s). Przetworzono %n wiadomość subskrypcji.Przetworzono %n wiadomości subskrypcji.Przetworzono %n wiadomości subskrypcji.Przetworzono %n wiadomości subskrypcji. - + Processed %n public key(s). Przetworzono %n klucz publiczny.Przetworzono %n kluczy publicznych.Przetworzono %n kluczy publicznych.Przetworzono %n kluczy publicznych. @@ -2197,17 +2246,17 @@ Generowanie adresów „losowych” jest wybrane domyślnie, jednak deterministy newchandialog - + Successfully created / joined chan %1 Pomyślnie utworzono / dołączono do kanału %1 - + Chan creation / joining failed Utworzenie / dołączenie do kanału nie powiodło się - + Chan creation / joining cancelled Utworzenie / dołączenie do kanału przerwane @@ -2215,29 +2264,21 @@ Generowanie adresów „losowych” jest wybrane domyślnie, jednak deterministy proofofwork - + C PoW module built successfully. Moduł C PoW zbudowany poprawnie. - + Failed to build C PoW module. Please build it manually. Nie można zbudować modułu C PoW. Prosimy zbudować go ręcznie. - + C PoW module unavailable. Please build it. Moduł C PoW niedostępny. Prosimy zbudować go. - - qrcodeDialog - - - QR-code - Kod QR - - regenerateAddressesDialog @@ -2294,218 +2335,218 @@ Generowanie adresów „losowych” jest wybrane domyślnie, jednak deterministy settingsDialog - + Settings Ustawienia - + Start Bitmessage on user login Uruchom Bitmessage po zalogowaniu - + Tray Zasobnik systemowy - + Start Bitmessage in the tray (don't show main window) Uruchom Bitmessage w zasobniku (nie pokazuj głównego okna) - + Minimize to tray Minimalizuj do zasobnika - + Close to tray Zamknij do zasobnika - + Show notification when message received Wyświetl powiadomienia o przychodzących wiadomościach - + Run in Portable Mode Uruchom w trybie przenośnym - + In Portable Mode, messages and config files are stored in the same directory as the program rather than the normal application-data folder. This makes it convenient to run Bitmessage from a USB thumb drive. W trybie przenośnym, wiadomości i pliki konfiguracyjne są przechowywane w tym samym katalogu co program, zamiast w osobistym katalogu danych użytkownika. To sprawia, że wygodnie można uruchamiać Bitmessage z pamięci przenośnych. - + Willingly include unencrypted destination address when sending to a mobile device Chętnie umieść niezaszyfrowany adres docelowy podczas wysyłania na urządzenia przenośne. - + Use Identicons Użyj graficznych awatarów - + Reply below Quote Odpowiedź pod cytatem - + Interface Language Język interfejsu - + System Settings system Język systemu - + User Interface Interfejs - + Listening port Port nasłuchujący - + Listen for connections on port: Nasłuchuj połaczeń na porcie: - + UPnP: UPnP: - + Bandwidth limit Limity przepustowości - + Maximum download rate (kB/s): [0: unlimited] Maksymalna prędkość pobierania (w kB/s): [0: bez limitu] - + Maximum upload rate (kB/s): [0: unlimited] Maksymalna prędkość wysyłania (w kB/s): [0: bez limitu] - + Proxy server / Tor Serwer proxy / Tor - + Type: Typ: - + Server hostname: Adres serwera: - + Port: Port: - + Authentication Uwierzytelnienie - + Username: Użytkownik: - + Pass: Hasło: - + Listen for incoming connections when using proxy Nasłuchuj przychodzących połączeń podczas używania proxy - + none brak - + SOCKS4a SOCKS4a - + SOCKS5 SOCKS5 - + Network Settings Sieć - + Total difficulty: Całkowita trudność: - + The 'Total difficulty' affects the absolute amount of work the sender must complete. Doubling this value doubles the amount of work. 'Całkowita trudność' ma wpływ na całkowitą ilość pracy jaką nadawca musi wykonać. Podwojenie tej wartości, podwaja ilość pracy do wykonania. - + Small message difficulty: Trudność małej wiadomości: - + When 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. Kiedy ktoś wysyła Ci wiadomość, jego komputer musi najpierw wykonać dowód pracy. Trudność tej pracy domyślnie wynosi 1. Możesz podwyższyć tę wartość dla nowo-utworzonych adresów podwyższając wartości tutaj. Każdy nowy adres będzie wymagał przez nadawców wyższej trudności. Jest jeden wyjątek: jeżeli dodasz kolegę do swojej książki adresowej, Bitmessage automatycznie powiadomi go kiedy następnym razem wyślesz do niego wiadomość, że musi tylko wykonać minimalną ilość pracy: trudność 1. - + 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. 'Trudność małej wiadomości' głównie ma wpływ na trudność wysyłania małych wiadomości. Podwojenie tej wartości, prawie podwaja pracę potrzebną do wysłania małej wiadomości, ale w rzeczywistości nie ma wpływu na większe wiadomości. - + Demanded difficulty Wymagana trudność - + Here 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. Tutaj możesz ustawić maksymalną ilość pracy jaką zamierzasz wykonać aby wysłać wiadomość innej osobie. Ustawienie tych wartości na 0 oznacza, że każda wartość jest akceptowana. - + Maximum acceptable total difficulty: Maksymalna akceptowalna całkowita trudność: - + Maximum acceptable small message difficulty: Maksymalna akceptowalna trudność dla małych wiadomości: - + Max acceptable difficulty Maksymalna akceptowalna trudność @@ -2515,87 +2556,87 @@ Generowanie adresów „losowych” jest wybrane domyślnie, jednak deterministy - + <html><head/><body><p>Bitmessage 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 <span style=" font-style:italic;">test. </span></p><p>(Getting your own Bitmessage address into Namecoin is still rather difficult).</p><p>Bitmessage can use either namecoind directly or a running nmcontrol instance.</p></body></html> <html><head/><body><p>Bitmessage potrafi wykorzystać inny program oparty na Bitcoinie - Namecoin - aby sprawić adresy czytelnymi dla ludzi. Na przykład, zamiast podawać koledze swój długi adres Bitmessage, możesz po prostu powiedzieć mu aby wysłał wiadomość pod <span style=" font-style:italic;">id/ksywka</span>.</p><p>(Utworzenie swojego adresu Bitmessage w Namecoinie jest ciągle racze trudne).</p><p>Bitmessage może skorzystać albo bezpośrednio z namecoind, albo z działającego wątku nmcontrol.</p></body></html> - + Host: Host: - + Password: Hasło: - + Test Test - + Connect to: Połącz z: - + Namecoind Namecoind - + NMControl NMControl - + Namecoin integration Połączenie z Namecoin - + <html><head/><body><p>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.</p><p>Leave these input fields blank for the default behavior. </p></body></html> <html><head/><body><p>Domyślnie jeżeli wyślesz wiadomość do kogoś i ta osoba będzie poza siecią przez jakiś czas, Bitmessage spróbuje ponownie wysłać wiadomość trochę później, i potem ponownie. Program będzie próbował wysyłać wiadomość do czasu aż odbiorca potwierdzi odbiór. Tutaj możesz zmienić kiedy Bitmessage ma zaprzestać próby wysyłania.</p><p>Pozostaw te poza puste, aby ustawić domyślne zachowanie.</p></body></html> - + Give up after Poddaj się po - + and i - + days dniach - + months. miesiącach. - + Resends Expire Niedoręczone wiadomości - + Hide connection notifications Nie pokazuj powiadomień o połączeniu - + Maximum outbound connections: [0: none] Maksymalnych połączeń wychodzących: [0: brak] - + Hardware GPU acceleration (OpenCL): Przyspieszenie sprzętowe GPU (OpenCL): diff --git a/src/translations/bitmessage_ru.qm b/src/translations/bitmessage_ru.qm index 53d58f012d..8c0269b9e1 100644 Binary files a/src/translations/bitmessage_ru.qm and b/src/translations/bitmessage_ru.qm differ diff --git a/src/translations/bitmessage_ru.ts b/src/translations/bitmessage_ru.ts index ffb2eac99a..4a80f62eca 100644 --- a/src/translations/bitmessage_ru.ts +++ b/src/translations/bitmessage_ru.ts @@ -112,7 +112,7 @@ Please type the desired email address (including @mailchuck.com) below: Mailchuck - + # You can use this to configure your email gateway account # Uncomment the setting you want to use # Here are the options: @@ -153,166 +153,172 @@ Please type the desired email address (including @mailchuck.com) below: # the money directly. To turn it off again, set "feeamount" to 0. Requires # subscription. - # Эти параметры можно использовать для настройки аккаунта email-шлюза -# Раскомментируйте (уберите символ #) те параметры, которые хотите использовать -# Параметры: -# + + + + + # You can use this to configure your email gateway account +# Uncomment the setting you want to use +# Here are the options: +# # pgp: server -# Email-шлюз будет создавать и управлять PGP-ключами за вас, а также подписывать, проверять, -# шифровать и дешифровать от вашего имени. Используйте это, если вы желаете использовать PGP, но очень ленивы. -# Этот настройка требует подписки. +# The email gateway will create and maintain PGP keys for you and sign, verify, +# encrypt and decrypt on your behalf. When you want to use PGP but are lazy, +# use this. Requires subscription. # # pgp: local -# Шлюз электронной почты не будет проводить операции PGP от вашего имени. -# Вы можете не использовать PGP, или использовать его локально. +# The email gateway will not conduct PGP operations on your behalf. You can +# either not use PGP at all, or use it locally. # # attachments: yes -# Вложения во входящий email-сообщениях будут загружены на MEGA.nz, -# вы сможете скачать из оттуда по ссылке. Требуется подписка. +# Incoming attachments in the email will be uploaded to MEGA.nz, and you can +# download them from there by following the link. Requires a subscription. # # attachments: no -# Вложения будут проигнорированы. -# +# Attachments will be ignored. +# # archive: yes -# Входящие email-сообщения будут заархивированы на сервере. Используйте эту настройку -# в целях отладки, или если вам нужно подтверждение email третьей стороной. -# Конечно, это означает, что оператор сервиса будет иметь возможность читать ваши email -# даже после доставки их вам. +# Your incoming emails will be archived on the server. Use this if you need +# help with debugging problems or you need a third party proof of emails. This +# however means that the operator of the service will be able to read your +# emails even after they have been delivered to you. # # archive: no -# Входящие email-сообщения будут удалены с сервера после доставки их вам. +# Incoming emails will be deleted from the server as soon as they are relayed +# to you. # -# masterpubkey_btc: BIP44 xpub key или electrum v1 public seed -# offset_btc: целое число (по умолчанию 0) -# feeamount: число, содержащее до 8 десятичных цифр -# feecurrency: BTC, XBT, USD, EUR или GBP -# Используйте эту настроку, если хотите чтобы отправитель уплатил вознаграждение за отправку email вам. -# Если настройка включена, и неизвестный отправитель посылает вам email, -# то отправителю будет предложено уплатить указанное вознаграждение. -# Схема использует детерминистические открытые ключи, вы получите деньги напрямую. -# Чтобы выключить уплату вознаграждения, установите "feeamount" равным 0. -# Требует подписки. +# masterpubkey_btc: BIP44 xpub key or electrum v1 public seed +# offset_btc: integer (defaults to 0) +# feeamount: number with up to 8 decimal places +# feecurrency: BTC, XBT, USD, EUR or GBP +# Use these if you want to charge people who send you emails. If this is on and +# an unknown person sends you an email, they will be requested to pay the fee +# specified. As this scheme uses deterministic public keys, you will receive +# the money directly. To turn it off again, set "feeamount" to 0. Requires +# subscription. + + MainWindow - + Reply to sender Ответить отправителю - + Reply to channel Ответить в канал - + Add sender to your Address Book Добавить отправителя в адресную книгу - + Add sender to your Blacklist Добавить отправителя в чёрный список - + Move to Trash Поместить в корзину - + Undelete Отменить удаление - + View HTML code as formatted text Просмотреть HTML код как отформатированный текст - + Save message as... Сохранить сообщение как ... - + Mark Unread Отметить как непрочитанное - + New Новый адрес - + Enable Включить - + Disable Выключить - + Set avatar... Установить аватар... - + Copy address to clipboard Скопировать адрес в буфер обмена - + Special address behavior... Особое поведение адресов... - + Email gateway Email-шлюз - + Delete Удалить - + Send message to this address Отправить сообщение на этот адрес - + Subscribe to this address Подписаться на рассылку с этого адреса - + Add New Address Добавить новый адрес - + Copy destination address to clipboard Скопировать адрес отправки в буфер обмена - + Force send Форсировать отправку - + One of your addresses, %1, is an old version 1 address. Version 1 addresses are no longer supported. May we delete it now? Один из Ваших адресов, %1, является устаревшим адресом версии 1. Адреса версии 1 больше не поддерживаются. Хотите ли Вы удалить его сейчас? - + Waiting for their encryption key. Will request it again soon. Ожидаем ключ шифрования от Вашего собеседника. Запрос будет повторен через некоторое время. @@ -322,17 +328,17 @@ Please type the desired email address (including @mailchuck.com) below: - + Queued. В очереди. - + Message sent. Waiting for acknowledgement. Sent at %1 Сообщение отправлено. Ожидаем подтверждения. Отправлено в %1 - + Message sent. Sent at %1 Сообщение отправлено в %1 @@ -342,47 +348,47 @@ Please type the desired email address (including @mailchuck.com) below: - + Acknowledgement of the message received %1 Доставлено в %1 - + Broadcast queued. Рассылка ожидает очереди. - + Broadcast on %1 Рассылка на %1 - + Problem: The work demanded by the recipient is more difficult than you are willing to do. %1 Проблема: Ваш получатель требует более сложных вычислений, чем максимум, указанный в Ваших настройках. %1 - + Problem: The recipient's encryption key is no good. Could not encrypt message. %1 Проблема: ключ получателя неправильный. Невозможно зашифровать сообщение. %1 - + Forced difficulty override. Send should start soon. Форсирована смена сложности. Отправляем через некоторое время. - + Unknown status: %1 %2 Неизвестный статус: %1 %2 - + Not Connected Не соединено - + Show Bitmessage Показать Bitmessage @@ -392,12 +398,12 @@ Please type the desired email address (including @mailchuck.com) below: Отправить - + Subscribe Подписки - + Channel Канал @@ -407,13 +413,13 @@ Please type the desired email address (including @mailchuck.com) below: Выйти - + You may manage your keys by editing the keys.dat file stored in the same directory as this program. It is important that you back up this file. Вы можете управлять Вашими ключами, редактируя файл keys.dat, находящийся в той же папке, что и эта программа. Создайте резервную копию этого файла перед тем как будете его редактировать. - + You may manage your keys by editing the keys.dat file stored in %1 It is important that you back up this file. @@ -422,59 +428,56 @@ It is important that you back up this file. Создайте резервную копию этого файла перед тем как будете его редактировать. - + Open keys.dat? Открыть файл keys.dat? - + You may manage your keys by editing the keys.dat file stored in the same directory as this program. It is important that you back up this file. Would you like to open the file now? (Be sure to close Bitmessage before making any changes.) - Вы можете управлять Вашими ключами, редактируя файл keys.dat, находящийся в той же папке, что и эта программа. -Создайте резервную копию этого файла перед тем как будете его редактировать. Хотели бы Вы открыть этот файл сейчас? -(пожалуйста, закройте Bitmessage до того как Вы внесёте в этот файл какие-либо изменения.) + Вы можете управлять вашими ключами, редактируя файл keys.dat, находящийся в той же папке, что и эта программа. +Создайте резервную копию этого файла перед тем как будете его редактировать. Хотите открыть этот файл сейчас? +(пожалуйста, закройте Bitmessage перед тем, как вносить в него какие-либо изменения.) - + You may manage your keys by editing the keys.dat file stored in %1 It is important that you back up this file. Would you like to open the file now? (Be sure to close Bitmessage before making any changes.) - Вы можете управлять Вашими ключами, редактируя файл keys.dat, находящийся в - %1 -Создайте резервную копию этого файла перед тем как будете его редактировать. Хотели бы Вы открыть этот файл сейчас? -(пожалуйста, закройте Bitmessage до того как Вы внесёте в этот файл какие-либо изменения.) + - + Delete trash? Очистить корзину? - + Are you sure you want to delete all trashed messages? Вы уверены что хотите очистить корзину? - + bad passphrase Неподходящая секретная фраза - + You must type your passphrase. If you don't have one then this is not the form for you. Вы должны ввести секретную фразу. Если Вы не хотите этого делать, то Вы выбрали неправильную опцию. - + Bad address version number Неверный номер версии адреса - + Your address version number must be a number: either 3 or 4. Адрес номера версии должен быть числом: либо 3, либо 4. - + Your address version number must be either 3 or 4. Адрес номера версии должен быть либо 3, либо 4. @@ -544,22 +547,22 @@ It is important that you back up this file. Would you like to open the file now? - + Connection lost Соединение потеряно - + Connected Соединено - + Message trashed Сообщение удалено - + The TTL, or Time-To-Live is the length of time that the network will hold the message. The recipient must get it during this time. If your Bitmessage client does not hear an acknowledgement, it will resend the message automatically. The longer the Time-To-Live, the @@ -570,17 +573,17 @@ It is important that you back up this file. Would you like to open the file now? сообщение. Часто разумным вариантом будет установка TTL на 4 или 5 дней. - + Message too long Сообщение слишком длинное - + The message that you are trying to send is too long by %1 bytes. (The maximum is 261644 bytes). Please cut it down before sending. Сообщение, которое вы пытаетесь отправить, длиннее максимально допустимого на %1 байт. (Максимально допустимое значение 261644 байта). Пожалуйста, сократите сообщение перед отправкой. - + Error: Your account wasn't registered at an email gateway. Sending registration now as %1, please wait for the registration to be processed before retrying sending. Ошибка: ваш аккаунт не зарегистрирован на Email-шлюзе. Отправка регистрации %1, пожалуйста, подождите пока процесс регистрации не завершится, прежде чем попытаться отправить сообщение заново. @@ -625,57 +628,57 @@ It is important that you back up this file. Would you like to open the file now? - + Error: You must specify a From address. If you don't have one, go to the 'Your Identities' tab. Вы должны указать адрес в поле "От кого". Вы можете найти Ваш адрес во вкладке "Ваши Адреса". - + Address version number Версия адреса - + Concerning the address %1, Bitmessage cannot understand address version numbers of %2. Perhaps upgrade Bitmessage to the latest version. - По поводу адреса %1: Bitmessage не поддерживаем адреса версии %2. Возможно, Вам нужно обновить клиент Bitmessage. + По поводу адреса %1: Bitmessage не поддерживает адреса версии %2. Возможно вам нужно обновить клиент Bitmessage. - + Stream number Номер потока - + Concerning the address %1, Bitmessage cannot handle stream numbers of %2. Perhaps upgrade Bitmessage to the latest version. - По поводу адреса %1: Bitmessage не поддерживаем стрим номер %2. Возможно, Вам нужно обновить клиент Bitmessage. + По поводу адреса %1: Bitmessage не поддерживает поток номер %2. Возможно вам нужно обновить клиент Bitmessage. - + Warning: You are currently not connected. Bitmessage will do the work necessary to send the message but it won't send until you connect. - Внимание: Вы не подключены к сети. Bitmessage выполнит работу, требуемую для отправки сообщения, но не отправит его до тех пор, пока Вы не подключитесь. + Внимание: вы не подключены к сети. Bitmessage выполнит работу, требуемую для отправки сообщения, но не отправит его до тех пор, пока вы не подключитесь. - + Message queued. Сообщение в очереди. - + Your 'To' field is empty. Вы не заполнили поле 'Кому'. - + Right click one or more entries in your address book and select 'Send message to this address'. Нажмите правую кнопку мыши на каком-либо адресе и выберите "Отправить сообщение на этот адрес". - + Fetched address from namecoin identity. Получить адрес через Namecoin. - + New Message Новое сообщение @@ -690,57 +693,57 @@ It is important that you back up this file. Would you like to open the file now? - + Address is valid. Адрес введен правильно. - + The address you entered was invalid. Ignoring it. Вы ввели неправильный адрес. Это адрес проигнорирован. - + Error: You cannot add the same address to your address book twice. Try renaming the existing one if you want. Ошибка: Вы не можете добавлять один и тот же адрес в Адресную Книгу несколько раз. Попробуйте переименовать существующий адрес. - + Error: You cannot add the same address to your subscriptions twice. Perhaps rename the existing one if you want. Ошибка: вы не можете добавить один и тот же адрес в ваши подписки дважды. Пожалуйста, переименуйте имеющийся адрес, если хотите. - + Restart Перезапустить - + You must restart Bitmessage for the port number change to take effect. Вы должны перезапустить Bitmessage, чтобы смена номера порта имела эффект. - + Bitmessage will use your proxy from now on but you may want to manually restart Bitmessage now to close existing connections (if any). - Bitmessage будет использовать Ваш прокси, начиная прямо сейчас. Тем не менее Вам имеет смысл перезапустить Bitmessage, чтобы закрыть уже существующие соединения. + Bitmessage будет использовать ваш прокси, начиная с этого момента. Тем не менее, имеет смысл перезапустить Bitmessage, чтобы закрыть уже существующие соединения. - + Number needed Требуется число - + Your maximum download and upload rate must be numbers. Ignoring what you typed. Скорости загрузки и выгрузки должны быть числами. Игнорирую то, что вы набрали. - + Will not resend ever Не пересылать никогда - + Note that the time limit you entered is less than the amount of time Bitmessage waits for the first resend attempt therefore your messages will never be resent. Обратите внимание, что лимит времени, который вы ввели, меньше чем время, которое Bitmessage ждет перед первой попыткой переотправки сообщения, поэтому ваши сообщения никогда не будут переотправлены. @@ -775,24 +778,24 @@ It is important that you back up this file. Would you like to open the file now? Вы действительно должны ввести секретную фразу. - + Address is gone Адрес утерян - + Bitmessage cannot find your address %1. Perhaps you removed it? Bitmessage не может найти Ваш адрес %1. Возможно Вы удалили его? - + Address disabled Адрес выключен - + Error: The address from which you are trying to send is disabled. You'll have to enable it on the 'Your Identities' tab before using it. - Ошибка: адрес, с которого Вы пытаетесь отправить, выключен. Вам нужно будет включить этот адрес во вкладке "Ваши адреса". + Ошибка: адрес, с которого вы пытаетесь отправить, выключен. Вам нужно включить этот адрес во вкладке "Ваши адреса" перед использованием. @@ -800,42 +803,42 @@ It is important that you back up this file. Would you like to open the file now? - + Entry added to the blacklist. Edit the label to your liking. Запись добавлена в чёрный список. Измените название по своему вкусу. - + Error: You cannot add the same address to your blacklist twice. Try renaming the existing one if you want. Ошибка: вы не можете добавить один и тот же адрес в чёрный список дважды. Попробуйте переименовать существующий адрес. - + Moved items to trash. Удалено в корзину. - + Undeleted item. Элемент восстановлен. - + Save As... Сохранить как ... - + Write error. Ошибка записи. - + No addresses selected. Вы не выбрали адрес. - + If you delete the subscription, messages that you already received will become inaccessible. Maybe you can consider disabling the subscription instead. Disabled subscriptions will not receive new messages, but you can still view messages you already received. Are you sure you want to delete the subscription? @@ -844,7 +847,7 @@ Are you sure you want to delete the subscription? Вы уверены, что хотите отменить подписку? - + If you delete the channel, messages that you already received will become inaccessible. Maybe you can consider disabling the channel instead. Disabled channels will not receive new messages, but you can still view messages you already received. Are you sure you want to delete the channel? @@ -853,32 +856,32 @@ Are you sure you want to delete the channel? Вы уверены, что хотите удалить канал? - + Do you really want to remove this avatar? Вы уверены, что хотите удалить этот аватар? - + You have already set an avatar for this address. Do you really want to overwrite it? У вас уже есть аватар для этого адреса. Вы уверены, что хотите перезаписать аватар? - + Start-on-login not yet supported on your OS. Запуск программы при входе в систему ещё не поддерживается в вашей операционной системе. - + Minimize-to-tray not yet supported on your OS. Сворачивание в трей ещё не поддерживается в вашей операционной системе. - + Tray notifications not yet supported on your OS. Уведомления в трее ещё не поддерживаеются в вашей операционной системе. - + Testing... Проверяем... @@ -1128,7 +1131,7 @@ Are you sure you want to delete the channel? Подключить или создать чан - + All accounts Все аккаунты @@ -1138,12 +1141,12 @@ Are you sure you want to delete the channel? Увеличение %1% - + Error: You cannot add the same address to your list twice. Perhaps rename the existing one if you want. Ошибка: вы не можете добавить один и тот же адрес в ваш лист дважды. Попробуйте переименовать существующий адрес. - + Add new entry Добавить новую запись @@ -1153,42 +1156,42 @@ Are you sure you want to delete the channel? - + New version of PyBitmessage is available: %1. Download it from https://github.com/Bitmessage/PyBitmessage/releases/latest Доступна новая версия PyBitmessage: %1. Загрузите её: https://github.com/Bitmessage/PyBitmessage/releases/latest - + Waiting for PoW to finish... %1% Ожидание окончания PoW... %1% - + Shutting down Pybitmessage... %1% Завершение PyBitmessage... %1% - + Waiting for objects to be sent... %1% Ожидание отправки объектов... %1% - + Saving settings... %1% Сохранение настроек... %1% - + Shutting down core... %1% Завершение работы ядра... %1% - + Stopping notifications... %1% Остановка сервиса уведомлений... %1% - + Shutdown imminent... %1% Завершение вот-вот произойдет... %1% @@ -1198,42 +1201,42 @@ Are you sure you want to delete the channel? %n час%n часа%n часов%n час(а/ов) - + %n day(s) %n день%n дня%n дней%n дней - + Shutting down PyBitmessage... %1% Завершение PyBitmessage... %1% - + Sent Отправлено - + Generating one new address Создание одного нового адреса - + Done generating address. Doing work necessary to broadcast it... Создание адреса завершено. Выполнение работы, требуемой для его рассылки... - + Generating %1 new addresses. Создание %1 новых адресов. - + %1 is already in 'Your Identities'. Not adding it again. %1 уже имеется в ваших адресах. Не добавляю его снова. - + Done generating address Создание адресов завершено. @@ -1243,96 +1246,96 @@ Are you sure you want to delete the channel? - + Disk full Диск переполнен - + Alert: Your disk or data storage volume is full. Bitmessage will now exit. Внимание: свободное место на диске закончилось. Bitmessage завершит свою работу. - + Error! Could not find sender address (your address) in the keys.dat file. Ошибка: невозможно найти адрес отправителя (ваш адрес) в файле ключей keys.dat - + Doing work necessary to send broadcast... Выполнение работы, требуемой для рассылки... - + Broadcast sent on %1 Рассылка отправлена на %1 - + Encryption key was requested earlier. Ключ шифрования запрошен ранее. - + Sending a request for the recipient's encryption key. Отправка запроса ключа шифрования получателя. - + Looking up the receiver's public key Поиск открытого ключа получателя - + Problem: Destination is a mobile device who requests that the destination be included in the message but this is disallowed in your settings. %1 Проблема: адресат является мобильным устройством, которое требует, чтобы адрес назначения был включен в сообщение, однако, это запрещено в ваших настройках. %1 - + Doing work necessary to send message. There is no required difficulty for version 2 addresses like this. Выполнение работы, требуемой для отправки сообщения. Для адреса версии 2 (как этот), не требуется указание сложности. - + Doing work necessary to send message. Receiver's required difficulty: %1 and %2 Выполнение работы, требуемой для отправки сообщения. Получатель запросил сложность: %1 и %2 - + Problem: The work demanded by the recipient (%1 and %2) is more difficult than you are willing to do. %3 Проблема: сложность, затребованная получателем (%1 и %2) гораздо больше, чем вы готовы сделать. %3 - + Problem: You are trying to send a message to yourself or a chan but your encryption key could not be found in the keys.dat file. Could not encrypt message. %1 Проблема: вы пытаетесь отправить сообщение самому себе или в чан, но ваш ключ шифрования не найден в файле ключей keys.dat. Невозможно зашифровать сообщение. %1 - + Doing work necessary to send message. Выполнение работы, требуемой для отправки сообщения. - + Message sent. Waiting for acknowledgement. Sent on %1 Отправлено. Ожидаем подтверждения. Отправлено в %1 - + Doing work necessary to request encryption key. Выполнение работы, требуемой для запроса ключа шифрования. - + Broadcasting the public key request. This program will auto-retry if they are offline. Рассылка запросов открытого ключа шифрования. Программа будет повторять попытки, если они оффлайн. - + Sending public key request. Waiting for reply. Requested at %1 Отправка запроса открытого ключа шифрования. Ожидание ответа. Запрошено в %1 @@ -1347,107 +1350,97 @@ Receiver's required difficulty: %1 and %2 Распределение портов UPnP отменено - + Mark all messages as read Отметить все сообщения как прочтенные - + Are you sure you would like to mark all messages read? Вы уверены, что хотите отметить все сообщения как прочтенные? - + Doing work necessary to send broadcast. Выполнение работы, требуемой для отправки рассылки. - + Proof of work pending Ожидается доказательство работы - + %n object(s) pending proof of work %n объект в ожидании доказательства работы%n объекта в ожидании доказательства работы%n объектов в ожидании доказательства работы%n объектов в ожидании доказательства работы - + %n object(s) waiting to be distributed %n объект ожидает раздачи%n объекта ожидают раздачи%n объектов ожидают раздачи%n объектов ожидают раздачи - + Wait until these tasks finish? Подождать завершения этих задач? - - Problem communicating with proxy: %1. Please check your network settings. - Проблема коммуникации с прокси: %1. Пожалуйста, проверьте ваши сетевые настройки. - - - - SOCKS5 Authentication problem: %1. Please check your SOCKS5 settings. - Проблема аутентификации SOCKS5: %1. Пожалуйста, проверьте настройки SOCKS5. - - - - The time on your computer, %1, may be wrong. Please verify your settings. - Время на компьютере, %1, возможно неправильное. Пожалуйста, проверьте ваши настройки. - - - + The name %1 was not found. Имя %1 не найдено. - + The namecoin query failed (%1) Запрос к namecoin не удался (%1). - + The namecoin query failed. Запрос к namecoin не удался. - + The name %1 has no valid JSON data. Имя %1 не содержит корректных данных JSON. - + The name %1 has no associated Bitmessage address. Имя %1 не имеет связанного адреса Bitmessage. - + Success! Namecoind version %1 running. Успех! Namecoind версии %1 работает. - + Success! NMControll is up and running. Успех! NMControl запущен и работает. - + Couldn't understand NMControl. Не удалось разобрать ответ NMControl. + + + The connection to namecoin failed. + Не удалось соединиться с namecoin. + Your GPU(s) did not calculate correctly, disabling OpenCL. Please report to the developers. Ваша видеокарта вычислила неправильно, отключаем OpenCL. Пожалуйста, сообщите разработчикам. - + Set notification sound... Установить звук уведомления... - + Welcome to easy and secure Bitmessage * send messages to other people @@ -1461,119 +1454,119 @@ Receiver's required difficulty: %1 and %2 * участвуйте в обсуждениях в чанах - + not recommended for chans не рекомендовано для чанов - + Quiet Mode Тихий режим - + Problems connecting? Try enabling UPnP in the Network Settings Проблемы подключения? Попробуйте включить UPnP в сетевых настройках. - + You are trying to send an email instead of a bitmessage. This requires registering with a gateway. Attempt to register? Вы пытаетесь отправить email вместо bitmessage. Для этого нужно зарегистрироваться на шлюзе. Попробовать зарегистрироваться? - + Error: Bitmessage addresses start with BM- Please check the recipient address %1 Ошибка: адреса Bitmessage начинаются с "BM-". Пожалуйста, проверьте адрес получателя %1. - + Error: The recipient address %1 is not typed or copied correctly. Please check it. Ошибка: адрес получателя %1 набран или скопирован неправильно. Пожалуйста, проверьте его. - + Error: The recipient address %1 contains invalid characters. Please check it. Ошибка: адрес получателя %1 содержит недопустимые символы. Пожалуйста, проверьте его. - + Error: The version of the recipient address %1 is too high. Either you need to upgrade your Bitmessage software or your acquaintance is being clever. Ошибка: версия адреса получателя %1 слишком высокая. Либо вам нужно обновить программу Bitmessage, либо ваш знакомый - умник. - + Error: Some data encoded in the recipient address %1 is too short. There might be something wrong with the software of your acquaintance. Ошибка: часть данных, закодированных в адресе получателя %1 слишком короткая. Видимо, что-то не так с программой, используемой вашим знакомым. - + Error: Some data encoded in the recipient address %1 is too long. There might be something wrong with the software of your acquaintance. Ошибка: часть данных, закодированных в адресе получателя %1 слишком длинная. Видимо, что-то не так с программой, используемой вашим знакомым. - + Error: Some data encoded in the recipient address %1 is malformed. There might be something wrong with the software of your acquaintance. Ошибка: часть данных, закодированных в адресе получателя %1 сформирована неправильно. Видимо, что-то не так с программой, используемой вашим знакомым. - + Error: Something is wrong with the recipient address %1. Ошибка: что-то не так с адресом получателя %1. - + Error: %1 Ошибка: %1 - + From %1 От %1 - + Synchronisation pending Ожидается синхронизация - + Bitmessage hasn't synchronised with the network, %n object(s) to be downloaded. If you quit now, it may cause delivery delays. Wait until the synchronisation finishes? Bitmessage не синхронизирован с сетью, незагруженных объектов: %n. Выход сейчас может привести к задержкам доставки. Подождать завершения синхронизации?Bitmessage не синхронизирован с сетью, незагруженных объектов: %n. Выход сейчас может привести к задержкам доставки. Подождать завершения синхронизации?Bitmessage не синхронизирован с сетью, незагруженных объектов: %n. Выход сейчас может привести к задержкам доставки. Подождать завершения синхронизации?Bitmessage не синхронизирован с сетью, незагруженных объектов: %n. Выход сейчас может привести к задержкам доставки. Подождать завершения синхронизации? - + Not connected Не подключено - + Bitmessage isn't connected to the network. If you quit now, it may cause delivery delays. Wait until connected and the synchronisation finishes? Bitmessage не подключен к сети. Выход сейчас может привести к задержкам доставки. Подождать подключения и завершения синхронизации? - + Waiting for network connection... Ожидание сетевого подключения... - + Waiting for finishing synchronisation... Ожидание окончания синхронизации... - + You have already set a notification sound for this address book entry. Do you really want to overwrite it? У вас уже есть звук уведомления для этого адресата. Вы уверены, что хотите перезаписать звук уведомления? - + Error occurred: could not load message from disk. Произошла ошибка: не удалось загрузить сообщение с диска. Display the %n recent broadcast(s) from this address. - + Показать %1 прошлую рассылку с этого адреса.Показать %1 прошлых рассылки с этого адреса.Показать %1 прошлых рассылок с этого адреса.Показать %1 прошлых рассылок с этого адреса. @@ -1591,24 +1584,24 @@ Receiver's required difficulty: %1 and %2 Очистить - + inbox входящие - + new новые - + sent отправленные - + trash - + корзина @@ -1637,14 +1630,14 @@ Receiver's required difficulty: %1 and %2 MsgDecode - + The message has an unknown encoding. Perhaps you should upgrade Bitmessage. Сообщение в неизвестной кодировке. Возможно, вам следует обновить Bitmessage. - + Unknown encoding Неизвестная кодировка @@ -1870,12 +1863,12 @@ The 'Random Number' option is selected by default but deterministic ad Адрес - + Blacklist Чёрный список - + Whitelist Белый список @@ -2226,14 +2219,6 @@ The 'Random Number' option is selected by default but deterministic ad Модуль C для PoW недоступен. Пожалуйста, соберите его. - - qrcodeDialog - - - QR-code - QR-код - - regenerateAddressesDialog @@ -2290,218 +2275,218 @@ The 'Random Number' option is selected by default but deterministic ad settingsDialog - + Settings Настройки - + Start Bitmessage on user login Запускать Bitmessage при входе в систему - + Tray Трей - + Start Bitmessage in the tray (don't show main window) Запускать Bitmessage в свернутом виде (не показывать главное окно) - + Minimize to tray Сворачивать в трей - + Close to tray Закрывать в трей - + Show notification when message received Показывать уведомления при получении новых сообщений - + Run in Portable Mode Запустить в переносном режиме - + In Portable Mode, messages and config files are stored in the same directory as the program rather than the normal application-data folder. This makes it convenient to run Bitmessage from a USB thumb drive. В переносном режиме, все сообщения и конфигурационные файлы сохраняются в той же самой папке что и сама программа. Это делает более удобным использование Bitmessage с USB-флэшки. - + Willingly include unencrypted destination address when sending to a mobile device Специально прикреплять незашифрованный адрес получателя, когда посылаем на мобильное устройство - + Use Identicons Включить иконки адресов - + Reply below Quote Отвечать после цитаты - + Interface Language Язык интерфейса - + System Settings system Язык по умолчанию - + User Interface Пользовательские - + Listening port Порт прослушивания - + Listen for connections on port: Прослушивать соединения на порту: - + UPnP: UPnP: - + Bandwidth limit Ограничение пропускной способности - + Maximum download rate (kB/s): [0: unlimited] Максимальная скорость загрузки (кБ/с): [0: не ограничено] - + Maximum upload rate (kB/s): [0: unlimited] Максимальная скорость отдачи (кБ/с): [0: не ограничено] - + Proxy server / Tor Прокси сервер / Tor - + Type: Тип: - + Server hostname: Адрес сервера: - + Port: Порт: - + Authentication Авторизация - + Username: Имя пользователя: - + Pass: Пароль: - + Listen for incoming connections when using proxy Прослушивать входящие соединения если используется прокси - + none отсутствует - + SOCKS4a SOCKS4a - + SOCKS5 SOCKS5 - + Network Settings Сетевые настройки - + Total difficulty: Общая сложность: - + The 'Total difficulty' affects the absolute amount of work the sender must complete. Doubling this value doubles the amount of work. "Общая сложность" влияет на абсолютное количество вычислений, которые отправитель должен провести, чтобы отправить сообщение. Увеличив это число в два раза, вы увеличите в два раза объем требуемых вычислений. - + Small message difficulty: Сложность для маленьких сообщений: - + When 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. Когда кто-либо отправляет Вам сообщение, его компьютер должен сперва решить определённую вычислительную задачу. Сложность этой задачи по умолчанию равна 1. Вы можете повысить эту сложность для новых адресов, которые Вы создадите, здесь. Таким образом, любые новые адреса, которые Вы создадите, могут требовать от отправителей сложность большую чем 1. Однако, есть одно исключение: если Вы специально добавите Вашего собеседника в адресную книгу, то Bitmessage автоматически уведомит его о том, что для него минимальная сложность будет составлять всегда всего лишь 1. - + 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. "Сложность для маленьких сообщений" влияет исключительно на небольшие сообщения. Увеличив это число в два раза, вы сделаете отправку маленьких сообщений в два раза сложнее, в то время как сложность отправки больших сообщений не изменится. - + Demanded difficulty Требуемая сложность - + Here 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. Здесь Вы можете установить максимальную вычислительную работу, которую Вы согласны проделать, чтобы отправить сообщение другому пользователю. Ноль означает, что любое значение допустимо. - + Maximum acceptable total difficulty: Максимально допустимая общая сложность: - + Maximum acceptable small message difficulty: Максимально допустимая сложность для маленький сообщений: - + Max acceptable difficulty Макс допустимая сложность @@ -2511,87 +2496,87 @@ The 'Random Number' option is selected by default but deterministic ad - + <html><head/><body><p>Bitmessage 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 <span style=" font-style:italic;">test. </span></p><p>(Getting your own Bitmessage address into Namecoin is still rather difficult).</p><p>Bitmessage can use either namecoind directly or a running nmcontrol instance.</p></body></html> <html><head/><body><p>Bitmessage умеет пользоваться программой Namecoin для того, чтобы сделать адреса более дружественными для пользователей. Например, вместо того, чтобы диктовать Вашему другу длинный и нудный адрес Bitmessage, Вы можете попросить его отправить сообщение на адрес вида <span style=" font-style:italic;">test. </span></p><p>(Перенести Ваш Bitmessage адрес в Namecoin по-прежнему пока довольно сложно).</p><p>Bitmessage может использовать либо прямо namecoind, либо уже запущенную программу nmcontrol.</p></body></html> - + Host: Адрес: - + Password: Пароль: - + Test Проверить - + Connect to: - Подсоединиться к: + Соединиться с: - + Namecoind Namecoind - + NMControl NMControl - + Namecoin integration Интеграция с Namecoin - + <html><head/><body><p>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.</p><p>Leave these input fields blank for the default behavior. </p></body></html> <html><head/><body><p>По умолчанию, когда вы отправляете сообщение кому-либо, и адресат находится оффлайн несколько дней, ваш Bitmessage перепосылает сообщение. Это будет продолжаться с увеличивающимся по экспоненте интервалом; сообщение будет переотправляться, например, через 5, 10, 20 дней, пока адресат их запрашивает. Здесь вы можете изменить это поведение, заставив Bitmessage прекращать переотправку по прошествии указанного количества дней или месяцев.</p><p>Оставьте поля пустыми, чтобы вернуться к поведению по умолчанию.</p></body></html> - + Give up after Прекратить через - + and и - + days дней - + months. месяцев. - + Resends Expire Окончание попыток отправки - + Hide connection notifications Спрятать уведомления о подключениях - + Maximum outbound connections: [0: none] Максимальное число исходящих подключений: [0: неограничено] - + Hardware GPU acceleration (OpenCL): Аппаратное ускорение GPU diff --git a/src/translations/bitmessage_zh_cn.qm b/src/translations/bitmessage_zh_cn.qm index 204047e143..7cb1898395 100644 Binary files a/src/translations/bitmessage_zh_cn.qm and b/src/translations/bitmessage_zh_cn.qm differ diff --git a/src/translations/bitmessage_zh_cn.ts b/src/translations/bitmessage_zh_cn.ts index 3a5c4aab07..474f8c6c6a 100644 --- a/src/translations/bitmessage_zh_cn.ts +++ b/src/translations/bitmessage_zh_cn.ts @@ -60,27 +60,27 @@ @mailchuck.com - + Registration failed: 注册失败: - + The requested email address is not available, please try a new one. 请求的电子邮件地址不可用,请换一个新的试试。 - + Sending email gateway registration request 发送电​​子邮件网关注册请求 - + Sending email gateway unregistration request 发送电​​子邮件网关注销请求 - + Sending email gateway status request 发送电​​子邮件网关状态请求 @@ -112,7 +112,7 @@ Please type the desired email address (including @mailchuck.com) below: Mailchuck - + # You can use this to configure your email gateway account # Uncomment the setting you want to use # Here are the options: @@ -153,17 +153,61 @@ Please type the desired email address (including @mailchuck.com) below: # the money directly. To turn it off again, set "feeamount" to 0. Requires # subscription. - #您可以用它来配置你的电子邮件网关帐户 + + + + + # You can use this to configure your email gateway account +# Uncomment the setting you want to use +# Here are the options: +# +# pgp: server +# The email gateway will create and maintain PGP keys for you and sign, verify, +# encrypt and decrypt on your behalf. When you want to use PGP but are lazy, +# use this. Requires subscription. +# +# pgp: local +# The email gateway will not conduct PGP operations on your behalf. You can +# either not use PGP at all, or use it locally. +# +# attachments: yes +# Incoming attachments in the email will be uploaded to MEGA.nz, and you can +# download them from there by following the link. Requires a subscription. +# +# attachments: no +# Attachments will be ignored. +# +# archive: yes +# Your incoming emails will be archived on the server. Use this if you need +# help with debugging problems or you need a third party proof of emails. This +# however means that the operator of the service will be able to read your +# emails even after they have been delivered to you. +# +# archive: no +# Incoming emails will be deleted from the server as soon as they are relayed +# to you. +# +# masterpubkey_btc: BIP44 xpub key or electrum v1 public seed +# offset_btc: integer (defaults to 0) +# feeamount: number with up to 8 decimal places +# feecurrency: BTC, XBT, USD, EUR or GBP +# Use these if you want to charge people who send you emails. If this is on and +# an unknown person sends you an email, they will be requested to pay the fee +# specified. As this scheme uses deterministic public keys, you will receive +# the money directly. To turn it off again, set "feeamount" to 0. Requires +# subscription. + + #您可以用它来配置您的电子邮件网关帐户 #取消您要使用的设定 #这里的选项: # # pgp: server #电子邮件网关将创建和维护PGP密钥,为您签名和验证, -#代表加密和解密。当你想使用PGP,但懒惰, +#代表加密和解密。当您想使用PGP,但懒惰, #用这个。需要订阅。 # # pgp: local -#电子邮件网关不会代你进行PGP操作。您可以 +#电子邮件网关不会代您进行PGP操作。您可以 #选择或者不使用PGP, 或在本地使用它。 # # attachement: yes @@ -177,7 +221,7 @@ Please type the desired email address (including @mailchuck.com) below: #您收到的邮件将在服务器上存档。如果您有需要请使用 #帮助调试问题,或者您需要第三方电子邮件的证明。这 #然而,意味着服务的操作运将能够读取您的 -#电子邮件即使电子邮件已经传送给你。 +#电子邮件即使电子邮件已经传送给您。 # # archive: no # 已传入的电子邮件将从服务器被删除只要他们已中继。 @@ -186,132 +230,133 @@ Please type the desired email address (including @mailchuck.com) below: #offset_btc:整数(默认为0) #feeamount:多达8位小数 #feecurrency号:BTC,XBT,美元,欧元或英镑 -#用这些,如果你想主管谁送你的电子邮件的人。如果这是在和 +#用这些,如果您想主管谁送您的电子邮件的人。如果这是在和 #一个不明身份的人向您发送一封电子邮件,他们将被要求支付规定的费用 -#。由于这个方案使用确定性的公共密钥,你会直接接收 +#。由于这个方案使用确定性的公共密钥,您会直接接收 #钱。要再次将其关闭,设置“feeamount”0 -#需要订阅。 +#需要订阅。 + MainWindow - + Reply to sender 回复发件人 - + Reply to channel 回复通道 - + Add sender to your Address Book 将发送者添加到您的通讯簿 - + Add sender to your Blacklist 将发件人添加到您的黑名单 - + Move to Trash 移入回收站 - + Undelete 取消删除 - + View HTML code as formatted text 作为HTML查看 - + Save message as... 将消息保存为... - + Mark Unread 标记为未读 - + New 新建 - + Enable 启用 - + Disable 禁用 - + Set avatar... 设置头像... - + Copy address to clipboard 将地址复制到剪贴板 - + Special address behavior... 特别的地址行为... - + Email gateway 电子邮件网关 - + Delete 删除 - + Send message to this address 发送消息到这个地址 - + Subscribe to this address 订阅到这个地址 - + Add New Address 创建新地址 - + Copy destination address to clipboard 复制目标地址到剪贴板 - + Force send 强制发送 - + One of your addresses, %1, is an old version 1 address. Version 1 addresses are no longer supported. May we delete it now? 您的地址中的一个, %1,是一个过时的版本1地址. 版本1地址已经不再受到支持了. 我们可以将它删除掉么? - + Waiting for their encryption key. Will request it again soon. 正在等待他们的加密密钥,我们会在稍后再次请求。 @@ -321,17 +366,17 @@ Please type the desired email address (including @mailchuck.com) below: - + Queued. 已经添加到队列。 - + Message sent. Waiting for acknowledgement. Sent at %1 消息已经发送. 正在等待回执. 发送于 %1 - + Message sent. Sent at %1 消息已经发送. 发送于 %1 @@ -341,7 +386,7 @@ Please type the desired email address (including @mailchuck.com) below: - + Acknowledgement of the message received %1 消息的回执已经收到于 %1 @@ -351,37 +396,37 @@ Please type the desired email address (including @mailchuck.com) below: 广播已经添加到队列中。 - + Broadcast on %1 已经广播于 %1 - + Problem: The work demanded by the recipient is more difficult than you are willing to do. %1 错误: 收件人要求的做工量大于我们的最大接受做工量。 %1 - + Problem: The recipient's encryption key is no good. Could not encrypt message. %1 错误: 收件人的加密密钥是无效的。不能加密消息。 %1 - + Forced difficulty override. Send should start soon. 已经忽略最大做工量限制。发送很快就会开始。 - + Unknown status: %1 %2 未知状态: %1 %2 - + Not Connected 未连接 - + Show Bitmessage 显示比特信 @@ -391,12 +436,12 @@ Please type the desired email address (including @mailchuck.com) below: 发送 - + Subscribe 订阅 - + Channel 频道 @@ -406,66 +451,66 @@ Please type the desired email address (including @mailchuck.com) below: 退出 - + You may manage your keys by editing the keys.dat file stored in the same directory as this program. It is important that you back up this file. 您可以通过编辑和程序储存在同一个目录的 keys.dat 来编辑密钥。备份这个文件十分重要。 - + You may manage your keys by editing the keys.dat file stored in %1 It is important that you back up this file. 您可以通过编辑储存在 %1 的 keys.dat 来编辑密钥。备份这个文件十分重要。 - + Open keys.dat? 打开 keys.dat ? - + You may manage your keys by editing the keys.dat file stored in the same directory as this program. It is important that you back up this file. Would you like to open the file now? (Be sure to close Bitmessage before making any changes.) 您可以通过编辑和程序储存在同一个目录的 keys.dat 来编辑密钥。备份这个文件十分重要。您现在想打开这个文件么?(请在进行任何修改前关闭比特信) - + You may manage your keys by editing the keys.dat file stored in %1 It is important that you back up this file. Would you like to open the file now? (Be sure to close Bitmessage before making any changes.) 您可以通过编辑储存在 %1 的 keys.dat 来编辑密钥。备份这个文件十分重要。您现在想打开这个文件么?(请在进行任何修改前关闭比特信) - + Delete trash? 清空回收站? - + Are you sure you want to delete all trashed messages? 您确定要删除全部被回收的消息么? - + bad passphrase 错误的密钥 - + You must type your passphrase. If you don't have one then this is not the form for you. 您必须输入您的密钥。如果您没有的话,这个表单不适用于您。 - + Bad address version number 地址的版本号无效 - + Your address version number must be a number: either 3 or 4. 您的地址的版本号必须是一个数字: 3 或 4. - + Your address version number must be either 3 or 4. 您的地址的版本号必须是 3 或 4. @@ -535,12 +580,12 @@ It is important that you back up this file. Would you like to open the file now? - + Connection lost 连接已丢失 - + Connected 已经连接 @@ -555,8 +600,8 @@ It is important that you back up this file. Would you like to open the file now? The recipient must get it during this time. If your Bitmessage client does not hear an acknowledgement, it will resend the message automatically. The longer the Time-To-Live, the more work your computer must do to send the message. A Time-To-Live of four or five days is often appropriate. - 這TTL,或Time-To-Time是保留信息网络时间的长度. -收件人必须在此期间得到它. 如果您的Bitmessage客户沒有听到确认, 它会自动重新发送信息. Time-To-Live的时间越长, 您的电脑必须要做更多工作來发送信息. 四天或五天的 Time-To-Time, 经常是合适的. + TTL,或Time-To-Time是保留信息网络时间的长度. +收件人必须在此期间得到它. 如果您的Bitmessage客户沒有听到确认, 它会自动重新发送信息. Time-To-Live的时间越长, 您的电脑必须要做更多工作來发送信息. 四天或五天的 Time-To-Time, 通常是合适的. @@ -566,7 +611,7 @@ It is important that you back up this file. Would you like to open the file now? The message that you are trying to send is too long by %1 bytes. (The maximum is 261644 bytes). Please cut it down before sending. - 你正在尝试发送的信息已超过%1个字节太长, (最大为261644个字节). 发送前请剪下来。 + 您正在尝试发送的信息已超过 %1 个字节太长(最大为261644个字节),发送前请先缩短一些。 @@ -626,7 +671,7 @@ It is important that you back up this file. Would you like to open the file now? Concerning the address %1, Bitmessage cannot understand address version numbers of %2. Perhaps upgrade Bitmessage to the latest version. - 地址 %1 的地址版本号 %2 无法被比特信理解。也许你应该升级你的比特信到最新版本。 + 地址 %1 的地址版本号 %2 无法被比特信理解。也许您应该升级您的比特信到最新版本。 @@ -636,7 +681,7 @@ It is important that you back up this file. Would you like to open the file now? Concerning the address %1, Bitmessage cannot handle stream numbers of %2. Perhaps upgrade Bitmessage to the latest version. - 地址 %1 的节点流序号 %2 无法被比特信理解。也许你应该升级你的比特信到最新版本。 + 地址 %1 的节点流序号 %2 无法被比特信所理解。也许您应该升级您的比特信到最新版本。 @@ -659,12 +704,12 @@ It is important that you back up this file. Would you like to open the file now? 在您的地址本的一个条目上右击,之后选择”发送消息到这个地址“。 - + Fetched address from namecoin identity. 已经自namecoin接收了地址。 - + New Message 新消息 @@ -679,47 +724,47 @@ It is important that you back up this file. Would you like to open the file now? - + Address is valid. 地址有效。 - + The address you entered was invalid. Ignoring it. 您输入的地址是无效的,将被忽略。 - + Error: You cannot add the same address to your address book twice. Try renaming the existing one if you want. 错误:您无法将一个地址添加到您的地址本两次,请尝试重命名已经存在的那个。 - + Error: You cannot add the same address to your subscriptions twice. Perhaps rename the existing one if you want. 错误: 您不能在同一地址添加到您的订阅两次. 也许您可重命名现有之一. - + Restart 重启 - + You must restart Bitmessage for the port number change to take effect. 您必须重启以便使比特信对于使用的端口的改变生效。 - + Bitmessage will use your proxy from now on but you may want to manually restart Bitmessage now to close existing connections (if any). 比特信将会从现在开始使用代理,但是您可能想手动重启比特信以便使之前的连接关闭(如果有的话)。 - + Number needed 需求数字 - + Your maximum download and upload rate must be numbers. Ignoring what you typed. 您最大的下载和上传速率必须是数字. 忽略您键入的内容. @@ -744,42 +789,42 @@ It is important that you back up this file. Would you like to open the file now? - + Passphrase mismatch 密钥不匹配 - + The passphrase you entered twice doesn't match. Try again. 您两次输入的密码并不匹配,请再试一次。 - + Choose a passphrase 选择一个密钥 - + You really do need a passphrase. 您真的需要一个密码。 - + Address is gone 已经失去了地址 - + Bitmessage cannot find your address %1. Perhaps you removed it? - 比特信无法找到你的地址 %1。 也许你已经把它删掉了? + 比特信无法找到您的地址 %1 ,也许您已经把它删掉了? - + Address disabled 地址已经禁用 - + Error: The address from which you are trying to send is disabled. You'll have to enable it on the 'Your Identities' tab before using it. 错误: 您想以一个您已经禁用的地址发出消息。在使用之前您需要在“您的身份”处再次启用。 @@ -789,85 +834,85 @@ It is important that you back up this file. Would you like to open the file now? - + Entry added to the blacklist. Edit the label to your liking. 条目添加到黑名单. 根据自己的喜好编辑标签. - + Error: You cannot add the same address to your blacklist twice. Try renaming the existing one if you want. 错误: 您不能在同一地址添加到您的黑名单两次. 也许您可重命名现有之一. - + Moved items to trash. 已经移动项目到回收站。 - + Undeleted item. 未删除的项目。 - + Save As... 另存为... - + Write error. 写入失败。 - + No addresses selected. 没有选择地址。 - + If you delete the subscription, messages that you already received will become inaccessible. Maybe you can consider disabling the subscription instead. Disabled subscriptions will not receive new messages, but you can still view messages you already received. Are you sure you want to delete the subscription? - 如果删除订阅, 您已经收到的信息将无法访问. 也许你可以考虑禁用订阅.禁用订阅将不会收到新信息, 但您仍然可以看到你已经收到的信息. + 如果删除订阅, 您已经收到的信息将无法访问. 也许您可以考虑禁用订阅.禁用订阅将不会收到新信息, 但您仍然可以看到您已经收到的信息. -你确定要删除订阅? +您确定要删除订阅? - + If you delete the channel, messages that you already received will become inaccessible. Maybe you can consider disabling the channel instead. Disabled channels will not receive new messages, but you can still view messages you already received. Are you sure you want to delete the channel? - 如果您删除的频道, 你已经收到的信息将无法访问. 也许你可以考虑禁用频道. 禁用频道将不会收到新信息, 但你仍然可以看到你已经收到的信息. + 如果您删除的频道, 您已经收到的信息将无法访问. 也许您可以考虑禁用频道. 禁用频道将不会收到新信息, 但您仍然可以看到您已经收到的信息. -你确定要删除频道? +您确定要删除频道? - + Do you really want to remove this avatar? 您真的想移除这个头像么? - + You have already set an avatar for this address. Do you really want to overwrite it? 您已经为这个地址设置了头像了。您真的想移除么? - + Start-on-login not yet supported on your OS. 登录时启动尚未支持您在使用的操作系统。 - + Minimize-to-tray not yet supported on your OS. 最小化到托盘尚未支持您的操作系统。 - + Tray notifications not yet supported on your OS. 托盘提醒尚未支持您所使用的操作系统。 - + Testing... 正在测试... @@ -877,37 +922,37 @@ Are you sure you want to delete the channel? - + The address should start with ''BM-'' 地址应该以"BM-"开始 - + The address is not typed or copied correctly (the checksum failed). 地址没有被正确的键入或复制(校验码校验失败)。 - + The version number of this address is higher than this software can support. Please upgrade Bitmessage. 这个地址的版本号大于此软件的最大支持。 请升级比特信。 - + The address contains invalid characters. 这个地址中包含无效字符。 - + Some data encoded in the address is too short. 在这个地址中编码的部分信息过少。 - + Some data encoded in the address is too long. 在这个地址中编码的部分信息过长。 - + Some data encoded in the address is malformed. 在地址编码的某些数据格式不正确. @@ -917,12 +962,12 @@ Are you sure you want to delete the channel? - + Address is an old type. We cannot display its past broadcasts. 地址没有近期的广播。我们无法显示之间的广播。 - + There are no recent broadcasts from this address to display. 没有可以显示的近期广播。 @@ -1049,7 +1094,7 @@ Are you sure you want to delete the channel? Chans - Chans + 频道 @@ -1117,22 +1162,22 @@ Are you sure you want to delete the channel? 加入或创建一个频道 - + All accounts 所有帐户 - + Zoom level %1% 缩放级别%1% - + Error: You cannot add the same address to your list twice. Perhaps rename the existing one if you want. 错误: 您不能在同一地址添加到列表中两次. 也许您可重命名现有之一. - + Add new entry 添加新条目 @@ -1147,37 +1192,37 @@ Are you sure you want to delete the channel? PyBitmessage的新版本可用: %1. 从https://github.com/Bitmessage/PyBitmessage/releases/latest下载 - + Waiting for PoW to finish... %1% 等待PoW完成...%1% - + Shutting down Pybitmessage... %1% 关闭Pybitmessage ...%1% - + Waiting for objects to be sent... %1% 等待要发送对象...%1% - + Saving settings... %1% 保存设置...%1% - + Shutting down core... %1% 关闭核心...%1% - + Stopping notifications... %1% 停止通知...%1% - + Shutdown imminent... %1% 关闭即将来临...%1% @@ -1187,42 +1232,42 @@ Are you sure you want to delete the channel? %n 小时 - + %n day(s) %n 天 - + Shutting down PyBitmessage... %1% 关闭PyBitmessage...%1% - + Sent 发送 - + Generating one new address 生成一个新的地址 - + Done generating address. Doing work necessary to broadcast it... 完成生成地址. 做必要的工作, 以播放它... - + Generating %1 new addresses. 生成%1个新地址. - + %1 is already in 'Your Identities'. Not adding it again. %1已经在'您的身份'. 不必重新添加. - + Done generating address 完成生成地址 @@ -1232,111 +1277,111 @@ Are you sure you want to delete the channel? - + Disk full 磁盘已满 - + Alert: Your disk or data storage volume is full. Bitmessage will now exit. 警告: 您的磁盘或数据存储量已满. 比特信将立即退出. - + Error! Could not find sender address (your address) in the keys.dat file. 错误! 找不到在keys.dat 件发件人的地址 ( 您的地址). - + Doing work necessary to send broadcast... 做必要的工作, 以发送广播... - + Broadcast sent on %1 广播发送%1 - + Encryption key was requested earlier. 加密密钥已请求. - + Sending a request for the recipient's encryption key. 发送收件人的加密密钥的请求. - + Looking up the receiver's public key 展望接收方的公钥 - + Problem: Destination is a mobile device who requests that the destination be included in the message but this is disallowed in your settings. %1 - 问题: 目标是移动电话设备所请求的目的地包括在消息中, 但是这是在你的设置禁止. %1 + 问题:对方是移动设备,并且对方的地址包含在此消息中,但是您的设置禁止了。 %1 - + Doing work necessary to send message. There is no required difficulty for version 2 addresses like this. 做必要的工作, 以发送信息. 这样第2版的地址没有难度. - + Doing work necessary to send message. Receiver's required difficulty: %1 and %2 做必要的工作, 以发送短信. 接收者的要求难度: %1与%2 - + Problem: The work demanded by the recipient (%1 and %2) is more difficult than you are willing to do. %3 问题: 由接收者(%1%2)要求的工作量比您愿意做的工作量來得更困难. %3 - + Problem: You are trying to send a message to yourself or a chan but your encryption key could not be found in the keys.dat file. Could not encrypt message. %1 问题: 您正在尝试将信息发送给自己或频道, 但您的加密密钥无法在keys.dat文件中找到. 无法加密信息. %1 - + Doing work necessary to send message. 做必要的工作, 以发送信息. - + Message sent. Waiting for acknowledgement. Sent on %1 信息发送. 等待确认. 已发送%1 - + Doing work necessary to request encryption key. 做必要的工作以要求加密密钥. - + Broadcasting the public key request. This program will auto-retry if they are offline. 广播公钥请求. 这个程序将自动重试, 如果他们处于离线状态. - + Sending public key request. Waiting for reply. Requested at %1 发送公钥的请求. 等待回复. 请求在%1 - + UPnP port mapping established on port %1 UPnP端口映射建立在端口%1 - + UPnP port mapping removed UPnP端口映射被删除 - + Mark all messages as read 标记全部信息为已读 @@ -1346,102 +1391,87 @@ Receiver's required difficulty: %1 and %2 确定将所有信息标记为已读吗? - + Doing work necessary to send broadcast. 持续进行必要的工作,以发送广播。 - + Proof of work pending 待传输内容的校验 - + %n object(s) pending proof of work %n 待传输内容校验任务 - + %n object(s) waiting to be distributed %n 任务等待分配 - + Wait until these tasks finish? 等待所有任务执行完? - - Problem communicating with proxy: %1. Please check your network settings. - 与代理通信故障率:%1。请检查你的网络连接。 - - - - SOCKS5 Authentication problem: %1. Please check your SOCKS5 settings. - SOCK5认证错误:%1。请检查你的SOCK5设置。 - - - - The time on your computer, %1, may be wrong. Please verify your settings. - 你电脑上时间有误:%1。请检查你的设置。 - - - + The name %1 was not found. 名字%1未找到。 - + The namecoin query failed (%1) 域名币查询失败(%1) - - The namecoin query failed. - 域名币查询失败。 + + Unknown namecoin interface type: %1 + 未知的 Namecoin 界面类型: %1 - - The name %1 has no valid JSON data. - 名字%1没有有效地JSON数据。 + + The namecoin query failed. + 域名币查询失败。 - + The name %1 has no associated Bitmessage address. 名字%1没有关联比特信地址。 - + Success! Namecoind version %1 running. 成功!域名币系统%1运行中。 - + Success! NMControll is up and running. 成功!域名币控制上线运行! - + Couldn't understand NMControl. 不能理解 NMControl。 - + The connection to namecoin failed. 连接到 Namecoin 失败。 - + Your GPU(s) did not calculate correctly, disabling OpenCL. Please report to the developers. - 你的GPU不能够正确计算,正在关闭OpenGL。请报告给开发者。 + 您的GPU不能够正确计算,正在关闭OpenGL。请报告给开发者。 - + Set notification sound... 设置通知提示音... - + Welcome to easy and secure Bitmessage * send messages to other people @@ -1455,17 +1485,17 @@ Receiver's required difficulty: %1 and %2 *在频道里和其他人讨论 - + not recommended for chans 频道内不建议的内容 - + Quiet Mode 静默模式 - + Problems connecting? Try enabling UPnP in the Network Settings 连接问题?请尝试在网络设置里打开UPnP @@ -1492,7 +1522,7 @@ Receiver's required difficulty: %1 and %2 Error: The version of the recipient address %1 is too high. Either you need to upgrade your Bitmessage software or your acquaintance is being clever. - 错误:收信地址%1版本太高。要么你需要更新你的软件,要么对方需要降级 。 + 错误:收信地址 %1 版本太高。要么您需要更新您的软件,要么对方需要降级 。 @@ -1515,57 +1545,77 @@ Receiver's required difficulty: %1 and %2 错误:收信地址%1有问题。 - + Error: %1 错误:%1 - + From %1 来自 %1 - + + Disconnecting + 正在断开连接 + + + + Connecting + 已连接 + + + + Bitmessage will now drop all connections. Are you sure? + 比特信将要丢弃所有连接,您确定吗? + + + + Bitmessage will now start connecting to network. Are you sure? + 比特信将会开始连接至网络中,您确定吗? + + + Synchronisation pending 待同步 - + Bitmessage hasn't synchronised with the network, %n object(s) to be downloaded. If you quit now, it may cause delivery delays. Wait until the synchronisation finishes? - Bitmessage还没有与网络同步,%n 件任务需要下载。如果你现在退出软件,可能会造成传输延时。是否等同步完成? + Bitmessage还没有与网络同步,%n 件任务需要下载。如果您现在退出软件,可能会造成传输延时。是否等同步完成? - + Not connected 未连接成功。 - + Bitmessage isn't connected to the network. If you quit now, it may cause delivery delays. Wait until connected and the synchronisation finishes? Bitmessage未连接到网络。如果现在退出软件,可能会造成传输延时。是否等待同步完成? - + Waiting for network connection... 等待网络连接…… - + Waiting for finishing synchronisation... 等待同步完成…… - + You have already set a notification sound for this address book entry. Do you really want to overwrite it? 您已经为该地址簿条目设置了通知提示音。您想要覆盖它吗? - + Error occurred: could not load message from disk. 发生错误:无法从磁盘读取消息。 - + Display the %n recent broadcast(s) from this address. 显示从此地址最近 %n 的广播。 @@ -1585,45 +1635,45 @@ Receiver's required difficulty: %1 and %2 清除 - + inbox 收件箱 - + new 新信息 - + sent 已发送 - + trash - 垃圾箱 + 回收站 MessageView - + Follow external link 查看外部链接 - + The link "%1" will open in a browser. It may be a security risk, it could de-anonymise you or download malicious data. Are you sure? - 此链接“%1”将在浏览器中打开。可能会有安全风险,可能会暴露你或下载恶意数据。确定吗? + 此链接“%1”将在浏览器中打开。可能会有安全风险,可能会暴露您或下载恶意数据。确定吗? - + HTML detected, click here to display 检测到HTML,单击此处来显示内容。 - + Click here to disable HTML 单击此处以禁止HTML。 @@ -1631,14 +1681,14 @@ Receiver's required difficulty: %1 and %2 MsgDecode - + The message has an unknown encoding. Perhaps you should upgrade Bitmessage. 这些消息使用了未知编码方式。 -你可能需要更新Bitmessage软件。 +您可能需要更新Bitmessage软件。 - + Unknown encoding 未知编码 @@ -1654,7 +1704,7 @@ Perhaps you should upgrade Bitmessage. Here you may generate as many addresses as you like. Indeed, creating and abandoning addresses is encouraged. 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: - 在这里,您想创建多少地址就创建多少。诚然,创建和丢弃地址受到鼓励。你既可以使用随机数来创建地址,也可以使用密钥。如果您使用密钥的话,生成的地址叫“静态地址”。随机数选项默认为选择,不过相比而言静态地址既有缺点也有优点: + 在这里,您想创建多少地址就创建多少。诚然,创建和丢弃地址受到鼓励。您既可以使用随机数来创建地址,也可以使用密钥。如果您使用密钥的话,生成的地址叫“静态地址”。随机数选项默认为选择,不过相比而言静态地址既有缺点也有优点: @@ -1739,7 +1789,7 @@ The 'Random Number' option is selected by default but deterministic ad (saves you some bandwidth and processing power) - (节省你的带宽和处理能力) + (节省您的带宽和处理能力) @@ -1793,7 +1843,7 @@ The 'Random Number' option is selected by default but deterministic ad 伪邮件列表名称: - + This is a chan address. You cannot use it as a pseudo-mailing list. 这是一个频道地址,您无法把它作为伪邮件列表。 @@ -1864,12 +1914,12 @@ The 'Random Number' option is selected by default but deterministic ad 地址 - + Blacklist 黑名单 - + Whitelist 白名单 @@ -1935,7 +1985,7 @@ The 'Random Number' option is selected by default but deterministic ad You have made at least one connection to a peer using an outgoing connection but you have not yet received any incoming connections. Your firewall or home router probably isn't configured to forward incoming TCP connections to your computer. Bitmessage will work just fine but it would help the Bitmessage network if you allowed for incoming connections and will help you be a better-connected node. - 你有至少一个到其他节点的出站连接,但是尚未收到入站连接。您的防火墙或路由器可能尚未设置转发入站TCP连接到您的电脑。比特信将正常运行,不过如果您允许入站连接的话将帮助比特信网络并成为一个通信状态更好的节点。 + 您有至少一个到其他节点的出站连接,但是尚未收到入站连接。您的防火墙或路由器可能尚未设置转发入站TCP连接到您的电脑。比特信将正常运行,不过如果您允许入站连接的话将帮助比特信网络并成为一个通信状态更好的节点。 @@ -1948,7 +1998,7 @@ The 'Random Number' option is selected by default but deterministic ad 您有和其他节点的连接且您的防火墙已经正确配置。 - + You are using TCP port %1. (This can be changed in the settings). 您正在使用TCP端口 %1 。(可以在设置中修改)。 @@ -2001,27 +2051,27 @@ The 'Random Number' option is selected by default but deterministic ad - + Since startup on %1 自从%1启动 - + Down: %1/s Total: %2 下: %1/秒 总计: %2 - + Up: %1/s Total: %2 上: %1/秒 总计: %2 - + Total Connections: %1 总的连接数: %1 - + Inventory lookups per second: %1 每秒库存查询: %1 @@ -2041,27 +2091,27 @@ The 'Random Number' option is selected by default but deterministic ad 网络状态 - + byte(s) 字节 - + Object(s) to be synced: %n 要同步的对象: %n - + Processed %n person-to-person message(s). 处理%n人对人的信息. - + Processed %n broadcast message(s). 处理%n广播信息. - + Processed %n public key(s). 处理%n公钥. @@ -2161,7 +2211,7 @@ The 'Random Number' option is selected by default but deterministic ad <html><head/><body><p>A chan exists when a group of people share the same decryption keys. The keys and bitmessage address used by a chan are generated from a human-friendly word or phrase (the chan name). To send a message to everyone in the chan, send a message to the chan address.</p><p>Chans are experimental and completely unmoderatable.</p><p>Enter a name for your chan. If you choose a sufficiently complex chan name (like a strong and unique passphrase) and none of your friends share it publicly, then the chan will be secure and private. However if you and someone else both create a chan with the same chan name, the same chan will be shared.</p></body></html> - <html><head/><body><p>当一群人共享同一样的加密钥匙时会创建一个频道。使用一个词组来命名密钥和bitmessage地址。发送信息到频道地址就可以发送消息给每个成员。</p><p>频道功能为实验性功能,也不稳定。</p><p>为你的频道命名。如果你选择使用一个十分复杂的名字命令并且你的朋友不会公开它,那这个频道就是安全和私密的。然而如果你和其他人都创建了一个同样命名的频道,那么相同名字的频道将会被共享。</p></body></html> + <html><head/><body><p>当一群人共享同一样的加密钥匙时会创建一个频道。使用一个词组来命名密钥和Bitmessage地址。发送信息到频道地址就可以发送消息给每个成员。</p><p>频道功能为实验性功能,也不稳定。</p><p>为您的频道命名。如果您选择使用一个十分复杂的名字命令并且您的朋友不会公开它,那这个频道就是安全和私密的。然而如果您和其他人都创建了一个同样命名的频道,那么相同名字的频道将会被共享。</p></body></html> @@ -2187,17 +2237,17 @@ The 'Random Number' option is selected by default but deterministic ad newchandialog - + Successfully created / joined chan %1 成功创建或加入频道%1 - + Chan creation / joining failed 频道创建或加入失败 - + Chan creation / joining cancelled 频道创建或加入已取消 @@ -2205,29 +2255,21 @@ The 'Random Number' option is selected by default but deterministic ad proofofwork - + C PoW module built successfully. C PoW模块编译成功。 - + Failed to build C PoW module. Please build it manually. 无法编译C PoW模块。请手动编译。 - + C PoW module unavailable. Please build it. C PoW模块不可用。请编译它。 - - qrcodeDialog - - - QR-code - 二维码 - - regenerateAddressesDialog @@ -2284,218 +2326,218 @@ The 'Random Number' option is selected by default but deterministic ad settingsDialog - + Settings 设置 - + Start Bitmessage on user login 在用户登录时启动比特信 - + Tray 任务栏 - + Start Bitmessage in the tray (don't show main window) 启动比特信到托盘 (不要显示主窗口) - + Minimize to tray 最小化到托盘 - + Close to tray 关闭任务栏 - + Show notification when message received 在收到消息时提示 - + Run in Portable Mode 以便携方式运行 - + In Portable Mode, messages and config files are stored in the same directory as the program rather than the normal application-data folder. This makes it convenient to run Bitmessage from a USB thumb drive. 在便携模式下, 消息和配置文件和程序保存在同一个目录而不是通常的程序数据文件夹。 这使在U盘中允许比特信很方便。 - + Willingly include unencrypted destination address when sending to a mobile device 愿意在发送到手机时使用不加密的目标地址 - + Use Identicons 用户身份 - + Reply below Quote 回复 引述如下 - + Interface Language 界面语言 - + System Settings system 系统设置 - + User Interface 用户界面 - + Listening port 监听端口 - + Listen for connections on port: 监听连接于端口: - + UPnP: UPnP: - + Bandwidth limit 带宽限制 - + Maximum download rate (kB/s): [0: unlimited] 最大下载速率(kB/秒): [0: 无限制] - + Maximum upload rate (kB/s): [0: unlimited] 最大上传速度 (kB/秒): [0: 无限制] - + Proxy server / Tor 代理服务器 / Tor - + Type: 类型: - + Server hostname: 服务器主机名: - + Port: 端口: - + Authentication 认证 - + Username: 用户名: - + Pass: 密码: - + Listen for incoming connections when using proxy 在使用代理时仍然监听入站连接 - + none - + SOCKS4a SOCKS4a - + SOCKS5 SOCKS5 - + Network Settings 网络设置 - + Total difficulty: 总难度: - + The 'Total difficulty' affects the absolute amount of work the sender must complete. Doubling this value doubles the amount of work. “总难度”影响发送者所需要的做工总数。当这个值翻倍时,做工的总数也翻倍。 - + Small message difficulty: 小消息难度: - + When 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. 当一个人向您发送消息的时候, 他们的电脑必须先做工。这个难度的默认值是1,您可以在创建新的地址前提高这个值。任何新创建的地址都会要求更高的做工量。这里有一个例外,当您将您的朋友添加到地址本的时候,比特信将自动提示他们,当他们下一次向您发送的时候,他们需要的做功量将总是1. - + 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. “小消息困难度”几乎仅影响发送消息。当这个值翻倍时,发小消息时做工的总数也翻倍,但是并不影响大的消息。 - + Demanded difficulty 要求的难度 - + Here 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. - 你可以在这里设置您所愿意接受的发送消息的最大难度。0代表接受任何难度。 + 您可以在这里设置您所愿意接受的发送消息的最大难度。0代表接受任何难度。 - + Maximum acceptable total difficulty: 最大接受难度: - + Maximum acceptable small message difficulty: 最大接受的小消息难度: - + Max acceptable difficulty 最大可接受难度 @@ -2505,87 +2547,87 @@ The 'Random Number' option is selected by default but deterministic ad - + <html><head/><body><p>Bitmessage 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 <span style=" font-style:italic;">test. </span></p><p>(Getting your own Bitmessage address into Namecoin is still rather difficult).</p><p>Bitmessage can use either namecoind directly or a running nmcontrol instance.</p></body></html> <html><head/><body><p>比特信可以利用基于比特币的Namecoin让地址更加友好。比如除了告诉您的朋友您的长长的比特信地址,您还可以告诉他们发消息给 <span style=" font-style:italic;">test. </span></p><p>把您的地址放入Namecoin还是相当的难的.</p><p>比特信可以不但直接连接到namecoin守护程序或者连接到运行中的nmcontrol实例.</p></body></html> - + Host: 主机名: - + Password: 密码: - + Test 测试 - + Connect to: 连接到: - + Namecoind Namecoind - + NMControl NMControl - + Namecoin integration Namecoin整合 - + <html><head/><body><p>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.</p><p>Leave these input fields blank for the default behavior. </p></body></html> - <html><head/><body><p>您发给他们的消息默认会在网络上保存两天,之后比特信会再重发一次. 重发时间会随指数上升; 消息会在5, 10, 20... 天后重发并以此类推. 直到收到收件人的回执. 你可以在这里改变这一行为,让比特信在尝试一段时间后放弃.</p><p>留空意味着默认行为. </p></body></html> + <html><head/><body><p>您发给他们的消息默认会在网络上保存两天,之后比特信会再重发一次. 重发时间会随指数上升; 消息会在5, 10, 20... 天后重发并以此类推. 直到收到收件人的回执. 您可以在这里改变这一行为,让比特信在尝试一段时间后放弃.</p><p>留空意味着默认行为. </p></body></html> - + Give up after - + and - + days - + months. 月后放弃。 - + Resends Expire 重发超时 - + Hide connection notifications 隐藏连接通知 - + Maximum outbound connections: [0: none] 最大外部连接:[0: 无] - + Hardware GPU acceleration (OpenCL): 硬件GPU加速(OpenCL): diff --git a/src/upnp.py b/src/upnp.py index 46d55956f7..58a4ccec56 100644 --- a/src/upnp.py +++ b/src/upnp.py @@ -1,21 +1,31 @@ -# A simple upnp module to forward port for BitMessage -# Reference: http://mattscodecave.com/posts/using-python-and-upnp-to-forward-a-port -import httplib -from random import randint +# pylint: disable=too-many-statements,too-many-branches,protected-access,no-self-use +""" +Complete UPnP port forwarding implementation in separate thread. +Reference: http://mattscodecave.com/posts/using-python-and-upnp-to-forward-a-port.html +""" + +import re import socket -from struct import unpack, pack -import threading import time -from bmconfigparser import BMConfigParser -from network.connectionpool import BMConnectionPool -from helper_threading import * +from random import randint +from xml.dom.minidom import Document # nosec B408 + +from defusedxml.minidom import parseString +from six.moves import http_client as httplib +from six.moves.urllib.parse import urlparse +from six.moves.urllib.request import urlopen + import queues -import shared import state import tr +from bmconfigparser import config +from debug import logger +from network import StoppableThread, connectionpool, knownnodes +from network.node import Peer + def createRequestXML(service, action, arguments=None): - from xml.dom.minidom import Document + """Router UPnP requests are XML formatted""" doc = Document() @@ -63,22 +73,24 @@ def createRequestXML(service, action, arguments=None): # our tree is ready, conver it to a string return doc.toxml() + class UPnPError(Exception): + """Handle a UPnP error""" + def __init__(self, message): - self.message + super(UPnPError, self).__init__() + logger.error(message) + -class Router: +class Router: # pylint: disable=old-style-class + """Encapulate routing""" name = "" path = "" address = None routerPath = None extPort = None - + def __init__(self, ssdpResponse, address): - import urllib2 - from xml.dom.minidom import parseString - from urlparse import urlparse - from debug import logger self.address = address @@ -92,12 +104,15 @@ def __init__(self, ssdpResponse, address): try: self.routerPath = urlparse(header['location']) if not self.routerPath or not hasattr(self.routerPath, "hostname"): - logger.error ("UPnP: no hostname: %s", header['location']) + logger.error("UPnP: no hostname: %s", header['location']) except KeyError: - logger.error ("UPnP: missing location header") + logger.error("UPnP: missing location header") # get the profile xml file and read it into a variable - directory = urllib2.urlopen(header['location']).read() + parsed_url = urlparse(header['location']) + if parsed_url.scheme not in ['http', 'https']: + raise UPnPError("Unsupported URL scheme: %s" % parsed_url.scheme) + directory = urlopen(header['location']).read() # nosec B310 # create a DOM object that represents the `directory` document dom = parseString(directory) @@ -108,45 +123,60 @@ def __init__(self, ssdpResponse, address): for service in service_types: if service.childNodes[0].data.find('WANIPConnection') > 0 or \ - service.childNodes[0].data.find('WANPPPConnection') > 0: + 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, + externalPort, + internalPort, + internalClient, + protocol, + description, + leaseDuration=0, + enabled=1, + ): + """Add UPnP port mapping""" - def AddPortMapping(self, externalPort, internalPort, internalClient, protocol, description, leaseDuration = 0, enabled = 1): - from debug import logger resp = self.soapRequest(self.upnp_schema + ':1', 'AddPortMapping', [ - ('NewRemoteHost', ''), - ('NewExternalPort', str(externalPort)), - ('NewProtocol', protocol), - ('NewInternalPort', str(internalPort)), - ('NewInternalClient', internalClient), - ('NewEnabled', str(enabled)), - ('NewPortMappingDescription', str(description)), - ('NewLeaseDuration', str(leaseDuration)) - ]) + ('NewRemoteHost', ''), + ('NewExternalPort', str(externalPort)), + ('NewProtocol', protocol), + ('NewInternalPort', str(internalPort)), + ('NewInternalClient', internalClient), + ('NewEnabled', str(enabled)), + ('NewPortMappingDescription', str(description)), + ('NewLeaseDuration', str(leaseDuration)) + ]) self.extPort = externalPort - logger.info("Successfully established UPnP mapping for %s:%i on external port %i", internalClient, internalPort, externalPort) + logger.info("Successfully established UPnP mapping for %s:%i on external port %i", + internalClient, internalPort, externalPort) return resp def DeletePortMapping(self, externalPort, protocol): - from debug import logger + """Delete UPnP port mapping""" + resp = self.soapRequest(self.upnp_schema + ':1', 'DeletePortMapping', [ - ('NewRemoteHost', ''), - ('NewExternalPort', str(externalPort)), - ('NewProtocol', protocol), - ]) + ('NewRemoteHost', ''), + ('NewExternalPort', str(externalPort)), + ('NewProtocol', protocol), + ]) logger.info("Removed UPnP mapping on external port %i", externalPort) return resp def GetExternalIPAddress(self): - from xml.dom.minidom import parseString - resp = self.soapRequest(self.upnp_schema + ':1', 'GetExternalIPAddress') - dom = parseString(resp) - return dom.getElementsByTagName('NewExternalIPAddress')[0].childNodes[0].data - + """Get the external address""" + + resp = self.soapRequest( + self.upnp_schema + ':1', 'GetExternalIPAddress') + dom = parseString(resp.read()) + return dom.getElementsByTagName( + 'NewExternalIPAddress')[0].childNodes[0].data + def soapRequest(self, service, action, arguments=None): - from xml.dom.minidom import parseString - from debug import logger + """Make a request to a router""" + conn = httplib.HTTPConnection(self.routerPath.hostname, self.routerPath.port) conn.request( 'POST', @@ -155,8 +185,8 @@ def soapRequest(self, service, action, arguments=None): { 'SOAPAction': '"urn:schemas-upnp-org:service:%s#%s"' % (service, action), 'Content-Type': 'text/xml' - } - ) + } + ) resp = conn.getresponse() conn.close() if resp.status == 500: @@ -164,26 +194,26 @@ def soapRequest(self, service, action, arguments=None): try: dom = parseString(respData) errinfo = dom.getElementsByTagName('errorDescription') - if len(errinfo) > 0: + if errinfo: logger.error("UPnP error: %s", respData) raise UPnPError(errinfo[0].childNodes[0].data) - except: - raise UPnPError("Unable to parse SOAP error: %s" %(respData)) + except: # noqa:E722 + raise UPnPError("Unable to parse SOAP error: %s" % (respData)) return resp -class uPnPThread(threading.Thread, StoppableThread): + +class uPnPThread(StoppableThread): + """Start a thread to handle UPnP activity""" + SSDP_ADDR = "239.255.255.250" GOOGLE_DNS = "8.8.8.8" SSDP_PORT = 1900 SSDP_MX = 2 SSDP_ST = "urn:schemas-upnp-org:device:InternetGatewayDevice:1" - def __init__ (self): - threading.Thread.__init__(self, name="uPnPThread") - try: - self.extPort = BMConfigParser().getint('bitmessagesettings', 'extport') - except: - self.extPort = None + def __init__(self): + super(uPnPThread, self).__init__(name="uPnPThread") + self.extPort = config.safeGetInt('bitmessagesettings', 'extport', default=None) self.localIP = self.getLocalIP() self.routers = [] self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) @@ -191,11 +221,10 @@ def __init__ (self): self.sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2) self.sock.settimeout(5) self.sendSleep = 60 - self.initStop() def run(self): - from debug import logger - + """Start the thread to manage UPnP activity""" + logger.debug("Starting UPnP thread") logger.debug("Local IP: %s", self.localIP) lastSent = 0 @@ -203,23 +232,25 @@ def run(self): # 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) - self.localPort = BMConfigParser().getint('bitmessagesettings', 'port') - while state.shutdown == 0 and BMConfigParser().safeGetBoolean('bitmessagesettings', 'upnp'): - if time.time() - lastSent > self.sendSleep and len(self.routers) == 0: + # pylint: disable=attribute-defined-outside-init + self.localPort = config.getint('bitmessagesettings', 'port') + + while state.shutdown == 0 and config.safeGetBoolean('bitmessagesettings', 'upnp'): + if time.time() - lastSent > self.sendSleep and not self.routers: try: self.sendSearchRouter() - except: + except: # nosec B110 # noqa:E722 # pylint:disable=bare-except pass lastSent = time.time() try: - while state.shutdown == 0 and BMConfigParser().safeGetBoolean('bitmessagesettings', 'upnp'): - resp,(ip,port) = self.sock.recvfrom(1000) + while state.shutdown == 0 and config.safeGetBoolean('bitmessagesettings', 'upnp'): + resp, (ip, _) = self.sock.recvfrom(1000) if resp is None: continue newRouter = Router(resp, ip) @@ -230,83 +261,94 @@ def run(self): logger.debug("Found UPnP router at %s", ip) self.routers.append(newRouter) self.createPortMapping(newRouter) - queues.UISignalQueue.put(('updateStatusBar', tr._translate("MainWindow",'UPnP port mapping established on port %1').arg(str(self.extPort)))) - # retry connections so that the submitted port is refreshed - with shared.alreadyAttemptedConnectionsListLock: - shared.alreadyAttemptedConnectionsList.clear() - shared.alreadyAttemptedConnectionsListResetTime = int( - time.time()) + try: + self_peer = Peer( + newRouter.GetExternalIPAddress(), + self.extPort + ) + except: # noqa:E722 + logger.debug('Failed to get external IP') + else: + with knownnodes.knownNodesLock: + knownnodes.addKnownNode( + 1, self_peer, is_self=True) + queues.UISignalQueue.put(('updateStatusBar', tr._translate( + "MainWindow", 'UPnP port mapping established on port %1' + ).arg(str(self.extPort)))) break - except socket.timeout as e: + except socket.timeout: pass - except: + except: # noqa:E722 logger.error("Failure running UPnP router search.", exc_info=True) for router in self.routers: if router.extPort is None: self.createPortMapping(router) try: self.sock.shutdown(socket.SHUT_RDWR) - except: + except (IOError, OSError): # noqa:E722 pass try: self.sock.close() - except: + except (IOError, OSError): # noqa:E722 pass deleted = False for router in self.routers: if router.extPort is not None: deleted = True self.deletePortMapping(router) - shared.extPort = None if deleted: - queues.UISignalQueue.put(('updateStatusBar', tr._translate("MainWindow",'UPnP port mapping removed'))) + queues.UISignalQueue.put(('updateStatusBar', tr._translate("MainWindow", 'UPnP port mapping removed'))) logger.debug("UPnP thread done") def getLocalIP(self): + """Get the local IP of the node""" + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) s.connect((uPnPThread.GOOGLE_DNS, 1)) return s.getsockname()[0] def sendSearchRouter(self): - from debug import logger + """Querying for UPnP services""" + ssdpRequest = "M-SEARCH * HTTP/1.1\r\n" + \ - "HOST: %s:%d\r\n" % (uPnPThread.SSDP_ADDR, uPnPThread.SSDP_PORT) + \ - "MAN: \"ssdp:discover\"\r\n" + \ - "MX: %d\r\n" % (uPnPThread.SSDP_MX, ) + \ - "ST: %s\r\n" % (uPnPThread.SSDP_ST, ) + "\r\n" + "HOST: %s:%d\r\n" % (uPnPThread.SSDP_ADDR, uPnPThread.SSDP_PORT) + \ + "MAN: \"ssdp:discover\"\r\n" + \ + "MX: %d\r\n" % (uPnPThread.SSDP_MX, ) + \ + "ST: %s\r\n" % (uPnPThread.SSDP_ST, ) + "\r\n" try: logger.debug("Sending UPnP query") self.sock.sendto(ssdpRequest, (uPnPThread.SSDP_ADDR, uPnPThread.SSDP_PORT)) - except: + except: # noqa:E722 logger.exception("UPnP send query failed") def createPortMapping(self, router): - from debug import logger + """Add a port mapping""" for i in range(50): try: - routerIP, = unpack('>I', socket.inet_aton(router.address)) localIP = self.localIP if i == 0: - extPort = self.localPort # try same port first + extPort = self.localPort # try same port first elif i == 1 and self.extPort: - extPort = self.extPort # try external port from last time next + extPort = self.extPort # try external port from last time next else: - extPort = randint(32767, 65535) - logger.debug("Attempt %i, requesting UPnP mapping for %s:%i on external port %i", i, localIP, self.localPort, extPort) + extPort = randint(32767, 65535) # nosec B311 + logger.debug( + "Attempt %i, requesting UPnP mapping for %s:%i on external port %i", + i, + localIP, + self.localPort, + extPort) router.AddPortMapping(extPort, self.localPort, localIP, 'TCP', 'BitMessage') - shared.extPort = extPort 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) def deletePortMapping(self, router): + """Delete a port mapping""" router.DeletePortMapping(router.extPort, 'TCP') - - - diff --git a/start.sh b/start.sh new file mode 100755 index 0000000000..75ed7af3d5 --- /dev/null +++ b/start.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +python2 pybitmessage/bitmessagemain.py "$@" diff --git a/stdeb.cfg b/stdeb.cfg new file mode 100644 index 0000000000..0d4cfbcbe3 --- /dev/null +++ b/stdeb.cfg @@ -0,0 +1,9 @@ +[DEFAULT] +Package: pybitmessage +Section: net +Build-Depends: dh-python, libssl-dev, python-all-dev, python-setuptools, python-six +Depends: openssl, python-setuptools +Recommends: apparmor, python-msgpack, python-qt4, python-stem, tor +Suggests: python-pyopencl, python-jsonrpclib, python-defusedxml, python-qrcode +Suite: bionic +Setup-Env-Vars: DEB_BUILD_OPTIONS=nocheck diff --git a/tests-kivy.py b/tests-kivy.py new file mode 100644 index 0000000000..9bc08880e6 --- /dev/null +++ b/tests-kivy.py @@ -0,0 +1,44 @@ +#!/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""" + loader = unittest.defaultTestLoader + loader.sortTestMethodsUsing = lambda a, b: random.randint(-1, 1) + return loader.discover('src.bitmessagekivy.tests') + + +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/tests.py b/tests.py new file mode 100644 index 0000000000..713b25ef53 --- /dev/null +++ b/tests.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python +"""Custom tests runner script for tox and python3""" +import random # noseq +import sys +import unittest + + +def unittest_discover(): + """Explicit test suite creation""" + if sys.hexversion >= 0x3000000: + from pybitmessage import pathmagic + pathmagic.setup() + loader = unittest.defaultTestLoader + # randomize the order of tests in test cases + loader.sortTestMethodsUsing = lambda a, b: random.randint(-1, 1) + # pybitmessage symlink disappears on Windows! + testsuite = loader.discover('pybitmessage.tests') + testsuite.addTests([loader.discover('pybitmessage.pyelliptic')]) + + return testsuite + + +if __name__ == "__main__": + success = unittest.TextTestRunner(verbosity=2).run( + unittest_discover()).wasSuccessful() + try: + from pybitmessage.tests import common + except ImportError: + checkup = False + else: + checkup = common.checkup() + + if checkup and not success: + print(checkup) + + sys.exit(not success or checkup) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000000..de80ed9962 --- /dev/null +++ b/tox.ini @@ -0,0 +1,92 @@ +[tox] +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 + 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] + -r docs/requirements.txt +commands = python setup.py build_sphinx + +[testenv:py27-portable] +skip_install = true +commands = python pybitmessage/bitmessagemain.py -t + +[testenv:py35] +skip_install = true + +[testenv:py36] +setenv = + BITMESSAGE_TEST_POW = true + {[testenv]setenv} + +[testenv:reset] +skip_install = true +deps = coverage +commands = coverage erase + +[testenv:stats] +skip_install = true +deps = coverage +commands = + coverage report + coverage xml + +[coverage:run] +source = src +omit = + tests.py + */tests/* + src/bitmessagekivy/* + src/version.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-positional-arguments = 8 +max-attributes = 8