diff --git a/.buildbot/android/Dockerfile b/.buildbot/android/Dockerfile deleted file mode 100755 index d657451d..00000000 --- a/.buildbot/android/Dockerfile +++ /dev/null @@ -1,105 +0,0 @@ -# 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 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 deleted file mode 100755 index f2c08ac5..00000000 --- a/.buildbot/android/build.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/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 deleted file mode 100755 index 65a0fe7d..00000000 --- a/.buildbot/android/test.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/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 deleted file mode 100644 index c4dde327..00000000 --- a/.buildbot/appimage/Dockerfile +++ /dev/null @@ -1,28 +0,0 @@ -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/tox-bionic/build.sh diff --git a/.buildbot/appimage/build.sh b/.buildbot/appimage/build.sh deleted file mode 100755 index 0dc50f63..00000000 --- a/.buildbot/appimage/build.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/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://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage \ - && chmod +x ${BUILDER} - -chmod 1777 /tmp - -export ARCH=amd64 -export APPIMAGE_ARCH=x86_64 -export RUNTIME=${APPIMAGE_ARCH} - -build_appimage - -export ARCH=armhf -export APPIMAGE_ARCH=${ARCH} -export RUNTIME=gnueabihf -export CC=arm-linux-gnueabihf-gcc -export CXX=${CC} - -build_appimage - -export ARCH=arm64 -export APPIMAGE_ARCH=aarch64 -export RUNTIME=${APPIMAGE_ARCH} -export CC=aarch64-linux-gnu-gcc -export CXX=${CC} - -build_appimage - -EXISTING_OWNER=$(stat -c %u ../out) || mkdir -p ../out - -sha256sum PyBitmessage*.AppImage >> ../out/SHA256SUMS -cp PyBitmessage*.AppImage ../out - -if [ ${EXISTING_OWNER} ]; then - chown ${EXISTING_OWNER} ../out/PyBitmessage*.AppImage ../out/SHA256SUMS -fi diff --git a/.buildbot/appimage/test.sh b/.buildbot/appimage/test.sh deleted file mode 100755 index 871fc83a..00000000 --- a/.buildbot/appimage/test.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/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 deleted file mode 100644 index c9c98b01..00000000 --- a/.buildbot/kivy/Dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -# 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 deleted file mode 100755 index 87aae8f7..00000000 --- a/.buildbot/kivy/build.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/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 deleted file mode 100755 index 3231f250..00000000 --- a/.buildbot/kivy/test.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/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 deleted file mode 100644 index 7fde093d..00000000 --- a/.buildbot/snap/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100755 index 3a83ade7..00000000 --- a/.buildbot/snap/build.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/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 deleted file mode 100644 index 1acf58dc..00000000 --- a/.buildbot/tox-bionic/Dockerfile +++ /dev/null @@ -1,26 +0,0 @@ -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 deleted file mode 100755 index 87f670ce..00000000 --- a/.buildbot/tox-bionic/build.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -sudo service tor start diff --git a/.buildbot/tox-bionic/test.sh b/.buildbot/tox-bionic/test.sh deleted file mode 100755 index b280953a..00000000 --- a/.buildbot/tox-bionic/test.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -tox -e lint-basic || exit 1 -tox diff --git a/.buildbot/tox-focal/Dockerfile b/.buildbot/tox-focal/Dockerfile deleted file mode 100644 index fecc0819..00000000 --- a/.buildbot/tox-focal/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 120000 index a9f8525c..00000000 --- a/.buildbot/tox-focal/test.sh +++ /dev/null @@ -1 +0,0 @@ -../tox-bionic/test.sh \ No newline at end of file diff --git a/.buildbot/tox-jammy/Dockerfile b/.buildbot/tox-jammy/Dockerfile deleted file mode 100644 index b15c3b8f..00000000 --- a/.buildbot/tox-jammy/Dockerfile +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100755 index ab6134c4..00000000 --- a/.buildbot/tox-jammy/test.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -tox -e lint || exit 1 -tox -e py310 diff --git a/.buildbot/winebuild/Dockerfile b/.buildbot/winebuild/Dockerfile deleted file mode 100644 index 9b687f8f..00000000 --- a/.buildbot/winebuild/Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100755 index fdf5bedc..00000000 --- a/.buildbot/winebuild/build.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh - -xvfb-run -a buildscripts/winbuild.sh || exit 1 - -mkdir -p ../out -mv packages/pyinstaller/dist/Bitmessage*.exe ../out -cd ../out -sha256sum Bitmessage*.exe > SHA256SUMS diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index ed4f2b89..00000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,41 +0,0 @@ -FROM ubuntu:jammy - -ARG USERNAME=user -ARG USER_UID=1000 -ARG USER_GID=$USER_UID - -RUN apt-get update -RUN DEBIAN_FRONTEND=noninteractive apt-get install -y \ - curl \ - flake8 \ - gh \ - git \ - gnupg2 \ - jq \ - libcap-dev \ - libssl-dev \ - pylint \ - python-setuptools \ - python2.7 \ - python2.7-dev \ - python3 \ - python3-dev \ - python3-flake8 \ - python3-pip \ - python3-pycodestyle \ - software-properties-common \ - sudo \ - zsh - -RUN apt-add-repository ppa:deadsnakes/ppa - -RUN pip install 'tox<4' 'virtualenv<20.22.0' - -RUN groupadd --gid $USER_GID $USERNAME \ - && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \ - && chsh -s /usr/bin/zsh user \ - && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \ - && chmod 0440 /etc/sudoers.d/$USERNAME - -USER $USERNAME -WORKDIR /home/$USERNAME diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 91a470aa..00000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Codespaces Python3", - "extensions": [ - "cschleiden.vscode-github-actions", - "eamodio.gitlens", - "github.vscode-pull-request-github", - "ms-azuretools.vscode-docker", - "ms-python.flake8", - "ms-python.pylint", - "ms-python.python", - "ms-vsliveshare.vsliveshare", - "nwgh.bandit", - "the-compiler.python-tox", - "vscode-icons-team.vscode-icons", - "visualstudioexptteam.vscodeintellicode" - ], - "dockerFile": "Dockerfile", - "postCreateCommand": "pip3 install -r requirements.txt", - "updateContentCommand": "python2.7 setup.py install --user", - "remoteEnv": { - "PATH": "${containerEnv:PATH}:/home/user/.local/bin" - }, - "settings": { - "flake8.args": ["--config=setup.cfg"], - "pylint.args": ["--rcfile=setup.cfg"], - "terminal.integrated.shell.linux": "/usr/bin/zsh", - "terminal.integrated.defaultProfile.linux": "zsh", - "terminal.integrated.fontFamily": "'SourceCodePro+Powerline+Awesome Regular'", - "terminal.integrated.fontSize": 14, - "files.exclude": { - "**/CODE_OF_CONDUCT.md": true, - "**/LICENSE": true - } - } -} diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index c21698ec..00000000 --- a/.dockerignore +++ /dev/null @@ -1,8 +0,0 @@ -bin -build -dist -__pycache__ -.buildozer -.tox -mprofile_* -**.so diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 776a13c1..00000000 --- a/.gitattributes +++ /dev/null @@ -1,3 +0,0 @@ -# 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/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 415583a9..00000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,7 +0,0 @@ -# Basic dependabot.yml for kivymd -version: 2 -updates: - - package-ecosystem: "pip" - directory: "/" - schedule: - interval: "daily" diff --git a/.gitignore b/.gitignore index fc331499..8153e385 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ **pyc +**dat **.DS_Store src/build src/dist @@ -12,16 +13,9 @@ 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/.gitmodules b/.gitmodules new file mode 100644 index 00000000..501ab53e --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "packages/flatpak/shared-modules"] + path = packages/flatpak/shared-modules + url = https://github.com/flathub/shared-modules.git diff --git a/.readthedocs.yml b/.readthedocs.yml index 136ef6e9..474ae9ab 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,12 +1,9 @@ version: 2 -build: - os: ubuntu-20.04 - tools: - python: "2.7" - python: + version: 2.7 install: - requirements: docs/requirements.txt - - method: pip + - method: setuptools path: . + system_packages: true diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..d7141188 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,17 @@ +language: python +python: + - "2.7" +addons: + apt: + packages: + - build-essential + - libcap-dev + - tor +install: + - pip install -r requirements.txt + - ln -s src pybitmessage # tests environment + - python setup.py install +script: + - python checkdeps.py + - src/bitmessagemain.py -t + - python setup.py test diff --git a/COPYING b/COPYING index 279cef2a..078bf213 100644 --- a/COPYING +++ b/COPYING @@ -1,5 +1,5 @@ Copyright (c) 2012-2016 Jonathan Warren -Copyright (c) 2012-2022 The Bitmessage Developers +Copyright (c) 2012-2020 The Bitmessage Developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Dockerfile b/Dockerfile index b409d27a..918f737d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,37 +1,49 @@ # A container for PyBitmessage daemon -FROM ubuntu:bionic +FROM ubuntu:xenial 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 + python-msgpack dh-python python-all-dev build-essential libssl-dev \ + python-stdeb fakeroot python-pip libcap-dev + +RUN pip install --upgrade pip EXPOSE 8444 8442 ENV HOME /home/bitmessage ENV BITMESSAGE_HOME ${HOME} +ENV VER 0.6.3.2 + WORKDIR ${HOME} ADD . ${HOME} -COPY packages/docker/launcher.sh /usr/bin/ -# Install -RUN pip2 install jsonrpclib . +# Install tests dependencies +RUN pip install -r requirements.txt -# Cleanup -RUN rm -rf /var/lib/apt/lists/* -RUN rm -rf ${HOME} +# Build and install deb +RUN python2 setup.py sdist \ + && py2dsc-deb dist/pybitmessage-${VER}.tar.gz \ + && dpkg -i deb_dist/python-pybitmessage_${VER}-1_amd64.deb # Create a user -RUN useradd -r bitmessage && chown -R bitmessage ${HOME} +RUN useradd bitmessage && chown -R bitmessage ${HOME} USER bitmessage # Generate default config -RUN pybitmessage -t +RUN src/bitmessagemain.py -t && mv keys.dat /tmp -ENTRYPOINT ["launcher.sh"] -CMD ["-d"] +# Clean HOME +RUN rm -rf ${HOME}/* + +# Setup environment +RUN mv /tmp/keys.dat . \ + && APIPASS=$(tr -dc a-zA-Z0-9 < /dev/urandom | head -c32 && echo) \ + && echo "\napiusername: api\napipassword: $APIPASS" \ + && echo "apienabled = true\napiinterface = 0.0.0.0\napiusername = api\napipassword = $APIPASS" >> keys.dat + +CMD ["pybitmessage", "-d"] diff --git a/INSTALL.md b/INSTALL.md index 4f11b199..f2d05d87 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,117 +1,116 @@ # 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 +For an up-to-date version of these instructions, please visit the +[Bitmessage Wiki](https://bitmessage.org/wiki/Compiling_instructions). -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. +PyBitmessage can be run in either one of two ways: -When you run the appimage the bundle is loop mounted to a location like -`/tmp/.mount_PyBitm97wj4K` with `squashfs-tools`. +- straight from source -The appimage name has several informational filds: -``` -PyBitmessage--g[-alpha]-.AppImage -``` + or +- from an installed +package. -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. - -You can also build the appimage with local code. For that you need installed -docker: - -``` -$ docker build -t bm-appimage -f .buildbot/appimage/Dockerfile . -$ docker run -t --rm -v "$(pwd)"/dist:/out bm-appimage .buildbot/appimage/build.sh -``` - -The appimages should be in the dist dir. - - -## Helper Script for building from source -Go to the directory with PyBitmessage source code and run: -``` -python checkdeps.py -``` -If there are missing dependencies, it will explain you what is missing -and for many Unix-like systems also what you have to do to resolve it. You need -to repeat calling the script until you get nothing mandatory missing. How you -then run setuptools depends on whether you want to install it to -user's directory or system. - -### If checkdeps fails, then verify manually which dependencies are missing from below +## Dependencies 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 +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 -For Debian-based (Ubuntu, Raspbian, PiBang, others) +## Running PyBitmessage +PyBitmessage can be run in 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: ``` -python2.7 openssl libssl-dev python-msgpack python-qt4 python-six +cd PyBitmessage/src/ +git fetch --all +git reset --hard origin/master +python bitmessagemain.py ``` -For Arch Linux +Voilà! Bitmessage is updated! + +#### Linux +_Some recent Linux distributions don't support QT4 anymore, hence PyBitmessage +won't run with a GUI. However, if you build PyBitmessage as a flatpak, it will +run in a sandbox which provides QT4. See the **Linux flatpak** instructions +in the **Creating a package for installation** section of this document._ + +To run PyBitmessage from the command-line, you must download the source, then +run `src/bitmessagemain.py`. ``` -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 +git clone git://github.com/Bitmessage/PyBitmessage.git +cd PyBitmessage/ && python src/bitmessagemain.py ``` -## setuptools -This is now the recommended and in most cases the easiest way for -installing PyBitmessage. +That's it! *Honestly*! -There are 2 options for installing with setuptools: root and user. +#### 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). -### as root: +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. + +#### OS X +First off, install Homebrew. ``` -python setup.py install -pybitmessage +ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" ``` -### as user: +Now, install the required dependencies ``` -python setup.py install --user -~/.local/bin/pybitmessage +brew install git python openssl cartr/qt4/pyqt@4 ``` -## pip venv (daemon): -Create virtualenv with Python 2.x version +Download and run PyBitmessage: ``` -virtualenv -p python2 env +git clone git://github.com/Bitmessage/PyBitmessage.git +cd PyBitmessage && python src/bitmessagemain.py ``` -Activate 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 ``` -source env/bin/activate +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 ``` -Build & run pybitmessage -``` -pip install . -pybitmessage -d -``` +#### Linux flatpak +See [packages/flatpak/README.md](packages/flatpak/README.md) -## Alternative way to run PyBitmessage, without setuptools (this isn't recommended) -run `./start.sh`. +#### 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. + +#### Windows +## TODO: Create Windows package creation instructions diff --git a/LICENSE b/LICENSE index fd772201..c2eeff82 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) Copyright (c) 2012-2016 Jonathan Warren -Copyright (c) 2012-2022 The Bitmessage Developers +Copyright (c) 2012-2020 The Bitmessage Developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in @@ -91,4 +91,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +SOFTWARE. \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in index 15a6bf81..3cbc72cd 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,4 +2,3 @@ include COPYING include README.md include requirements.txt recursive-include desktop * -recursive-include packages/apparmor * diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md similarity index 73% rename from .github/PULL_REQUEST_TEMPLATE.md rename to PULL_REQUEST_TEMPLATE.md index fb735a84..c820c50d 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -1,18 +1,21 @@ ## Repository contributions to the PyBitmessage project +- You can get paid for merged commits if you register at [Tip4Commit](https://tip4commit.com/github/Bitmessage/PyBitmessage) + ### Code - Try to refer to github issue tracker or other permanent sources of discussion about the issue. -- It is clear from the diff *what* you have done, it may be less clear *why* you have done it so explain why this change is necessary rather than what it does. +- It is clear from the diff *what* you have done, it may be less clear *why* you have done it so explain why this change is necessary rather than what it does ### Documentation -Use `tox -e py27-doc` to build a local copy of the documentation. +- If there has been a change to the code, there's a good possibility there should be a corresponding change to the documentation +- If you can't run `fab build_docs` successfully, ask for someone to run it against your branch ### 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` +- If you can't run `fab tests` successfully, ask for someone to run it against your branch ## Translations diff --git a/README.md b/README.md index 06c97c01..17049e7a 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Feel welcome to join chan "bitmessage", BM-2cWy7cvHoq3f1rYMerRJp8PT653jjSuEdY References ---------- * [Project Website](https://bitmessage.org) -* [Protocol Specification](https://pybitmessage.rtfd.io/en/v0.6/protocol.html) +* [Protocol Specification](https://bitmessage.org/wiki/Protocol_specification) * [Whitepaper](https://bitmessage.org/bitmessage.pdf) * [Installation](https://bitmessage.org/wiki/Compiling_instructions) * [Discuss on Reddit](https://www.reddit.com/r/bitmessage) diff --git a/android_instruction.rst b/android_instruction.rst new file mode 100644 index 00000000..e6c7797d --- /dev/null +++ b/android_instruction.rst @@ -0,0 +1,43 @@ +PyBitmessage(Android) + +This sample aims to be as close to a real world example of a mobile. It has a more refined design and also provides a practical example of how a mobile app would interact and communicate with its addresses. + +Steps for trying out this sample: + +Compile and install the mobile app onto your mobile device or emulator. + + +Getting Started + +This sample uses the kivy as Kivy is an open source, cross-platform Python framework for the development of applications that make use of innovative, multi-touch user interfaces. The aim is to allow for quick and easy interaction design and rapid prototyping whilst making your code reusable and deployable. + +Kivy is written in Python and Cython, supports various input devices and has an extensive widget library. With the same codebase, you can target Windows, OS X, Linux, Android and iOS. All Kivy widgets are built with multitouch support. + +Kivy in support take Buildozer which is a tool that automates the entire build process. It downloads and sets up all the prerequisite for python-for-android, including the android SDK and NDK, then builds an apk that can be automatically pushed to the device. + +Buildozer currently works only in Linux, and is an alpha release, but it already works well and can significantly simplify the apk build. + +To build this project, use the "Buildozer android release deploy run" command or use. +Buildozer ue=sed for creating application packages easily.The goal is to have one "buildozer.spec" file in your app directory, describing your application requirements and settings such as title, icon, included modules etc. Buildozer will use that spec to create a package for Android, iOS, Windows, OSX and/or Linux. + +Installing Requirements + +You can create a package for android using the python-for-android project as with using the Buildozer tool to automate the entire process. You can also see Packaging your application for the Kivy Launcher to run kivy programs without compiling them. + +You can get buildozer at https://github.com/kivy/buildozer or you can directly install using pip install buildozer + +This will install buildozer in your system. Afterwards, navigate to your project directory and run: + +buildozer init + +This creates a buildozer.spec file controlling your build configuration. You should edit it appropriately with your app name etc. You can set variables to control most or all of the parameters passed to python-for-android. + +Install buildozer’s dependencies. + +Finally, plug in your android device and run: + +buildozer android debug deploy run >> To build, push and automatically run the apk on your device. Here we used debug as tested in debug mode for now. + +Packaging your application for the Kivy Launcher + + diff --git a/bandit.yml b/bandit.yml new file mode 100644 index 00000000..4d24be14 --- /dev/null +++ b/bandit.yml @@ -0,0 +1,4 @@ +# Codacy uses Bandit. + +# Asserts are accepted throughout the project. +skips: ['B101'] diff --git a/buildscripts/androiddev.sh b/buildscripts/androiddev.sh deleted file mode 100755 index 1634d4c0..00000000 --- a/buildscripts/androiddev.sh +++ /dev/null @@ -1,112 +0,0 @@ -#!/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 deleted file mode 100755 index a5691783..00000000 --- a/buildscripts/appimage.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/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/buildscripts/update_translation_source.sh b/buildscripts/update_translation_source.sh deleted file mode 100644 index 205767cb..00000000 --- a/buildscripts/update_translation_source.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh - -xgettext -Lpython --output=src/translations/messages.pot \ -src/bitmessagekivy/mpybit.py src/bitmessagekivy/main.kv \ -src/bitmessagekivy/baseclass/*.py src/bitmessagekivy/kv/*.kv diff --git a/buildscripts/winbuild.sh b/buildscripts/winbuild.sh index fab0b3e0..da5997bd 100755 --- a/buildscripts/winbuild.sh +++ b/buildscripts/winbuild.sh @@ -1,7 +1,7 @@ #!/bin/bash # INIT -MACHINE_TYPE=$(uname -m) +MACHINE_TYPE=`uname -m` BASE_DIR=$(pwd) PYTHON_VERSION=2.7.17 PYQT_VERSION=4-4.11.4-gpl-Py2.7-Qt4.8.7 @@ -15,7 +15,7 @@ function download_sources_32 { 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://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 @@ -34,7 +34,7 @@ function download_sources_64 { } function download_sources { - if [ "${MACHINE_TYPE}" == 'x86_64' ]; then + if [ ${MACHINE_TYPE} == 'x86_64' ]; then download_sources_64 else download_sources_32 @@ -43,18 +43,18 @@ function download_sources { function install_wine { echo "Setting up wine" - if [ "${MACHINE_TYPE}" == 'x86_64' ]; then + 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 ${WINEPREFIX} rm -rf packages/pyinstaller/{build,dist} } function install_python(){ - cd ${SRCPATH} || exit 1 - if [ "${MACHINE_TYPE}" == 'x86_64' ]; then + cd ${SRCPATH} + 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" @@ -63,19 +63,20 @@ function install_python(){ 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 + cd ${SRCPATH} 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 + # add cert + if [ -f /usr/local/share/ca-certificates/bitmessage-proxy.crt ]; then + wine python -m pip config set global.cert 'z:\usr\local\share\ca-certificates\bitmessage-proxy.crt' + fi echo "Upgrading pip" wine python -m pip install --upgrade pip } function install_pyqt(){ - if [ "${MACHINE_TYPE}" == 'x86_64' ]; then + if [ ${MACHINE_TYPE} == 'x86_64' ]; then echo "Installing PyQt-${PYQT_VERSION} 64b" wine PyQt${PYQT_VERSION}-x64.exe /S /WX else @@ -85,7 +86,7 @@ function install_pyqt(){ } function install_openssl(){ - if [ "${MACHINE_TYPE}" == 'x86_64' ]; then + if [ ${MACHINE_TYPE} == 'x86_64' ]; then echo "Installing OpenSSL ${OPENSSL_VERSION} 64b" wine Win64OpenSSL-${OPENSSL_VERSION}.exe /q /norestart /silent /verysilent /sp- /suppressmsgboxes else @@ -96,91 +97,56 @@ function install_openssl(){ function install_pyinstaller() { - cd "${BASE_DIR}" || exit 1 - echo "Installing PyInstaller" - if [ "${MACHINE_TYPE}" == 'x86_64' ]; then - # 3.6 is the last version to support python 2.7 - # 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 + cd ${BASE_DIR} + echo "Installing PyInstaller" + if [ ${MACHINE_TYPE} == 'x86_64' ]; then + wine python -m pip install pyinstaller + 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() +function install_msgpack() { - 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 + cd ${BASE_DIR} + echo "Installing msgpack" + wine python -m pip install msgpack-python } function install_pyopencl() { - cd "${SRCPATH}" || exit 1 + cd ${SRCPATH} echo "Installing PyOpenCL" - if [ "${MACHINE_TYPE}" == 'x86_64' ]; then + 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" + 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 + cd ${BASE_DIR} + cd src/bitmsghash + 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 + x86_64-w64-mingw32-g++ -D_WIN32 -Wall -O3 -march=native -I$HOME/.wine64/drive_c/OpenSSL-Win64/include -I/usr/x86_64-w64-mingw32/include -L$HOME/.wine64/drive_c/OpenSSL-Win64/lib -c bitmsghash.cpp + x86_64-w64-mingw32-g++ -static-libgcc -shared bitmsghash.o -D_WIN32 -O3 -march=native -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 + i686-w64-mingw32-g++ -D_WIN32 -Wall -m32 -O3 -march=native -I$HOME/.wine32/drive_c/OpenSSL-Win32/include -I/usr/i686-w64-mingw32/include -L$HOME/.wine32/drive_c/OpenSSL-Win32/lib -c bitmsghash.cpp + i686-w64-mingw32-g++ -static-libgcc -shared bitmsghash.o -D_WIN32 -O3 -march=native -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 + cd ${BASE_DIR} + cd packages/pyinstaller 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 @@ -197,8 +163,7 @@ install_python install_pyqt install_openssl install_pyopencl -install_pip_depends +install_msgpack install_pyinstaller build_dll build_exe -dryrun_exe diff --git a/checkdeps.py b/checkdeps.py index 0a28a6d2..c3dedc1d 100755 --- a/checkdeps.py +++ b/checkdeps.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python2 """ Check dependencies and give recommendations about how to satisfy them @@ -47,7 +47,6 @@ EXTRAS_REQUIRE_DEPS = { "Debian": ["libcap-dev python-prctl"], "Ubuntu": ["libcap-dev python-prctl"], "Ubuntu 12": ["libcap-dev python-prctl"], - "Ubuntu 20": [""], "openSUSE": [""], "Fedora": ["prctl"], "Guix": [""], @@ -145,27 +144,20 @@ for lhs, rhs in EXTRAS_REQUIRE.items(): 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) + 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 (not compiler or prereqs) and OPSYS in PACKAGE_MANAGER: print("You can install the missing dependencies by running, as root:") diff --git a/configure b/configure new file mode 100755 index 00000000..0519ecba --- /dev/null +++ b/configure @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/desktop/pybitmessage.desktop b/desktop/pybitmessage.desktop index c30276e4..05970440 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;Network; +Categories=Office;Email; diff --git a/dev/bloomfiltertest.py b/dev/bloomfiltertest.py index 19c93c76..8f7b5f69 100644 --- a/dev/bloomfiltertest.py +++ b/dev/bloomfiltertest.py @@ -52,15 +52,15 @@ for row in cur.fetchall(): # 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 new file mode 100644 index 00000000..d5a8be8e --- /dev/null +++ b/dev/msgtest.py @@ -0,0 +1,27 @@ +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 bfb55d78..cc4c2197 100644 --- a/dev/powinterrupttest.py +++ b/dev/powinterrupttest.py @@ -11,7 +11,7 @@ shutdown = 0 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 7268b65f..a2f31d38 100644 --- a/dev/ssltest.py +++ b/dev/ssltest.py @@ -52,8 +52,7 @@ def sslHandshake(sock, server=False): 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 + 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'), @@ -66,29 +65,29 @@ def sslHandshake(sock, server=False): 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) @@ -100,5 +99,5 @@ if __name__ == "__main__": 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 deleted file mode 100755 index 18b6569a..00000000 --- a/docker-test.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/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/_static/custom.css b/docs/_static/custom.css index e0ba75c1..5192985c 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -2,17 +2,3 @@ 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 deleted file mode 100644 index eec4bd2c..00000000 --- a/docs/address.rst +++ /dev/null @@ -1,106 +0,0 @@ -Address -======= - -Bitmessage adresses are Base58 encoded public key hashes. An address looks like -``BM-BcbRqcFFSQUUmXFKsPJgVQPSiFA3Xash``. All Addresses start with ``BM-``, -however clients should accept addresses without the prefix. PyBitmessage does -this. The reason behind this idea is the fact, that when double clicking on an -address for copy and paste, the prefix is usually not selected due to the dash -being a common separator. - -Public Key usage ----------------- - -Addresses may look complicated but they fulfill the purpose of verifying the -sender. A Message claiming to be from a specific address can simply be checked by -decoding a special field in the data packet with the public key, that represents -the address. If the decryption succeeds, the message is from the address it -claims to be. - -Length ------- - -Without the ``BM-`` prefix, an address is usually 32-34 chars long. Since an -address is a hash it can be calculated by the client in a way, that the first -bytes are zero (``\0``) and bitmessage strips these. This causes the client to do -much more work to be lucky and find such an address. This is an optional checkbox -in address generation dialog. - -Versions --------- - - * v1 addresses used a single RSA key pair - * v2 addresses use 2 ECC key pairs - * v3 addresses extends v2 addresses to allow specifying the proof of work - requirements. The pubkey object is signed to mitigate against - forgery/tampering. - * v4 addresses protect against harvesting addresses from getpubkey and pubkey - objects - -Address Types -------------- - -There are two address types the user can generate in PyBitmessage. The resulting -addresses have no difference, but the method how they are created differs. - -Random Address -^^^^^^^^^^^^^^ - -Random addresses are generated from a randomly chosen number. The resulting -address cannot be regenerated without knowledge of the number and therefore the -keys.dat should be backed up. Generating random addresses takes slightly longer -due to the POW required for the public key broadcast. - -Usage -""""" - - * Generate unique addresses - * Generate one time addresses. - - -Deterministic Address -^^^^^^^^^^^^^^^^^^^^^ - -For this type of Address a passphrase is required, that is used to seed the -random generator. Using the same passphrase creates the same addresses. -Using deterministic addresses should be done with caution, using a word from a -dictionary or a common number can lead to others generating the same address and -thus being able to receive messages not intended for them. Generating a -deterministic address will not publish the public key. The key is sent in case -somebody requests it. This saves :doc:`pow` time, when generating a bunch of -addresses. - -Usage -""""" - - * Create the same address on multiple systems without the need of copying - keys.dat or an Address Block. - * create a Channel. (Use the *Join/create chan* option in the file menu instead) - * Being able to restore the address in case of address database corruption or - deletation. - -Address generation ------------------- - - 1. Create a private and a public key for encryption and signing (resulting in - 4 keys) - 2. Merge the public part of the signing key and the encryption key together. - (encoded in uncompressed X9.62 format) (A) - 3. Take the SHA512 hash of A. (B) - 4. Take the RIPEMD160 of B. (C) - 5. Repeat step 1-4 until you have a result that starts with a zero - (Or two zeros, if you want a short address). (D) - 6. Remove the zeros at the beginning of D. (E) - 7. Put the stream number (as a var_int) in front of E. (F) - 8. Put the address version (as a var_int) in front of F. (G) - 9. Take a double SHA512 (hash of a hash) of G and use the first four bytes as a - checksum, that you append to the end. (H) - 10. base58 encode H. (J) - 11. Put "BM-" in front J. (K) - -K is your full address - - .. note:: Bitmessage's base58 encoding uses the following sequence - (the same as Bitcoin's): - "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz". - Many existing libraries for base58 do not use this ordering. diff --git a/docs/conf.py b/docs/conf.py index b0cfef7b..3464e056 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -19,7 +19,7 @@ import version # noqa:E402 # -- Project information ----------------------------------------------------- project = u'PyBitmessage' -copyright = u'2019-2022, The Bitmessage Team' # pylint: disable=redefined-builtin +copyright = u'2019, The Bitmessage Team' # pylint: disable=redefined-builtin author = u'The Bitmessage Team' # The short X.Y version @@ -201,9 +201,8 @@ epub_exclude_files = ['search.html'] autodoc_mock_imports = [ 'debug', 'pybitmessage.bitmessagekivy', - 'pybitmessage.bitmessageqt.foldertree', + 'pybitmessage.bitmessageqt.addressvalidator', 'pybitmessage.helper_startup', - 'pybitmessage.mockbm', 'pybitmessage.network.httpd', 'pybitmessage.network.https', 'ctypes', @@ -217,10 +216,9 @@ autodoc_mock_imports = [ 'pycanberra', 'pyopencl', 'PyQt4', - 'PyQt5', + 'pyxdg', 'qrcode', 'stem', - 'xdg', ] autodoc_member_order = 'bysource' @@ -229,11 +227,10 @@ 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' + 'bitmessageqt/addressvalidator.py', 'bitmessageqt/migrationwizard.py', + 'bitmessageqt/newaddresswizard.py', 'helper_startup.py', + 'kivymd', 'main.py', 'navigationdrawer', 'network/http*', + 'pybitmessage', 'tests', 'version.py' ] apidoc_module_first = True apidoc_separate_modules = True diff --git a/docs/contribute.dir/develop.dir/documentation.rst b/docs/contribute.dir/develop.dir/documentation.rst new file mode 100644 index 00000000..e32acbb4 --- /dev/null +++ b/docs/contribute.dir/develop.dir/documentation.rst @@ -0,0 +1,18 @@ +Documentation +============= + +Sphinx is used to pull richly formatted comments out of code, merge them with hand-written documentation and render it +in HTML and other formats. + +To build the docs, simply run `$ fab -H localhost build_docs` once you have set up Fabric. + +Restructured Text (RsT) vs MarkDown (md) +---------------------------------------- + +There's much on the internet about this. Suffice to say RsT_ is preferred for Python documentation while md is preferred for web markup or for certain other languages. + +.. _Rst: [http://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html]` style is preferred, + +`md` files can also be incorporated by using mdinclude directives in the indices. They are translated to RsT before rendering to the various formats. Headers are translated as a hard-coded level `(H1: =, H2: -, H3: ^, H4: ~, H5: ", H6: #`. This represents a small consideration for both styles. If markup is not translated to rst well enough, switch to rst. + + diff --git a/docs/contribute.dir/develop.dir/fabric.rst b/docs/contribute.dir/develop.dir/fabric.rst new file mode 100644 index 00000000..8003f33a --- /dev/null +++ b/docs/contribute.dir/develop.dir/fabric.rst @@ -0,0 +1,2 @@ +.. mdinclude:: ../../../fabfile/README.md + diff --git a/docs/contribute.dir/develop.dir/opsec.rst b/docs/contribute.dir/develop.dir/opsec.rst new file mode 100644 index 00000000..1af43668 --- /dev/null +++ b/docs/contribute.dir/develop.dir/opsec.rst @@ -0,0 +1,38 @@ +Operational Security +==================== + +Bitmessage has many features that are designed to protect your anonymity during normal use. There are other things that you must or should do if you value your anonymity. + +Castles in the sand +------------------- + +You cannot build a strong castle on unstable foundations. If your operating system is not wholly owned by you then it is impossible to make guarantees about an application. + + * Carefully choose your operating system + * Ensure your operating system is up to date + * Ensure any dependencies and requirements are up to date + +Extrordinary claims require extrordinary evidence +------------------------------------------------- + +If we are to make bold claims about protecting your privacy we should demonstrate this by strong actions. + +- PGP signed commits +- looking to audit +- warrant canary + +Digital footprint +------------------ + +Your internet use can reveal metadata you wouldn't expect. This can be connected with other information about you if you're not careful. + + * Use separate addresses for different purposes + * Don't make the same mistakes all the time + * Your language use is unique. The more you type, the more you fingerprint yourself. The words you know and use often vs the words you don't know or use often. + +Cleaning history +---------------- + + * Tell your browser not to store BitMessage addresses + * Microsoft Office seems to offer the ability to define sensitive informations types. If browsers don't already they may in the future. Consider adding `BM-\w{x..y}`. + diff --git a/docs/contribute.dir/develop.dir/overview.rst b/docs/contribute.dir/develop.dir/overview.rst new file mode 100644 index 00000000..8bbc8299 --- /dev/null +++ b/docs/contribute.dir/develop.dir/overview.rst @@ -0,0 +1,88 @@ +Developing +========== + +Devops tasks +------------ + +Bitmessage makes use of fabric_ to define tasks such as building documentation or running checks and tests on the code. If you can install + +.. _fabric: https://fabfile.org + +Code style and linters +---------------------- + +We aim to be PEP8 compliant but we recognize that we have a long way still to go. Currently we have style and lint exceptions specified at the most specific place we can. We are ignoring certain issues project-wide in order to avoid alert-blindness, avoid style and lint regressions and to allow continuous integration to hook into the output from the tools. While it is hoped that all new changes pass the checks, fixing some existing violations are mini-projects in themselves. Current thinking on ignorable violations is reflected in the options and comments in setup.cfg. Module and line-level lint warnings represent refactoring opportunities. + +Pull requests +------------- + +There is a template at PULL_REQUEST_TEMPLATE.md that appears in the pull-request description. Please replace this text with something appropriate to your changes based on the ideas in the template. + +Bike-shedding +------------- + +Beyond having well-documented, Pythonic code with static analysis tool checks, extensive test coverage and powerful devops tools, what else can we have? Without violating any linters there is room for making arbitrary decisions solely for the sake of project consistency. These are the stuff of the pedant's PR comments. Rather than have such conversations in PR comments, we can lay out the result of discussion here. + +I'm putting up a strawman for each topic here, mostly based on my memory of reading related Stack Overflow articles etc. If contributors feel strongly (and we don't have anything better to do) then maybe we can convince each other to update this section. + +Trailing vs hanging braces + Data + Hanging closing brace is preferred, trailing commas always to help reduce churn in diffs + Function, class, method args + Inline until line-length, then style as per data + Nesting + Functions + Short: group hanging close parentheses + Long: one closing parentheses per line + +Single vs double quotes + Single quotes are preferred; less strain on the hands, eyes + + Double quotes are only better so as to contain single quotes, but we want to contain doubles as often + +Line continuation + Implicit parentheses continuation is preferred + +British vs American spelling + We should be consistent, it looks like we have American to British at approx 140 to 60 in the code. There's not enough occurrences that we couldn't be consistent one way or the other. It breaks my heart that British spelling could lose this one but I'm happy to 'z' things up for the sake of consistency. So I put forward British to be preferred. Either that strawman wins out, or I incite interest in ~bike-shedding~ guiding the direction of this crucial topic from others. + +Dependency graph +---------------- + +These images are not very useful right now but the aim is to tweak the settings of one or more of them to be informative, and/or divide them up into smaller graphs. + +To re-build them, run `fab build_docs:dep_graphs=true`. Note that the dot graph takes a lot of time. + +.. figure:: ../../../../_static/deps-neato.png + :alt: neato graph of dependencies + :width: 100 pc + + :index:`Neato` graph of dependencies + +.. figure:: ../../../../_static/deps-sfdp.png + :alt: SFDP graph of dependencies + :width: 100 pc + + :index:`SFDP` graph of dependencies + +.. figure:: ../../../../_static/deps-dot.png + :alt: Dot graph of dependencies + :width: 100 pc + + :index:`Dot` graph of dependencies + +Key management +-------------- + +Nitro key +^^^^^^^^^ + +Regular contributors are enouraged to take further steps to protect their key and the Nitro Key (Start) is recommended by the BitMessage project for this purpose. + +Debian-quirks +~~~~~~~~~~~~~ + +Stretch makes use of the directory ~/.gnupg/private-keys-v1.d/ to store the private keys. This simplifies some steps of the Nitro Key instructions. See step 5 of Debian's subkeys_ wiki page + +.. _subkeys: https://wiki.debian.org/Subkeys + diff --git a/docs/contribute.dir/develop.dir/testing.rst b/docs/contribute.dir/develop.dir/testing.rst new file mode 100644 index 00000000..2947c7d6 --- /dev/null +++ b/docs/contribute.dir/develop.dir/testing.rst @@ -0,0 +1,4 @@ +Testing +======= + +Currently there is a Travis job somewhere which runs python setup.py test. This doesn't find any tests when run locally for some reason. diff --git a/docs/contribute.dir/develop.dir/todo.rst b/docs/contribute.dir/develop.dir/todo.rst new file mode 100644 index 00000000..34f71f42 --- /dev/null +++ b/docs/contribute.dir/develop.dir/todo.rst @@ -0,0 +1,4 @@ +TODO list +========= + +.. todolist:: diff --git a/docs/contribute.dir/develop.rst b/docs/contribute.dir/develop.rst new file mode 100644 index 00000000..e5563eb2 --- /dev/null +++ b/docs/contribute.dir/develop.rst @@ -0,0 +1,8 @@ +Developing +========== + +.. toctree:: + :maxdepth: 2 + :glob: + + develop.dir/* diff --git a/docs/contribute.dir/processes.rst b/docs/contribute.dir/processes.rst new file mode 100644 index 00000000..eb913325 --- /dev/null +++ b/docs/contribute.dir/processes.rst @@ -0,0 +1,28 @@ +Processes +========= + +In order to keep the Bitmessage project running, the team runs a number of systems and accounts that form the +development pipeline and continuous delivery process. We are always striving to improve this process. Towards +that end it is documented here. + + +Project website +--------------- + +The bitmessage website_ + +Github +------ + +Our official Github_ account is Bitmessage. Our issue tracker is here as well. + + +BitMessage +---------- + +We eat our own dog food! You can send us bug reports via the [chan] bitmessage BM-2cWy7cvHoq3f1rYMerRJp8PT653jjSuEdY + + +.. _website: https://bitmessage.org +.. _Github: https://github.com/Bitmessage/PyBitmessage + diff --git a/docs/contribute.rst b/docs/contribute.rst new file mode 100644 index 00000000..7c79e22c --- /dev/null +++ b/docs/contribute.rst @@ -0,0 +1,25 @@ +Contributing +============ + +.. toctree:: + :maxdepth: 2 + :glob: + + contribute.dir/* + + +- Report_ +- Develop_(develop) +- Translate_ +- Donate_ +- Fork the code and open a PR_ on github +- Search the `issue tracker` on github or open a new issue +- Send bug report to the chan + +.. _Report: https://github.com/Bitmessage/PyBitmessage/issues +.. _Develop: https://github.com/Bitmessage/PyBitmessage +.. _Translate: https://www.transifex.com/bitmessage-project/pybitmessage/ +.. _Donate: https://tip4commit.com/github/Bitmessage/PyBitmessage +.. _PR: https://github.com/Bitmessage/PyBitmessage/pulls +.. _`issue tracker`: https://github.com/Bitmessage/PyBitmessage/issues + contributing/* diff --git a/docs/encrypted_payload.rst b/docs/encrypted_payload.rst deleted file mode 100644 index 346d370d..00000000 --- a/docs/encrypted_payload.rst +++ /dev/null @@ -1,19 +0,0 @@ -+------------+-------------+-----------+--------------------------------------------+ -| 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 deleted file mode 100644 index 61c7fb3e..00000000 --- a/docs/encryption.rst +++ /dev/null @@ -1,257 +0,0 @@ -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 deleted file mode 100644 index 25539ad4..00000000 --- a/docs/extended_encoding.rst +++ /dev/null @@ -1,55 +0,0 @@ -Extended encoding -================= - -Extended encoding is an attempt to create a standard for transmitting structured -data. The goals are flexibility, wide platform support and extensibility. It is -currently available in the v0.6 branch and can be enabled by holding "Shift" -while clicking on Send. It is planned that v5 addresses will have to support -this. It's a work in progress, the basic plain text message works but don't -expect anthing else at this time. - -The data structure is in msgpack, then compressed with zlib. The top level is -a key/value store, and the "" key (empty string) contains the value of the type -of object, which can then have its individual format and standards. - -Text fields are encoded using UTF-8. - -Types ------ - -You can find the implementations in the ``src/messagetypes`` directory of -PyBitmessage. Each type has its own file which includes one class, and they are -dynamically loaded on startup. It's planned that this will also contain -initialisation, rendering and so on, so that developers can simply add a new -object type by adding a single file in the messagetypes directory and not have -to change any other part of the code. - -message -^^^^^^^ - -The replacement for the old messages. Mandatory keys are ``body`` and -``subject``, others are currently not implemented and not mandatory. Proposed -other keys: - -``parents``: - array of msgids referring to messages that logically precede it in a - conversation. Allows to create a threaded conversation view - -``files``: - array of files (which is a key/value pair): - - ``name``: - file name, mandatory - ``data``: - the binary data of the file - ``type``: - MIME content type - ``disposition``: - MIME content disposition, possible values are "inline" and "attachment" - -vote -^^^^ - -Dummy code available in the repository. Supposed to serve voting in a chan -(thumbs up/down) for decentralised moderation. Does not actually do anything at -the moment and specification can change. diff --git a/docs/index.rst b/docs/index.rst index 6edb0313..cc8c9523 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,23 +1,19 @@ .. mdinclude:: ../README.md - :end-line: 20 -Protocol documentation ----------------------- -.. toctree:: - :maxdepth: 2 - - protocol - address - encryption - pow - -Code documentation ------------------- +Documentation +------------- .. toctree:: :maxdepth: 3 autodoc/pybitmessage +Legacy pages +------------ +.. toctree:: + :maxdepth: 2 + + usage + contribute Indices and tables ------------------ @@ -25,6 +21,3 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` * :ref:`search` - -.. mdinclude:: ../README.md - :start-line: 21 diff --git a/docs/pow.rst b/docs/pow.rst deleted file mode 100644 index 3786b075..00000000 --- a/docs/pow.rst +++ /dev/null @@ -1,77 +0,0 @@ -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 deleted file mode 100644 index 16c3f174..00000000 --- a/docs/pow_formula.rst +++ /dev/null @@ -1,7 +0,0 @@ - -.. 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 deleted file mode 100644 index 17a13dd9..00000000 --- a/docs/protocol.rst +++ /dev/null @@ -1,997 +0,0 @@ -Protocol specification -====================== - -.. warning:: All objects sent on the network should support protocol v3 - starting on Sun, 16 Nov 2014 22:00:00 GMT. - -.. toctree:: - :maxdepth: 2 - -Common standards ----------------- - -Hashes -^^^^^^ - -Most of the time `SHA-512 `_ hashes are -used, however `RIPEMD-160 `_ is also used -when creating an address. - -A double-round of SHA-512 is used for the Proof Of Work. Example of -double-SHA-512 encoding of string "hello": - -.. highlight:: nasm - -:: - - hello - 9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043(first round of sha-512) - 0592a10584ffabf96539f3d780d776828c67da1ab5b169e9e8aed838aaecc9ed36d49ff1423c55f019e050c66c6324f53588be88894fef4dcffdb74b98e2b200(second round of sha-512) - -For Bitmessage addresses (RIPEMD-160) this would give: - -:: - - hello - 9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043(first round is sha-512) - 79a324faeebcbf9849f310545ed531556882487e (with ripemd-160) - - -Common structures ------------------ - -All integers are encoded in big endian. (This is different from Bitcoin). - -.. list-table:: Message structure - :header-rows: 1 - :widths: auto - - * - Field Size - - Description - - Data type - - Comments - * - 4 - - magic - - uint32_t - - Magic value indicating message origin network, and used to seek to next - message when stream state is unknown - * - 12 - - command - - char[12] - - ASCII string identifying the packet content, NULL padded (non-NULL - padding results in packet rejected) - * - 4 - - length - - uint32_t - - Length of payload in number of bytes. Because of other restrictions, - there is no reason why this length would ever be larger than 1600003 - bytes. Some clients include a sanity-check to avoid processing messages - which are larger than this. - * - 4 - - checksum - - uint32_t - - First 4 bytes of sha512(payload) - * - ? - - message_payload - - uchar[] - - The actual data, a :ref:`message ` or an object_. - Not to be confused with objectPayload. - -Known magic values: - -+-------------+-------------------+ -| Magic value | Sent over wire as | -+=============+===================+ -| 0xE9BEB4D9 | E9 BE B4 D9 | -+-------------+-------------------+ - -.. _varint: - -Variable length integer -^^^^^^^^^^^^^^^^^^^^^^^ - -Integer can be encoded depending on the represented value to save space. -Variable length integers always precede an array/vector of a type of data that -may vary in length. Varints **must** use the minimum possible number of bytes to -encode a value. For example, the value 6 can be encoded with one byte therefore -a varint that uses three bytes to encode the value 6 is malformed and the -decoding task must be aborted. - -+---------------+----------------+------------------------------------------+ -| Value | Storage length | Format | -+===============+================+==========================================+ -| < 0xfd | 1 | uint8_t | -+---------------+----------------+------------------------------------------+ -| <= 0xffff | 3 | 0xfd followed by the integer as uint16_t | -+---------------+----------------+------------------------------------------+ -| <= 0xffffffff | 5 | 0xfe followed by the integer as uint32_t | -+---------------+----------------+------------------------------------------+ -| - | 9 | 0xff followed by the integer as uint64_t | -+---------------+----------------+------------------------------------------+ - -Variable length string -^^^^^^^^^^^^^^^^^^^^^^ - -Variable length string can be stored using a variable length integer followed by -the string itself. - -+------------+-------------+------------+----------------------------------+ -| Field Size | Description | Data type | Comments | -+============+=============+============+==================================+ -| 1+ | length | |var_int| | Length of the string | -+------------+-------------+------------+----------------------------------+ -| ? | string | char[] | The string itself (can be empty) | -+------------+-------------+------------+----------------------------------+ - -Variable length list of integers -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -n integers can be stored using n+1 :ref:`variable length integers ` -where the first var_int equals n. - -+------------+-------------+-----------+----------------------------+ -| Field Size | Description | Data type | Comments | -+============+=============+===========+============================+ -| 1+ | count | |var_int| | Number of var_ints below | -+------------+-------------+-----------+----------------------------+ -| 1+ | | var_int | The first value stored | -+------------+-------------+-----------+----------------------------+ -| 1+ | | var_int | The second value stored... | -+------------+-------------+-----------+----------------------------+ -| 1+ | | var_int | etc... | -+------------+-------------+-----------+----------------------------+ - -.. |var_int| replace:: :ref:`var_int ` - -Network address -^^^^^^^^^^^^^^^ - -When a network address is needed somewhere, this structure is used. Network -addresses are not prefixed with a timestamp or stream in the version_ message. - -.. list-table:: - :header-rows: 1 - :widths: auto - - * - Field Size - - Description - - Data type - - Comments - * - 8 - - time - - uint64 - - the Time. - * - 4 - - stream - - uint32 - - Stream number for this node - * - 8 - - services - - uint64_t - - same service(s) listed in version_ - * - 16 - - IPv6/4 - - char[16] - - IPv6 address. IPv4 addresses are written into the message as a 16 byte - `IPv4-mapped IPv6 address `_ - (12 bytes 00 00 00 00 00 00 00 00 00 00 FF FF, followed by the 4 bytes of - the IPv4 address). - * - 2 - - port - - uint16_t - - port number - -Inventory Vectors -^^^^^^^^^^^^^^^^^ - -Inventory vectors are used for notifying other nodes about objects they have or -data which is being requested. Two rounds of SHA-512 are used, resulting in a -64 byte hash. Only the first 32 bytes are used; the later 32 bytes are ignored. - -Inventory vectors consist of the following data format: - -+------------+-------------+-----------+--------------------+ -| Field Size | Description | Data type | Comments | -+============+=============+===========+====================+ -| 32 | hash | char[32] | Hash of the object | -+------------+-------------+-----------+--------------------+ - -Encrypted payload -^^^^^^^^^^^^^^^^^ - -Bitmessage uses `ECIES `_ to encrypt its messages. For more information see :doc:`encryption` - -.. include:: encrypted_payload.rst - -Unencrypted Message Data -^^^^^^^^^^^^^^^^^^^^^^^^ - -.. list-table:: - :header-rows: 1 - :widths: auto - - * - Field Size - - Description - - Data type - - Comments - * - 1+ - - msg_version - - var_int - - Message format version. **This field is not included after the - protocol v3 upgrade period**. - * - 1+ - - address_version - - var_int - - Sender's address version number. This is needed in order to calculate - the sender's address to show in the UI, and also to allow for forwards - compatible changes to the public-key data included below. - * - 1+ - - stream - - var_int - - Sender's stream number - * - 4 - - behavior bitfield - - uint32_t - - A bitfield of optional behaviors and features that can be expected from - the node with this pubkey included in this msg message (the sender's - pubkey). - * - 64 - - public signing key - - uchar[] - - The ECC public key used for signing (uncompressed format; - normally prepended with \x04) - * - 64 - - public encryption key - - uchar[] - - The ECC public key used for encryption (uncompressed format; - normally prepended with \x04) - * - 1+ - - nonce_trials_per_byte - - var_int - - Used to calculate the difficulty target of messages accepted by this - node. The higher this value, the more difficult the Proof of Work must - be before this individual will accept the message. This number is the - average number of nonce trials a node will have to perform to meet the - Proof of Work requirement. 1000 is the network minimum so any lower - values will be automatically raised to 1000. **This field is new and is - only included when the address_version >= 3**. - * - 1+ - - extra_bytes - - var_int - - Used to calculate the difficulty target of messages accepted by this - node. The higher this value, the more difficult the Proof of Work must - be before this individual will accept the message. This number is added - to the data length to make sending small messages more difficult. - 1000 is the network minimum so any lower values will be automatically - raised to 1000. **This field is new and is only included when the - address_version >= 3**. - * - 20 - - destination ripe - - uchar[] - - The ripe hash of the public key of the receiver of the message - * - 1+ - - encoding - - var_int - - :ref:`Message Encoding ` type - * - 1+ - - message_length - - var_int - - Message Length - * - message_length - - message - - uchar[] - - The message. - * - 1+ - - ack_length - - var_int - - Length of the acknowledgement data - * - ack_length - - ack_data - - uchar[] - - The acknowledgement data to be transmitted. This takes the form of a - Bitmessage protocol message, like another msg message. The POW therein - must already be completed. - * - 1+ - - sig_length - - var_int - - Length of the signature - * - sig_length - - signature - - uchar[] - - The ECDSA signature which covers the object header starting with the - time, appended with the data described in this table down to the - ack_data. - -.. _msg-encodings: - -Message Encodings -""""""""""""""""" - -.. list-table:: - :header-rows: 1 - :widths: auto - - * - Value - - Name - - Description - * - 0 - - IGNORE - - Any data with this number may be ignored. The sending node might simply - be sharing its public key with you. - * - 1 - - TRIVIAL - - UTF-8. No 'Subject' or 'Body' sections. Useful for simple strings - of data, like URIs or magnet links. - * - 2 - - SIMPLE - - UTF-8. Uses 'Subject' and 'Body' sections. No MIME is used. - :: - messageToTransmit = 'Subject:' + subject + '\n' + 'Body:' + message - * - 3 - - EXTENDED - - See :doc:`extended_encoding` - -Further values for the message encodings can be decided upon by the community. -Any MIME or MIME-like encoding format, should they be used, should make use of -Bitmessage's 8-bit bytes. - -.. _behavior-bitfield: - -Pubkey bitfield features -"""""""""""""""""""""""" - -.. list-table:: - :header-rows: 1 - :widths: auto - - * - Bit - - Name - - Description - * - 0 - - undefined - - The most significant bit at the beginning of the structure. Undefined - * - 1 - - undefined - - The next most significant bit. Undefined - * - ... - - ... - - ... - * - 27 - - onion_router - - (**Proposal**) Node can be used to onion-route messages. In theory any - node can onion route, but since it requires more resources, they may have - the functionality disabled. This field will be used to indicate that the - node is willing to do this. - * - 28 - - forward_secrecy - - (**Proposal**) Receiving node supports a forward secrecy encryption - extension. The exact design is pending. - * - 29 - - chat - - (**Proposal**) Address if for chatting rather than messaging. - * - 30 - - include_destination - - (**Proposal**) Receiving node expects that the RIPE hash encoded in their - address preceedes the encrypted message data of msg messages bound for - them. - - .. note:: since hardly anyone implements this, this will be redesigned as - `simple recipient verification `_ - * - 31 - - does_ack - - If true, the receiving node does send acknowledgements (rather than - dropping them). - -.. _msg-types: - -Message types -------------- - -Undefined messages received on the wire must be ignored. - -version -^^^^^^^ - -When a node creates an outgoing connection, it will immediately advertise its -version. The remote node will respond with its version. No futher communication -is possible until both peers have exchanged their version. - -.. list-table:: Payload - :header-rows: 1 - :widths: auto - - * - Field Size - - Description - - Data type - - Comments - * - 4 - - version - - int32_t - - Identifies protocol version being used by the node. Should equal 3. - Nodes should disconnect if the remote node's version is lower but - continue with the connection if it is higher. - * - 8 - - services - - uint64_t - - bitfield of features to be enabled for this connection - * - 8 - - timestamp - - int64_t - - standard UNIX timestamp in seconds - * - 26 - - addr_recv - - net_addr - - The network address of the node receiving this message (not including the - time or stream number) - * - 26 - - addr_from - - net_addr - - The network address of the node emitting this message (not including the - time or stream number and the ip itself is ignored by the receiver) - * - 8 - - nonce - - uint64_t - - Random nonce used to detect connections to self. - * - 1+ - - user_agent - - var_str - - :doc:`useragent` (0x00 if string is 0 bytes long). Sending nodes must not - include a user_agent longer than 5000 bytes. - * - 1+ - - stream_numbers - - var_int_list - - The stream numbers that the emitting node is interested in. Sending nodes - must not include more than 160000 stream numbers. - -A "verack" packet shall be sent if the version packet was accepted. Once you -have sent and received a verack messages with the remote node, send an addr -message advertising up to 1000 peers of which you are aware, and one or more -inv messages advertising all of the valid objects of which you are aware. - -.. list-table:: The following services are currently assigned - :header-rows: 1 - :widths: auto - - * - Value - - Name - - Description - * - 1 - - NODE_NETWORK - - This is a normal network node. - * - 2 - - NODE_SSL - - This node supports SSL/TLS in the current connect (python < 2.7.9 only - supports a SSL client, so in that case it would only have this on when - the connection is a client). - * - 3 - - NODE_POW - - (**Proposal**) This node may do PoW on behalf of some its peers (PoW - offloading/delegating), but it doesn't have to. Clients may have to meet - additional requirements (e.g. TLS authentication) - * - 4 - - NODE_DANDELION - - Node supports `dandelion `_ - -verack -^^^^^^ - -The *verack* message is sent in reply to *version*. This message consists of -only a :ref:`message header ` with the command string -"verack". The TCP timeout starts out at 20 seconds; after verack messages are -exchanged, the timeout is raised to 10 minutes. - -If both sides announce that they support SSL, they **must** perform an SSL -handshake immediately after they both send and receive verack. During this SSL -handshake, the TCP client acts as an SSL client, and the TCP server acts as an -SSL server. The current implementation (v0.5.4 or later) requires the -AECDH-AES256-SHA cipher over TLSv1 protocol, and prefers the secp256k1 curve -(but other curves may be accepted, depending on the version of python and -OpenSSL used). - -addr -^^^^ - -Provide information on known nodes of the network. Non-advertised nodes should -be forgotten after typically 3 hours - -Payload: - -+------------+-------------+-----------+---------------------------------------+ -| Field Size | Description | Data type | Comments | -+============+=============+===========+=======================================+ -| 1+ | count | |var_int| | Number of address entries (max: 1000) | -+------------+-------------+-----------+---------------------------------------+ -| 38 | addr_list | net_addr | Address of other nodes on the network.| -+------------+-------------+-----------+---------------------------------------+ - -inv -^^^ - -Allows a node to advertise its knowledge of one or more objects. Payload -(maximum payload length: 50000 items): - -+------------+-------------+------------+-----------------------------+ -| Field Size | Description | Data type | Comments | -+============+=============+============+=============================+ -| ? | count | |var_int| | Number of inventory entries | -+------------+-------------+------------+-----------------------------+ -| 32x? | inventory | inv_vect[] | Inventory vectors | -+------------+-------------+------------+-----------------------------+ - -getdata -^^^^^^^ - -getdata is used in response to an inv message to retrieve the content of a -specific object after filtering known elements. - -Payload (maximum payload length: 50000 entries): - -+------------+-------------+------------+-----------------------------+ -| Field Size | Description | Data type | Comments | -+============+=============+============+=============================+ -| ? | count | |var_int| | Number of inventory entries | -+------------+-------------+------------+-----------------------------+ -| 32x? | inventory | inv_vect[] | Inventory vectors | -+------------+-------------+------------+-----------------------------+ - -error -^^^^^ -.. note:: New in version 3 - -This message may be silently ignored (and therefor handled like any other -"unknown" message). - -The message is intended to inform the other node about protocol errors and -can be used for debugging and improving code. - -.. list-table:: - :header-rows: 1 - :widths: auto - - * - Field Size - - Description - - Data type - - Comments - * - 1+ - - fatal - - |var_int| - - This qualifies the error. If set to 0, than its just a "warning". - You can expect, everything still worked fine. If set to 1, than - it's an error, so you may expect, something was going wrong - (e.g. an object got lost). If set to 2, it's a fatal error. The node - will drop the line for that error and maybe ban you for some time. - * - 1+ - - ban time - - var_int - - If the error is fatal, you can specify the ban time in seconds, here. - You inform the other node, that you will not accept further connections - for this number of seconds. For non fatal errors this field has - no meaning and should be zero. - * - 1+ - - inventory vector - - var_str - - If the error is related to an object, this Variable length string - contains the inventory vector of that object. If the error is not - related to an object, this string is empty. - * - 1+ - - error text - - var_str - - A human readable string in English, which describes the error. - - -object -^^^^^^ - -An object is a message which is shared throughout a stream. It is the only -message which propagates; all others are only between two nodes. Objects have a -type, like 'msg', or 'broadcast'. To be a valid object, the -:doc:`pow` must be done. The maximum allowable length of an object -(not to be confused with the ``objectPayload``) is |2^18| bytes. - -.. |2^18| replace:: 2\ :sup:`18`\ - -.. list-table:: Message structure - :header-rows: 1 - :widths: auto - - * - Field Size - - Description - - Data type - - Comments - * - 8 - - nonce - - uint64_t - - Random nonce used for the :doc:`pow` - * - 8 - - expiresTime - - uint64_t - - The "end of life" time of this object (be aware, in version 2 of the - protocol this was the generation time). Objects shall be shared with - peers until its end-of-life time has been reached. The node should store - the inventory vector of that object for some extra period of time to - avoid reloading it from another node with a small time delay. The time - may be no further than 28 days + 3 hours in the future. - * - 4 - - objectType - - uint32_t - - Four values are currently defined: 0-"getpubkey", 1-"pubkey", 2-"msg", - 3-"broadcast". All other values are reserved. Nodes should relay objects - even if they use an undefined object type. - * - 1+ - - version - - var_int - - The object's version. Note that msg objects won't contain a version - until Sun, 16 Nov 2014 22:00:00 GMT. - * - 1+ - - stream number - - var_int - - The stream number in which this object may propagate - * - ? - - objectPayload - - uchar[] - - This field varies depending on the object type; see below. - - -Unsupported messages -^^^^^^^^^^^^^^^^^^^^ - -If a node receives an unknown message it **must** silently ignore it. This is -for further extensions of the protocol with other messages. Nodes that don't -understand such a new message type shall be able to work correct with the -message types they understand. - -Maybe some version 2 nodes did already implement it that way, but in version 3 -it is **part of the protocol specification**, that a node **must** -silently ignore unsupported messages. - - -Object types ------------- - -Here are the payloads for various object types. - -getpubkey -^^^^^^^^^ - -When a node has the hash of a public key (from an address) but not the public -key itself, it must send out a request for the public key. - -.. list-table:: - :header-rows: 1 - :widths: auto - - * - Field Size - - Description - - Data type - - Comments - * - 20 - - ripe - - uchar[] - - The ripemd hash of the public key. This field is only included when the - address version is <= 3. - * - 32 - - tag - - uchar[] - - The tag derived from the address version, stream number, and ripe. This - field is only included when the address version is >= 4. - -pubkey -^^^^^^ - -A version 2 pubkey. This is still in use and supported by current clients but -*new* v2 addresses are not generated by clients. - -.. list-table:: - :header-rows: 1 - :widths: auto - - * - Field Size - - Description - - Data type - - Comments - * - 4 - - |behavior_bitfield| - - uint32_t - - A bitfield of optional behaviors and features that can be expected from - the node receiving the message. - * - 64 - - public signing key - - uchar[] - - The ECC public key used for signing (uncompressed format; - normally prepended with \x04 ) - * - 64 - - public encryption key - - uchar[] - - The ECC public key used for encryption (uncompressed format; - normally prepended with \x04 ) - -.. list-table:: A version 3 pubkey - :header-rows: 1 - :widths: auto - - * - Field Size - - Description - - Data type - - Comments - * - 4 - - |behavior_bitfield| - - uint32_t - - A bitfield of optional behaviors and features that can be expected from - the node receiving the message. - * - 64 - - public signing key - - uchar[] - - The ECC public key used for signing (uncompressed format; - normally prepended with \x04 ) - * - 64 - - public encryption key - - uchar[] - - The ECC public key used for encryption (uncompressed format; - normally prepended with \x04 ) - * - 1+ - - nonce_trials_per_byte - - var_int - - Used to calculate the difficulty target of messages accepted by this - node. The higher this value, the more difficult the Proof of Work must - be before this individual will accept the message. This number is the - average number of nonce trials a node will have to perform to meet the - Proof of Work requirement. 1000 is the network minimum so any lower - values will be automatically raised to 1000. - * - 1+ - - extra_bytes - - var_int - - Used to calculate the difficulty target of messages accepted by this - node. The higher this value, the more difficult the Proof of Work must - be before this individual will accept the message. This number is added - to the data length to make sending small messages more difficult. - 1000 is the network minimum so any lower values will be automatically - raised to 1000. - * - 1+ - - sig_length - - var_int - - Length of the signature - * - sig_length - - signature - - uchar[] - - The ECDSA signature which, as of protocol v3, covers the object - header starting with the time, appended with the data described in this - table down to the extra_bytes. - -.. list-table:: A version 4 pubkey - :header-rows: 1 - :widths: auto - - * - Field Size - - Description - - Data type - - Comments - * - 32 - - tag - - uchar[] - - The tag, made up of bytes 32-64 of the double hash of the address data - (see example python code below) - * - ? - - encrypted - - uchar[] - - Encrypted pubkey data. - -When version 4 pubkeys are created, most of the data in the pubkey is encrypted. -This is done in such a way that only someone who has the Bitmessage address -which corresponds to a pubkey can decrypt and use that pubkey. This prevents -people from gathering pubkeys sent around the network and using the data from -them to create messages to be used in spam or in flooding attacks. - -In order to encrypt the pubkey data, a double SHA-512 hash is calculated from -the address version number, stream number, and ripe hash of the Bitmessage -address that the pubkey corresponds to. The first 32 bytes of this hash are used -to create a public and private key pair with which to encrypt and decrypt the -pubkey data, using the same algorithm as message encryption -(see :doc:`encryption`). The remaining 32 bytes of this hash are added to the -unencrypted part of the pubkey and used as a tag, as above. This allows nodes to -determine which pubkey to decrypt when they wish to send a message. - -In PyBitmessage, the double hash of the address data is calculated using the -python code below: - -.. code-block:: python - - doubleHashOfAddressData = hashlib.sha512(hashlib.sha512( - encodeVarint(addressVersionNumber) + encodeVarint(streamNumber) + hash - ).digest()).digest() - - -.. list-table:: Encrypted data in version 4 pubkeys: - :header-rows: 1 - :widths: auto - - * - Field Size - - Description - - Data type - - Comments - * - 4 - - |behavior_bitfield| - - uint32_t - - A bitfield of optional behaviors and features that can be expected from - the node receiving the message. - * - 64 - - public signing key - - uchar[] - - The ECC public key used for signing (uncompressed format; - normally prepended with \x04 ) - * - 64 - - public encryption key - - uchar[] - - The ECC public key used for encryption (uncompressed format; - normally prepended with \x04 ) - * - 1+ - - nonce_trials_per_byte - - var_int - - Used to calculate the difficulty target of messages accepted by this - node. The higher this value, the more difficult the Proof of Work must - be before this individual will accept the message. This number is the - average number of nonce trials a node will have to perform to meet the - Proof of Work requirement. 1000 is the network minimum so any lower - values will be automatically raised to 1000. - * - 1+ - - extra_bytes - - var_int - - Used to calculate the difficulty target of messages accepted by this - node. The higher this value, the more difficult the Proof of Work must - be before this individual will accept the message. This number is added - to the data length to make sending small messages more difficult. - 1000 is the network minimum so any lower values will be automatically - raised to 1000. - * - 1+ - - sig_length - - var_int - - Length of the signature - * - sig_length - - signature - - uchar[] - - The ECDSA signature which covers everything from the object header - starting with the time, then appended with the decrypted data down to - the extra_bytes. This was changed in protocol v3. - -msg -^^^ - -Used for person-to-person messages. Note that msg objects won't contain a -version in the object header until Sun, 16 Nov 2014 22:00:00 GMT. - -.. list-table:: - :header-rows: 1 - :widths: auto - - * - Field Size - - Description - - Data type - - Comments - * - ? - - encrypted - - uchar[] - - Encrypted data. See `Encrypted payload`_. - See also `Unencrypted Message Data`_ - -broadcast -^^^^^^^^^ - -Users who are subscribed to the sending address will see the message appear in -their inbox. Broadcasts are version 4 or 5. - -Pubkey objects and v5 broadcast objects are encrypted the same way: The data -encoded in the sender's Bitmessage address is hashed twice. The first 32 bytes -of the resulting hash constitutes the "private" encryption key and the last -32 bytes constitute a **tag** so that anyone listening can easily decide if -this particular message is interesting. The sender calculates the public key -from the private key and then encrypts the object with this public key. Thus -anyone who knows the Bitmessage address of the sender of a broadcast or pubkey -object can decrypt it. - -The version of broadcast objects was previously 2 or 3 but was changed to 4 or -5 for protocol v3. Having a broadcast version of 5 indicates that a tag is used -which, in turn, is used when the sender's address version is >=4. - -.. list-table:: - :header-rows: 1 - :widths: auto - - * - Field Size - - Description - - Data type - - Comments - * - 32 - - tag - - uchar[] - - The tag. This field is new and only included when the broadcast version - is >= 5. Changed in protocol v3 - * - ? - - encrypted - - uchar[] - - Encrypted broadcast data. The keys are derived as described in the - paragraph above. See Encrypted payload for details about the encryption - algorithm itself. - -Unencrypted data format: - -.. list-table:: - :header-rows: 1 - :widths: auto - - * - Field Size - - Description - - Data type - - Comments - * - 1+ - - broadcast version - - var_int - - The version number of this broadcast protocol message which is equal - to 2 or 3. This is included here so that it can be signed. This is - no longer included in protocol v3 - * - 1+ - - address version - - var_int - - The sender's address version - * - 1+ - - stream number - - var_int - - The sender's stream number - * - 4 - - |behavior_bitfield| - - uint32_t - - A bitfield of optional behaviors and features that can be expected from - the owner of this pubkey. - * - 64 - - public signing key - - uchar[] - - The ECC public key used for signing (uncompressed format; - normally prepended with \x04) - * - 64 - - public encryption key - - uchar[] - - The ECC public key used for encryption (uncompressed format; - normally prepended with \x04) - * - 1+ - - nonce_trials_per_byte - - var_int - - Used to calculate the difficulty target of messages accepted by this - node. The higher this value, the more difficult the Proof of Work must - be before this individual will accept the message. This number is the - average number of nonce trials a node will have to perform to meet the - Proof of Work requirement. 1000 is the network minimum so any lower - values will be automatically raised to 1000. This field is new and is - only included when the address_version >= 3. - * - 1+ - - extra_bytes - - var_int - - Used to calculate the difficulty target of messages accepted by this - node. The higher this value, the more difficult the Proof of Work must - be before this individual will accept the message. This number is added - to the data length to make sending small messages more difficult. - 1000 is the network minimum so any lower values will be automatically - raised to 1000. This field is new and is only included when the - address_version >= 3. - * - 1+ - - encoding - - var_int - - The encoding type of the message - * - 1+ - - messageLength - - var_int - - The message length in bytes - * - messageLength - - message - - uchar[] - - The message - * - 1+ - - sig_length - - var_int - - Length of the signature - * - sig_length - - signature - - uchar[] - - The signature which did cover the unencrypted data from the broadcast - version down through the message. In protocol v3, it covers the - unencrypted object header starting with the time, all appended with - the decrypted data. - -.. |behavior_bitfield| replace:: :ref:`behavior bitfield ` diff --git a/docs/requirements.txt b/docs/requirements.txt index f8b4b17c..55219ec5 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,2 @@ -mistune<=0.8.4 -m2r<=0.2.1 -sphinx_rtd_theme +m2r sphinxcontrib-apidoc -docutils<=0.17.1 diff --git a/docs/usage.rst b/docs/usage.rst new file mode 100644 index 00000000..cf745121 --- /dev/null +++ b/docs/usage.rst @@ -0,0 +1,21 @@ +Usage +===== + +GUI +--- + +Bitmessage has a PyQT GUI_ + +CLI +--- + +Bitmessage has a CLI_ + +API +--- + +Bitmessage has an XML-RPC API_ + +.. _GUI: https://bitmessage.org/wiki/PyBitmessage_Help +.. _CLI: https://bitmessage.org/wiki/PyBitmessage_Help +.. _API: https://bitmessage.org/wiki/API_Reference diff --git a/docs/useragent.rst b/docs/useragent.rst deleted file mode 100644 index 3523a274..00000000 --- a/docs/useragent.rst +++ /dev/null @@ -1,53 +0,0 @@ -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/examples/api_client.py b/examples/api_client.py new file mode 100644 index 00000000..dbad0f0b --- /dev/null +++ b/examples/api_client.py @@ -0,0 +1,72 @@ +# 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 + +if __name__ == '__main__': + + 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/fabfile/README.md b/fabfile/README.md new file mode 100644 index 00000000..5e90147c --- /dev/null +++ b/fabfile/README.md @@ -0,0 +1,101 @@ +# Fabric + +[Fabric](https://www.fabfile.org) is a Python library for performing devops tasks. You can think of it a bit like a +makefile on steroids for Python. Its api abstracts away the clunky way you would run shell commands in Python, check +return values and manage stdio. Tasks may be targetted at particular hosts or group of hosts. + +# Using Fabric + + $ cd PyBitmessage + $ fab + +For a list of available commands: + + $ fab -l + +General fabric commandline help + + $ fab -h + +Arguments can be given: + + $ fab task1:arg1=arg1value,arg2=arg2value task2:option1 + +Tasks target hosts. Hosts can be specified with -H, or roles can be defined and you can target groups of hosts with -R. +Furthermore, you can use -- to run arbitrary shell commands rather than tasks: + + $ fab -H localhost task1 + $ fab -R webservers -- sudo /etc/httpd restart + +# Getting started + + * Install [Fabric](http://docs.fabfile.org/en/1.14/usage/fab.html), + [fabric-virtualenv](https://pypi.org/project/fabric-virtualenv/) and + [virtualenvwrapper](https://virtualenvwrapper.readthedocs.io/en/latest/) + system-wide using your preferred method. + * Create a virtualenv called pybitmessage and install fabfile/requirements.txt + $ mkvirtualenv -r fabfile/requirements.txt --system-site-packages pybitmessage-devops + * Ensure you can ssh localhost with no intervention, which may include: + * ssh [sshd_config server] and [ssh_config client] configuration + * authorized_keys file + * load ssh key + * check(!) and accept the host key + * From the PyBitmessage directory you can now run fab commands! + +# Rationale + +There are a number of advantages that should benefit us: + + * Common tasks can be written in Python and executed consistently + * Common tasks are now under source control + * All developers can run the same commands, if the underlying command sequence for a task changes (after review, obv) + the user does not have to care + * Tasks can be combined either programmatically or on the commandline and run in series or parallel + * Whole environments can be managed very effectively in conjunction with a configuration management system + + +# /etc/ssh/sshd_config + +If you're going to be using ssh to connect to localhost you want to avoid weakening your security. The best way of +doing this is blocking port 22 with a firewall. As a belt and braces approach you can also edit the +/etc/ssh/sshd_config file to restrict login further: + +``` +PubkeyAuthentication no + +... + +Match ::1 + PubkeyAuthentication yes +``` +Adapted from [stackexchange](https://unix.stackexchange.com/questions/406245/limit-ssh-access-to-specific-clients-by-ip-address) + + +# ~/.ssh/config + +Fabric will honour your ~/.ssh/config file for your convenience. Since you will spend more time with this key unlocked +than others you should use a different key: + +``` +Host localhost + HostName localhost + IdentityFile ~/.ssh/id_rsa_localhost + +Host github + HostName github.com + IdentityFile ~/.ssh/id_rsa_github +``` + +# Ideas for further development + +## Smaller + + * Decorators and context managers are useful for accepting common params like verbosity, force or doing command-level help + * if `git status` or `git status --staged` produce results, prefer that to generate the file list + + +## Larger + + * Support documentation translations, aim for current transifex'ed languages + * Fabric 2 is finally out, go @bitprophet! Invoke/Fabric2 is a rewrite of Fabric supporting Python3. Probably makes + sense for us to stick to the battle-hardened 1.x branch, at least until we support Python3. diff --git a/fabfile/__init__.py b/fabfile/__init__.py new file mode 100644 index 00000000..9aec62bb --- /dev/null +++ b/fabfile/__init__.py @@ -0,0 +1,36 @@ +""" +Fabric is a Python library for performing devops tasks. If you have Fabric installed (systemwide or via pip) you can +run commands like this: + + $ fab code_quality + +For a list of commands: + + $ fab -l + +For help on fabric itself: + + $ fab -h + +For more help on a particular command +""" + +from fabric.api import env + +from fabfile.tasks import code_quality, build_docs, push_docs, clean, test + + +# Without this, `fab -l` would display the whole docstring as preamble +__doc__ = "" + +# This list defines which tasks are made available to the user +__all__ = [ + "code_quality", + "test", + "build_docs", + "push_docs", + "clean", +] + +# Honour the user's ssh client configuration +env.use_ssh_config = True diff --git a/fabfile/lib.py b/fabfile/lib.py new file mode 100644 index 00000000..7af40231 --- /dev/null +++ b/fabfile/lib.py @@ -0,0 +1,200 @@ +# pylint: disable=not-context-manager +""" +A library of functions and constants for tasks to make use of. + +""" + +import os +import sys +import re +from functools import wraps + +from fabric.api import run, hide, cd, env +from fabric.context_managers import settings, shell_env +from fabvenv import virtualenv + + +FABRIC_ROOT = os.path.dirname(__file__) +PROJECT_ROOT = os.path.dirname(FABRIC_ROOT) +VENV_ROOT = os.path.expanduser(os.path.join('~', '.virtualenvs', 'pybitmessage-devops')) +PYTHONPATH = os.path.join(PROJECT_ROOT, 'src',) + + +def coerce_list(value): + """Coerce a value into a list""" + if isinstance(value, str): + return value.split(',') + else: + sys.exit("Bad string value {}".format(value)) + + +def coerce_bool(value): + """Coerce a value into a boolean""" + if isinstance(value, bool): + return value + elif any( + [ + value in [0, '0'], + value.lower().startswith('n'), + ] + ): + return False + elif any( + [ + value in [1, '1'], + value.lower().startswith('y'), + ] + ): + return True + else: + sys.exit("Bad boolean value {}".format(value)) + + +def flatten(data): + """Recursively flatten lists""" + result = [] + for item in data: + if isinstance(item, list): + result.append(flatten(item)) + else: + result.append(item) + return result + + +def filelist_from_git(rev=None): + """Return a list of files based on git output""" + cmd = 'git diff --name-only' + if rev: + if rev in ['cached', 'staged']: + cmd += ' --{}'.format(rev) + elif rev == 'working': + pass + else: + cmd += ' -r {}'.format(rev) + + with cd(PROJECT_ROOT): + with hide('running', 'stdout'): + results = [] + ansi_escape = re.compile(r'\x1B\[[0-?]*[ -/]*[@-~]') + clean = ansi_escape.sub('', run(cmd)) + clean = re.sub('\n', '', clean) + for line in clean.split('\r'): + if line.endswith(".py"): + results.append(os.path.abspath(line)) + return results + + +def pycodestyle(path_to_file): + """Run pycodestyle on a file""" + with virtualenv(VENV_ROOT): + with hide('warnings', 'running', 'stdout', 'stderr'): + with settings(warn_only=True): + return run( + 'pycodestyle --config={0} {1}'.format( + os.path.join( + PROJECT_ROOT, + 'setup.cfg', + ), + path_to_file, + ), + ) + + +def flake8(path_to_file): + """Run flake8 on a file""" + with virtualenv(VENV_ROOT): + with hide('warnings', 'running', 'stdout'): + with settings(warn_only=True): + return run( + 'flake8 --config={0} {1}'.format( + os.path.join( + PROJECT_ROOT, + 'setup.cfg', + ), + path_to_file, + ), + ) + + +def pylint(path_to_file): + """Run pylint on a file""" + with virtualenv(VENV_ROOT): + with hide('warnings', 'running', 'stdout', 'stderr'): + with settings(warn_only=True): + with shell_env(PYTHONPATH=PYTHONPATH): + return run( + 'pylint --rcfile={0} {1}'.format( + os.path.join( + PROJECT_ROOT, + 'setup.cfg', + ), + path_to_file, + ), + ) + + +def autopep8(path_to_file): + """Run autopep8 on a file""" + with virtualenv(VENV_ROOT): + with hide('running'): + with settings(warn_only=True): + return run( + "autopep8 --experimental --aggressive --aggressive -i --max-line-length=119 {}".format( + path_to_file + ), + ) + + +def get_filtered_pycodestyle_output(path_to_file): + """Clean up the raw results for pycodestyle""" + + return [ + i + for i in pycodestyle(path_to_file).split(os.linesep) + if i + ] + + +def get_filtered_flake8_output(path_to_file): + """Clean up the raw results for flake8""" + + return [ + i + for i in flake8(path_to_file).split(os.linesep) + if i + ] + + +def get_filtered_pylint_output(path_to_file): + """Clean up the raw results for pylint""" + + return [ + i + for i in pylint(path_to_file).split(os.linesep) + if all([ + i, + not i.startswith(' '), + not i.startswith('\r'), + not i.startswith('-'), + not i.startswith('Y'), + not i.startswith('*'), + not i.startswith('Using config file'), + ]) + ] + + +def default_hosts(hosts): + """Decorator to apply default hosts to a task""" + + def real_decorator(func): + """Manipulate env""" + env.hosts = env.hosts or hosts + + @wraps(func) + def wrapper(*args, **kwargs): + """Original function called from here""" + return func(*args, **kwargs) + + return wrapper + + return real_decorator diff --git a/fabfile/requirements.txt b/fabfile/requirements.txt new file mode 100644 index 00000000..0d0a3962 --- /dev/null +++ b/fabfile/requirements.txt @@ -0,0 +1,9 @@ +# These requirements are for the Fabric commands that support devops tasks for PyBitmessage, not for running +# PyBitmessage itself. +# TODO: Consider moving to an extra_requires group in setup.py + +pycodestyle==2.3.1 # https://github.com/PyCQA/pycodestyle/issues/741 +flake8 +pylint +-e git://github.com/hhatto/autopep8.git@ver1.2.2#egg=autopep8 # Needed for fixing E712 +pep8 # autopep8 doesn't seem to like pycodestyle diff --git a/fabfile/tasks.py b/fabfile/tasks.py new file mode 100644 index 00000000..fb05937d --- /dev/null +++ b/fabfile/tasks.py @@ -0,0 +1,318 @@ +# pylint: disable=not-context-manager +""" +Fabric tasks for PyBitmessage devops operations. + +Note that where tasks declare params to be bools, they use coerce_bool() and so will accept any commandline (string) +representation of true or false that coerce_bool() understands. + +""" + +import os +import sys + +from fabric.api import run, task, hide, cd, settings, sudo +from fabric.contrib.project import rsync_project +from fabvenv import virtualenv + +from fabfile.lib import ( + autopep8, PROJECT_ROOT, VENV_ROOT, coerce_bool, flatten, filelist_from_git, default_hosts, + get_filtered_pycodestyle_output, get_filtered_flake8_output, get_filtered_pylint_output, +) + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'src'))) # noqa:E402 +from version import softwareVersion # pylint: disable=wrong-import-position + + +def get_tool_results(file_list): + """Take a list of files and resuln the results of applying the tools""" + + results = [] + for path_to_file in file_list: + result = {} + result['pycodestyle_violations'] = get_filtered_pycodestyle_output(path_to_file) + result['flake8_violations'] = get_filtered_flake8_output(path_to_file) + result['pylint_violations'] = get_filtered_pylint_output(path_to_file) + result['path_to_file'] = path_to_file + result['total_violations'] = sum([ + len(result['pycodestyle_violations']), + len(result['flake8_violations']), + len(result['pylint_violations']), + ]) + results.append(result) + return results + + +def print_results(results, top, verbose, details): + """Print an item with the appropriate verbosity / detail""" + + if verbose and results: + print ''.join( + [ + os.linesep, + 'total pycodestyle flake8 pylint path_to_file', + os.linesep, + ] + ) + + for item in sort_and_slice(results, top): + + if verbose: + line = "{0} {1} {2} {3} {4}".format( + item['total_violations'], + len(item['pycodestyle_violations']), + len(item['flake8_violations']), + len(item['pylint_violations']), + item['path_to_file'], + ) + else: + line = item['path_to_file'] + print line + + if details: + print "pycodestyle:" + for detail in flatten(item['pycodestyle_violations']): + print detail + print + + print "flake8:" + for detail in flatten(item['flake8_violations']): + print detail + print + + print "pylint:" + for detail in flatten(item['pylint_violations']): + print detail + print + + +def sort_and_slice(results, top): + """Sort dictionary items by the `total_violations` key and honour top""" + + returnables = [] + for item in sorted( + results, + reverse=True, + key=lambda x: x['total_violations'] + )[:top]: + returnables.append(item) + return returnables + + +def generate_file_list(filename): + """Return an unfiltered list of absolute paths to the files to act on""" + + with hide('warnings', 'running', 'stdout'): + with virtualenv(VENV_ROOT): + + if filename: + filename = os.path.abspath(filename) + if not os.path.exists(filename): + print "Bad filename, specify a Python file" + sys.exit(1) + else: + file_list = [filename] + + else: + with cd(PROJECT_ROOT): + file_list = [ + os.path.abspath(i.rstrip('\r')) + for i in run('find . -name "*.py"').split(os.linesep) + ] + + return file_list + + +def create_dependency_graphs(): + """ + To better understand the relationship between methods, dependency graphs can be drawn between functions and + methods. + + Since the resulting images are large and could be out of date on the next commit, storing them in the repo is + pointless. Instead, it makes sense to build a dependency graph for a particular, documented version of the code. + + .. todo:: Consider saving a hash of the intermediate dotty file so unnecessary image generation can be avoided. + + """ + with virtualenv(VENV_ROOT): + with hide('running', 'stdout'): + + # .. todo:: consider a better place to put this, use a particular commit + with cd(PROJECT_ROOT): + with settings(warn_only=True): + if run('stat pyan').return_code: + run('git clone https://github.com/davidfraser/pyan.git') + with cd(os.path.join(PROJECT_ROOT, 'pyan')): + run('git checkout pre-python3') + + # .. todo:: Use better settings. This is MVP to make a diagram + with cd(PROJECT_ROOT): + file_list = run("find . -type f -name '*.py' ! -path './src/.eggs/*'").split('\r\n') + for cmd in [ + 'neato -Goverlap=false -Tpng > deps-neato.png', + 'sfdp -Goverlap=false -Tpng > deps-sfdp.png', + 'dot -Goverlap=false -Tpng > deps-dot.png', + ]: + pyan_cmd = './pyan/pyan.py {} --dot'.format(' '.join(file_list)) + sed_cmd = r"sed s'/http\-old/http_old/'g" # dot doesn't like dashes + run('|'.join([pyan_cmd, sed_cmd, cmd])) + + run('mv *.png docs/_build/html/_static/') + + +@task +@default_hosts(['localhost']) +def code_quality(verbose=True, details=False, fix=False, filename=None, top=10, rev=None): + """ + Check code quality. + + By default this command will analyse each Python file in the project with a variety of tools and display the count + or details of the violations discovered, sorted by most violations first. + + Default usage: + + $ fab -H localhost code_quality + + :param rev: If not None, act on files changed since this commit. 'cached/staged' and 'working' have special meaning + :type rev: str or None, default None + :param top: Display / fix only the top N violating files, a value of 0 will display / fix all files + :type top: int, default 10 + :param verbose: Display a header and the counts, without this you just get the filenames in order + :type verbose: bool, default True + :param details: Display the violations one per line after the count / file summary + :type details: bool, default False + :param fix: Run autopep8 aggressively on the displayed file(s) + :type fix: bool, default False + :param filename: Don't test/fix the top N, just the specified file + :type filename: string, valid path to a file, default all files in the project + :return: None, exit status equals total number of violations + :rtype: None + + Intended to be temporary until we have improved code quality and have safeguards to maintain it in place. + + """ + # pylint: disable=too-many-arguments + + verbose = coerce_bool(verbose) + details = coerce_bool(details) + fix = coerce_bool(fix) + top = int(top) or -1 + + file_list = generate_file_list(filename) if not rev else filelist_from_git(rev) + results = get_tool_results(file_list) + + if fix: + for item in sort_and_slice(results, top): + autopep8(item['path_to_file']) + # Recalculate results after autopep8 to surprise the user the least + results = get_tool_results(file_list) + + print_results(results, top, verbose, details) + sys.exit(sum([item['total_violations'] for item in results])) + + +@task +@default_hosts(['localhost']) +def test(): + """Run tests on the code""" + + with cd(PROJECT_ROOT): + with virtualenv(VENV_ROOT): + + run('pip uninstall -y pybitmessage') + run('python setup.py install') + + run('pybitmessage -t') + run('python setup.py test') + + +@task +@default_hosts(['localhost']) +def build_docs(dep_graph=False, apidoc=True): + """ + Build the documentation locally. + + :param dep_graph: Build the dependency graphs + :type dep_graph: Bool, default False + :param apidoc: Build the automatically generated rst files from the source code + :type apidoc: Bool, default True + + Default usage: + + $ fab -H localhost build_docs + + Implementation: + + First, a dependency graph is generated and converted into an image that is referenced in the development page. + + Next, the sphinx-apidoc command is (usually) run which searches the code. As part of this it loads the modules and + if this has side-effects then they will be evident. Any documentation strings that make use of Python documentation + conventions (like parameter specification) or the Restructured Text (RsT) syntax will be extracted. + + Next, the `make html` command is run to generate HTML output. Other formats (epub, pdf) are available. + + .. todo:: support other languages + + """ + + apidoc = coerce_bool(apidoc) + + if coerce_bool(dep_graph): + create_dependency_graphs() + + with virtualenv(VENV_ROOT): + with hide('running'): + + apidoc_result = 0 + if apidoc: + run('mkdir -p {}'.format(os.path.join(PROJECT_ROOT, 'docs', 'autodoc'))) + with cd(os.path.join(PROJECT_ROOT, 'docs', 'autodoc')): + with settings(warn_only=True): + run('rm *.rst') + with cd(os.path.join(PROJECT_ROOT, 'docs')): + apidoc_result = run('sphinx-apidoc -o autodoc ..').return_code + + with cd(os.path.join(PROJECT_ROOT, 'docs')): + make_result = run('make html').return_code + return_code = apidoc_result + make_result + + sys.exit(return_code) + + +@task +@default_hosts(['localhost']) +def push_docs(path=None): + """ + Upload the generated docs to a public server. + + Default usage: + + $ fab -H localhost push_docs + + .. todo:: support other languages + .. todo:: integrate with configuration management data to get web root and webserver restart command + + """ + + # Making assumptions + WEB_ROOT = path if path is not None else os.path.join('var', 'www', 'html', 'pybitmessage', 'en', 'latest') + VERSION_ROOT = os.path.join(os.path.dirname(WEB_ROOT), softwareVersion) + + rsync_project( + remote_dir=VERSION_ROOT, + local_dir=os.path.join(PROJECT_ROOT, 'docs', '_build', 'html') + ) + result = run('ln -sf {0} {1}'.format(WEB_ROOT, VERSION_ROOT)) + if result.return_code: + print 'Linking the new release failed' + + # More assumptions + sudo('systemctl restart apache2') + + +@task +@default_hosts(['localhost']) +def clean(): + """Clean up files generated by fabric commands.""" + with hide('running', 'stdout'): + with cd(PROJECT_ROOT): + run(r"find . -name '*.pyc' -exec rm '{}' \;") diff --git a/kivy-requirements.txt b/kivy-requirements.txt deleted file mode 100644 index 185a3ae7..00000000 --- a/kivy-requirements.txt +++ /dev/null @@ -1,11 +0,0 @@ -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 diff --git a/packages/AppImage/AppImageBuilder.yml b/packages/AppImage/AppImageBuilder.yml deleted file mode 100644 index 2c5890d2..00000000 --- a/packages/AppImage/AppImageBuilder.yml +++ /dev/null @@ -1,81 +0,0 @@ -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 deleted file mode 100644 index 3eeaef64..00000000 --- a/packages/AppImage/PyBitmessage.yml +++ /dev/null @@ -1,40 +0,0 @@ -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 deleted file mode 100644 index 0e343236..00000000 --- a/packages/AppImage/qt.conf +++ /dev/null @@ -1,2 +0,0 @@ -[Paths] -Prefix = ../lib/x86_64-linux-gnu/qt4 diff --git a/packages/apparmor/pybitmessage b/packages/apparmor/pybitmessage deleted file mode 100644 index 3ec3d237..00000000 --- a/packages/apparmor/pybitmessage +++ /dev/null @@ -1,19 +0,0 @@ -# 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 d15c3a48..1db9f5b1 100644 --- a/packages/collectd/pybitmessagestatus.py +++ b/packages/collectd/pybitmessagestatus.py @@ -7,13 +7,11 @@ import 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 = "" @@ -30,22 +28,17 @@ def config_callback(ObjConfiguration): apiInterface = node.values[0] elif key.lower() == "apiport" and node.values: apiPort = node.values[0] - pybmurl = "http://{}:{}@{}:{}/".format(apiUsername, apiPassword, apiInterface, str(int(apiPort))) + pybmurl = "http://" + apiUsername + ":" + apiPassword + "@" + apiInterface+ ":" + str(int(apiPort)) + "/" collectd.info('pybitmessagestatus.py config done') - def read_callback(): try: clientStatus = json.loads(api.clientStatus()) - except (ValueError, TypeError): - collectd.info("Exception loading or parsing JSON") - return - except: # noqa:E722 + except: 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": @@ -55,11 +48,10 @@ def read_callback(): metric.type_instance = i.lower() try: metric.values = [clientStatus[i]] - except (TypeError, KeyError): + except: 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 deleted file mode 100644 index ff53e4e7..00000000 --- a/packages/docker/Dockerfile.bionic +++ /dev/null @@ -1,120 +0,0 @@ -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 deleted file mode 100644 index 4dcdf60b..00000000 --- a/packages/docker/Dockerfile.kivy-travis +++ /dev/null @@ -1,64 +0,0 @@ -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 deleted file mode 100644 index 0e6ee5c3..00000000 --- a/packages/docker/buildbot-entrypoint.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/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 deleted file mode 100755 index c0e48855..00000000 --- a/packages/docker/launcher.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/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/flatpak/README.md b/packages/flatpak/README.md new file mode 100644 index 00000000..f0fc6551 --- /dev/null +++ b/packages/flatpak/README.md @@ -0,0 +1,68 @@ +# PyBitmessage Linux flatpak instructions +_Some recent Linux distributions don't support QT4 anymore, hence PyBitmessage +won't run with a GUI. However, if you build PyBitmessage as a flatpak, it will +run in a sandbox which provides QT4.__ + +## Requirements +First make sure you have `flatpak` and `flatpak-builder` installed. Follow the +instructions for your distribution on [flathub](https://flatpak.org/setup/). The +instructions there only cover the installation of `flatpak`, but +`flatpak-builder` should be the same. + +## Build and Install +Once you have `flatpak` and `flatpak-builder` installed: +``` +git clone git://github.com/Bitmessage/PyBitmessage.git +cd PyBitmessage/ +git submodule update --init --recursive +flatpak-builder --install --user -install-deps-from=flathub --force-clean --state-dir=build/.flatpak-builder build/_flatpak org.bitmessage.PyBitmessage.json +``` +This will install PyBitmessage to your local flatpak user repository, but it +takes a while to compile because QT4 and PyQt4 have to be build, among others. + +# Run +When installation is done you can launch PyBitmessage via the **command line**: +`flatpak run -v org.bitmessage.PyBitmessage` + +Flatpak also exports a `.desktop` file, so you should be able to find and launch +PyBitmessage via the **application launcher** of your Desktop (Gnome, KDE, ...). + +# Export +You can create a single file "bundle", which allows you to copy and install the +PyBitmessage flatpak on other devices of the same architecture as the build machine. + +## Create a local flatpak repository +``` +flatpak-builder --repo=build/_flatpak_repo --force-clean --state-dir=build/.flatpak-builder build/_flatpak packages/flatpak/org.bitmessage.PyBitmessage.json +``` +This will create a local flatpak repository in `build/_flatpak_repo/`. + +## Create a bundle +``` +flatpak build-bundle build/_flatpak_repo build/pybitmessage.flatpak org.bitmessage.PyBitmessage +``` +This will create a `pybitmessage.flatpak` bundle file in the `build/` directory. + +This bundle can be copied to other systems or installed locally: +``` +flatpak install pybitmessage.flatpak +``` + +The application can be run using flatpak: +``` +flatpak run org.bitmessage.PyBitmessage +``` + +It can then be uninstalled with this command: +``` +flatpak uninstall org.bitmessage.PyBitmessage +``` + +This way of building an application is very convenient when preparing flatpaks +for testing on another system of the same processor architecture. + +## Cleanup +If you want to free up disk space you can remove the `Sdk` runtime again: +`flatpak uninstall org.freedesktop.Sdk//18.08` + +You can also delete the `build` directory again. diff --git a/packages/flatpak/org.bitmessage.PyBitmessage.json b/packages/flatpak/org.bitmessage.PyBitmessage.json new file mode 100644 index 00000000..a92fc8ea --- /dev/null +++ b/packages/flatpak/org.bitmessage.PyBitmessage.json @@ -0,0 +1,89 @@ +{ + "app-id": "org.bitmessage.PyBitmessage", + "runtime": "org.freedesktop.Platform", + "runtime-version": "18.08", + "sdk": "org.freedesktop.Sdk", + "command": "pybitmessage", + "finish-args" : [ + "--share=network", + "--socket=x11", + "--share=ipc", + "--filesystem=xdg-config/PyBitmessage:create" + ], + "modules": [ + "shared-modules/python2.7/python-2.7.json", + "shared-modules/qt4/qt4-4.8.7-minimal.json", + { + "name": "python-sip", + "sources": [ + { + "type": "archive", + "url": "https://www.riverbankcomputing.com/static/Downloads/sip/4.19.22/sip-4.19.22.tar.gz", + "sha256": "e1b768824ec1a2ee38dd536b6b6b3d06de27b00a2f5f55470d1b512306e3be45" + } + ], + "buildsystem": "simple", + "build-commands": [ + "python configure.py --sip-module PyQt4.sip --no-dist-info", + "make", + "make install" + ] + }, + { + "name": "python-qt4", + "sources": [ + { + "type": "archive", + "url": "http://sourceforge.net/projects/pyqt/files/PyQt4/PyQt-4.12.3/PyQt4_gpl_x11-4.12.3.tar.gz", + "sha256": "a00f5abef240a7b5852b7924fa5fdf5174569525dc076cd368a566619e56d472" + } + ], + "buildsystem": "simple", + "build-commands": [ + "python configure.py -w --confirm-license", + "make", + "make install" + ] + }, + { + "name" : "PyBitmessage-dependencies", + "buildsystem" : "simple", + "build-options": { + "build-args": [ + "--share=network" + ] + }, + "build-commands": [ + "pip --version", + "pip install setuptools msgpack" + ] + }, + { + "name" : "PyBitmessage", + "buildsystem" : "simple", + "build-options": { + "build-args": [ + "--share=network" + ] + }, + "build-commands": [ + "python --version", + "python checkdeps.py", + "python setup.py install --prefix=/app --exec-prefix=/app", + "sed -i 's~/usr/bin/~/app/bin/~' /app/bin/pybitmessage", + "cat /app/bin/pybitmessage", + "mv /app/share/applications/pybitmessage.desktop /app/share/applications/org.bitmessage.PyBitmessage.desktop", + "sed -i 's~Icon=pybitmessage~Icon=org.bitmessage.PyBitmessage~' /app/share/applications/org.bitmessage.PyBitmessage.desktop", + "mv /app/share/icons/hicolor/scalable/apps/pybitmessage.svg /app/share/icons/hicolor/scalable/apps/org.bitmessage.PyBitmessage.svg", + "mv /app/share/icons/hicolor/24x24/apps/pybitmessage.png /app/share/icons/hicolor/24x24/apps/org.bitmessage.PyBitmessage.png", + "which pybitmessage" + ], + "sources" : [ + { + "type" : "dir", + "path" : "../../" + } + ] + } + ] +} diff --git a/packages/flatpak/shared-modules b/packages/flatpak/shared-modules new file mode 160000 index 00000000..3d5959a7 --- /dev/null +++ b/packages/flatpak/shared-modules @@ -0,0 +1 @@ +Subproject commit 3d5959a72ec0d0be923367e08ff01d04f2b6dba8 diff --git a/packages/pyinstaller/bitmessagemain.spec b/packages/pyinstaller/bitmessagemain.spec index fb2b572d..92e52f6a 100644 --- a/packages/pyinstaller/bitmessagemain.spec +++ b/packages/pyinstaller/bitmessagemain.spec @@ -1,60 +1,52 @@ -# -*- mode: python -*- import ctypes import os -import sys import time +import sys -from PyInstaller.utils.hooks import copy_metadata - - -DEBUG = False +if ctypes.sizeof(ctypes.c_voidp) == 4: + arch=32 +else: + arch=64 + +sslName = 'OpenSSL-Win%s' % ("32" if arch == 32 else "64") 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 +srcPath = os.path.join(spec_root[:-20], "src") +qtBase = "PyQt4" openSSLPath = os.path.join(cdrivePath, sslName) msvcrDllPath = os.path.join(cdrivePath, "windows", "system32") +pythonDllPath = os.path.join(cdrivePath, "Python27") outPath = os.path.join(spec_root, "bitmessagemain") -qtBase = "PyQt4" -sys.path.insert(0, srcPath) -os.chdir(srcPath) +importPath = srcPath +sys.path.insert(0,importPath) +os.chdir(sys.path[0]) +from version import softwareVersion +today = time.strftime("%Y%m%d") snapshot = False -hookspath = os.path.join(spec_root, 'hooks') - -excludes = ['bsddb', 'bz2', 'tcl', 'tk', 'Tkinter', 'tests'] -if not DEBUG: - excludes += ['pybitmessage.tests', 'pyelliptic.tests'] +os.rename(os.path.join(srcPath, '__init__.py'), os.path.join(srcPath, '__init__.py.backup')) +# -*- mode: python -*- 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.path.join(srcPath, 'bitmessagemain.py')], + pathex=[outPath], + hiddenimports=['bitmessageqt.languagebox', 'pyopencl','numpy', 'win32com' , 'setuptools.msvc' ,'_cffi_backend'], + hookspath=None, + runtime_hooks=None + ) +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(os.path.join(srcPath, 'translations')): if file_[-3:] != ".qm": continue - extraDatas.append(( - os.path.join('translations', file_), + 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') @@ -65,80 +57,57 @@ def addTranslations(): for file_ in os.listdir(qtdir): if file_[0:3] != "qt_" or file_[5:8] != ".qm": continue - extraDatas.append(( - os.path.join('translations', file_), + extraDatas.append((os.path.join('translations', file_), os.path.join(qtdir, 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') -] +def addUIs(): + import os + extraDatas = [] + for file_ in os.listdir(os.path.join(srcPath, 'bitmessageqt')): + if file_[-3:] != ".ui": + continue + extraDatas.append((os.path.join('ui', file_), os.path.join(srcPath, + 'bitmessageqt', file_), 'DATA')) + return extraDatas # append the translations directory a.datas += addTranslations() -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 - -today = time.strftime("%Y%m%d") - -fname = '%s_%%s_%s.exe' % ( - ('Bitmessagedev', today) if snapshot else ('Bitmessage', softwareVersion) -) % ("x86" if arch == 32 else "x64") +a.datas += addUIs() +a.binaries += [('libeay32.dll', os.path.join(openSSLPath, 'libeay32.dll'), 'BINARY'), + ('python27.dll', os.path.join(pythonDllPath, 'python27.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') + ] + + +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) + pyz = PYZ(a.pure) -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') -) +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')) + +coll = COLLECT(exe, + a.binaries, + a.zipfiles, + a.datas, + strip=False, + upx=False, + name='main') -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 deleted file mode 100644 index e796c1f5..00000000 --- a/packages/pyinstaller/hooks/pyinstaller_rthook_plugins.py +++ /dev/null @@ -1,17 +0,0 @@ -"""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 deleted file mode 100644 index 47c27936..00000000 --- a/packages/snap/snapcraft.yaml +++ /dev/null @@ -1,76 +0,0 @@ -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 new file mode 100644 index 00000000..fa1c4c91 --- /dev/null +++ b/packages/unmaintained/Makefile @@ -0,0 +1,62 @@ +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 new file mode 100755 index 00000000..9caed2dc --- /dev/null +++ b/packages/unmaintained/debian.sh @@ -0,0 +1,48 @@ +#!/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 new file mode 100644 index 00000000..9fc04ddb --- /dev/null +++ b/packages/unmaintained/debian/changelog @@ -0,0 +1,483 @@ +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 new file mode 100644 index 00000000..ec635144 --- /dev/null +++ b/packages/unmaintained/debian/compat @@ -0,0 +1 @@ +9 diff --git a/packages/unmaintained/debian/control b/packages/unmaintained/debian/control new file mode 100644 index 00000000..e72de58a --- /dev/null +++ b/packages/unmaintained/debian/control @@ -0,0 +1,21 @@ +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 new file mode 100644 index 00000000..b341b873 --- /dev/null +++ b/packages/unmaintained/debian/copyright @@ -0,0 +1,30 @@ +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 new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/packages/unmaintained/debian/docs @@ -0,0 +1 @@ + diff --git a/packages/unmaintained/debian/manpages b/packages/unmaintained/debian/manpages new file mode 100644 index 00000000..54af5648 --- /dev/null +++ b/packages/unmaintained/debian/manpages @@ -0,0 +1 @@ +man/pybitmessage.1.gz diff --git a/packages/unmaintained/debian/pybm b/packages/unmaintained/debian/pybm new file mode 100644 index 00000000..95e61e54 --- /dev/null +++ b/packages/unmaintained/debian/pybm @@ -0,0 +1,4 @@ +#!/bin/sh +cd /usr/share/pybitmessage +exec python bitmessagemain.py + diff --git a/packages/unmaintained/debian/rules b/packages/unmaintained/debian/rules new file mode 100755 index 00000000..5b29d243 --- /dev/null +++ b/packages/unmaintained/debian/rules @@ -0,0 +1,43 @@ +#!/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 new file mode 100644 index 00000000..163aaf8d --- /dev/null +++ b/packages/unmaintained/debian/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/packages/unmaintained/debian/source/include-binaries b/packages/unmaintained/debian/source/include-binaries new file mode 100644 index 00000000..f676fce8 --- /dev/null +++ b/packages/unmaintained/debian/source/include-binaries @@ -0,0 +1,18 @@ +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/requirements.txt b/requirements.txt index c787d2dd..c55e5cf1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,3 @@ -coverage +python_prctl psutil -pycryptodome -PyQt5;python_version>="3.7" and platform_machine=="x86_64" -mock;python_version<="2.7" -python_prctl;platform_system=="Linux" -six -xvfbwrapper;platform_system=="Linux" +pycrypto diff --git a/run-kivy-tests-in-docker.sh b/run-kivy-tests-in-docker.sh deleted file mode 100755 index f34bbd19..00000000 --- a/run-kivy-tests-in-docker.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/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 index 28ceaede..a4e0547c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,7 +8,6 @@ max-line-length = 119 [flake8] max-line-length = 119 -exclude = bitmessagecli.py,bitmessagecurses,bitmessageqt,plugins,tests,umsgpack ignore = E722,F841,W503 # E722: pylint is preferred for bare-except # F841: pylint is preferred for unused-variable diff --git a/setup.py b/setup.py index 30436bec..3e585b6b 100644 --- a/setup.py +++ b/setup.py @@ -1,11 +1,8 @@ #!/usr/bin/env python2.7 import os -import platform import shutil -import sys -from importlib import import_module from setuptools import setup, Extension from setuptools.command.install import install @@ -13,23 +10,18 @@ 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'] + 'docs': ['sphinx', 'sphinxcontrib-apidoc', 'm2r'] } class InstallCmd(install): - """Custom setuptools install command preparing icons""" - def run(self): # prepare icons directories try: @@ -62,7 +54,7 @@ if __name__ == "__main__": libraries=['pthread', 'crypto'], ) - installRequires = ['six'] + installRequires = [] packages = [ 'pybitmessage', 'pybitmessage.bitmessageqt', @@ -74,26 +66,6 @@ if __name__ == "__main__": 'pybitmessage.pyelliptic', '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 @@ -101,32 +73,14 @@ if __name__ == "__main__": try: import msgpack installRequires.append( - "msgpack-python" if msgpack.version[:2] < (0, 6) else "msgpack") + "msgpack-python" if msgpack.version[:2] == (0, 4) else "msgpack") except ImportError: try: - import_module('umsgpack') + import umsgpack installRequires.append("umsgpack") except ImportError: 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', version=softwareVersion, @@ -135,11 +89,13 @@ if __name__ == "__main__": long_description=README, license='MIT', # TODO: add author info + #author='', + #author_email='', url='https://bitmessage.org', # TODO: add keywords + #keywords='', install_requires=installRequires, tests_require=requirements, - test_suite='tests.unittest_discover', extras_require=EXTRAS_REQUIRE, classifiers=[ "License :: OSI Approved :: MIT License" @@ -151,8 +107,19 @@ if __name__ == "__main__": ], package_dir={'pybitmessage': 'src'}, packages=packages, - package_data=package_data, - data_files=data_files, + 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']) + ], ext_modules=[bitmsghash], zip_safe=False, entry_points={ @@ -174,15 +141,12 @@ if __name__ == "__main__": 'libmessaging =' 'pybitmessage.plugins.indicator_libmessaging [gir]' ], - '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 [] + # 'console_scripts': [ + # 'pybitmessage = pybitmessage.bitmessagemain:main' + # ] }, scripts=['src/pybitmessage'], cmdclass={'install': InstallCmd}, diff --git a/src/addresses.py b/src/addresses.py index 885c1f64..0d3d4400 100644 --- a/src/addresses.py +++ b/src/addresses.py @@ -1,58 +1,50 @@ """ Operations with addresses """ -# pylint: disable=inconsistent-return-statements - -import logging +# pylint: disable=redefined-outer-name,inconsistent-return-statements +import hashlib from binascii import hexlify, unhexlify from struct import pack, unpack -try: - from highlevelcrypto import double_sha512 -except ImportError: - from .highlevelcrypto import double_sha512 - - -logger = logging.getLogger('default') +from debug import logger ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" -def encodeBase58(num): +def encodeBase58(num, alphabet=ALPHABET): """Encode a number in Base X Args: num: The number to encode alphabet: The alphabet to use for encoding """ - if num < 0: - return None if num == 0: - return ALPHABET[0] + return alphabet[0] arr = [] - base = len(ALPHABET) + base = len(alphabet) while num: - num, rem = divmod(num, base) - arr.append(ALPHABET[rem]) + rem = num % base + num = num // base + arr.append(alphabet[rem]) arr.reverse() return ''.join(arr) -def decodeBase58(string): +def decodeBase58(string, alphabet=ALPHABET): """Decode a Base X encoded string into the number 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 @@ -139,6 +131,15 @@ def decodeVarint(data): return (encodedValue, 9) +def calculateInventoryHash(data): + """Calculate inventory hash from object data""" + sha = hashlib.new('sha512') + sha2 = hashlib.new('sha512') + sha.update(data) + sha2.update(sha.digest()) + return sha2.digest()[0:32] + + def encodeAddress(version, stream, ripe): """Convert ripe to address""" if version >= 2 and version < 4: @@ -147,27 +148,28 @@ def encodeAddress(version, stream, ripe): 'Programming error in encodeAddress: The length of' ' a given ripe hash was not 20.' ) - - if ripe[:2] == b'\x00\x00': + if ripe[:2] == '\x00\x00': ripe = ripe[2:] - elif ripe[:1] == b'\x00': + elif ripe[:1] == '\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(b'\x00') + ripe = ripe.lstrip('\x00') storedBinaryData = encodeVarint(version) + encodeVarint(stream) + ripe # Generate the checksum - checksum = double_sha512(storedBinaryData)[0:4] + sha = hashlib.new('sha512') + sha.update(storedBinaryData) + currentHash = sha.digest() + sha = hashlib.new('sha512') + sha.update(currentHash) + checksum = sha.digest()[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) @@ -189,8 +191,8 @@ 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 in python2 - hexdata = hex(integer)[2:].rstrip('L') + # with a 0x and appended with a L + hexdata = hex(integer)[2:-1] if len(hexdata) % 2 != 0: hexdata = '0' + hexdata @@ -198,7 +200,13 @@ def decodeAddress(address): data = unhexlify(hexdata) checksum = data[-4:] - if checksum != double_sha512(data[:-4])[0:4]: + sha = hashlib.new('sha512') + sha.update(data[:-4]) + currentHash = sha.digest() + sha = hashlib.new('sha512') + sha.update(currentHash) + + if checksum != sha.digest()[0:4]: status = 'checksumfailed' return status, 0, 0, '' @@ -234,13 +242,13 @@ def decodeAddress(address): data[bytesUsedByVersionNumber + bytesUsedByStreamNumber:-4] if len(embeddedRipeData) == 19: return status, addressVersionNumber, streamNumber, \ - b'\x00' + embeddedRipeData + '\x00' + embeddedRipeData elif len(embeddedRipeData) == 20: return status, addressVersionNumber, streamNumber, \ embeddedRipeData elif len(embeddedRipeData) == 18: return status, addressVersionNumber, streamNumber, \ - b'\x00\x00' + embeddedRipeData + '\x00\x00' + embeddedRipeData elif len(embeddedRipeData) < 18: return 'ripetooshort', 0, 0, '' elif len(embeddedRipeData) > 20: @@ -249,7 +257,7 @@ def decodeAddress(address): elif addressVersionNumber == 4: embeddedRipeData = \ data[bytesUsedByVersionNumber + bytesUsedByStreamNumber:-4] - if embeddedRipeData[0:1] == b'\x00': + if embeddedRipeData[0:1] == '\x00': # In order to enforce address non-malleability, encoded # RIPE data must have NULL bytes removed from the front return 'encodingproblem', 0, 0, '' @@ -257,7 +265,7 @@ def decodeAddress(address): return 'ripetoolong', 0, 0, '' elif len(embeddedRipeData) < 4: return 'ripetooshort', 0, 0, '' - x00string = b'\x00' * (20 - len(embeddedRipeData)) + x00string = '\x00' * (20 - len(embeddedRipeData)) return status, addressVersionNumber, streamNumber, \ x00string + embeddedRipeData @@ -266,3 +274,69 @@ 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 + + +# TODO: make test case +if __name__ == "__main__": + from pyelliptic import arithmetic + + 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 f9bf55de..70da0cda 100644 --- a/src/api.py +++ b/src/api.py @@ -1,190 +1,71 @@ +""" +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. +""" # Copyright (c) 2012-2016 Jonathan Warren -# Copyright (c) 2012-2023 The Bitmessage developers - -""" -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`. -""" - +# Copyright (c) 2012-2020 The Bitmessage developers +# pylint: disable=too-many-lines,no-self-use,unused-variable,unused-argument import base64 import errno import hashlib import json -import random +import random # nosec import socket -import subprocess # nosec B404 +import subprocess import time from binascii import hexlify, unhexlify -from struct import pack, unpack - -import six -from six.moves import configparser, http_client, xmlrpc_server +from SimpleXMLRPCServer import SimpleXMLRPCRequestHandler, SimpleXMLRPCServer +from struct import pack +import defaults import helper_inbox import helper_sent -import protocol +import network.stats import proofofwork import queues import shared - import shutdown import state from addresses import ( addBMIfNotPresent, + calculateInventoryHash, decodeAddress, decodeVarint, varintDecodeError ) -from bmconfigparser import config +from bmconfigparser import BMConfigParser from debug import logger -from defaults import ( - networkDefaultProofOfWorkNonceTrialsPerByte, - networkDefaultPayloadLengthExtraBytes) -from helper_sql import ( - SqlBulkExecute, sqlExecute, sqlQuery, sqlStoredProcedure, sql_ready) -from highlevelcrypto import calculateInventoryHash - -try: - from network import connectionpool -except ImportError: - connectionpool = None - -from network import stats, StoppableThread, invQueue +from helper_ackPayload import genAckPayload +from helper_sql import SqlBulkExecute, sqlExecute, sqlQuery, sqlStoredProcedure +from inventory import Inventory +from network.threads import StoppableThread from version import softwareVersion -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_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.' - } +class APIError(Exception): + """APIError exception class""" - 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 + 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.faultCode, self.faultString) + return "API Error %04i: %s" % (self.error_number, self.error_message) + + +class StoppableXMLRPCServer(SimpleXMLRPCServer): + """A SimpleXMLRPCServer that honours state.shutdown""" + allow_reuse_address = True + + def serve_forever(self): + """Start the SimpleXMLRPCServer""" + # pylint: disable=arguments-differ + while state.shutdown == 0: + self.handle_request() # This thread, of which there is only one, runs the API. @@ -198,8 +79,8 @@ class singleAPI(StoppableThread): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: s.connect(( - config.get('bitmessagesettings', 'apiinterface'), - config.getint('bitmessagesettings', 'apiport') + BMConfigParser().get('bitmessagesettings', 'apiinterface'), + BMConfigParser().getint('bitmessagesettings', 'apiport') )) s.shutdown(socket.SHUT_RDWR) s.close() @@ -207,162 +88,61 @@ class singleAPI(StoppableThread): 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') + port = BMConfigParser().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( + port = random.randint(32767, 65535) + se = StoppableXMLRPCServer( + (BMConfigParser().get( 'bitmessagesettings', 'apiinterface'), port), - BMXMLRPCRequestHandler, True, encoding='UTF-8') + MySimpleXMLRPCRequestHandler, True, True) 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( + BMConfigParser().set( 'bitmessagesettings', 'apiport', str(port)) - config.save() + BMConfigParser().save() break - - se.register_instance(BMRPCDispatcher()) se.register_introspection_functions() - apiNotifyPath = config.safeGet( + apiNotifyPath = BMConfigParser().safeGet( 'bitmessagesettings', 'apinotifypath') if apiNotifyPath: logger.info('Trying to call %s', apiNotifyPath) try: - subprocess.call([apiNotifyPath, "startingUp"]) # nosec B603 + subprocess.call([apiNotifyPath, "startingUp"]) except OSError: logger.warning( 'Failed to call %s, removing apinotifypath setting', apiNotifyPath) - config.remove_option( + BMConfigParser().remove_option( 'bitmessagesettings', 'apinotifypath') se.serve_forever() -class CommandHandler(type): +class MySimpleXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): """ - The metaclass for `BMRPCDispatcher` which fills _handlers dict by - methods decorated with @command + 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/ """ - 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 + # pylint: disable=too-many-public-methods - -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). -# 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. @@ -370,9 +150,8 @@ class BMXMLRPCRequestHandler(xmlrpc_server.SimpleXMLRPCRequestHandler): 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 - `SimpleXMLRPCServer.SimpleXMLRPCRequestHandler`, - just hacked to handle cookies + Note: this method is the same as in SimpleXMLRPCRequestHandler, + just hacked to handle cookies """ # Check that the path is legal @@ -390,43 +169,27 @@ class BMXMLRPCRequestHandler(xmlrpc_server.SimpleXMLRPCRequestHandler): L = [] while size_remaining: chunk_size = min(size_remaining, max_chunk_size) - chunk = self.rfile.read(chunk_size) - if not chunk: - break - L.append(chunk) + L.append(self.rfile.read(chunk_size)) size_remaining -= len(L[-1]) - data = b''.join(L) + data = ''.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 + # 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. + # pylint: disable=protected-access + response = self.server._marshaled_dispatch( + data, getattr(self, '_dispatch', None) + ) + except BaseException: # This should only happen if the module is buggy # internal error, report as HTTP server error - self.send_response(http_client.INTERNAL_SERVER_ERROR) + self.send_response(500) self.end_headers() else: # got a valid XML RPC response - self.send_response(http_client.OK) - self.send_header("Content-type", self.server.content_type) + self.send_response(200) + self.send_header("Content-type", "text/xml") self.send_header("Content-length", str(len(response))) # HACK :start -> sends cookies here @@ -447,20 +210,18 @@ class BMXMLRPCRequestHandler(xmlrpc_server.SimpleXMLRPCRequestHandler): shutdown.doCleanShutdown() def APIAuthenticateClient(self): - """ - Predicate to check for valid API credentials in the request header - """ + """Predicate to check for valid API credentials in the request header""" if 'Authorization' in self.headers: # handle Basic authentication - encstr = self.headers.get('Authorization').split()[1] - emailid, password = base64.b64decode( - encstr).decode('utf-8').split(':') + _, encstr = self.headers.get('Authorization').split() + emailid, password = encstr.decode('base64').split(':') return ( - emailid == config.get( - 'bitmessagesettings', 'apiusername' - ) and password == config.get( - 'bitmessagesettings', 'apipassword')) + emailid == BMConfigParser().get( + 'bitmessagesettings', 'apiusername') and + password == BMConfigParser().get( + 'bitmessagesettings', 'apipassword') + ) else: logger.warning( 'Authentication failed because header lacks' @@ -469,14 +230,7 @@ class BMXMLRPCRequestHandler(xmlrpc_server.SimpleXMLRPCRequestHandler): return False - -# 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): + def _decode(self, text, decode_type): try: if decode_type == 'hex': return unhexlify(text) @@ -484,22 +238,29 @@ class BMRPCDispatcher(object): return base64.b64decode(text) except Exception as e: raise APIError( - 22, 'Decode error - %s. Had trouble while decoding string: %r' + 22, "Decode error - %s. Had trouble while decoding string: %r" % (e, text) ) + return None def _verifyAddress(self, address): status, addressVersionNumber, streamNumber, ripe = \ decodeAddress(address) if status != 'success': + logger.warning( + '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) + 10, + 'Address version number too high (or zero) in address: ' + + address) if status == 'varintmalformed': raise APIError(26, 'Malformed varint in address: ' + address) raise APIError( @@ -515,108 +276,70 @@ class BMRPCDispatcher(object): ' Check the address.' ) - 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( # pylint: disable=too-many-arguments - 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) - } + return (status, addressVersionNumber, streamNumber, ripe) # 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': + def HandleListAddresses(self, method): + """Handle a request to list addresses""" + 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': label = base64.b64encode(label) - data.append({ + data += json.dumps({ 'label': label, - 'address': address, + 'address': addressInKeysFile, 'stream': streamNumber, - 'enabled': self.config.safeGetBoolean(address, 'enabled'), - 'chan': self.config.safeGetBoolean(address, 'chan') - }) - return {'addresses': data} + 'enabled': + BMConfigParser().getboolean(addressInKeysFile, 'enabled'), + 'chan': chan + }, indent=4, separators=(',', ': ')) + data += ']}' + return 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: + def HandleListAddressBookEntries(self, params): + """Handle a request to list address book entries""" + + 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 label = shared.fixPotentiallyInvalidUTF8Data(label) - data.append({ + if len(data) > 20: + data += ',' + data += json.dumps({ 'label': base64.b64encode(label), - 'address': address - }) - return {'addresses': data} + 'address': address}, indent=4, separators=(',', ': ')) + data += ']}' + return 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.""" + def HandleAddAddressBookEntry(self, params): + """Handle a request to add an address book entry""" + + if len(params) != 2: + raise APIError(0, "I need label and address") + address, label = params label = self._decode(label, "base64") address = addBMIfNotPresent(address) self._verifyAddress(address) - # TODO: add unique together constraint in the table queryreturn = sqlQuery( "SELECT address FROM addressbook WHERE address=?", address) if queryreturn != []: @@ -629,10 +352,12 @@ class BMRPCDispatcher(object): queues.UISignalQueue.put(('rerenderAddressBook', '')) return "Added address %s to address book" % address - # the deleteAddressbook alias should be deleted eventually. - @command('deleteAddressBookEntry', 'legacy:deleteAddressbook') - def HandleDeleteAddressBookEntry(self, address): - """Delete an entry from address book.""" + def HandleDeleteAddressBookEntry(self, params): + """Handle a request to delete an address book entry""" + + if len(params) != 1: + raise APIError(0, "I need an address") + address, = params address = addBMIfNotPresent(address) self._verifyAddress(address) sqlExecute('DELETE FROM addressbook WHERE address=?', address) @@ -641,40 +366,49 @@ class BMRPCDispatcher(object): queues.UISignalQueue.put(('rerenderAddressBook', '')) return "Deleted address book 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. + def HandleCreateRandomAddress(self, params): + """Handle a request to create a random address""" - :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). - """ + if not params: + raise APIError(0, 'I need parameters!') - 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)) + 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!') label = self._decode(label, "base64") try: - label.decode('utf-8') - except UnicodeDecodeError: + unicode(label, 'utf-8') + except BaseException: 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, "", @@ -682,51 +416,98 @@ class BMRPCDispatcher(object): )) return queues.apiAddressGeneratorReturnQueue.get() - # pylint: disable=too-many-arguments - @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. + def HandleCreateDeterministicAddresses(self, params): + """Handle a request to create a deterministic address""" + # pylint: disable=too-many-branches, too-many-statements - :param str passphrase: base64 encoded passphrase - :param int numberOfAddresses: number of addresses to create, - up to 999 + if not params: + raise APIError(0, 'I need parameters!') - *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. - """ + elif len(params) == 1: + passphrase, = params + numberOfAddresses = 1 + addressVersionNumber = 0 + streamNumber = 0 + eighteenByteRipe = False + nonceTrialsPerByte = BMConfigParser().get( + 'bitmessagesettings', 'defaultnoncetrialsperbyte') + payloadLengthExtraBytes = BMConfigParser().get( + 'bitmessagesettings', 'defaultpayloadlengthextrabytes') - 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) + 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 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)) + 23, 'Bool expected in eighteenByteRipe, saw %s instead' % + type(eighteenByteRipe)) passphrase = self._decode(passphrase, "base64") # 0 means "just use the proper addressVersionNumber" if addressVersionNumber == 0: addressVersionNumber = 4 - if addressVersionNumber not in (3, 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). %i isn\'t supported.' - % addressVersionNumber) + ' (which means auto-select). %i isn\'t supported.' % + addressVersionNumber) if streamNumber == 0: # 0 means "just use the most available stream" - streamNumber = 1 # FIXME hard coded stream no + streamNumber = 1 if streamNumber != 1: raise APIError( 3, 'The stream number must be 1 (or 0 which means' @@ -751,24 +532,27 @@ class BMRPCDispatcher(object): '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 - 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. - """ + def HandleGetDeterministicAddress(self, params): + """Handle a request to get a deterministic address""" + if len(params) != 3: + raise APIError(0, 'I need exactly 3 parameters.') + passphrase, addressVersionNumber, streamNumber = params numberOfAddresses = 1 eighteenByteRipe = False if not passphrase: raise APIError(1, 'The specified passphrase is blank.') passphrase = self._decode(passphrase, "base64") - if addressVersionNumber not in (3, 4): + if addressVersionNumber != 3 and addressVersionNumber != 4: raise APIError( 2, 'The address version number currently must be 3 or 4. %i' ' isn\'t supported.' % addressVersionNumber) @@ -786,22 +570,24 @@ class BMRPCDispatcher(object): )) return queues.apiAddressGeneratorReturnQueue.get() - @command('createChan') - def HandleCreateChan(self, passphrase): - """ - Creates a new chan. passphrase must be base64 encoded. - Returns the corresponding Bitmessage address. - """ + def HandleCreateChan(self, params): + """Handle a request to create a chan""" + if not params: + raise APIError(0, 'I need parameters.') + + elif len(params) == 1: + passphrase, = params passphrase = self._decode(passphrase, "base64") + 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: - passphrase.decode('utf-8') + unicode(passphrase, 'utf-8') label = str_chan + ' ' + passphrase - except UnicodeDecodeError: + except BaseException: label = str_chan + ' ' + repr(passphrase) addressVersionNumber = 4 @@ -814,316 +600,393 @@ class BMRPCDispatcher(object): passphrase, True )) queueReturn = queues.apiAddressGeneratorReturnQueue.get() - try: - return queueReturn[0] - except IndexError: + if not queueReturn: raise APIError(24, 'Chan address is already present.') + address = queueReturn[0] + return address - @command('joinChan') - def HandleJoinChan(self, passphrase, suppliedAddress): - """ - Join a chan. passphrase must be base64 encoded. Returns 'success'. - """ + def HandleJoinChan(self, params): + """Handle a request to join a chan""" + if len(params) < 2: + raise APIError(0, 'I need two parameters.') + elif len(params) == 2: + passphrase, suppliedAddress = params passphrase = self._decode(passphrase, "base64") 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: - passphrase.decode('utf-8') + unicode(passphrase, 'utf-8') label = str_chan + ' ' + passphrase - except UnicodeDecodeError: + except BaseException: label = str_chan + ' ' + repr(passphrase) - - self._verifyAddress(suppliedAddress) + status, addressVersionNumber, streamNumber, toRipe = ( + self._verifyAddress(suppliedAddress)) suppliedAddress = addBMIfNotPresent(suppliedAddress) queues.apiAddressGeneratorReturnQueue.queue.clear() 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.') + addressGeneratorReturnValue = \ + queues.apiAddressGeneratorReturnQueue.get() + if addressGeneratorReturnValue[0] == \ + 'chan name does not match address': + raise APIError(18, 'Chan name does not match address.') + if not addressGeneratorReturnValue: + raise APIError(24, 'Chan address is already present.') return "success" - @command('leaveChan') - def HandleLeaveChan(self, address): - """ - Leave a chan. Returns 'success'. + def HandleLeaveChan(self, params): + """Handle a request to leave a chan""" - .. note:: at this time, the address is still shown in the UI - until a restart. - """ - self._verifyAddress(address) + if not params: + raise APIError(0, 'I need parameters.') + elif len(params) == 1: + address, = params + status, addressVersionNumber, streamNumber, toRipe = ( + self._verifyAddress(address)) address = addBMIfNotPresent(address) - if not self.config.safeGetBoolean(address, 'chan'): + 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.') - 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" + BMConfigParser().remove_section(address) + with open(state.appdata + 'keys.dat', 'wb') as configfile: + BMConfigParser().write(configfile) + return 'success' - @command('deleteAddress') - def HandleDeleteAddress(self, address): - """ - Permanently delete the address from keys.dat file. Returns 'success'. - """ - self._verifyAddress(address) + def HandleDeleteAddress(self, params): + """Handle a request to delete an address""" + + if not params: + raise APIError(0, 'I need parameters.') + elif len(params) == 1: + address, = params + status, addressVersionNumber, streamNumber, toRipe = ( + self._verifyAddress(address)) address = addBMIfNotPresent(address) - try: - self.config.remove_section(address) - except configparser.NoSectionError: + if not BMConfigParser().has_section(address): raise APIError( 13, 'Could not find this address in your keys.dat file.') - self.config.save() + BMConfigParser().remove_section(address) + with open(state.appdata + 'keys.dat', 'wb') as configfile: + BMConfigParser().write(configfile) 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): + """Handle a request to get all inbox messages""" queryreturn = sqlQuery( "SELECT msgid, toaddress, fromaddress, subject, received, message," - " encodingtype, read FROM inbox WHERE folder='inbox'" + " encodingtype, read FROM inbox where folder='inbox'" " ORDER BY received" ) - return {"inboxMessages": [ - self._dump_inbox_message(*data) for data in queryreturn - ]} + 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 - @command('getAllInboxMessageIds', 'getAllInboxMessageIDs') - def HandleGetAllInboxMessageIds(self): - """ - The same as *getAllInboxMessages* but returns only *msgid*s, - result key - *inboxMessageIds*. - """ + def HandleGetAllInboxMessageIds(self, params): + """Handle a request to get all inbox message IDs""" 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 - return {"inboxMessageIds": [ - {'msgid': hexlify(msgid)} for msgid, in queryreturn - ]} + def HandleGetInboxMessageById(self, params): + """Handle a request to get an inbox messsage by ID""" - @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 params: + 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] if not isinstance(readStatus, bool): raise APIError( - 23, 'Bool expected in readStatus, saw %s instead.' - % type(readStatus)) + 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 - try: - if (queryreturn[0][0] == 1) != readStatus: - sqlExecute( - "UPDATE inbox set read = ? WHERE msgid=?", - readStatus, msgid) - queues.UISignalQueue.put(('changedInboxUnread', None)) - except IndexError: - pass + 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 ) - try: - return {"inboxMessage": [ - self._dump_inbox_message(*queryreturn[0])]} - except IndexError: - pass # FIXME inconsistent + 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 - @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. - """ + def HandleGetAllSentMessages(self, params): + """Handle a request to get all sent messages""" 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 - ]} + 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 - @command('getAllSentMessageIds', 'getAllSentMessageIDs') - def HandleGetAllSentMessageIds(self): - """ - The same as *getAllInboxMessageIds* but for sent, - result key - *sentMessageIds*. - """ + def HandleGetAllSentMessageIds(self, params): + """Handle a request to get all sent message IDs""" queryreturn = sqlQuery( - "SELECT msgid FROM sent WHERE folder='sent'" + "SELECT msgid FROM sent where folder='sent'" " ORDER BY lastactiontime" ) - return {"sentMessageIds": [ - {'msgid': hexlify(msgid)} for msgid, in queryreturn - ]} + 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 - # 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. - """ + def HandleInboxMessagesByReceiver(self, params): + """Handle a request to get inbox messages by receiver""" + if not params: + raise APIError(0, 'I need parameters!') + toAddress = params[0] 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 - ]} + "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 - @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* - """ + def HandleGetSentMessageById(self, params): + """Handle a request to get a sent message by ID""" - msgid = self._decode(hid, "hex") + if not params: + 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 ) - try: - return {"sentMessage": [ - self._dump_sent_message(*queryreturn[0]) - ]} - except IndexError: - pass # FIXME inconsistent + 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 - @command('getSentMessagesByAddress', 'getSentMessagesBySender') - def HandleGetSentMessagesByAddress(self, fromAddress): - """ - The same as *getAllSentMessages* but returns only messages - from fromAddress. - """ + def HandleGetSentMessagesByAddress(self, params): + """Handle a request to get sent messages by address""" + if not params: + 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 ) - return {"sentMessages": [ - self._dump_sent_message(*data) for data in queryreturn - ]} + 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 - @command('getSentMessageByAckData') - def HandleGetSentMessagesByAckData(self, ackData): - """ - Similiar to *getSentMessageById* but searches by ackdata - (also hex encoded). - """ + def HandleGetSentMessagesByAckData(self, params): + """Handle a request to get sent messages by ack data""" - ackData = self._decode(ackData, "hex") + if not params: + 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 - try: - return {"sentMessage": [ - self._dump_sent_message(*queryreturn[0]) - ]} - except IndexError: - pass # FIXME inconsistent + def HandleTrashMessage(self, params): + """Handle a request to trash a message by ID""" + + if not params: + raise APIError(0, 'I need parameters!') + msgid = self._decode(params[0], "hex") - @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).' - @command('trashInboxMessage') - def HandleTrashInboxMessage(self, msgid): - """Trash inbox message by msgid (encoded in hex).""" - msgid = self._decode(msgid, "hex") + def HandleTrashInboxMessage(self, params): + """Handle a request to trash an inbox message by ID""" + + if not params: + raise APIError(0, 'I need parameters!') + msgid = self._decode(params[0], "hex") helper_inbox.trash(msgid) return 'Trashed inbox message (assuming message existed).' - @command('trashSentMessage') - def HandleTrashSentMessage(self, msgid): - """Trash sent message by msgid (encoded in hex).""" - msgid = self._decode(msgid, "hex") + def HandleTrashSentMessage(self, params): + """Handle a request to trash a sent message by ID""" + + if not params: + raise APIError(0, 'I need parameters!') + msgid = self._decode(params[0], "hex") sqlExecute('''UPDATE sent SET folder='trash' WHERE msgid=?''', msgid) return 'Trashed sent message (assuming message existed).' - @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): + def HandleSendMessage(self, params): # pylint: disable=too-many-locals + """Handle a request to send a message""" + + if not params: + 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]: raise APIError(6, 'The encoding type must be 2 or 3.') subject = self._decode(subject, "base64") message = self._decode(message, "base64") @@ -1135,40 +998,70 @@ class BMRPCDispatcher(object): TTL = 28 * 24 * 60 * 60 toAddress = addBMIfNotPresent(toAddress) fromAddress = addBMIfNotPresent(fromAddress) + status, addressVersionNumber, streamNumber, toRipe = \ + self._verifyAddress(toAddress) self._verifyAddress(fromAddress) try: - fromAddressEnabled = self.config.getboolean(fromAddress, 'enabled') - except configparser.NoSectionError: + fromAddressEnabled = BMConfigParser().getboolean( + fromAddress, 'enabled') + except BaseException: 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.') - ackdata = helper_sent.insert( - toAddress=toAddress, fromAddress=fromAddress, - subject=subject, message=message, encoding=encodingType, ttl=TTL) + 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) toLabel = '' queryreturn = sqlQuery( "SELECT label FROM addressbook WHERE address=?", toAddress) - try: - toLabel = queryreturn[0][0] - except IndexError: - pass - + if queryreturn != []: + for row in queryreturn: + toLabel, = row queues.UISignalQueue.put(('displayNewSentMessage', ( toAddress, toLabel, fromAddress, subject, message, ackdata))) + queues.workerQueue.put(('sendmessage', toAddress)) return hexlify(ackdata) - @command('sendBroadcast') - def HandleSendBroadcast( - self, fromAddress, subject, message, encodingType=2, - TTL=4 * 24 * 60 * 60): - """Send the broadcast message. Similiar to *sendMessage*.""" + def HandleSendBroadcast(self, params): + """Handle a request to send a broadcast message""" - if encodingType not in (2, 3): + if not params: + 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]: raise APIError(6, 'The encoding type must be 2 or 3.') subject = self._decode(subject, "base64") @@ -1182,64 +1075,81 @@ class BMRPCDispatcher(object): fromAddress = addBMIfNotPresent(fromAddress) self._verifyAddress(fromAddress) try: - fromAddressEnabled = self.config.getboolean(fromAddress, 'enabled') - except configparser.NoSectionError: + BMConfigParser().getboolean(fromAddress, 'enabled') + except BaseException: 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.') + 13, 'could not find your fromAddress in the keys.dat file.') + streamNumber = decodeAddress(fromAddress)[2] + ackdata = genAckPayload(streamNumber, 0) + toAddress = '[Broadcast subscribers]' + ripe = '' - toAddress = str_broadcast_subscribers + 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) - ackdata = helper_sent.insert( - fromAddress=fromAddress, subject=subject, - message=message, status='broadcastqueued', - encoding=encodingType) - - toLabel = str_broadcast_subscribers + toLabel = '[Broadcast subscribers]' queues.UISignalQueue.put(('displayNewSentMessage', ( toAddress, toLabel, fromAddress, subject, message, ackdata))) queues.workerQueue.put(('sendbroadcast', '')) return hexlify(ackdata) - @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. - """ + def HandleGetStatus(self, params): + """Handle a request to get the status of a sent message""" + if len(params) != 1: + raise APIError(0, 'I need one parameter!') + ackdata, = params 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) - try: - return queryreturn[0][0] - except IndexError: + if queryreturn == []: return 'notfound' + for row in queryreturn: + status, = row + return status - @command('addSubscription') - def HandleAddSubscription(self, address, label=''): - """Subscribe to the address. label must be base64 encoded.""" + def HandleAddSubscription(self, params): + """Handle a request to add a subscription""" - if label: + if not params: + raise APIError(0, 'I need parameters!') + if len(params) == 1: + address, = params + label = '' + if len(params) == 2: + address, label = params label = self._decode(label, "base64") try: - label.decode('utf-8') - except UnicodeDecodeError: + unicode(label, 'utf-8') + except BaseException: raise APIError(17, 'Label is not valid UTF-8 data.') - self._verifyAddress(address) + if len(params) > 2: + raise APIError(0, 'I need either 1 or 2 parameters!') address = addBMIfNotPresent(address) + self._verifyAddress(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: + if queryreturn != []: raise APIError(16, 'You are already subscribed to that address.') sqlExecute( "INSERT INTO subscriptions VALUES (?,?,?)", label, address, True) @@ -1248,117 +1158,103 @@ class BMRPCDispatcher(object): queues.UISignalQueue.put(('rerenderSubscriptions', '')) return 'Added subscription.' - @command('deleteSubscription') - def HandleDeleteSubscription(self, address): - """ - Unsubscribe from the address. The program does not check whether - you were subscribed in the first place. - """ + def HandleDeleteSubscription(self, params): + """Handle a request to delete a subscription""" + if len(params) != 1: + raise APIError(0, 'I need 1 parameter!') + address, = params 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.' - @command('listSubscriptions') - def ListSubscriptions(self): - """ - Returns dict with a list of all subscriptions - in the *subscriptions* key. - """ + def ListSubscriptions(self, params): + """Handle a request to list susbcriptions""" queryreturn = sqlQuery( "SELECT label, address, enabled FROM subscriptions") - data = [] - for label, address, enabled in queryreturn: + data = {'subscriptions': []} + for row in queryreturn: + label, address, enabled = row label = shared.fixPotentiallyInvalidUTF8Data(label) - data.append({ + data['subscriptions'].append({ 'label': base64.b64encode(label), 'address': address, 'enabled': enabled == 1 }) - return {'subscriptions': data} + return json.dumps(data, indent=4, separators=(',', ': ')) - @command('disseminatePreEncryptedMsg', 'disseminatePreparedObject') - def HandleDisseminatePreparedObject( - self, encryptedPayload, - nonceTrialsPerByte=networkDefaultProofOfWorkNonceTrialsPerByte, - payloadLengthExtraBytes=networkDefaultPayloadLengthExtraBytes - ): - """ - Handle a request to disseminate an encrypted message. + def HandleDisseminatePreEncryptedMsg(self, params): + """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. - """ + # 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 encryptedPayload = self._decode(encryptedPayload, "hex") - - nonce, = unpack('>Q', encryptedPayload[:8]) - objectType, toStreamNumber, expiresTime = \ - protocol.decodeObjectParameters(encryptedPayload) - - if nonce == 0: # Let us do the POW and attach it to the front - encryptedPayload = encryptedPayload[8:] - TTL = expiresTime - time.time() + 300 # a bit of extra padding - # Let us do the POW and attach it to the front - logger.debug("expiresTime: %s", expiresTime) - logger.debug("TTL: %s", TTL) - logger.debug("objectType: %s", objectType) - logger.info( - '(For msg message via API) Doing proof of work. Total required' - ' difficulty: %s\nRequired small message difficulty: %s', - float(nonceTrialsPerByte) - / networkDefaultProofOfWorkNonceTrialsPerByte, - float(payloadLengthExtraBytes) - / networkDefaultPayloadLengthExtraBytes, - ) - powStartTime = time.time() - target = 2**64 / ( - nonceTrialsPerByte * ( - len(encryptedPayload) + 8 + payloadLengthExtraBytes + (( - TTL * ( - len(encryptedPayload) + 8 + payloadLengthExtraBytes - )) / (2 ** 16)) - )) - initialHash = hashlib.sha512(encryptedPayload).digest() - trialValue, nonce = proofofwork.run(target, initialHash) - logger.info( - '(For msg message via API) Found proof of work %s\nNonce: %s\n' - 'POW took %s seconds. %s nonce trials per second.', - trialValue, nonce, int(time.time() - powStartTime), - nonce / (time.time() - powStartTime) - ) - encryptedPayload = pack('>Q', nonce) + encryptedPayload - - inventoryHash = calculateInventoryHash(encryptedPayload) - state.Inventory[inventoryHash] = ( - objectType, toStreamNumber, encryptedPayload, - expiresTime, b'' + # Let us do the POW and attach it to the front + target = 2**64 / ( + ( + len(encryptedPayload) + requiredPayloadLengthExtraBytes + 8 + ) * requiredAverageProofOfWorkNonceTrialsPerByte ) - logger.info( - 'Broadcasting inv for msg(API disseminatePreEncryptedMsg' - ' command): %s', hexlify(inventoryHash)) - invQueue.put((toStreamNumber, inventoryHash)) - return hexlify(inventoryHash).decode() + 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 BaseException: + pass + encryptedPayload = pack('>Q', nonce) + encryptedPayload + toStreamNumber = decodeVarint(encryptedPayload[16:26])[0] + 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): + """Handle a request to trash a sent message by ackdata""" - @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 - ackdata = self._decode(ackdata, "hex") + if not params: + raise APIError(0, 'I need parameters!') + ackdata = self._decode(params[0], "hex") sqlExecute("UPDATE sent SET folder='trash' WHERE ackdata=?", ackdata) return 'Trashed sent message (assuming message existed).' - @command('disseminatePubkey') - def HandleDissimatePubKey(self, payload): + def HandleDissimatePubKey(self, params): """Handle a request to disseminate a public key""" # The device issuing this command to PyBitmessage supplies a pubkey @@ -1366,19 +1262,19 @@ class BMRPCDispatcher(object): # 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 payload = self._decode(payload, "hex") # Let us do the POW target = 2 ** 64 / (( - len(payload) + networkDefaultPayloadLengthExtraBytes + 8 - ) * networkDefaultProofOfWorkNonceTrialsPerByte) - logger.info('(For pubkey message via API) Doing proof of work...') + len(payload) + defaults.networkDefaultPayloadLengthExtraBytes + 8 + ) * defaults.networkDefaultProofOfWorkNonceTrialsPerByte) + print '(For pubkey message via API) Doing proof of work...' initialHash = hashlib.sha512(payload).digest() trialValue, nonce = proofofwork.run(target, initialHash) - logger.info( - '(For pubkey message via API) Found proof of work %s Nonce: %s', - trialValue, nonce - ) + print '(For pubkey message via API) Found proof of work', trialValue, 'Nonce:', nonce payload = pack('>Q', nonce) + payload pubkeyReadPosition = 8 # bypass the nonce @@ -1387,30 +1283,30 @@ class BMRPCDispatcher(object): pubkeyReadPosition += 8 else: pubkeyReadPosition += 4 - addressVersionLength = decodeVarint( - payload[pubkeyReadPosition:pubkeyReadPosition + 10])[1] + addressVersion, addressVersionLength = decodeVarint( + payload[pubkeyReadPosition:pubkeyReadPosition + 10]) pubkeyReadPosition += addressVersionLength pubkeyStreamNumber = decodeVarint( payload[pubkeyReadPosition:pubkeyReadPosition + 10])[0] inventoryHash = calculateInventoryHash(payload) objectType = 1 # .. todo::: support v4 pubkeys TTL = 28 * 24 * 60 * 60 - state.Inventory[inventoryHash] = ( + 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)) + with shared.printLock: + print 'broadcasting inv within API command disseminatePubkey with hash:', hexlify(inventoryHash) + queues.invQueue.put((pubkeyStreamNumber, inventoryHash)) - @command( - 'getMessageDataByDestinationHash', 'getMessageDataByDestinationTag') - def HandleGetMessageDataByDestinationHash(self, requestedHash): + def HandleGetMessageDataByDestinationHash(self, params): """Handle a request to get message data by destination hash""" # 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' @@ -1424,7 +1320,8 @@ class BMRPCDispatcher(object): "SELECT hash, payload FROM inventory WHERE tag = ''" " and objecttype = 2") with SqlBulkExecute() as sql: - for hash01, payload in queryreturn: + for row in queryreturn: + hash01, payload = row readPosition = 16 # Nonce length + time length # Stream Number length readPosition += decodeVarint( @@ -1434,159 +1331,169 @@ class BMRPCDispatcher(object): queryreturn = sqlQuery( "SELECT payload FROM inventory WHERE tag = ?", requestedHash) - return {"receivedMessageDatas": [ - {'data': hexlify(payload)} for payload, in queryreturn - ]} + 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 - @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()) + def HandleClientStatus(self, params): + """Handle a request to get the status of the client""" + connections_num = len(network.stats.connectedHostsList()) if connections_num == 0: networkStatus = 'notConnected' - elif state.clientHasReceivedIncomingConnections: + elif shared.clientHasReceivedIncomingConnections: networkStatus = 'connectedAndReceivingIncomingConnections' else: networkStatus = 'connectedButHaveNotReceivedIncomingConnections' - return { + return json.dumps({ 'networkConnections': connections_num, - 'numberOfMessagesProcessed': state.numberOfMessagesProcessed, - 'numberOfBroadcastsProcessed': state.numberOfBroadcastsProcessed, - 'numberOfPubkeysProcessed': state.numberOfPubkeysProcessed, - 'pendingDownload': stats.pendingDownload(), + 'numberOfMessagesProcessed': shared.numberOfMessagesProcessed, + 'numberOfBroadcastsProcessed': shared.numberOfBroadcastsProcessed, + 'numberOfPubkeysProcessed': shared.numberOfPubkeysProcessed, 'networkStatus': networkStatus, 'softwareName': 'PyBitmessage', 'softwareVersion': softwareVersion - } + }, indent=4, separators=(',', ': ')) - @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 - } + def HandleDecodeAddress(self, params): + """Handle a request to decode an address""" - @command('helloWorld') - def HandleHelloWorld(self, a, b): + # 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): """Test two string params""" + + a, b = params return a + '-' + b - @command('add') - def HandleAdd(self, a, b): + def HandleAdd(self, params): """Test two numeric params""" + + a, b = params return a + b - @command('statusBar') - def HandleStatusBar(self, message): - """Update GUI statusbar message""" + def HandleStatusBar(self, params): + """Handle a request to update the status bar""" + + message, = params queues.UISignalQueue.put(('updateStatusBar', message)) - return "success" - @testmode('undeleteMessage') - def HandleUndeleteMessage(self, msgid): - """Undelete message""" - msgid = self._decode(msgid, "hex") - helper_inbox.undeleteMessage(msgid) - return "Undeleted message" + def HandleDeleteAndVacuum(self, params): + """Handle a request to run the deleteandvacuum stored procedure""" - @command('deleteAndVacuum') - def HandleDeleteAndVacuum(self): - """Cleanup trashes and vacuum messages database""" - sqlStoredProcedure('deleteandvacuume') - return 'done' + if not params: + sqlStoredProcedure('deleteandvacuume') + return 'done' + return None - @command('shutdown') - def HandleShutdown(self): - """Shutdown the bitmessage. Returns 'done'.""" - # backward compatible trick because False == 0 is True - state.shutdown = False - return 'done' + def HandleShutdown(self, params): + """Handle a request to shutdown the node""" + + if not params: + # backward compatible trick because False == 0 is True + state.shutdown = False + return 'done' + return None + + handlers = {} + handlers['helloWorld'] = HandleHelloWorld + handlers['add'] = HandleAdd + handlers['statusBar'] = HandleStatusBar + handlers['listAddresses'] = HandleListAddresses + handlers['listAddressBookEntries'] = HandleListAddressBookEntries + # the listAddressbook alias should be removed eventually. + handlers['listAddressbook'] = HandleListAddressBookEntries + handlers['addAddressBookEntry'] = HandleAddAddressBookEntry + # the addAddressbook alias should be deleted eventually. + handlers['addAddressbook'] = HandleAddAddressBookEntry + handlers['deleteAddressBookEntry'] = HandleDeleteAddressBookEntry + # The deleteAddressbook alias should be deleted eventually. + handlers['deleteAddressbook'] = HandleDeleteAddressBookEntry + 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 + # after some time getInboxMessagesByAddress should be removed + handlers['getInboxMessagesByAddress'] = HandleInboxMessagesByReceiver + 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 def _handle_request(self, method, params): - try: - # pylint: disable=attribute-defined-outside-init - self._method = method - func = self._handlers[method] - return func(self, *params) - except KeyError: + if method not in self.handlers: 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() + result = self.handlers[method](self, params) + state.last_api_response = time.time() + return result def _dispatch(self, method, params): - _fault = None + # pylint: disable=attribute-defined-outside-init + self.cookies = [] + + validuser = self.APIAuthenticateClient() + if not validuser: + time.sleep(2) + return "RPC Username or password incorrect or HTTP header lacks authentication at all." try: return self._handle_request(method, params) except APIError as e: - _fault = e + return str(e) except varintDecodeError as e: logger.error(e) - _fault = APIError( - 26, 'Data contains a malformed varint. Some details: %s' % e) + return "API Error 0026: Data contains a malformed varint. Some details: %s" % e except Exception as e: logger.exception(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__ + return "API Error 0021: Unexpected API Failure - %s" % e diff --git a/src/backend/address_generator.py b/src/backend/address_generator.py deleted file mode 100644 index 312c313b..00000000 --- a/src/backend/address_generator.py +++ /dev/null @@ -1,49 +0,0 @@ -""" -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 84c618af..01dbc9bb 100644 --- a/src/bitmessagecli.py +++ b/src/bitmessagecli.py @@ -23,7 +23,7 @@ import sys import time import xmlrpclib -from bmconfigparser import config +from bmconfigparser import BMConfigParser api = '' @@ -38,7 +38,7 @@ def userInput(message): global usrPrompt - print('\n' + message) + print '\n' + message uInput = raw_input('> ') if uInput.lower() == 'exit': # Returns the user to the main menu @@ -46,7 +46,7 @@ def userInput(message): main() elif uInput.lower() == 'quit': # Quits the program - print('\n Bye\n') + print '\n Bye\n' sys.exit(0) else: @@ -55,9 +55,9 @@ def userInput(message): def restartBmNotify(): """Prompt the user to restart Bitmessage""" - print('\n *******************************************************************') - print(' WARNING: If Bitmessage is running locally, you must restart it now.') - print(' *******************************************************************\n') + print '\n *******************************************************************' + print ' WARNING: If Bitmessage is running locally, you must restart it now.' + print ' *******************************************************************\n' # Begin keys.dat interactions @@ -86,48 +86,48 @@ def lookupAppdataFolder(): def configInit(): """Initialised the configuration""" - config.add_section('bitmessagesettings') + BMConfigParser().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 + BMConfigParser().set('bitmessagesettings', 'port', '8444') + BMConfigParser().set('bitmessagesettings', 'apienabled', 'true') # Sets apienabled to true in keys.dat with open(keysName, 'wb') as configfile: - config.write(configfile) + BMConfigParser().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 - config.read(keysPath) + BMConfigParser().read(keysPath) if apiEnabled is False: # API information there but the api is disabled. uInput = userInput("The API is not enabled. Would you like to do that now, (Y)es or (N)o?").lower() if uInput == "y": - config.set('bitmessagesettings', 'apienabled', 'true') # Sets apienabled to true in keys.dat + BMConfigParser().set('bitmessagesettings', 'apienabled', 'true') # Sets apienabled to true in keys.dat with open(keysPath, 'wb') as configfile: - config.write(configfile) + BMConfigParser().write(configfile) - print('Done') + 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() @@ -136,11 +136,11 @@ def apiInit(apiEnabled): return True else: # API information was not present. - print('\n ' + str(keysPath) + ' not properly configured!\n') + 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(' ') + print ' ' apiUsr = userInput("API Username") apiPwd = userInput("API Password") @@ -149,37 +149,37 @@ def apiInit(apiEnabled): 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') + 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) + BMConfigParser().set('bitmessagesettings', 'port', '8444') + BMConfigParser().set('bitmessagesettings', 'apienabled', 'true') + BMConfigParser().set('bitmessagesettings', 'apiport', apiPort) + BMConfigParser().set('bitmessagesettings', 'apiinterface', '127.0.0.1') + BMConfigParser().set('bitmessagesettings', 'apiusername', apiUsr) + BMConfigParser().set('bitmessagesettings', 'apipassword', apiPwd) + BMConfigParser().set('bitmessagesettings', 'daemon', daemon) with open(keysPath, 'wb') as configfile: - config.write(configfile) + BMConfigParser().write(configfile) - print('\n Finished configuring the keys.dat file with API information.\n') + 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() @@ -191,65 +191,65 @@ def apiData(): global keysPath global usrPrompt - config.read(keysPath) # First try to load the config file (the keys.dat file) from the program directory + BMConfigParser().read(keysPath) # First try to load the config file (the keys.dat file) from the program directory try: - config.get('bitmessagesettings', 'port') + BMConfigParser().get('bitmessagesettings', 'port') appDataFolder = '' - except: # noqa:E722 + except: # Could not load the keys.dat file in the program directory. Perhaps it is in the appdata directory. appDataFolder = lookupAppdataFolder() keysPath = appDataFolder + keysPath - config.read(keysPath) + BMConfigParser().read(keysPath) try: - config.get('bitmessagesettings', 'port') - except: # noqa:E722 + 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') + 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 in ("y", "yes"): + if (uInput == "y" or uInput == "yes"): configInit() keysPath = keysName usrPrompt = 0 main() - elif uInput in ("n", "no"): - print('\n Trying Again.\n') + elif (uInput == "n" or uInput == "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 - config.get('bitmessagesettings', 'apiport') - config.get('bitmessagesettings', 'apiinterface') - config.get('bitmessagesettings', 'apiusername') - config.get('bitmessagesettings', 'apipassword') + BMConfigParser().get('bitmessagesettings', 'apiport') + BMConfigParser().get('bitmessagesettings', 'apiinterface') + BMConfigParser().get('bitmessagesettings', 'apiusername') + BMConfigParser().get('bitmessagesettings', 'apipassword') - except: # noqa:E722 + except: 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')) + # apiInit(BMConfigParser().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') + 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') + print '\n API data successfully imported.\n' # Build the api credentials return "http://" + apiUsername + ":" + apiPassword + "@" + apiInterface + ":" + str(apiPort) + "/" @@ -263,7 +263,7 @@ def apiTest(): try: result = api.add(2, 3) - except: # noqa:E722 + except: return False return result == 5 @@ -277,50 +277,50 @@ def bmSettings(): keysPath = 'keys.dat' - config.read(keysPath) # Read the keys.dat + BMConfigParser().read(keysPath) # Read the keys.dat try: - port = config.get('bitmessagesettings', 'port') - except: # noqa:E722 - print('\n File not found.\n') + port = BMConfigParser().get('bitmessagesettings', 'port') + except: + print '\n File not found.\n' usrPrompt = 0 main() - 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') + 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 = 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') + 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(' ') + 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() @@ -328,76 +328,76 @@ def bmSettings(): 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.") - config.set('bitmessagesettings', 'port', str(uInput)) + BMConfigParser().set('bitmessagesettings', 'port', str(uInput)) elif uInput == "startonlogon": - print(' Current status: ' + str(startonlogon)) + print ' Current status: ' + str(startonlogon) uInput = userInput("Enter the new status.") - config.set('bitmessagesettings', 'startonlogon', str(uInput)) + BMConfigParser().set('bitmessagesettings', 'startonlogon', str(uInput)) elif uInput == "minimizetotray": - print(' Current status: ' + str(minimizetotray)) + print ' Current status: ' + str(minimizetotray) uInput = userInput("Enter the new status.") - config.set('bitmessagesettings', 'minimizetotray', str(uInput)) + BMConfigParser().set('bitmessagesettings', 'minimizetotray', str(uInput)) elif uInput == "showtraynotifications": - print(' Current status: ' + str(showtraynotifications)) + print ' Current status: ' + str(showtraynotifications) uInput = userInput("Enter the new status.") - config.set('bitmessagesettings', 'showtraynotifications', str(uInput)) + BMConfigParser().set('bitmessagesettings', 'showtraynotifications', str(uInput)) elif uInput == "startintray": - print(' Current status: ' + str(startintray)) + print ' Current status: ' + str(startintray) uInput = userInput("Enter the new status.") - config.set('bitmessagesettings', 'startintray', str(uInput)) + BMConfigParser().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.") - config.set('bitmessagesettings', 'defaultnoncetrialsperbyte', str(uInput)) + BMConfigParser().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.") - config.set('bitmessagesettings', 'defaultpayloadlengthextrabytes', str(uInput)) + BMConfigParser().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() - config.set('bitmessagesettings', 'daemon', str(uInput)) + BMConfigParser().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.") - config.set('bitmessagesettings', 'socksproxytype', str(uInput)) + BMConfigParser().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.") - config.set('bitmessagesettings', 'sockshostname', str(uInput)) + BMConfigParser().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.") - config.set('bitmessagesettings', 'socksport', str(uInput)) + BMConfigParser().set('bitmessagesettings', 'socksport', str(uInput)) elif uInput == "socksauthentication": - print(' Current status: ' + str(socksauthentication)) + print ' Current status: ' + str(socksauthentication) uInput = userInput("Enter the new status.") - config.set('bitmessagesettings', 'socksauthentication', str(uInput)) + BMConfigParser().set('bitmessagesettings', 'socksauthentication', str(uInput)) elif uInput == "socksusername": - print(' Current socks username: ' + socksusername) + print ' Current socks username: ' + socksusername uInput = userInput("Enter the new socksusername.") - config.set('bitmessagesettings', 'socksusername', str(uInput)) + BMConfigParser().set('bitmessagesettings', 'socksusername', str(uInput)) elif uInput == "sockspassword": - print(' Current socks password: ' + sockspassword) + print ' Current socks password: ' + sockspassword uInput = userInput("Enter the new password.") - config.set('bitmessagesettings', 'sockspassword', str(uInput)) + BMConfigParser().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 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: - config.write(configfile) + BMConfigParser().write(configfile) restartBmNotify() break @@ -405,7 +405,7 @@ def bmSettings(): usrPrompt = 1 main() else: - print("Invalid input.") + print "Invalid input." usrPrompt = 1 main() @@ -433,10 +433,10 @@ def subscribe(): if address == "c": usrPrompt = 1 - print(' ') + print ' ' main() elif validAddress(address) is False: - print('\n Invalid. "c" to cancel. Please try again.\n') + print '\n Invalid. "c" to cancel. Please try again.\n' else: break @@ -444,7 +444,7 @@ def subscribe(): label = label.encode('base64') api.addSubscription(address, label) - print('\n You are now subscribed to: ' + address + '\n') + print '\n You are now subscribed to: ' + address + '\n' def unsubscribe(): @@ -456,31 +456,31 @@ def unsubscribe(): if address == "c": usrPrompt = 1 - print(' ') + print ' ' main() elif validAddress(address) is False: - print('\n Invalid. "c" to cancel. Please try again.\n') + print '\n Invalid. "c" to cancel. Please try again.\n' else: break 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 - print('\nLabel, Address, Enabled\n') + print '\nLabel, Address, Enabled\n' try: - print(api.listSubscriptions()) - except: # noqa:E722 - print('\n Connection Error\n') + print api.listSubscriptions() + except: + print '\n Connection Error\n' usrPrompt = 0 main() - print(' ') + print ' ' def createChan(): @@ -490,9 +490,9 @@ def createChan(): password = userInput("Enter channel name") password = password.encode('base64') try: - print(api.createChan(password)) - except: # noqa:E722 - print('\n Connection Error\n') + print api.createChan(password) + except: + print '\n Connection Error\n' usrPrompt = 0 main() @@ -506,19 +506,19 @@ def joinChan(): if address == "c": usrPrompt = 1 - print(' ') + print ' ' main() elif validAddress(address) is False: - print('\n Invalid. "c" to cancel. Please try again.\n') + 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: # noqa:E722 - print('\n Connection Error\n') + print api.joinChan(password, address) + except: + print '\n Connection Error\n' usrPrompt = 0 main() @@ -532,17 +532,17 @@ def leaveChan(): if address == "c": usrPrompt = 1 - print(' ') + print ' ' main() elif validAddress(address) is False: - print('\n Invalid. "c" to cancel. Please try again.\n') + print '\n Invalid. "c" to cancel. Please try again.\n' else: break try: - print(api.leaveChan(address)) - except: # noqa:E722 - print('\n Connection Error\n') + print api.leaveChan(address) + except: + print '\n Connection Error\n' usrPrompt = 0 main() @@ -553,18 +553,18 @@ def listAdd(): try: jsonAddresses = json.loads(api.listAddresses()) numAddresses = len(jsonAddresses['addresses']) # Number of addresses - except: # noqa:E722 - print('\n Connection Error\n') + except: + print '\n Connection Error\n' usrPrompt = 0 main() - # print('\nAddress Number,Label,Address,Stream,Enabled\n') - print('\n --------------------------------------------------------------------------') - print(' | # | Label | Address |S#|Enabled|') - print(' |---|-------------------|-------------------------------------|--|-------|') + # 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 + 'utf') # may still misdiplay in some consoles address = str(jsonAddresses['addresses'][addNum]['address']) stream = str(jsonAddresses['addresses'][addNum]['stream']) enabled = str(jsonAddresses['addresses'][addNum]['enabled']) @@ -572,7 +572,7 @@ def listAdd(): if len(label) > 19: label = label[:16] + '...' - print(''.join([ + print ''.join([ ' |', str(addNum).ljust(3), '|', @@ -584,13 +584,13 @@ def listAdd(): '|', enabled.ljust(7), '|', - ])) + ]) - print(''.join([ + print ''.join([ ' ', 74 * '-', '\n', - ])) + ]) def genAdd(lbl, deterministic, passphrase, numOfAdd, addVNum, streamNum, ripe): @@ -602,8 +602,8 @@ def genAdd(lbl, deterministic, passphrase, numOfAdd, addVNum, streamNum, ripe): addressLabel = lbl.encode('base64') try: generatedAddress = api.createRandomAddress(addressLabel) - except: # noqa:E722 - print('\n Connection Error\n') + except: + print '\n Connection Error\n' usrPrompt = 0 main() @@ -613,8 +613,8 @@ def genAdd(lbl, deterministic, passphrase, numOfAdd, addVNum, streamNum, ripe): passphrase = passphrase.encode('base64') try: generatedAddress = api.createDeterministicAddresses(passphrase, numOfAdd, addVNum, streamNum, ripe) - except: # noqa:E722 - print('\n Connection Error\n') + except: + print '\n Connection Error\n' usrPrompt = 0 main() return generatedAddress @@ -646,7 +646,7 @@ def saveFile(fileName, fileData): with open(filePath, 'wb+') as path_to_file: path_to_file.write(fileData.decode("base64")) - print('\n Successfully saved ' + filePath + '\n') + print '\n Successfully saved ' + filePath + '\n' def attachment(): @@ -667,26 +667,26 @@ def attachment(): with open(filePath): break except IOError: - print('\n %s was not found on your filesystem or can not be opened.\n' % filePath) + 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(''.join([ + 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') + 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 @@ -694,17 +694,17 @@ def attachment(): 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) # Alert the user that the encoding process may take some time. - print('\n Encoding Attachment, Please Wait ...\n') + 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. @@ -737,9 +737,9 @@ Encoding:base64 uInput = userInput('Would you like to add another attachment, (Y)es or (N)o?').lower() - if uInput in ('y', 'yes'): # Allows multiple attachments to be added to one message + if uInput == 'y' or uInput == 'yes': # Allows multiple attachments to be added to one message theAttachmentS = str(theAttachmentS) + str(theAttachment) + '\n\n' - elif uInput in ('n', 'no'): + elif uInput == 'n' or uInput == 'no': break theAttachmentS = theAttachmentS + theAttachment @@ -759,10 +759,10 @@ def sendMsg(toAddress, fromAddress, subject, message): if toAddress == "c": usrPrompt = 1 - print(' ') + print ' ' main() elif validAddress(toAddress) is False: - print('\n Invalid Address. "c" to cancel. Please try again.\n') + print '\n Invalid Address. "c" to cancel. Please try again.\n' else: break @@ -770,15 +770,15 @@ def sendMsg(toAddress, fromAddress, subject, message): try: jsonAddresses = json.loads(api.listAddresses()) numAddresses = len(jsonAddresses['addresses']) # Number of addresses - except: # noqa:E722 - print('\n Connection Error\n') + except: + print '\n Connection Error\n' usrPrompt = 0 main() 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": @@ -795,7 +795,7 @@ def sendMsg(toAddress, fromAddress, subject, message): if found is False: if validAddress(fromAddress) is False: - print('\n Invalid Address. Please try again.\n') + print '\n Invalid Address. Please try again.\n' else: for addNum in range(0, numAddresses): # processes all of the addresses @@ -805,19 +805,19 @@ def sendMsg(toAddress, fromAddress, subject, message): break if found is False: - print('\n The address entered is not one of yours. Please try again.\n') + 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') + print '\n Using the only address in the addressbook to send from.\n' fromAddress = jsonAddresses['addresses'][0]['address'] - if not subject: + if subject == '': subject = userInput("Enter your Subject.") subject = subject.encode('base64') - if not message: + if message == '': message = userInput("Enter your Message.") uInput = userInput('Would you like to add an attachment, (Y)es or (N)o?').lower() @@ -828,9 +828,9 @@ def sendMsg(toAddress, fromAddress, subject, message): try: ackData = api.sendMessage(toAddress, fromAddress, subject, message) - print('\n Message Status:', api.getStatus(ackData), '\n') - except: # noqa:E722 - print('\n Connection Error\n') + print '\n Message Status:', api.getStatus(ackData), '\n' + except: + print '\n Connection Error\n' usrPrompt = 0 main() @@ -839,13 +839,13 @@ def sendBrd(fromAddress, subject, message): """Send a broadcast""" global usrPrompt - if not fromAddress: + if fromAddress == '': try: jsonAddresses = json.loads(api.listAddresses()) numAddresses = len(jsonAddresses['addresses']) # Number of addresses - except: # noqa:E722 - print('\n Connection Error\n') + except: + print '\n Connection Error\n' usrPrompt = 0 main() @@ -868,7 +868,7 @@ def sendBrd(fromAddress, subject, message): if found is False: if validAddress(fromAddress) is False: - print('\n Invalid Address. Please try again.\n') + print '\n Invalid Address. Please try again.\n' else: for addNum in range(0, numAddresses): # processes all of the addresses @@ -878,19 +878,19 @@ def sendBrd(fromAddress, subject, message): break if found is False: - print('\n The address entered is not one of yours. Please try again.\n') + 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') + print '\n Using the only address in the addressbook to send from.\n' fromAddress = jsonAddresses['addresses'][0]['address'] - if not subject: + if subject == '': subject = userInput("Enter your Subject.") subject = subject.encode('base64') - if not message: + if message == '': message = userInput("Enter your Message.") uInput = userInput('Would you like to add an attachment, (Y)es or (N)o?').lower() @@ -901,9 +901,9 @@ def sendBrd(fromAddress, subject, message): try: ackData = api.sendBroadcast(fromAddress, subject, message) - print('\n Message Status:', api.getStatus(ackData), '\n') - except: # noqa:E722 - print('\n Connection Error\n') + print '\n Message Status:', api.getStatus(ackData), '\n' + except: + print '\n Connection Error\n' usrPrompt = 0 main() @@ -915,8 +915,8 @@ def inbox(unreadOnly=False): try: inboxMessages = json.loads(api.getAllInboxMessages()) numMessages = len(inboxMessages['inboxMessages']) - except: # noqa:E722 - print('\n Connection Error\n') + except: + print '\n Connection Error\n' usrPrompt = 0 main() @@ -926,16 +926,16 @@ def inbox(unreadOnly=False): 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(''.join([ + 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 @@ -943,9 +943,9 @@ def inbox(unreadOnly=False): 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') + print '\n -----------------------------------' + print ' There are %d unread messages of %d messages in the inbox.' % (messagesUnread, numMessages) + print ' -----------------------------------\n' def outbox(): @@ -955,40 +955,33 @@ def outbox(): try: outboxMessages = json.loads(api.getAllSentMessages()) numMessages = len(outboxMessages['sentMessages']) - except: # noqa:E722 - print('\n Connection Error\n') + except: + 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 '\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 ' 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([ + 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') + print '\n -----------------------------------' + print ' There are ', numMessages, ' messages in the outbox.' + print ' -----------------------------------\n' def readSentMsg(msgNum): @@ -998,15 +991,15 @@ def readSentMsg(msgNum): try: outboxMessages = json.loads(api.getAllSentMessages()) numMessages = len(outboxMessages['sentMessages']) - except: # noqa:E722 - print('\n Connection Error\n') + except: + print '\n Connection Error\n' usrPrompt = 0 main() - print(' ') + print ' ' if msgNum >= numMessages: - print('\n Invalid Message Number.\n') + print '\n Invalid Message Number.\n' main() # Begin attachment detection @@ -1030,7 +1023,7 @@ def readSentMsg(msgNum): uInput = userInput( '\n Attachment Detected. Would you like to save the attachment, (Y)es or (N)o?').lower() - if uInput in ("y", 'yes'): + if uInput == "y" or uInput == 'yes': this_attachment = message[attPos + 9:attEndPos] saveFile(fileName, this_attachment) @@ -1042,19 +1035,19 @@ def readSentMsg(msgNum): # End attachment Detection - print('\n To:', getLabelForAddress(outboxMessages['sentMessages'][msgNum]['toAddress'])) # Get the to address) + 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([ + 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(' ') + ]) + print ' Message:\n' + print message # inboxMessages['inboxMessages'][msgNum]['message'].decode('base64') + print ' ' def readMsg(msgNum): @@ -1063,13 +1056,13 @@ def readMsg(msgNum): try: inboxMessages = json.loads(api.getAllInboxMessages()) numMessages = len(inboxMessages['inboxMessages']) - except: # noqa:E722 - print('\n Connection Error\n') + except: + print '\n Connection Error\n' usrPrompt = 0 main() if msgNum >= numMessages: - print('\n Invalid Message Number.\n') + print '\n Invalid Message Number.\n' main() # Begin attachment detection @@ -1093,7 +1086,7 @@ def readMsg(msgNum): uInput = userInput( '\n Attachment Detected. Would you like to save the attachment, (Y)es or (N)o?').lower() - if uInput in ("y", 'yes'): + if uInput == "y" or uInput == 'yes': this_attachment = message[attPos + 9:attEndPos] saveFile(fileName, this_attachment) @@ -1104,17 +1097,17 @@ def readMsg(msgNum): break # End attachment Detection - print('\n To:', getLabelForAddress(inboxMessages['inboxMessages'][msgNum]['toAddress'])) # Get the to address) + 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([ + 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(' ') + ]) + print ' Message:\n' + print message # inboxMessages['inboxMessages'][msgNum]['message'].decode('base64') + print ' ' return inboxMessages['inboxMessages'][msgNum]['msgid'] @@ -1125,8 +1118,8 @@ def replyMsg(msgNum, forwardORreply): forwardORreply = forwardORreply.lower() # makes it lowercase try: inboxMessages = json.loads(api.getAllInboxMessages()) - except: # noqa:E722 - print('\n Connection Error\n') + except: + print '\n Connection Error\n' usrPrompt = 0 main() @@ -1148,14 +1141,14 @@ def replyMsg(msgNum, forwardORreply): if toAdd == "c": usrPrompt = 1 - print(' ') + print ' ' main() elif validAddress(toAdd) is False: - print('\n Invalid Address. "c" to cancel. Please try again.\n') + 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() @@ -1186,8 +1179,8 @@ def delMsg(msgNum): msgId = inboxMessages['inboxMessages'][int(msgNum)]['msgid'] msgAck = api.trashMessage(msgId) - except: # noqa:E722 - print('\n Connection Error\n') + except: + print '\n Connection Error\n' usrPrompt = 0 main() @@ -1203,8 +1196,8 @@ def delSentMsg(msgNum): # gets the message ID via the message index number msgId = outboxMessages['sentMessages'][int(msgNum)]['msgid'] msgAck = api.trashSentMessage(msgId) - except: # noqa:E722 - print('\n Connection Error\n') + except: + print '\n Connection Error\n' usrPrompt = 0 main() @@ -1239,8 +1232,8 @@ def buildKnownAddresses(): for entry in addressBook['addresses']: if entry['address'] not in knownAddresses: knownAddresses[entry['address']] = "%s (%s)" % (entry['label'].decode('base64'), entry['address']) - except: # noqa:E722 - print('\n Connection Error\n') + except: + print '\n Connection Error\n' usrPrompt = 0 main() @@ -1254,8 +1247,8 @@ def buildKnownAddresses(): for entry in addresses['addresses']: if entry['address'] not in knownAddresses: knownAddresses[entry['address']] = "%s (%s)" % (entry['label'].decode('base64'), entry['address']) - except: # noqa:E722 - print('\n Connection Error\n') + except: + print '\n Connection Error\n' usrPrompt = 0 main() @@ -1270,18 +1263,21 @@ def listAddressBookEntries(): if "API Error" in response: return getAPIErrorCode(response) addressBook = json.loads(response) - print(' --------------------------------------------------------------') - print(' | Label | Address |') - print(' |--------------------|---------------------------------------|') + 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(' --------------------------------------------------------------') - except: # noqa:E722 - print('\n Connection Error\n') + print ' | ' + label.ljust(19) + '| ' + address.ljust(37) + ' |' + print ' --------------------------------------------------------------' + print + + except: + print '\n Connection Error\n' usrPrompt = 0 main() @@ -1295,8 +1291,8 @@ def addAddressToAddressBook(address, label): response = api.addAddressBookEntry(address, label.encode('base64')) if "API Error" in response: return getAPIErrorCode(response) - except: # noqa:E722 - print('\n Connection Error\n') + except: + print '\n Connection Error\n' usrPrompt = 0 main() @@ -1310,8 +1306,8 @@ def deleteAddressFromAddressBook(address): response = api.deleteAddressBookEntry(address) if "API Error" in response: return getAPIErrorCode(response) - except: # noqa:E722 - print('\n Connection Error\n') + except: + print '\n Connection Error\n' usrPrompt = 0 main() @@ -1334,8 +1330,8 @@ def markMessageRead(messageID): response = api.getInboxMessageByID(messageID, True) if "API Error" in response: return getAPIErrorCode(response) - except: # noqa:E722 - print('\n Connection Error\n') + except: + print '\n Connection Error\n' usrPrompt = 0 main() @@ -1349,8 +1345,8 @@ def markMessageUnread(messageID): response = api.getInboxMessageByID(messageID, False) if "API Error" in response: return getAPIErrorCode(response) - except: # noqa:E722 - print('\n Connection Error\n') + except: + print '\n Connection Error\n' usrPrompt = 0 main() @@ -1362,8 +1358,8 @@ def markAllMessagesRead(): try: inboxMessages = json.loads(api.getAllInboxMessages())['inboxMessages'] - except: # noqa:E722 - print('\n Connection Error\n') + except: + print '\n Connection Error\n' usrPrompt = 0 main() for message in inboxMessages: @@ -1378,8 +1374,8 @@ def markAllMessagesUnread(): try: inboxMessages = json.loads(api.getAllInboxMessages())['inboxMessages'] - except: # noqa:E722 - print('\n Connection Error\n') + except: + print '\n Connection Error\n' usrPrompt = 0 main() for message in inboxMessages: @@ -1388,22 +1384,22 @@ def markAllMessagesUnread(): def clientStatus(): - """Print (the client status""" + """Print the client status""" global usrPrompt try: client_status = json.loads(api.clientStatus()) - except: # noqa:E722 - print('\n Connection Error\n') + except: + print '\n Connection Error\n' usrPrompt = 0 main() - 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") + 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(): @@ -1413,7 +1409,7 @@ def shutdown(): api.shutdown() except socket.error: pass - print("\nShutdown command relayed\n") + print "\nShutdown command relayed\n" def UI(usrInput): @@ -1421,76 +1417,76 @@ def UI(usrInput): global usrPrompt - 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(' ') + 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 ' |------------------------|----------------------------------------------|' + 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(): - print('\n API connection test has: PASSED\n') + 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. bmSettings() - print(' ') + print ' ' main() elif usrInput == "quit": # Quits the application - print('\n Bye\n') + print '\n Bye\n' sys.exit(0) elif usrInput == "listaddresses": # Lists all of the identities in the addressbook @@ -1500,7 +1496,7 @@ def UI(usrInput): elif usrInput == "generateaddress": # Generates a new address uInput = userInput('\nWould you like to create a (D)eterministic or (R)andom address?').lower() - if uInput in ("d", "deterministic"): # Creates a deterministic address + if uInput in ["d", "deterministic"]: # Creates a deterministic address deterministic = True lbl = '' @@ -1512,17 +1508,17 @@ def UI(usrInput): 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 @@ -1530,18 +1526,18 @@ def UI(usrInput): 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') + print '\n Working...\n' address = getAddress(phrase, 4, 1) # ,vNumber,sNumber) - print('\n Address: ' + address + '\n') + print '\n Address: ' + address + '\n' usrPrompt = 1 main() @@ -1576,28 +1572,28 @@ def UI(usrInput): 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 uInput = userInput('Would you like to send a (M)essage or (B)roadcast?').lower() - if uInput in ('m', 'message'): + if (uInput == 'm' or uInput == 'message'): null = '' sendMsg(null, null, null, null) main() - elif uInput in ('b', 'broadcast'): + elif (uInput == 'b' or uInput == 'broadcast'): null = '' sendBrd(null, null, null) main() @@ -1606,67 +1602,67 @@ def UI(usrInput): uInput = userInput("Would you like to read a message from the (I)nbox or (O)utbox?").lower() - if uInput not in ('i', 'inbox', 'o', 'outbox'): - print('\n Invalid Input.\n') + if (uInput != 'i' and uInput != 'inbox' and uInput != 'o' and uInput != '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 in ('i', 'inbox'): - print('\n Loading...\n') + if (uInput == 'i' or uInput == '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 uInput not in ('y', 'yes'): + if not (uInput == 'y' or uInput == '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 in ('r', 'reply'): - print('\n Loading...\n') - print(' ') + if uInput in ['r', 'reply']: + print '\n Loading...\n' + print ' ' replyMsg(msgNum, 'reply') usrPrompt = 1 - elif uInput in ('f', 'forward'): - print('\n Loading...\n') - print(' ') + elif uInput == 'f' or uInput == 'forward': + print '\n Loading...\n' + print ' ' replyMsg(msgNum, 'forward') usrPrompt = 1 - elif uInput in ("d", 'delete'): + 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 in ('o', 'outbox'): + elif (uInput == 'o' or uInput == 'outbox'): readSentMsg(msgNum) # Gives the user the option to delete the message uInput = userInput("Would you like to (D)elete, or (Exit) this message?").lower() - if uInput in ("d", 'delete'): + if (uInput == "d" or uInput == '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() @@ -1675,12 +1671,12 @@ def UI(usrInput): uInput = userInput("Would you like to save a message from the (I)nbox or (O)utbox?").lower() - if uInput not in ('i', 'inbox', 'o', 'outbox'): - print('\n Invalid Input.\n') + if uInput not in ['i', 'inbox', 'o', 'outbox']: + print '\n Invalid Input.\n' usrPrompt = 1 main() - if uInput in ('i', 'inbox'): + if uInput in ['i', 'inbox']: inboxMessages = json.loads(api.getAllInboxMessages()) numMessages = len(inboxMessages['inboxMessages']) @@ -1688,7 +1684,7 @@ def UI(usrInput): msgNum = int(userInput("What is the number of the message you wish to save?")) if msgNum >= numMessages: - print('\n Invalid Message Number.\n') + print '\n Invalid Message Number.\n' else: break @@ -1704,7 +1700,7 @@ def UI(usrInput): msgNum = int(userInput("What is the number of the message you wish to save?")) if msgNum >= numMessages: - print('\n Invalid Message Number.\n') + print '\n Invalid Message Number.\n' else: break @@ -1722,7 +1718,7 @@ def UI(usrInput): uInput = userInput("Would you like to delete a message from the (I)nbox or (O)utbox?").lower() - if uInput in ('i', 'inbox'): + if uInput in ['i', 'inbox']: inboxMessages = json.loads(api.getAllInboxMessages()) numMessages = len(inboxMessages['inboxMessages']) @@ -1733,30 +1729,30 @@ def UI(usrInput): if msgNum == 'a' or msgNum == 'all': break elif int(msgNum) >= numMessages: - print('\n Invalid Message Number.\n') + print '\n Invalid Message Number.\n' else: break uInput = userInput("Are you sure, (Y)es or (N)o?").lower() # Prevent accidental deletion if uInput == "y": - if msgNum in ('a', 'all'): - print(' ') + 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) + 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 in ('o', 'outbox'): + elif uInput in ['o', 'outbox']: outboxMessages = json.loads(api.getAllSentMessages()) numMessages = len(outboxMessages['sentMessages']) @@ -1764,44 +1760,44 @@ def UI(usrInput): msgNum = userInput( 'Enter the number of the message you wish to delete or (A)ll to empty the inbox.').lower() - if msgNum in ('a', 'all'): + if msgNum in ['a', 'all']: break elif int(msgNum) >= numMessages: - print('\n Invalid Message Number.\n') + print '\n Invalid Message Number.\n' else: break uInput = userInput("Are you sure, (Y)es or (N)o?").lower() # Prevent accidental deletion if uInput == "y": - if msgNum in ('a', 'all'): - print(' ') + 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) + 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') + print '\n Error: API function not supported.\n' usrPrompt = 1 main() @@ -1810,9 +1806,9 @@ def UI(usrInput): label = userInput('Enter label') res = addAddressToAddressBook(address, label) if res == 16: - print('\n Error: Address already exists in Address Book.\n') + print '\n Error: Address already exists in Address Book.\n' if res == 20: - print('\n Error: API function not supported.\n') + print '\n Error: API function not supported.\n' usrPrompt = 1 main() @@ -1820,7 +1816,7 @@ def UI(usrInput): address = userInput('Enter address') res = deleteAddressFromAddressBook(address) if res == 20: - print('\n Error: API function not supported.\n') + print '\n Error: API function not supported.\n' usrPrompt = 1 main() @@ -1845,7 +1841,7 @@ def UI(usrInput): main() else: - print('\n "', usrInput, '" is not a command.\n') + print '\n "', usrInput, '" is not a command.\n' usrPrompt = 1 main() @@ -1857,24 +1853,24 @@ def main(): global usrPrompt if usrPrompt == 0: - print('\n ------------------------------') - print(' | Bitmessage Daemon by .dok |') - print(' | Version 0.3.1 for BM 0.6.2 |') - print(' ------------------------------') + 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 '\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) + print 'Type (H)elp for a list of commands.' # Startup message usrPrompt = 2 elif usrPrompt == 1: - print('\nType (H)elp for a list of commands.') # Startup message) + print '\nType (H)elp for a list of commands.' # Startup message usrPrompt = 2 try: diff --git a/src/bitmessagecurses/__init__.py b/src/bitmessagecurses/__init__.py index 64fd735b..d8daeef7 100644 --- a/src/bitmessagecurses/__init__.py +++ b/src/bitmessagecurses/__init__.py @@ -19,18 +19,17 @@ from textwrap import fill from threading import Timer from dialog import Dialog -import helper_sent import l10n import network.stats import queues import shared import shutdown -import state from addresses import addBMIfNotPresent, decodeAddress -from bmconfigparser import config +from bmconfigparser import BMConfigParser +from helper_ackPayload import genAckPayload from helper_sql import sqlExecute, sqlQuery - +from inventory import Inventory # pylint: disable=global-statement @@ -129,7 +128,7 @@ def set_background_title(d, title): """Setting background title""" try: d.set_background_title(title) - except: # noqa:E722 + except: d.add_persistent_args(("--backtitle", title)) @@ -137,15 +136,15 @@ def scrollbox(d, text, height=None, width=None): """Setting scroll box""" try: d.scrollbox(text, height, width, exit_label="Continue") - except: # noqa:E722 + except: d.msgbox(text, height or 0, width or 0, ok_label="Continue") def resetlookups(): """Reset the Inventory Lookups""" global inventorydata - inventorydata = state.Inventory.numberOfInventoryLookupsPerformed - state.Inventory.numberOfInventoryLookupsPerformed = 0 + inventorydata = Inventory().numberOfInventoryLookupsPerformed + Inventory().numberOfInventoryLookupsPerformed = 0 Timer(1, resetlookups, ()).start() @@ -273,14 +272,13 @@ def drawtab(stdscr): stdscr.addstr(8 + i, 18, str(item).ljust(2)) # Uptime and processing data - stdscr.addstr( - 6, 35, "Since startup on " + l10n.formatTimestamp(startuptime)) + stdscr.addstr(6, 35, "Since startup on " + l10n.formatTimestamp(startuptime, False)) stdscr.addstr(7, 40, "Processed " + str( - state.numberOfMessagesProcessed).ljust(4) + " person-to-person messages.") + shared.numberOfMessagesProcessed).ljust(4) + " person-to-person messages.") stdscr.addstr(8, 40, "Processed " + str( - state.numberOfBroadcastsProcessed).ljust(4) + " broadcast messages.") + shared.numberOfBroadcastsProcessed).ljust(4) + " broadcast messages.") stdscr.addstr(9, 40, "Processed " + str( - state.numberOfPubkeysProcessed).ljust(4) + " public keys.") + shared.numberOfPubkeysProcessed).ljust(4) + " public keys.") # Inventory data stdscr.addstr(11, 35, "Inventory lookups per second: " + str(inventorydata).ljust(3)) @@ -617,19 +615,19 @@ def handlech(c, stdscr): r, t = d.inputbox("New address label", init=label) if r == d.DIALOG_OK: label = t - config.set(a, "label", label) + BMConfigParser().set(a, "label", label) # Write config - config.save() + BMConfigParser().save() addresses[addrcur][0] = label elif t == "4": # Enable address a = addresses[addrcur][2] - config.set(a, "enabled", "true") # Set config + BMConfigParser().set(a, "enabled", "true") # Set config # Write config - config.save() + BMConfigParser().save() # Change color - if config.safeGetBoolean(a, 'chan'): + if BMConfigParser().safeGetBoolean(a, 'chan'): addresses[addrcur][3] = 9 # orange - elif config.safeGetBoolean(a, 'mailinglist'): + elif BMConfigParser().safeGetBoolean(a, 'mailinglist'): addresses[addrcur][3] = 5 # magenta else: addresses[addrcur][3] = 0 # black @@ -637,26 +635,26 @@ def handlech(c, stdscr): shared.reloadMyAddressHashes() # Reload address hashes elif t == "5": # Disable address a = addresses[addrcur][2] - config.set(a, "enabled", "false") # Set config + BMConfigParser().set(a, "enabled", "false") # Set config addresses[addrcur][3] = 8 # Set color to gray # Write config - config.save() + BMConfigParser().save() addresses[addrcur][1] = False shared.reloadMyAddressHashes() # Reload address hashes elif t == "6": # Delete address r, t = d.inputbox("Type in \"I want to delete this address\"", width=50) if r == d.DIALOG_OK and t == "I want to delete this address": - config.remove_section(addresses[addrcur][2]) - config.save() + BMConfigParser().remove_section(addresses[addrcur][2]) + BMConfigParser().save() del addresses[addrcur] elif t == "7": # Special address behavior a = addresses[addrcur][2] set_background_title(d, "Special address behavior") - if config.safeGetBoolean(a, "chan"): + if BMConfigParser().safeGetBoolean(a, "chan"): scrollbox(d, unicode( "This is a chan address. You cannot use it as a pseudo-mailing list.")) else: - m = config.safeGetBoolean(a, "mailinglist") + m = BMConfigParser().safeGetBoolean(a, "mailinglist") r, t = d.radiolist( "Select address behavior", choices=[ @@ -664,24 +662,24 @@ def handlech(c, stdscr): ("2", "Behave as a pseudo-mailing-list address", m)]) if r == d.DIALOG_OK: if t == "1" and m: - config.set(a, "mailinglist", "false") + BMConfigParser().set(a, "mailinglist", "false") if addresses[addrcur][1]: addresses[addrcur][3] = 0 # Set color to black else: addresses[addrcur][3] = 8 # Set color to gray elif t == "2" and m is False: try: - mn = config.get(a, "mailinglistname") + mn = BMConfigParser().get(a, "mailinglistname") except ConfigParser.NoOptionError: mn = "" r, t = d.inputbox("Mailing list name", init=mn) if r == d.DIALOG_OK: mn = t - config.set(a, "mailinglist", "true") - config.set(a, "mailinglistname", mn) + BMConfigParser().set(a, "mailinglist", "true") + BMConfigParser().set(a, "mailinglistname", mn) addresses[addrcur][3] = 6 # Set color to magenta # Write config - config.save() + BMConfigParser().save() elif menutab == 5: set_background_title(d, "Subscriptions Dialog Box") if len(subscriptions) <= subcur: @@ -919,7 +917,8 @@ def sendMessage(sender="", recv="", broadcast=None, subject="", body="", reply=F list(set(recvlist)) # Remove exact duplicates for addr in recvlist: if addr != "": - status, version, stream = decodeAddress(addr)[:3] + # pylint: disable=redefined-outer-name + status, version, stream, ripe = decodeAddress(addr) if status != "success": set_background_title(d, "Recipient address error") err = "Could not decode" + addr + " : " + status + "\n\n" @@ -964,8 +963,25 @@ def sendMessage(sender="", recv="", broadcast=None, subject="", body="", reply=F if not network.stats.connectedHostsList(): set_background_title(d, "Not connected warning") scrollbox(d, unicode("Because you are not currently connected to the network, ")) - helper_sent.insert( - toAddress=addr, fromAddress=sender, subject=subject, message=body) + stealthLevel = BMConfigParser().safeGetInt('bitmessagesettings', 'ackstealthlevel') + ackdata = genAckPayload(decodeAddress(addr)[2], 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')) queues.workerQueue.put(("sendmessage", addr)) else: # Broadcast if recv == "": @@ -973,9 +989,26 @@ def sendMessage(sender="", recv="", broadcast=None, subject="", body="", reply=F scrollbox(d, unicode("You must specify an address to send the message from.")) else: # dummy ackdata, no need for stealth - helper_sent.insert( - fromAddress=sender, subject=subject, - message=body, status='broadcastqueued') + ackdata = genAckPayload(decodeAddress(addr)[2], 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')) queues.workerQueue.put(('sendbroadcast', '')) @@ -983,7 +1016,7 @@ def sendMessage(sender="", recv="", broadcast=None, subject="", body="", reply=F def loadInbox(): """Load the list of messages""" sys.stdout = sys.__stdout__ - print("Loading inbox messages...") + print "Loading inbox messages..." sys.stdout = printlog where = "toaddress || fromaddress || subject || message" @@ -1001,8 +1034,8 @@ def loadInbox(): if toaddr == BROADCAST_STR: tolabel = BROADCAST_STR else: - tolabel = config.get(toaddr, "label") - except: # noqa:E722 + tolabel = BMConfigParser().get(toaddr, "label") + except: tolabel = "" if tolabel == "": tolabel = toaddr @@ -1010,8 +1043,8 @@ def loadInbox(): # Set label for from address fromlabel = "" - if config.has_section(fromaddr): - fromlabel = config.get(fromaddr, "label") + if BMConfigParser().has_section(fromaddr): + fromlabel = BMConfigParser().get(fromaddr, "label") if fromlabel == "": # Check Address Book qr = sqlQuery("SELECT label FROM addressbook WHERE address=?", fromaddr) if qr != []: @@ -1027,16 +1060,15 @@ def loadInbox(): fromlabel = shared.fixPotentiallyInvalidUTF8Data(fromlabel) # Load into array - inbox.append([ - msgid, tolabel, toaddr, fromlabel, fromaddr, subject, - l10n.formatTimestamp(received), read]) + inbox.append([msgid, tolabel, toaddr, fromlabel, fromaddr, subject, l10n.formatTimestamp( + received, False), read]) inbox.reverse() def loadSent(): """Load the messages that sent""" sys.stdout = sys.__stdout__ - print("Loading sent messages...") + print "Loading sent messages..." sys.stdout = printlog where = "toaddress || fromaddress || subject || message" @@ -1061,15 +1093,15 @@ def loadSent(): for r in qr: tolabel, = r if tolabel == "": - if config.has_section(toaddr): - tolabel = config.get(toaddr, "label") + if BMConfigParser().has_section(toaddr): + tolabel = BMConfigParser().get(toaddr, "label") if tolabel == "": tolabel = toaddr # Set label for from address fromlabel = "" - if config.has_section(fromaddr): - fromlabel = config.get(fromaddr, "label") + if BMConfigParser().has_section(fromaddr): + fromlabel = BMConfigParser().get(fromaddr, "label") if fromlabel == "": fromlabel = fromaddr @@ -1081,20 +1113,20 @@ def loadSent(): elif status == "msgqueued": statstr = "Message queued" elif status == "msgsent": - t = l10n.formatTimestamp(lastactiontime) + t = l10n.formatTimestamp(lastactiontime, False) statstr = "Message sent at " + t + ".Waiting for acknowledgement." elif status == "msgsentnoackexpected": - t = l10n.formatTimestamp(lastactiontime) + t = l10n.formatTimestamp(lastactiontime, False) 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) + t = l10n.formatTimestamp(lastactiontime, False) statstr = "Acknowledgment of the message received at " + t + "." elif status == "broadcastqueued": statstr = "Broadcast queued." elif status == "broadcastsent": - t = l10n.formatTimestamp(lastactiontime) + t = l10n.formatTimestamp(lastactiontime, False) statstr = "Broadcast sent at " + t + "." elif status == "forcepow": statstr = "Forced difficulty override. Message will start sending soon." @@ -1103,7 +1135,7 @@ 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) + t = l10n.formatTimestamp(lastactiontime, False) statstr = "Unknown status " + status + " at " + t + "." # Load into array @@ -1115,14 +1147,14 @@ def loadSent(): subject, statstr, ackdata, - l10n.formatTimestamp(lastactiontime)]) + l10n.formatTimestamp(lastactiontime, False)]) sentbox.reverse() def loadAddrBook(): """Load address book""" sys.stdout = sys.__stdout__ - print("Loading address book...") + print "Loading address book..." sys.stdout = printlog ret = sqlQuery("SELECT label, address FROM addressbook") @@ -1145,7 +1177,7 @@ def loadSubscriptions(): def loadBlackWhiteList(): """load black/white list""" global bwtype - bwtype = config.get("bitmessagesettings", "blackwhitelist") + bwtype = BMConfigParser().get("bitmessagesettings", "blackwhitelist") if bwtype == "black": ret = sqlQuery("SELECT label, address, enabled FROM blacklist") else: @@ -1204,16 +1236,16 @@ def run(stdscr): curses.init_pair(9, curses.COLOR_YELLOW, curses.COLOR_BLACK) # orangish # Init list of address in 'Your Identities' tab - configSections = config.addresses() + configSections = BMConfigParser().addresses() for addressInKeysFile in configSections: - isEnabled = config.getboolean(addressInKeysFile, "enabled") - addresses.append([config.get(addressInKeysFile, "label"), isEnabled, addressInKeysFile]) + isEnabled = BMConfigParser().getboolean(addressInKeysFile, "enabled") + addresses.append([BMConfigParser().get(addressInKeysFile, "label"), isEnabled, addressInKeysFile]) # Set address color if not isEnabled: addresses[len(addresses) - 1].append(8) # gray - elif config.safeGetBoolean(addressInKeysFile, 'chan'): + elif BMConfigParser().safeGetBoolean(addressInKeysFile, 'chan'): addresses[len(addresses) - 1].append(9) # orange - elif config.safeGetBoolean(addressInKeysFile, 'mailinglist'): + elif BMConfigParser().safeGetBoolean(addressInKeysFile, 'mailinglist'): addresses[len(addresses) - 1].append(5) # magenta else: addresses[len(addresses) - 1].append(0) # black @@ -1229,7 +1261,7 @@ def run(stdscr): def doShutdown(): """Shutting the app down""" sys.stdout = sys.__stdout__ - print("Shutting down...") + print "Shutting down..." sys.stdout = printlog shutdown.doCleanShutdown() sys.stdout = sys.__stdout__ diff --git a/src/bitmessagekivy/base_navigation.py b/src/bitmessagekivy/base_navigation.py deleted file mode 100644 index 5f6b1aa5..00000000 --- a/src/bitmessagekivy/base_navigation.py +++ /dev/null @@ -1,110 +0,0 @@ -# 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 deleted file mode 100644 index e69de29b..00000000 diff --git a/src/bitmessagekivy/baseclass/addressbook.py b/src/bitmessagekivy/baseclass/addressbook.py deleted file mode 100644 index f18a0142..00000000 --- a/src/bitmessagekivy/baseclass/addressbook.py +++ /dev/null @@ -1,164 +0,0 @@ -# pylint: disable=unused-argument, consider-using-f-string, import-error -# pylint: disable=unnecessary-comprehension, no-member, no-name-in-module - -""" -addressbook.py -============== - -All saved addresses are managed in Addressbook - -""" - -import os -import logging -from functools import partial - -from kivy.properties import ( - ListProperty, - StringProperty -) -from kivy.uix.screenmanager import Screen -from kivy.app import App - -from pybitmessage.bitmessagekivy.get_platform import platform -from pybitmessage.bitmessagekivy import kivy_helper_search -from pybitmessage.bitmessagekivy.baseclass.common import ( - avatar_image_first_letter, toast, empty_screen_label, - ThemeClsColor, SwipeToDeleteItem, kivy_state_variables -) -from pybitmessage.bitmessagekivy.baseclass.popup import SavedAddressDetailPopup -from pybitmessage.bitmessagekivy.baseclass.addressbook_widgets import HelperAddressBook -from pybitmessage.helper_sql import sqlExecute - -logger = logging.getLogger('default') - - -class AddressBook(Screen, HelperAddressBook): - """AddressBook Screen class for kivy Ui""" - - queryreturn = ListProperty() - has_refreshed = True - address_label = StringProperty() - address = StringProperty() - label_str = "No contact Address found yet......" - no_search_res_found = "No search result found" - - def __init__(self, *args, **kwargs): - """Getting AddressBook Details""" - super(AddressBook, self).__init__(*args, **kwargs) - self.addbook_popup = None - self.kivy_state = kivy_state_variables() - - def loadAddresslist(self, account, where="", what=""): - """Clock Schdule for method AddressBook""" - if self.kivy_state.searching_text: - self.ids.scroll_y.scroll_y = 1.0 - where = ['label', 'address'] - what = self.kivy_state.searching_text - xAddress = '' - self.ids.tag_label.text = '' - self.queryreturn = kivy_helper_search.search_sql( - xAddress, account, "addressbook", where, what, False) - self.queryreturn = [obj for obj in reversed(self.queryreturn)] - if self.queryreturn: - self.ids.tag_label.text = 'Address Book' - self.has_refreshed = True - self.set_mdList(0, 20) - self.ids.scroll_y.bind(scroll_y=self.check_scroll_y) - else: - self.ids.ml.add_widget(empty_screen_label(self.label_str, self.no_search_res_found)) - - def set_mdList(self, start_index, end_index): - """Creating the mdList""" - for item in self.queryreturn[start_index:end_index]: - message_row = SwipeToDeleteItem( - text=item[0], - ) - listItem = message_row.ids.content - listItem.secondary_text = item[1] - listItem.theme_text_color = "Custom" - listItem.text_color = ThemeClsColor - image = os.path.join( - self.kivy_state.imageDir, "text_images", "{}.png".format(avatar_image_first_letter(item[0].strip())) - ) - message_row.ids.avater_img.source = image - listItem.bind(on_release=partial( - self.addBook_detail, item[1], item[0], message_row)) - message_row.ids.delete_msg.bind(on_press=partial(self.delete_address, item[1])) - self.ids.ml.add_widget(message_row) - - def check_scroll_y(self, instance, somethingelse): - """Load data on scroll""" - if self.ids.scroll_y.scroll_y <= -0.0 and self.has_refreshed: - self.ids.scroll_y.scroll_y = 0.06 - exist_addresses = len(self.ids.ml.children) - if exist_addresses != len(self.queryreturn): - self.update_addressBook_on_scroll(exist_addresses) - self.has_refreshed = ( - True if exist_addresses != len(self.queryreturn) else False - ) - - def update_addressBook_on_scroll(self, exist_addresses): - """Load more data on scroll down""" - self.set_mdList(exist_addresses, exist_addresses + 5) - - @staticmethod - def refreshs(*args): - """Refresh the Widget""" - - # @staticmethod - def addBook_detail(self, address, label, instance, *args): - """Addressbook details""" - if instance.state == 'closed': - instance.ids.delete_msg.disabled = True - if instance.open_progress == 0.0: - obj = SavedAddressDetailPopup() - self.address_label = obj.address_label = label - self.address = obj.address = address - width = .9 if platform == 'android' else .8 - self.addbook_popup = self.address_detail_popup( - obj, self.send_message_to, self.update_addbook_label, - self.close_pop, width) - self.addbook_popup.auto_dismiss = False - self.addbook_popup.open() - else: - instance.ids.delete_msg.disabled = False - - def delete_address(self, address, instance, *args): - """Delete inbox mail from inbox listing""" - self.ids.ml.remove_widget(instance.parent.parent) - # if len(self.ids.ml.children) == 0: - if self.ids.ml.children is not None: - self.ids.tag_label.text = '' - sqlExecute( - "DELETE FROM addressbook WHERE address = ?", address) - toast('Address Deleted') - - def close_pop(self, instance): - """Pop is Canceled""" - self.addbook_popup.dismiss() - toast('Canceled') - - def update_addbook_label(self, instance): - """Updating the label of address book address""" - address_list = kivy_helper_search.search_sql(folder="addressbook") - stored_labels = [labels[0] for labels in address_list] - add_dict = dict(address_list) - label = str(self.addbook_popup.content_cls.ids.add_label.text) - if label in stored_labels and self.address == add_dict[label]: - stored_labels.remove(label) - if label and label not in stored_labels: - sqlExecute(""" - UPDATE addressbook - SET label = ? - WHERE address = ?""", label, self.addbook_popup.content_cls.address) - App.get_running_app().root.ids.id_addressbook.ids.ml.clear_widgets() - App.get_running_app().root.ids.id_addressbook.loadAddresslist(None, 'All', '') - self.addbook_popup.dismiss() - toast('Saved') - - def send_message_to(self, instance): - """Method used to fill to_address of composer autofield""" - App.get_running_app().set_navbar_for_composer() - self.compose_message(None, self.address) - self.addbook_popup.dismiss() diff --git a/src/bitmessagekivy/baseclass/addressbook_widgets.py b/src/bitmessagekivy/baseclass/addressbook_widgets.py deleted file mode 100644 index 3654dfa3..00000000 --- a/src/bitmessagekivy/baseclass/addressbook_widgets.py +++ /dev/null @@ -1,50 +0,0 @@ -# pylint: disable=no-member, too-many-arguments, too-few-public-methods -""" -Addressbook widgets are here. -""" - -from kivy.app import App -from kivymd.uix.button import MDRaisedButton -from kivymd.uix.dialog import MDDialog - - -class HelperAddressBook(object): - """Widget used in Addressbook are here""" - - @staticmethod - def address_detail_popup(obj, send_message, update_address, close_popup, width): - """This function shows the address's details and opens the popup.""" - show_dialogue = MDDialog( - type="custom", - size_hint=(width, .25), - content_cls=obj, - buttons=[ - MDRaisedButton( - text="Send message to", - on_release=send_message, - ), - MDRaisedButton( - text="Save", - on_release=update_address, - ), - MDRaisedButton( - text="Cancel", - on_release=close_popup, - ), - ], - ) - return show_dialogue - - @staticmethod - def compose_message(from_addr=None, to_addr=None): - """This UI independent method for message sending to reciever""" - window_obj = App.get_runnint_app().root.ids - if to_addr: - window_obj.id_create.children[1].ids.txt_input.text = to_addr - if from_addr: - window_obj.id_create.children[1].ids.txt_input.text = from_addr - window_obj.id_create.children[1].ids.ti.text = '' - window_obj.id_create.children[1].ids.composer_dropdown.text = 'Select' - window_obj.id_create.children[1].ids.subject.text = '' - window_obj.id_create.children[1].ids.body.text = '' - window_obj.scr_mngr.current = 'create' diff --git a/src/bitmessagekivy/baseclass/allmail.py b/src/bitmessagekivy/baseclass/allmail.py deleted file mode 100644 index 12104c57..00000000 --- a/src/bitmessagekivy/baseclass/allmail.py +++ /dev/null @@ -1,70 +0,0 @@ -# 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 deleted file mode 100644 index c5f94b8a..00000000 --- a/src/bitmessagekivy/baseclass/chat.py +++ /dev/null @@ -1,11 +0,0 @@ -# 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 deleted file mode 100644 index 2cf4d76b..00000000 --- a/src/bitmessagekivy/baseclass/common.py +++ /dev/null @@ -1,236 +0,0 @@ -# pylint: disable=no-name-in-module, attribute-defined-outside-init, import-error, unused-argument -""" - All Common widgets of kivy are managed here. -""" - -import os -from datetime import datetime - -from kivy.core.window import Window -from kivy.metrics import dp -from kivy.uix.image import Image -from kivy.properties import ( - NumericProperty, - StringProperty, - ListProperty -) -from kivy.app import App - -from kivymd.uix.list import ( - ILeftBody, - IRightBodyTouch, -) -from kivymd.uix.label import MDLabel -from kivymd.toast import kivytoast -from kivymd.uix.card import MDCardSwipe -from kivymd.uix.chip import MDChip -from kivymd.uix.dialog import MDDialog -from kivymd.uix.button import MDFlatButton - -from pybitmessage.bitmessagekivy.get_platform import platform -from pybitmessage.bmconfigparser import config - -ThemeClsColor = [0.12, 0.58, 0.95, 1] - - -data_screens = { - "MailDetail": { - "kv_string": "maildetail", - "Factory": "MailDetail()", - "name_screen": "mailDetail", - "object": 0, - "Import": "from pybitmessage.bitmessagekivy.baseclass.maildetail import MailDetail", - }, -} - - -def load_image_path(): - """Return the path of kivy images""" - image_path = os.path.abspath(os.path.join('pybitmessage', 'images', 'kivy')) - return image_path - - -def get_identity_list(): - """Get list of identities and access 'identity_list' variable in .kv file""" - identity_list = ListProperty( - addr for addr in config.addresses() if config.getboolean(str(addr), 'enabled') - ) - return identity_list - - -def kivy_state_variables(): - """Return kivy_state variable""" - kivy_running_app = App.get_running_app() - kivy_state = kivy_running_app.kivy_state_obj - return kivy_state - - -def chip_tag(text): - """Create a new ChipTag""" - obj = MDChip() - # obj.size_hint = (None, None) - obj.size_hint = (0.16 if platform == "android" else 0.08, None) - obj.text = text - obj.icon = "" - obj.pos_hint = { - "center_x": 0.91 if platform == "android" else 0.94, - "center_y": 0.3 - } - obj.height = dp(18) - obj.text_color = (1, 1, 1, 1) - obj.radius = [8] - return obj - - -def toast(text): - """Method will display the toast message""" - kivytoast.toast(text) - - -def show_limited_cnt(total_msg): - """This method set the total count limit in badge_text""" - max_msg_count = '99+' - total_msg_limit = 99 - return max_msg_count if total_msg > total_msg_limit else str(total_msg) - - -def avatar_image_first_letter(letter_string): - """Returns first letter for the avatar image""" - try: - image_letter = letter_string.title()[0] - if image_letter.isalnum(): - return image_letter - return '!' - except IndexError: - return '!' - - -def add_time_widget(time): # pylint: disable=redefined-outer-name, W0201 - """This method is used to create TimeWidget""" - action_time = TimeTagRightSampleWidget( - text=str(show_time_history(time)), - font_style="Caption", - size=[120, 140] if platform == "android" else [64, 80], - ) - action_time.font_size = "11sp" - return action_time - - -def show_time_history(act_time): - """This method is used to return the message sent or receive time""" - action_time = datetime.fromtimestamp(int(act_time)) - crnt_date = datetime.now() - duration = crnt_date - action_time - if duration.days < 1: - return action_time.strftime("%I:%M %p") - if duration.days < 365: - return action_time.strftime("%d %b") - return action_time.strftime("%d/%m/%Y") - - -# pylint: disable=too-few-public-methods -class AvatarSampleWidget(ILeftBody, Image): - """AvatarSampleWidget class for kivy Ui""" - - -class TimeTagRightSampleWidget(IRightBodyTouch, MDLabel): - """TimeTagRightSampleWidget class for Ui""" - - -class SwipeToDeleteItem(MDCardSwipe): - """Swipe delete class for App UI""" - text = StringProperty() - cla = Window.size[0] / 2 - # cla = 800 - swipe_distance = NumericProperty(cla) - opening_time = NumericProperty(0.5) - - -class CustomSwipeToDeleteItem(MDCardSwipe): - """Custom swipe delete class for App UI""" - text = StringProperty() - cla = Window.size[0] / 2 - swipe_distance = NumericProperty(cla) - opening_time = NumericProperty(0.5) - - -def empty_screen_label(label_str=None, no_search_res_found=None): - """Returns default text on screen when no address is there.""" - kivy_state = kivy_state_variables() - content = MDLabel( - font_style='Caption', - theme_text_color='Primary', - text=no_search_res_found if kivy_state.searching_text else label_str, - halign='center', - size_hint_y=None, - valign='top') - return content - - -def retrieve_secondary_text(mail): - """Retriving mail details""" - secondary_txt_len = 10 - third_txt_len = 25 - dot_str = '...........' - dot_str2 = '...!' - third_text = mail[3].replace('\n', ' ') - - if len(third_text) > third_txt_len: - if len(mail[2]) > secondary_txt_len: # pylint: disable=no-else-return - return mail[2][:secondary_txt_len] + dot_str - else: - return mail[2] + '\n' + " " + (third_text[:third_txt_len] + dot_str2) - else: - return third_text - - -def set_mail_details(mail): - """Setting mail details""" - mail_details_data = { - 'text': mail[1].strip(), - 'secondary_text': retrieve_secondary_text(mail), - 'ackdata': mail[5], - 'senttime': mail[6] - } - return mail_details_data - - -def mdlist_message_content(queryreturn, data): - """Set Mails details in MD_list""" - for mail in queryreturn: - mdlist_data = set_mail_details(mail) - data.append(mdlist_data) - - -def msg_content_length(body, subject, max_length=50): - """This function concatinate body and subject if len(subject) > 50""" - continue_str = '........' - if len(subject) >= max_length: - subject = subject[:max_length] + continue_str - else: - subject = ((subject + ',' + body)[0:50] + continue_str).replace('\t', '').replace(' ', '') - return subject - - -def composer_common_dialog(alert_msg): - """Common alert popup for message composer""" - is_android_width = .8 - other_platform_width = .55 - dialog_height = .25 - width = is_android_width if platform == 'android' else other_platform_width - - dialog_box = MDDialog( - text=alert_msg, - size_hint=(width, dialog_height), - buttons=[ - MDFlatButton( - text="Ok", on_release=lambda x: callback_for_menu_items("Ok") - ), - ], - ) - dialog_box.open() - - def callback_for_menu_items(text_item, *arg): - """Callback of alert box""" - dialog_box.dismiss() - toast(text_item) diff --git a/src/bitmessagekivy/baseclass/common_mail_detail.py b/src/bitmessagekivy/baseclass/common_mail_detail.py deleted file mode 100644 index 78ae88d5..00000000 --- a/src/bitmessagekivy/baseclass/common_mail_detail.py +++ /dev/null @@ -1,22 +0,0 @@ -# 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 deleted file mode 100644 index 50f883d6..00000000 --- a/src/bitmessagekivy/baseclass/draft.py +++ /dev/null @@ -1,54 +0,0 @@ -# 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 deleted file mode 100644 index 17ea9a9b..00000000 --- a/src/bitmessagekivy/baseclass/inbox.py +++ /dev/null @@ -1,59 +0,0 @@ -# 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 deleted file mode 100644 index c5dd9ef4..00000000 --- a/src/bitmessagekivy/baseclass/login.py +++ /dev/null @@ -1,97 +0,0 @@ -# pylint: disable=no-member, too-many-arguments, too-few-public-methods -# pylint: disable=no-name-in-module, unused-argument, arguments-differ - -""" -Login screen appears when the App is first time starts and when new Address is generated. -""" - - -from kivy.clock import Clock -from kivy.properties import StringProperty, BooleanProperty -from kivy.uix.boxlayout import BoxLayout -from kivy.uix.screenmanager import Screen -from kivy.app import App - -from kivymd.uix.behaviors.elevation import RectangularElevationBehavior - -from pybitmessage.backend.address_generator import AddressGenerator -from pybitmessage.bitmessagekivy.baseclass.common import toast -from pybitmessage.bmconfigparser import config - - -class Login(Screen): - """Login Screeen class for kivy Ui""" - log_text1 = ( - 'You may generate addresses by using either random numbers' - ' or by using a passphrase If you use a passphrase, the address' - ' is called a deterministic; address The Random Number option is' - ' selected by default but deterministic addresses have several pros' - ' and cons:') - log_text2 = ('If talk about pros You can recreate your addresses on any computer' - ' from memory, You need-not worry about backing up your keys.dat file' - ' as long as you can remember your passphrase and aside talk about cons' - ' You must remember (or write down) your You must remember the address' - ' version number and the stream number along with your passphrase If you' - ' choose a weak passphrase and someone on the Internet can brute-force it,' - ' they can read your messages and send messages as you') - - -class Random(Screen): - """Random Screen class for Ui""" - - is_active = BooleanProperty(False) - checked = StringProperty("") - - def generateaddress(self): - """Method for Address Generator""" - entered_label = str(self.ids.add_random_bx.children[0].ids.lab.text).strip() - if not entered_label: - self.ids.add_random_bx.children[0].ids.lab.focus = True - is_address = AddressGenerator.random_address_generation( - entered_label, streamNumberForAddress=1, eighteenByteRipe=False, - nonceTrialsPerByte=1000, payloadLengthExtraBytes=1000 - ) - if is_address: - toast('Creating New Address ...') - self.parent.parent.ids.toolbar.opacity = 1 - self.parent.parent.ids.toolbar.disabled = False - App.get_running_app().loadMyAddressScreen(True) - self.manager.current = 'myaddress' - Clock.schedule_once(self.address_created_callback, 6) - - def address_created_callback(self, dt=0): - """New address created""" - App.get_running_app().loadMyAddressScreen(False) - App.get_running_app().root.ids.id_myaddress.ids.ml.clear_widgets() - App.get_running_app().root.ids.id_myaddress.is_add_created = True - App.get_running_app().root.ids.id_myaddress.init_ui() - self.reset_address_spinner() - toast('New address created') - - def reset_address_spinner(self): - """reseting spinner address and UI""" - addresses = [addr for addr in config.addresses() - if config.getboolean(str(addr), 'enabled')] - self.manager.parent.ids.content_drawer.ids.identity_dropdown.values = [] - self.manager.parent.ids.content_drawer.ids.identity_dropdown.values = addresses - self.manager.parent.ids.id_create.children[1].ids.composer_dropdown.values = [] - self.manager.parent.ids.id_create.children[1].ids.composer_dropdown.values = addresses - - @staticmethod - def add_validation(instance): - """Retrieve created labels and validate""" - entered_label = str(instance.text.strip()) - AddressGenerator.address_validation(instance, entered_label) - - def reset_address_label(self): - """Resetting address labels""" - if not self.ids.add_random_bx.children: - self.ids.add_random_bx.add_widget(RandomBoxlayout()) - - -class InfoLayout(BoxLayout, RectangularElevationBehavior): - """InfoLayout class for kivy Ui""" - - -class RandomBoxlayout(BoxLayout): - """RandomBoxlayout class for BoxLayout behaviour""" diff --git a/src/bitmessagekivy/baseclass/maildetail.py b/src/bitmessagekivy/baseclass/maildetail.py deleted file mode 100644 index 11fb6c79..00000000 --- a/src/bitmessagekivy/baseclass/maildetail.py +++ /dev/null @@ -1,242 +0,0 @@ -# pylint: disable=unused-argument, consider-using-f-string, import-error, attribute-defined-outside-init -# pylint: disable=unnecessary-comprehension, no-member, no-name-in-module, too-few-public-methods - -""" -Maildetail screen for inbox, sent, draft and trash. -""" - -import os -from datetime import datetime - -from kivy.core.clipboard import Clipboard -from kivy.clock import Clock -from kivy.properties import ( - StringProperty, - NumericProperty -) -from kivy.uix.screenmanager import Screen -from kivy.factory import Factory -from kivy.app import App - -from kivymd.uix.button import MDFlatButton, MDIconButton -from kivymd.uix.dialog import MDDialog -from kivymd.uix.list import ( - OneLineListItem, - IRightBodyTouch -) - -from pybitmessage.bitmessagekivy.baseclass.common import ( - toast, avatar_image_first_letter, show_time_history, kivy_state_variables -) -from pybitmessage.bitmessagekivy.baseclass.popup import SenderDetailPopup -from pybitmessage.bitmessagekivy.get_platform import platform -from pybitmessage.helper_sql import sqlQuery - - -class OneLineListTitle(OneLineListItem): - """OneLineListTitle class for kivy Ui""" - __events__ = ('on_long_press', ) - long_press_time = NumericProperty(1) - - def on_state(self, instance, value): - """On state""" - if value == 'down': - lpt = self.long_press_time - self._clockev = Clock.schedule_once(self._do_long_press, lpt) - else: - self._clockev.cancel() - - def _do_long_press(self, dt): - """Do long press""" - self.dispatch('on_long_press') - - def on_long_press(self, *largs): - """On long press""" - self.copymessageTitle(self.text) - - def copymessageTitle(self, title_text): - """this method is for displaying dialog box""" - self.title_text = title_text - width = .8 if platform == 'android' else .55 - self.dialog_box = MDDialog( - text=title_text, - size_hint=(width, .25), - buttons=[ - MDFlatButton( - text="Copy", on_release=self.callback_for_copy_title - ), - MDFlatButton( - text="Cancel", on_release=self.callback_for_copy_title, - ), - ],) - self.dialog_box.open() - - def callback_for_copy_title(self, instance): - """Callback of alert box""" - if instance.text == 'Copy': - Clipboard.copy(self.title_text) - self.dialog_box.dismiss() - toast(instance.text) - - -class IconRightSampleWidget(IRightBodyTouch, MDIconButton): - """IconRightSampleWidget class for kivy Ui""" - - -class MailDetail(Screen): # pylint: disable=too-many-instance-attributes - """MailDetail Screen class for kivy Ui""" - - to_addr = StringProperty() - from_addr = StringProperty() - subject = StringProperty() - message = StringProperty() - status = StringProperty() - page_type = StringProperty() - time_tag = StringProperty() - avatarImg = StringProperty() - no_subject = '(no subject)' - - def __init__(self, *args, **kwargs): - """Mail Details method""" - super(MailDetail, self).__init__(*args, **kwargs) - self.kivy_state = kivy_state_variables() - Clock.schedule_once(self.init_ui, 0) - - def init_ui(self, dt=0): - """Clock Schdule for method MailDetail mails""" - self.page_type = self.kivy_state.detail_page_type if self.kivy_state.detail_page_type else '' - try: - if self.kivy_state.detail_page_type in ('sent', 'draft'): - App.get_running_app().set_mail_detail_header() - elif self.kivy_state.detail_page_type == 'inbox': - data = sqlQuery( - "select toaddress, fromaddress, subject, message, received from inbox" - " where msgid = ?", self.kivy_state.mail_id) - self.assign_mail_details(data) - App.get_running_app().set_mail_detail_header() - except Exception as e: # pylint: disable=unused-variable - print('Something wents wrong!!') - - def assign_mail_details(self, data): - """Assigning mail details""" - subject = data[0][2].decode() if isinstance(data[0][2], bytes) else data[0][2] - body = data[0][3].decode() if isinstance(data[0][2], bytes) else data[0][3] - self.to_addr = data[0][0] if len(data[0][0]) > 4 else ' ' - self.from_addr = data[0][1] - - self.subject = subject.capitalize( - ) if subject.capitalize() else self.no_subject - self.message = body - if len(data[0]) == 7: - self.status = data[0][4] - self.time_tag = show_time_history(data[0][4]) if self.kivy_state.detail_page_type == 'inbox' \ - else show_time_history(data[0][6]) - self.avatarImg = os.path.join(self.kivy_state.imageDir, 'draft-icon.png') \ - if self.kivy_state.detail_page_type == 'draft' \ - else (os.path.join(self.kivy_state.imageDir, 'text_images', '{0}.png'.format(avatar_image_first_letter( - self.subject.strip())))) - self.timeinseconds = data[0][4] if self.kivy_state.detail_page_type == 'inbox' else data[0][6] - - def delete_mail(self): - """Method for mail delete""" - msg_count_objs = App.get_running_app().root.ids.content_drawer.ids - self.kivy_state.searching_text = '' - self.children[0].children[0].active = True - if self.kivy_state.detail_page_type == 'sent': - App.get_running_app().root.ids.id_sent.ids.sent_search.ids.search_field.text = '' - msg_count_objs.send_cnt.ids.badge_txt.text = str(int(self.kivy_state.sent_count) - 1) - self.kivy_state.sent_count = str(int(self.kivy_state.sent_count) - 1) - self.parent.screens[2].ids.ml.clear_widgets() - self.parent.screens[2].loadSent(self.kivy_state.selected_address) - elif self.kivy_state.detail_page_type == 'inbox': - App.get_running_app().root.ids.id_inbox.ids.inbox_search.ids.search_field.text = '' - msg_count_objs.inbox_cnt.ids.badge_txt.text = str( - int(self.kivy_state.inbox_count) - 1) - self.kivy_state.inbox_count = str(int(self.kivy_state.inbox_count) - 1) - self.parent.screens[0].ids.ml.clear_widgets() - self.parent.screens[0].loadMessagelist(self.kivy_state.selected_address) - - elif self.kivy_state.detail_page_type == 'draft': - msg_count_objs.draft_cnt.ids.badge_txt.text = str( - int(self.kivy_state.draft_count) - 1) - self.kivy_state.draft_count = str(int(self.kivy_state.draft_count) - 1) - self.parent.screens[13].clear_widgets() - self.parent.screens[13].add_widget(Factory.Draft()) - - if self.kivy_state.detail_page_type != 'draft': - msg_count_objs.trash_cnt.ids.badge_txt.text = str( - int(self.kivy_state.trash_count) + 1) - msg_count_objs.allmail_cnt.ids.badge_txt.text = str( - int(self.kivy_state.all_count) - 1) - self.kivy_state.trash_count = str(int(self.kivy_state.trash_count) + 1) - self.kivy_state.all_count = str(int(self.kivy_state.all_count) - 1) if \ - int(self.kivy_state.all_count) else '0' - self.parent.screens[3].clear_widgets() - self.parent.screens[3].add_widget(Factory.Trash()) - self.parent.screens[14].clear_widgets() - self.parent.screens[14].add_widget(Factory.AllMails()) - Clock.schedule_once(self.callback_for_delete, 4) - - def callback_for_delete(self, dt=0): - """Delete method from allmails""" - if self.kivy_state.detail_page_type: - self.children[0].children[0].active = False - App.get_running_app().set_common_header() - self.parent.current = 'allmails' \ - if self.kivy_state.is_allmail else self.kivy_state.detail_page_type - self.kivy_state.detail_page_type = '' - toast('Deleted') - - def get_message_details_to_reply(self, data): - """Getting message details and fill into fields when reply""" - sender_address = ' wrote:--------------\n' - message_time = '\n\n --------------On ' - composer_obj = self.parent.screens[1].children[1].ids - composer_obj.ti.text = data[0][0] - composer_obj.composer_dropdown.text = data[0][0] - composer_obj.txt_input.text = data[0][1] - split_subject = data[0][2].split('Re:', 1) - composer_obj.subject.text = 'Re: ' + (split_subject[1] if len(split_subject) > 1 else split_subject[0]) - time_obj = datetime.fromtimestamp(int(data[0][4])) - time_tag = time_obj.strftime("%d %b %Y, %I:%M %p") - sender_name = data[0][1] - composer_obj.body.text = ( - message_time + time_tag + ', ' + sender_name + sender_address + data[0][3]) - composer_obj.body.focus = True - composer_obj.body.cursor = (0, 0) - - def inbox_reply(self): - """Reply inbox messages""" - self.kivy_state.in_composer = True - App.get_running_app().root.ids.id_create.children[1].ids.rv.data = '' - App.get_running_app().root.ids.sc3.children[1].ids.rv.data = '' - self.parent.current = 'create' - App.get_running_app().set_navbar_for_composer() - - def get_message_details_for_draft_reply(self, data): - """Getting and setting message details fill into fields when draft reply""" - composer_ids = ( - self.parent.parent.ids.id_create.children[1].ids) - composer_ids.ti.text = data[0][1] - composer_ids.btn.text = data[0][1] - composer_ids.txt_input.text = data[0][0] - composer_ids.subject.text = data[0][2] if data[0][2] != self.no_subject else '' - composer_ids.body.text = data[0][3] - - def write_msg(self, navApp): - """Write on draft mail""" - self.kivy_state.send_draft_mail = self.kivy_state.mail_id - self.parent.current = 'create' - navApp.set_navbar_for_composer() - - def detailedPopup(self): - """Detailed popup""" - obj = SenderDetailPopup() - obj.open() - arg = (self.to_addr, self.from_addr, self.timeinseconds) - obj.assignDetail(*arg) - - @staticmethod - def callback_for_menu_items(text_item, *arg): - """Callback of alert box""" - toast(text_item) diff --git a/src/bitmessagekivy/baseclass/msg_composer.py b/src/bitmessagekivy/baseclass/msg_composer.py deleted file mode 100644 index a36996e0..00000000 --- a/src/bitmessagekivy/baseclass/msg_composer.py +++ /dev/null @@ -1,188 +0,0 @@ -# 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 deleted file mode 100644 index 0a46bae9..00000000 --- a/src/bitmessagekivy/baseclass/myaddress.py +++ /dev/null @@ -1,230 +0,0 @@ -# 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 deleted file mode 100644 index 23e2342f..00000000 --- a/src/bitmessagekivy/baseclass/myaddress_widgets.py +++ /dev/null @@ -1,64 +0,0 @@ -# pylint: disable=too-many-arguments, no-name-in-module, import-error -# pylint: disable=too-few-public-methods, no-member, too-many-ancestors - -""" -MyAddress widgets are here. -""" - -from kivymd.uix.button import MDFlatButton -from kivymd.uix.dialog import MDDialog -from kivymd.uix.label import MDLabel -from kivymd.uix.list import IRightBodyTouch - -from pybitmessage.bitmessagekivy.get_platform import platform -from pybitmessage.bitmessagekivy.baseclass.common import ThemeClsColor - - -class BadgeText(IRightBodyTouch, MDLabel): - """BadgeText class for kivy UI""" - - -class HelperMyAddress(object): - """Widget used in MyAddress are here""" - dialog_height = .25 - - @staticmethod - def is_active_badge(): - """This function show the 'active' label of active Address.""" - active_status = 'Active' - is_android_width = 90 - width = 50 - height = 60 - badge_obj = BadgeText( - size_hint=(None, None), - size=[is_android_width if platform == 'android' else width, height], - text=active_status, halign='center', - font_style='Body1', theme_text_color='Custom', - text_color=ThemeClsColor, font_size='13sp' - ) - return badge_obj - - @staticmethod - def myaddress_detail_popup(obj, width): - """This method show the details of address as popup opens.""" - show_myaddress_dialogue = MDDialog( - type="custom", - size_hint=(width, HelperMyAddress.dialog_height), - content_cls=obj, - ) - return show_myaddress_dialogue - - @staticmethod - def inactive_address_popup(width, callback_for_menu_items): - """This method shows the warning popup if the address is inactive""" - dialog_text = 'Address is not currently active. Please click on Toggle button to active it.' - dialog_box = MDDialog( - text=dialog_text, - size_hint=(width, HelperMyAddress.dialog_height), - buttons=[ - MDFlatButton( - text="Ok", on_release=lambda x: callback_for_menu_items("Ok") - ), - ], - ) - return dialog_box diff --git a/src/bitmessagekivy/baseclass/network.py b/src/bitmessagekivy/baseclass/network.py deleted file mode 100644 index dcb3f082..00000000 --- a/src/bitmessagekivy/baseclass/network.py +++ /dev/null @@ -1,54 +0,0 @@ -# pylint: disable=unused-argument, consider-using-f-string -# pylint: disable=no-name-in-module, too-few-public-methods - -""" - Network status -""" - -import os - -from kivy.clock import Clock -from kivy.properties import StringProperty -from kivy.uix.screenmanager import Screen - -from pybitmessage import state - -if os.environ.get('INSTALL_TESTS', False) and not state.backend_py3_compatible: - from pybitmessage.mockbm import kivy_main - stats = kivy_main.network.stats - objectracker = kivy_main.network.objectracker -else: - from pybitmessage.network import stats, objectracker - - -class NetworkStat(Screen): - """NetworkStat class for kivy Ui""" - - text_variable_1 = StringProperty( - '{0}::{1}'.format('Total Connections', '0')) - text_variable_2 = StringProperty( - 'Processed {0} per-to-per messages'.format('0')) - text_variable_3 = StringProperty( - 'Processed {0} brodcast messages'.format('0')) - text_variable_4 = StringProperty( - 'Processed {0} public keys'.format('0')) - text_variable_5 = StringProperty( - 'Processed {0} object to be synced'.format('0')) - - def __init__(self, *args, **kwargs): - """Init method for network stat""" - super(NetworkStat, self).__init__(*args, **kwargs) - Clock.schedule_interval(self.init_ui, 1) - - def init_ui(self, dt=0): - """Clock Schdule for method networkstat screen""" - self.text_variable_1 = '{0} :: {1}'.format( - 'Total Connections', str(len(stats.connectedHostsList()))) - self.text_variable_2 = 'Processed {0} per-to-per messages'.format( - str(state.numberOfMessagesProcessed)) - self.text_variable_3 = 'Processed {0} brodcast messages'.format( - str(state.numberOfBroadcastsProcessed)) - self.text_variable_4 = 'Processed {0} public keys'.format( - str(state.numberOfPubkeysProcessed)) - self.text_variable_5 = '{0} object to be synced'.format( - len(objectracker.missingObjects)) diff --git a/src/bitmessagekivy/baseclass/payment.py b/src/bitmessagekivy/baseclass/payment.py deleted file mode 100644 index 6749e1bb..00000000 --- a/src/bitmessagekivy/baseclass/payment.py +++ /dev/null @@ -1,66 +0,0 @@ -# pylint: disable=import-error, no-name-in-module, too-few-public-methods, too-many-ancestors - -''' - Payment/subscription frontend -''' - -from kivy.uix.boxlayout import BoxLayout -from kivy.uix.screenmanager import Screen -from kivy.app import App - -from kivymd.uix.behaviors.elevation import RectangularElevationBehavior -from kivymd.uix.label import MDLabel -from kivymd.uix.list import ( - IRightBodyTouch, - OneLineAvatarIconListItem -) - -from pybitmessage.bitmessagekivy.baseclass.common import toast, kivy_state_variables - - -class Payment(Screen): - """Payment Screen class for kivy Ui""" - - def __init__(self, *args, **kwargs): - """Instantiate kivy state variable""" - super(Payment, self).__init__(*args, **kwargs) - self.kivy_state = kivy_state_variables() - - # TODO: get_free_credits() is not used anywhere, will be used later for Payment/subscription. - def get_free_credits(self, instance): # pylint: disable=unused-argument - """Get the available credits""" - # pylint: disable=no-self-use - self.kivy_state.available_credit = 0 - existing_credits = 0 - if existing_credits > 0: - toast( - 'We already have added free credit' - ' for the subscription to your account!') - else: - toast('Credit added to your account!') - # TODO: There is no sc18 screen id is available, - # need to create sc18 for Credits screen inside main.kv - App.get_running_app().root.ids.sc18.ids.cred.text = '{0}'.format( - self.kivy_state.available_credit) - - -class Category(BoxLayout, RectangularElevationBehavior): - """Category class for kivy Ui""" - elevation_normal = .01 - - -class ProductLayout(BoxLayout, RectangularElevationBehavior): - """ProductLayout class for kivy Ui""" - elevation_normal = .01 - - -class PaymentMethodLayout(BoxLayout): - """PaymentMethodLayout class for kivy Ui""" - - -class ListItemWithLabel(OneLineAvatarIconListItem): - """ListItemWithLabel class for kivy Ui""" - - -class RightLabel(IRightBodyTouch, MDLabel): - """RightLabel class for kivy Ui""" diff --git a/src/bitmessagekivy/baseclass/popup.py b/src/bitmessagekivy/baseclass/popup.py deleted file mode 100644 index d2a4c859..00000000 --- a/src/bitmessagekivy/baseclass/popup.py +++ /dev/null @@ -1,231 +0,0 @@ -# pylint: disable=import-error, attribute-defined-outside-init -# pylint: disable=no-member, no-name-in-module, unused-argument, too-few-public-methods - -""" -All the popup are managed here. - -""" -import logging -from datetime import datetime - -from kivy.clock import Clock -from kivy.metrics import dp -from kivy.properties import StringProperty -from kivy.uix.boxlayout import BoxLayout -from kivy.uix.popup import Popup -from kivy.app import App - -from pybitmessage.bitmessagekivy import kivy_helper_search -from pybitmessage.bitmessagekivy.get_platform import platform - -from pybitmessage.bitmessagekivy.baseclass.common import toast - -from pybitmessage.addresses import decodeAddress - -logger = logging.getLogger('default') - - -class AddressChangingLoader(Popup): - """Run a Screen Loader when changing the Identity for kivy UI""" - - def __init__(self, **kwargs): - super(AddressChangingLoader, self).__init__(**kwargs) - Clock.schedule_once(self.dismiss_popup, 0.5) - - def dismiss_popup(self, dt): - """Dismiss popups""" - self.dismiss() - - -class AddAddressPopup(BoxLayout): - """Popup for adding new address to addressbook""" - - validation_dict = { - "missingbm": "The address should start with ''BM-''", - "checksumfailed": "The address is not typed or copied correctly", - "versiontoohigh": "The version number of this address is higher than this" - " software can support. Please upgrade Bitmessage.", - "invalidcharacters": "The address contains invalid characters.", - "ripetooshort": "Some data encoded in the address is too short.", - "ripetoolong": "Some data encoded in the address is too long.", - "varintmalformed": "Some data encoded in the address is malformed." - } - valid = False - - def __init__(self, **kwargs): - super(AddAddressPopup, self).__init__(**kwargs) - - def checkAddress_valid(self, instance): - """Checking address is valid or not""" - my_addresses = ( - App.get_running_app().root.ids.content_drawer.ids.identity_dropdown.values) - add_book = [addr[1] for addr in kivy_helper_search.search_sql( - folder="addressbook")] - entered_text = str(instance.text).strip() - if entered_text in add_book: - text = 'Address is already in the addressbook.' - elif entered_text in my_addresses: - text = 'You can not save your own address.' - elif entered_text: - text = self.addressChanged(entered_text) - - if entered_text in my_addresses or entered_text in add_book: - self.ids.address.error = True - self.ids.address.helper_text = text - elif entered_text and self.valid: - self.ids.address.error = False - elif entered_text: - self.ids.address.error = True - self.ids.address.helper_text = text - else: - self.ids.address.error = True - self.ids.address.helper_text = 'This field is required' - - def checkLabel_valid(self, instance): - """Checking address label is unique or not""" - entered_label = instance.text.strip() - addr_labels = [labels[0] for labels in kivy_helper_search.search_sql( - folder="addressbook")] - if entered_label in addr_labels: - self.ids.label.error = True - self.ids.label.helper_text = 'Label name already exists.' - elif entered_label: - self.ids.label.error = False - else: - self.ids.label.error = True - self.ids.label.helper_text = 'This field is required' - - def _onSuccess(self, addressVersion, streamNumber, ripe): - pass - - def addressChanged(self, addr): - """Address validation callback, performs validation and gives feedback""" - status, addressVersion, streamNumber, ripe = decodeAddress( - str(addr)) - self.valid = status == 'success' - - if self.valid: - text = "Address is valid." - self._onSuccess(addressVersion, streamNumber, ripe) - return text - return self.validation_dict.get(status) - - -class SavedAddressDetailPopup(BoxLayout): - """Pop-up for Saved Address details for kivy UI""" - - address_label = StringProperty() - address = StringProperty() - - def __init__(self, **kwargs): - """Set screen of address detail page""" - super(SavedAddressDetailPopup, self).__init__(**kwargs) - - def checkLabel_valid(self, instance): - """Checking address label is unique of not""" - entered_label = str(instance.text.strip()) - address_list = kivy_helper_search.search_sql(folder="addressbook") - addr_labels = [labels[0] for labels in address_list] - add_dict = dict(address_list) - if self.address and entered_label in addr_labels \ - and self.address != add_dict[entered_label]: - self.ids.add_label.error = True - self.ids.add_label.helper_text = 'label name already exists.' - elif entered_label: - self.ids.add_label.error = False - else: - self.ids.add_label.error = True - self.ids.add_label.helper_text = 'This field is required' - - -class MyaddDetailPopup(BoxLayout): - """MyaddDetailPopup class for kivy Ui""" - - address_label = StringProperty() - address = StringProperty() - - def __init__(self, **kwargs): - """My Address Details screen setting""" - super(MyaddDetailPopup, self).__init__(**kwargs) - - def send_message_from(self): - """Method used to fill from address of composer autofield""" - App.get_running_app().set_navbar_for_composer() - window_obj = App.get_running_app().root.ids - window_obj.id_create.children[1].ids.ti.text = self.address - window_obj.id_create.children[1].ids.composer_dropdown.text = self.address - window_obj.id_create.children[1].ids.txt_input.text = '' - window_obj.id_create.children[1].ids.subject.text = '' - window_obj.id_create.children[1].ids.body.text = '' - window_obj.scr_mngr.current = 'create' - self.parent.parent.parent.dismiss() - - def close_pop(self): - """Pop is Cancelled""" - self.parent.parent.parent.dismiss() - toast('Cancelled') - - -class AppClosingPopup(Popup): - """AppClosingPopup class for kivy Ui""" - - def __init__(self, **kwargs): - super(AppClosingPopup, self).__init__(**kwargs) - - def closingAction(self, text): - """Action on closing window""" - exit_message = "*******************EXITING FROM APPLICATION*******************" - if text == 'Yes': - logger.debug(exit_message) - import shutdown - shutdown.doCleanShutdown() - else: - self.dismiss() - toast(text) - - -class SenderDetailPopup(Popup): - """SenderDetailPopup class for kivy Ui""" - - to_addr = StringProperty() - from_addr = StringProperty() - time_tag = StringProperty() - - def __init__(self, **kwargs): - """this metthod initialized the send message detial popup""" - super(SenderDetailPopup, self).__init__(**kwargs) - - def assignDetail(self, to_addr, from_addr, timeinseconds): - """Detailes assigned""" - self.to_addr = to_addr - self.from_addr = from_addr - time_obj = datetime.fromtimestamp(int(timeinseconds)) - self.time_tag = time_obj.strftime("%d %b %Y, %I:%M %p") - device_type = 2 if platform == 'android' else 1.5 - pop_height = 1.2 * device_type * (self.ids.sd_label.height + self.ids.dismiss_btn.height) - if len(to_addr) > 3: - self.height = pop_height - self.ids.to_addId.size_hint_y = None - self.ids.to_addId.height = 50 - self.ids.to_addtitle.add_widget(ToAddressTitle()) - frmaddbox = ToAddrBoxlayout() - frmaddbox.set_toAddress(to_addr) - self.ids.to_addId.add_widget(frmaddbox) - else: - self.ids.space_1.height = dp(0) - self.ids.space_2.height = dp(0) - self.ids.myadd_popup_box.spacing = dp(8 if platform == 'android' else 3) - self.height = pop_height / 1.2 - - -class ToAddrBoxlayout(BoxLayout): - """ToAddrBoxlayout class for kivy Ui""" - to_addr = StringProperty() - - def set_toAddress(self, to_addr): - """This method is use to set to address""" - self.to_addr = to_addr - - -class ToAddressTitle(BoxLayout): - """ToAddressTitle class for BoxLayout behaviour""" diff --git a/src/bitmessagekivy/baseclass/qrcode.py b/src/bitmessagekivy/baseclass/qrcode.py deleted file mode 100644 index 4c6d99a0..00000000 --- a/src/bitmessagekivy/baseclass/qrcode.py +++ /dev/null @@ -1,34 +0,0 @@ -# pylint: disable=import-error, no-name-in-module, too-few-public-methods - -""" -Generate QRcode of saved addresses in addressbook. -""" - -import logging - -from kivy.app import App -from kivy.uix.screenmanager import Screen -from kivy.properties import StringProperty -from kivy_garden.qrcode import QRCodeWidget - -logger = logging.getLogger('default') - - -class ShowQRCode(Screen): - """ShowQRCode Screen class for kivy Ui""" - address = StringProperty() - - def __init__(self, *args, **kwargs): - """Instantiate kivy state variable""" - super(ShowQRCode, self).__init__(*args, **kwargs) - self.kivy_running_app = App.get_running_app() - - def qrdisplay(self, instance, address): - """Method used for showing QR Code""" - self.ids.qr.clear_widgets() - self.kivy_running_app.set_toolbar_for_QrCode() - self.address = address # used for label - self.ids.qr.add_widget(QRCodeWidget(data=self.address)) - self.ids.qr.children[0].show_border = False - instance.parent.parent.parent.dismiss() - logger.debug('Show QR code') diff --git a/src/bitmessagekivy/baseclass/scan_screen.py b/src/bitmessagekivy/baseclass/scan_screen.py deleted file mode 100644 index 3321a4fa..00000000 --- a/src/bitmessagekivy/baseclass/scan_screen.py +++ /dev/null @@ -1,105 +0,0 @@ -# pylint: disable=no-member, too-many-arguments, too-few-public-methods -# pylint: disable=no-name-in-module, unused-argument, arguments-differ - -""" -QR code Scan Screen used in message composer to get recipient address - -""" - -import os -import logging -import cv2 - -from kivy.clock import Clock -from kivy.lang import Builder -from kivy.properties import ( - BooleanProperty, - ObjectProperty, - StringProperty -) -from kivy.uix.screenmanager import Screen - -from pybitmessage.bitmessagekivy.get_platform import platform - -logger = logging.getLogger('default') - - -class ScanScreen(Screen): - """ScanScreen is for scaning Qr code""" - # pylint: disable=W0212 - camera_available = BooleanProperty(False) - previous_open_screen = StringProperty() - pop_up_instance = ObjectProperty() - - def __init__(self, *args, **kwargs): - """Getting AddressBook Details""" - super(ScanScreen, self).__init__(*args, **kwargs) - self.check_camera() - - def check_camera(self): - """This method is used for checking camera avaibility""" - if platform != "android": - cap = cv2.VideoCapture(0) - is_cam_open = cap.isOpened() - while is_cam_open: - logger.debug('Camera is available!') - self.camera_available = True - break - else: - logger.debug("Camera is not available!") - self.camera_available = False - else: - self.camera_available = True - - def get_screen(self, screen_name, instance=None): - """This method is used for getting previous screen name""" - self.previous_open_screen = screen_name - if screen_name != 'composer': - self.pop_up_instance = instance - - def on_pre_enter(self): - """ - on_pre_enter works little better on android - It affects screen transition on linux - """ - if not self.children: - tmp = Builder.load_file( - os.path.join( - os.path.dirname(os.path.dirname(__file__)), "kv", "{}.kv").format("scanner") - ) - self.add_widget(tmp) - if platform == "android": - Clock.schedule_once(self.start_camera, 0) - - def on_enter(self): - """ - on_enter works better on linux - It creates a black screen on android until camera gets loaded - """ - if platform != "android": - Clock.schedule_once(self.start_camera, 0) - - def on_leave(self): - """This method will call on leave""" - Clock.schedule_once(self.stop_camera, 0) - - def start_camera(self, *args): - """Its used for starting camera for scanning qrcode""" - # pylint: disable=attribute-defined-outside-init - self.xcam = self.children[0].ids.zbarcam.ids.xcamera - if platform == "android": - self.xcam.play = True - else: - Clock.schedule_once(self.open_cam, 0) - - def stop_camera(self, *args): - """Its used for stop the camera""" - self.xcam.play = False - if platform != "android": - self.xcam._camera._device.release() - - def open_cam(self, *args): - """It will open up the camera""" - if not self.xcam._camera._device.isOpened(): - self.xcam._camera._device.open(self.xcam._camera._index) - self.xcam.play = True diff --git a/src/bitmessagekivy/baseclass/sent.py b/src/bitmessagekivy/baseclass/sent.py deleted file mode 100644 index 59db0ab9..00000000 --- a/src/bitmessagekivy/baseclass/sent.py +++ /dev/null @@ -1,47 +0,0 @@ -# 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 deleted file mode 100644 index 1ceb35ee..00000000 --- a/src/bitmessagekivy/baseclass/settings.py +++ /dev/null @@ -1,10 +0,0 @@ -# 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 deleted file mode 100644 index eb62fdaa..00000000 --- a/src/bitmessagekivy/baseclass/trash.py +++ /dev/null @@ -1,33 +0,0 @@ -# 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 deleted file mode 100644 index 654b31f4..00000000 --- a/src/bitmessagekivy/get_platform.py +++ /dev/null @@ -1,31 +0,0 @@ -# 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 deleted file mode 100644 index 2e2f2e93..00000000 --- a/src/bitmessagekivy/identiconGeneration.py +++ /dev/null @@ -1,80 +0,0 @@ -""" -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 index c48ca3ad..684a1722 100644 --- a/src/bitmessagekivy/kivy_helper_search.py +++ b/src/bitmessagekivy/kivy_helper_search.py @@ -1,30 +1,19 @@ -""" -Sql queries for bitmessagekivy -""" -from pybitmessage.helper_sql import sqlQuery +from helper_sql import * -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""" +def search_sql(xAddress="toaddress", account=None, folder="inbox", where=None, what=None, unreadOnly=False): 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 ''' + + if folder == "sent": + sqlStatementBase = ''' + SELECT toaddress, fromaddress, subject, status, ackdata, lastactiontime + FROM sent ''' else: - sqlStatementBase = ( - '''SELECT folder, msgid, toaddress, message, fromaddress,''' - ''' subject, received, read FROM inbox ''' - ) + sqlStatementBase = '''SELECT folder, msgid, toaddress, fromaddress, subject, received, read + FROM inbox ''' sqlStatementParts = [] sqlArguments = [] if account is not None: @@ -35,37 +24,22 @@ def search_sql( 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 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) + sqlStatementParts.append("%s LIKE ?" % (where)) + sqlArguments.append(what) if unreadOnly: sqlStatementParts.append("read = 0") - if sqlStatementParts: + if len(sqlStatementParts) > 0: 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) + if folder == "sent": + sqlStatementBase += " ORDER BY lastactiontime" return sqlQuery(sqlStatementBase, sqlArguments) diff --git a/src/bitmessagekivy/kivy_state.py b/src/bitmessagekivy/kivy_state.py deleted file mode 100644 index 42051ff9..00000000 --- a/src/bitmessagekivy/kivy_state.py +++ /dev/null @@ -1,42 +0,0 @@ -# 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 deleted file mode 100644 index 73b4c1ef..00000000 --- a/src/bitmessagekivy/kv/addressbook.kv +++ /dev/null @@ -1,26 +0,0 @@ -: - 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 deleted file mode 100644 index 3df69e05..00000000 --- a/src/bitmessagekivy/kv/allmails.kv +++ /dev/null @@ -1,25 +0,0 @@ -: - 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 deleted file mode 100644 index e21ed503..00000000 --- a/src/bitmessagekivy/kv/chat.kv +++ /dev/null @@ -1,82 +0,0 @@ -#: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 deleted file mode 100644 index e59c32d7..00000000 --- a/src/bitmessagekivy/kv/chat_list.kv +++ /dev/null @@ -1,58 +0,0 @@ -: - 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 deleted file mode 100644 index 40843c47..00000000 --- a/src/bitmessagekivy/kv/chat_room.kv +++ /dev/null @@ -1,45 +0,0 @@ -#: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 deleted file mode 100644 index 275bd12c..00000000 --- a/src/bitmessagekivy/kv/common_widgets.kv +++ /dev/null @@ -1,62 +0,0 @@ -: - 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 deleted file mode 100644 index 1680d6f0..00000000 --- a/src/bitmessagekivy/kv/credits.kv +++ /dev/null @@ -1,28 +0,0 @@ -: - 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 deleted file mode 100644 index 56682d2b..00000000 --- a/src/bitmessagekivy/kv/draft.kv +++ /dev/null @@ -1,23 +0,0 @@ -: - 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 deleted file mode 100644 index b9cc8566..00000000 --- a/src/bitmessagekivy/kv/inbox.kv +++ /dev/null @@ -1,39 +0,0 @@ -: - 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 deleted file mode 100644 index d3e2f7f9..00000000 --- a/src/bitmessagekivy/kv/login.kv +++ /dev/null @@ -1,264 +0,0 @@ -#: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 deleted file mode 100644 index e98b8661..00000000 --- a/src/bitmessagekivy/kv/maildetail.kv +++ /dev/null @@ -1,87 +0,0 @@ -: - name: 'mailDetail' - ScrollView: - do_scroll_x: False - BoxLayout: - size_hint_y: None - orientation: 'vertical' - # height: dp(bod.height) + self.minimum_height - height: self.minimum_height - padding: dp(10) - # MDLabel: - # size_hint_y: None - # id: subj - # text: root.subject - # theme_text_color: 'Primary' - # halign: 'left' - # font_style: 'H5' - # height: dp(40) - # on_touch_down: root.allclick(self) - OneLineListTitle: - id: subj - text: app.tr._(root.subject) - divider: None - font_style: 'H5' - theme_text_color: 'Primary' - _no_ripple_effect: True - long_press_time: 1 - TwoLineAvatarIconListItem: - id: subaft - text: app.tr._(root.from_addr) - secondary_text: app.tr._('to ' + root.to_addr) - divider: None - on_press: root.detailedPopup() - BadgeText: - size_hint:(None, None) - size:[120, 140] if app.app_platform == 'android' else [64, 80] - text: app.tr._(root.time_tag) - halign:'center' - font_style:'Caption' - pos_hint: {'center_y': .8} - _txt_right_pad: dp(70) - font_size: '11sp' - MDChip: - size_hint: (.16 if app.app_platform == 'android' else .08 , None) - text: app.tr._(root.page_type) - icon: '' - text_color: (1,1,1,1) - pos_hint: {'center_x': .91 if app.app_platform == 'android' else .95, 'center_y': .3} - radius: [8] - height: self.parent.height/4 - AvatarSampleWidget: - source: root.avatarImg - MDLabel: - text: root.status - disabled: True - font_style: 'Body2' - halign:'left' - padding_x: 20 - # MDLabel: - # id: bod - # font_style: 'Subtitle2' - # theme_text_color: 'Primary' - # text: root.message - # halign: 'left' - # height: self.texture_size[1] - MyMDTextField: - id: bod - size_hint_y: None - font_style: 'Subtitle2' - text: root.message - multiline: True - readonly: True - line_color_normal: [0,0,0,0] - _current_line_color: [0,0,0,0] - line_color_focus: [0,0,0,0] - markup: True - font_size: '15sp' - canvas.before: - Color: - rgba: (0,0,0,1) - Loader: - - -: - canvas.before: - Color: - rgba: (0,0,0,1) diff --git a/src/bitmessagekivy/kv/msg_composer.kv b/src/bitmessagekivy/kv/msg_composer.kv deleted file mode 100644 index 13db4f4e..00000000 --- a/src/bitmessagekivy/kv/msg_composer.kv +++ /dev/null @@ -1,161 +0,0 @@ -: - 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 deleted file mode 100644 index 71dde54a..00000000 --- a/src/bitmessagekivy/kv/myaddress.kv +++ /dev/null @@ -1,33 +0,0 @@ -: - 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 deleted file mode 100644 index 17211e98..00000000 --- a/src/bitmessagekivy/kv/network.kv +++ /dev/null @@ -1,131 +0,0 @@ -: - 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 deleted file mode 100644 index 6d475f56..00000000 --- a/src/bitmessagekivy/kv/payment.kv +++ /dev/null @@ -1,325 +0,0 @@ -: - 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 deleted file mode 100644 index 2db74525..00000000 --- a/src/bitmessagekivy/kv/popup.kv +++ /dev/null @@ -1,333 +0,0 @@ -: - separator_color: 1, 1, 1, 1 - background: "White.png" - Button: - id: btn - disabled: True - background_disabled_normal: "White.png" - Image: - source: app.image_path + '/loader.gif' - anim_delay: 0 - #mipmap: True - size: root.size - - -: - id: popup_box - orientation: 'vertical' - # spacing:dp(20) - # spacing: "12dp" - size_hint_y: None - # height: "120dp" - height: label.height+address.height - BoxLayout: - orientation: 'vertical' - MDTextField: - id: label - multiline: False - hint_text: app.tr._("Label") - required: True - icon_right: 'label' - helper_text_mode: "on_error" - # TODO: on_text: root.checkLabel_valid(self) is not used now but it will be used later - canvas.before: - Color: - rgba: (0,0,0,1) - MDTextField: - id: address - hint_text: app.tr._("Address") - required: True - icon_right: 'book-plus' - helper_text_mode: "on_error" - multiline: False - # TODO: on_text: root.checkAddress_valid(self) is not used now but it will be used later - canvas.before: - Color: - rgba: (0,0,0,1) - -: - id: addbook_popup_box - size_hint_y: None - height: 2.5*(add_label.height) - orientation: 'vertical' - spacing:dp(5) - MDLabel - font_style: 'Subtitle2' - theme_text_color: 'Primary' - text: app.tr._("Label") - font_size: '17sp' - halign: 'left' - MDTextField: - id: add_label - font_style: 'Body1' - font_size: '15sp' - halign: 'left' - text: app.tr._(root.address_label) - theme_text_color: 'Primary' - required: True - helper_text_mode: "on_error" - on_text: root.checkLabel_valid(self) - canvas.before: - Color: - rgba: (0,0,0,1) - MDLabel: - font_style: 'Subtitle2' - theme_text_color: 'Primary' - text: app.tr._("Address") - font_size: '17sp' - halign: 'left' - Widget: - size_hint_y: None - height: dp(1) - BoxLayout: - orientation: 'horizontal' - MDLabel: - id: address - font_style: 'Body1' - theme_text_color: 'Primary' - text: app.tr._(root.address) - font_size: '15sp' - halign: 'left' - IconRightSampleWidget: - pos_hint: {'center_x': 0, 'center_y': 1} - icon: 'content-copy' - on_press: app.copy_composer_text(root.address) - - -: - id: myadd_popup - size_hint_y: None - height: "130dp" - spacing:dp(25) - - #height: dp(1.5*(myaddr_label.height)) - orientation: 'vertical' - MDLabel: - id: myaddr_label - font_style: 'Subtitle2' - theme_text_color: 'Primary' - text: app.tr._("Label") - font_size: '17sp' - halign: 'left' - MDLabel: - font_style: 'Body1' - theme_text_color: 'Primary' - text: root.address_label - font_size: '15sp' - halign: 'left' - MDLabel: - font_style: 'Subtitle2' - theme_text_color: 'Primary' - text: app.tr._("Address") - font_size: '17sp' - halign: 'left' - BoxLayout: - orientation: 'horizontal' - MDLabel: - id: label_address - font_style: 'Body1' - theme_text_color: 'Primary' - text: app.tr._(root.address) - font_size: '15sp' - halign: 'left' - IconRightSampleWidget: - pos_hint: {'center_x': 0, 'center_y': 1} - icon: 'content-copy' - on_press: app.copy_composer_text(root.address) - BoxLayout: - id: my_add_btn - spacing:5 - orientation: 'horizontal' - size_hint_y: None - height: self.minimum_height - MDRaisedButton: - size_hint: 2, None - height: dp(40) - on_press: root.send_message_from() - MDLabel: - font_style: 'H6' - text: app.tr._('Send message from') - font_size: '13sp' - color: (1,1,1,1) - halign: 'center' - MDRaisedButton: - size_hint: 1.5, None - height: dp(40) - on_press: app.set_screen('showqrcode') - on_press: app.root.ids.id_showqrcode.qrdisplay(root, root.address) - MDLabel: - font_style: 'H6' - text: app.tr._('Show QR code') - font_size: '13sp' - color: (1,1,1,1) - halign: 'center' - MDRaisedButton: - size_hint: 1.5, None - height: dp(40) - on_press: root.close_pop() - MDLabel: - font_style: 'H6' - text: app.tr._('Cancel') - font_size: '13sp' - color: (1,1,1,1) - halign: 'center' - -: - id: closing_popup - size_hint : (None,None) - height: 1.4*(popup_label.height+ my_add_btn.children[0].height) - width :app.window_size[0] - (app.window_size[0]/10 if app.app_platform == 'android' else app.window_size[0]/4) - background: app.image_path + '/popup.jpeg' - auto_dismiss: False - separator_height: 0 - BoxLayout: - id: myadd_popup_box - size_hint_y: None - spacing:dp(70) - orientation: 'vertical' - BoxLayout: - size_hint_y: None - orientation: 'vertical' - spacing:dp(25) - MDLabel: - id: popup_label - font_style: 'Subtitle2' - theme_text_color: 'Primary' - text: app.tr._("Bitmessage isn't connected to the network.\n If you quit now, it may cause delivery delays.\n Wait until connected and the synchronisation finishes?") - font_size: '17sp' - halign: 'center' - BoxLayout: - id: my_add_btn - spacing:5 - orientation: 'horizontal' - MDRaisedButton: - size_hint: 1.5, None - height: dp(40) - on_press: root.closingAction(self.children[0].text) - on_press: app.stop() - MDLabel: - font_style: 'H6' - text: app.tr._('Yes') - font_size: '13sp' - color: (1,1,1,1) - halign: 'center' - MDRaisedButton: - size_hint: 1.5, None - height: dp(40) - on_press: root.closingAction(self.children[0].text) - MDLabel: - font_style: 'H6' - text: app.tr._('No') - font_size: '13sp' - color: (1,1,1,1) - halign: 'center' - MDRaisedButton: - size_hint: 1.5, None - height: dp(40) - #on_press: root.dismiss() - on_press: root.closingAction(self.children[0].text) - MDLabel: - font_style: 'H6' - text: app.tr._('Cancel') - font_size: '13sp' - color: (1,1,1,1) - halign: 'center' - -: - id: myadd_popup - size_hint : (None,None) - # height: 2*(sd_label.height+ sd_btn.children[0].height) - width :app.window_size[0] - (app.window_size[0]/10 if app.app_platform == 'android' else app.window_size[0]/4) - background: app.image_path + '/popup.jpeg' - auto_dismiss: False - separator_height: 0 - BoxLayout: - id: myadd_popup_box - size_hint_y: None - orientation: 'vertical' - spacing:dp(8 if app.app_platform == 'android' else 3) - BoxLayout: - orientation: 'vertical' - MDLabel: - id: from_add_label - font_style: 'Subtitle2' - theme_text_color: 'Primary' - text: app.tr._("From :") - font_size: '15sp' - halign: 'left' - Widget: - size_hint_y: None - height: dp(1 if app.app_platform == 'android' else 0) - BoxLayout: - size_hint_y: None - height: 50 - orientation: 'horizontal' - MDLabel: - id: sd_label - font_style: 'Body2' - theme_text_color: 'Primary' - text: app.tr._("[b]" + root.from_addr + "[/b]") - font_size: '15sp' - halign: 'left' - markup: True - IconRightSampleWidget: - icon: 'content-copy' - on_press: app.copy_composer_text(root.from_addr) - Widget: - id: space_1 - size_hint_y: None - height: dp(2 if app.app_platform == 'android' else 0) - BoxLayout: - id: to_addtitle - Widget: - id:space_2 - size_hint_y: None - height: dp(1 if app.app_platform == 'android' else 0) - BoxLayout: - id: to_addId - BoxLayout: - size_hint_y: None - orientation: 'vertical' - height: 50 - MDLabel: - font_style: 'Body2' - theme_text_color: 'Primary' - text: app.tr._("Date : " + root.time_tag) - font_size: '15sp' - halign: 'left' - BoxLayout: - id: sd_btn - orientation: 'vertical' - MDRaisedButton: - id: dismiss_btn - on_press: root.dismiss() - size_hint: .2, 0 - pos_hint: {'x': 0.8, 'y': 0} - MDLabel: - font_style: 'H6' - text: app.tr._('Cancel') - font_size: '13sp' - color: (1,1,1,1) - halign: 'center' - -: - orientation: 'horizontal' - MDLabel: - font_style: 'Body2' - theme_text_color: 'Primary' - text: app.tr._(root.to_addr) - font_size: '15sp' - halign: 'left' - IconRightSampleWidget: - icon: 'content-copy' - on_press: app.copy_composer_text(root.to_addr) - -: - orientation: 'vertical' - MDLabel: - id: to_add_label - font_style: 'Subtitle2' - theme_text_color: 'Primary' - text: "To :" - font_size: '15sp' - halign: 'left' \ No newline at end of file diff --git a/src/bitmessagekivy/kv/qrcode.kv b/src/bitmessagekivy/kv/qrcode.kv deleted file mode 100644 index cadaa996..00000000 --- a/src/bitmessagekivy/kv/qrcode.kv +++ /dev/null @@ -1,33 +0,0 @@ -: - 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 deleted file mode 100644 index dbcff5a1..00000000 --- a/src/bitmessagekivy/kv/scan_screen.kv +++ /dev/null @@ -1,2 +0,0 @@ -: - name:'scanscreen' \ No newline at end of file diff --git a/src/bitmessagekivy/kv/scanner.kv b/src/bitmessagekivy/kv/scanner.kv deleted file mode 100644 index 1c56f6c2..00000000 --- a/src/bitmessagekivy/kv/scanner.kv +++ /dev/null @@ -1,37 +0,0 @@ -#:import ZBarSymbol pyzbar.pyzbar.ZBarSymbol - -BoxLayout: - orientation: 'vertical' - ZBarCam: - id: zbarcam - # optional, by default checks all types - code_types: ZBarSymbol.QRCODE, ZBarSymbol.EAN13 - scan_callback: app._after_scan - scanner_line_y_initial: self.size[1]/2 +self.qrwidth/2 - scanner_line_y_final: self.size[1]/2-self.qrwidth/2 - - canvas: - Color: - rgba: 0,0,0,.25 - - #left rect - Rectangle: - pos: self.pos[0], self.pos[1] - size: self.size[0]/2-self.qrwidth/2, self.size[1] - - #right rect - Rectangle: - pos: self.size[0]/2+self.qrwidth/2, 0 - size: self.size[0]/2-self.qrwidth/2, self.size[1] - - #top rect - Rectangle: - pos: self.size[0]/2-self.qrwidth/2, self.size[1]/2+self.qrwidth/2 - size: self.qrwidth, self.size[1]/2-self.qrwidth/2 - - #bottom rect - Rectangle: - pos: self.size[0]/2-self.qrwidth/2, 0 - size: self.qrwidth, self.size[1]/2-self.qrwidth/2 - - \ No newline at end of file diff --git a/src/bitmessagekivy/kv/sent.kv b/src/bitmessagekivy/kv/sent.kv deleted file mode 100644 index 11477ed6..00000000 --- a/src/bitmessagekivy/kv/sent.kv +++ /dev/null @@ -1,26 +0,0 @@ -: - 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 deleted file mode 100644 index f5796060..00000000 --- a/src/bitmessagekivy/kv/settings.kv +++ /dev/null @@ -1,948 +0,0 @@ -: - 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 deleted file mode 100644 index 97bcf7d7..00000000 --- a/src/bitmessagekivy/kv/trash.kv +++ /dev/null @@ -1,25 +0,0 @@ -: - 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 deleted file mode 100644 index b4132927..00000000 --- a/src/bitmessagekivy/load_kivy_screens_data.py +++ /dev/null @@ -1,25 +0,0 @@ -""" - 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 index 74723e32..ea8936c5 100644 --- a/src/bitmessagekivy/main.kv +++ b/src/bitmessagekivy/main.kv @@ -1,388 +1,354 @@ -#:import get_color_from_hex kivy.utils.get_color_from_hex -#:import Factory kivy.factory.Factory -#:import Spinner kivy.uix.spinner.Spinner +#:import la kivy.adapters.listadapter +#:import factory kivy.factory +#:import mpybit bitmessagekivy.mpybit +#:import C kivy.utils.get_color_from_hex -#:import colors kivymd.color_definitions.colors -#:import images_path kivymd.images_path +: + id: nav_drawer + NavigationDrawerIconButton: + Spinner: + pos_hint:{"x":0,"y":.3} + id: btn + background_color: app.theme_cls.primary_dark + text: app.showmeaddresses(name='text') + values: app.showmeaddresses(name='values') + on_text:app.getCurrentAccountData(self.text) -#:import IconLeftWidget kivymd.uix.list.IconLeftWidget -#:import MDCard kivymd.uix.card.MDCard -#:import MDCheckbox kivymd.uix.selectioncontrol.MDCheckbox -#:import MDFloatingActionButton kivymd.uix.button.MDFloatingActionButton -#:import MDList kivymd.uix.list.MDList -#:import MDScrollViewRefreshLayout kivymd.uix.refreshlayout.MDScrollViewRefreshLayout -#:import MDSpinner kivymd.uix.spinner.MDSpinner -#:import MDTextField kivymd.uix.textfield.MDTextField -#:import MDTabs kivymd.uix.tab.MDTabs -#:import MDTabsBase kivymd.uix.tab.MDTabsBase -#:import OneLineListItem kivymd.uix.list.OneLineListItem - - - -#:set color_button (0.784, 0.443, 0.216, 1) # brown -#:set color_button_pressed (0.659, 0.522, 0.431, 1) # darker brown -#:set color_font (0.957, 0.890, 0.843, 1) # off white - -: - font_size: '12.5sp' - background_normal: 'atlas://data/images/defaulttheme/textinput_active' - background_color: app.theme_cls.primary_color - color: color_font - - - on_press: root.currentlyActive() - active_color: root.theme_cls.primary_color if root.active else root.theme_cls.text_color - - IconLeftWidget: - icon: root.icon - theme_text_color: "Custom" - text_color: root.active_color - - BadgeText: - id: badge_txt - text: f"{root.badge_text}" - theme_text_color: "Custom" - halign: 'right' - -: - canvas: - Color: - rgba: self.theme_cls.divider_color - Line: - points: root.x, root.y + dp(8), root.x + self.width, root.y + dp(8) - - - - BoxLayout: - orientation: 'vertical' - - FloatLayout: - size_hint_y: None - height: "200dp" - - MDIconButton: - id: reset_image - icon: "refresh" - x: root.parent.x + dp(10) - pos_hint: {"top": 1, 'left': 1} - color: [1,0,0,1] - on_release: app.rest_default_avatar_img() - theme_text_color: "Custom" - text_color: app.theme_cls.primary_color - opacity: 1 if app.have_any_address() else 0 - disabled: False if app.have_any_address() else True - - MDIconButton: - id: file_manager - icon: "file-image" - x: root.parent.x + dp(10) - pos_hint: {"top": 1, 'right': 1} - color: [1,0,0,1] - on_release: app.file_manager_open() - theme_text_color: "Custom" - text_color: app.theme_cls.primary_color - opacity: 1 if app.have_any_address() else 0 - disabled: False if app.have_any_address() else True - - BoxLayout: - id: top_box - size_hint_y: None - height: "200dp" - x: root.parent.x - pos_hint: {"top": 1} - Image: - source: app.get_default_logo(self) - - ScrollView: - id: scroll_y - pos_hint: {"top": 1} - - GridLayout: - id: box_item - cols: 1 - size_hint_y: None - height: self.minimum_height - NavigationDrawerDivider: - NavigationDrawerSubheader: - text: app.tr._('Accounts') - height:"35dp" - NavigationItem: - text: 'dropdown_nav_item' - height: dp(48) - IdentitySpinner: - id: identity_dropdown - pos_hint:{"x":0,"y":0} - name: "identity_dropdown" - option_cls: Factory.get("MySpinnerOption") - font_size: '12.5sp' - text: app.getDefaultAccData(self) - color: color_font - background_normal: '' - background_color: app.theme_cls.primary_color - on_text: app.getCurrentAccountData(self.text) - ArrowImg: - NavigationItem: - id: inbox_cnt - text: app.tr._('Inbox') - icon: 'email-open' - divider: None - on_release: app.set_screen('inbox') - on_release: root.parent.set_state() - on_press: app.load_screen(self) - NavigationItem: - id: send_cnt - text: app.tr._('Sent') - icon: 'send' - divider: None - on_release: app.set_screen('sent') - on_release: root.parent.set_state() - NavigationItem: - id: draft_cnt - text: app.tr._('Draft') - icon: 'message-draw' - divider: None - on_release: app.root.ids.scr_mngr.current = 'draft' - on_release: root.parent.set_state() - NavigationItem: - id: trash_cnt - text: app.tr._('Trash') - icon: 'delete' - divider: None - on_release: app.root.ids.scr_mngr.current = 'trash' - on_press: root.parent.set_state() - on_press: app.load_screen(self) - NavigationItem: - id: allmail_cnt - text: app.tr._('All Mails') - icon: 'mailbox' - divider: None - on_release: app.root.ids.scr_mngr.current = 'allmails' - on_release: root.parent.set_state() - on_press: app.load_screen(self) - NavigationDrawerDivider: - NavigationDrawerSubheader: - text: app.tr._('Chat') - NavigationItem: - id: draft_cnt - text: app.tr._('Chat') - icon: 'chat' - divider: None - on_release: app.root.ids.scr_mngr.current = 'chat' - on_release: root.parent.set_state() - NavigationDrawerDivider: - NavigationDrawerSubheader: - text: app.tr._("All labels") - NavigationItem: - text: app.tr._('Address Book') - icon: 'book-multiple' - divider: None - on_release: app.root.ids.scr_mngr.current = 'addressbook' - on_release: root.parent.set_state() - NavigationItem: - text: app.tr._('Settings') - icon: 'application-settings' - divider: None - on_release: app.root.ids.scr_mngr.current = 'set' - on_release: root.parent.set_state() - NavigationItem: - text: app.tr._('Payment plan') - icon: 'shopping' - divider: None - on_release: app.root.ids.scr_mngr.current = 'payment' - on_release: root.parent.set_state() - NavigationItem: - text: app.tr._('New address') - icon: 'account-plus' - divider: None - on_release: app.root.ids.scr_mngr.current = 'login' - on_release: root.parent.set_state() - on_press: app.reset_login_screen() - NavigationItem: - text: app.tr._('Network status') - icon: 'server-network' - divider: None - on_release: app.root.ids.scr_mngr.current = 'networkstat' - on_release: root.parent.set_state() - NavigationItem: - text: app.tr._('My addresses') - icon: 'account-multiple' - divider: None - on_release: app.root.ids.scr_mngr.current = 'myaddress' - on_release: root.parent.set_state() - -MDNavigationLayout: - id: nav_layout - - MDTopAppBar: + NavigationDrawerIconButton: + icon: 'email-open' + text: "inbox" + on_release: app.root.ids.scr_mngr.current = 'inbox' + NavigationDrawerIconButton: + icon: 'mail-send' + text: "sent" + on_release: app.root.ids.scr_mngr.current = 'sent' + NavigationDrawerIconButton: + icon: 'dropbox' + text: "trash" + on_release: app.root.ids.scr_mngr.current = 'trash' + NavigationDrawerIconButton: + icon: 'email' + text: "drafts" + on_release: app.root.ids.scr_mngr.current = 'dialog' + NavigationDrawerIconButton: + icon: 'markunread-mailbox' + text: "test" + on_release: app.root.ids.scr_mngr.current = 'test' + NavigationDrawerIconButton: + text: "new identity" + icon:'accounts-add' + on_release: app.root.ids.scr_mngr.current = 'newidentity' + +BoxLayout: + orientation: 'vertical' + Toolbar: 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()]] + title: app.getCurrentAccount() + background_color: app.theme_cls.primary_dark + left_action_items: [['menu', lambda x: app.nav_drawer.toggle()]] + Button: + text:"EXIT" + color: 0,0,0,1 + background_color: (0,0,0,0) + size_hint_y: 0.4 + size_hint_x: 0.1 + pos_hint: {'x': 0.8, 'y':0.4} + on_press: app.say_exit() + 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 + id:sc1 Sent: - id:id_sent + id:sc2 Trash: - id:id_trash - AllMails: - id:id_allmail - Draft: - id:id_draft - AddressBook: - id:id_addressbook - ShowQRCode: - id:id_showqrcode - Chat: - id: id_chat + id:sc3 + Dialog: + id:sc4 + Test: + id:sc5 + Create: + id:sc6 + NewIdentity: + id:sc7 + Page: + id:sc8 + AddressSuccessful: + id:sc9 - MDNavigationDrawer: - id: nav_drawer + Button: + id:create + height:100 + size_hint_y: 0.13 + size_hint_x: 0.1 + pos_hint: {'x': 0.85, 'y': 0.5} + background_color: (0,0,0,0) + on_press: scr_mngr.current = 'create' + Image: + source: 'images/plus.png' + y: self.parent.y - 7.5 + x: self.parent.x + self.parent.width - 50 + size: 70, 70 - 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 - - -: +: + text: '' size_hint_y: None - height: self.minimum_height + height: 48 + ignore_perpendicular_swipes: True + data_index: 0 + min_move: 20 / self.width - MDIconButton: - icon: 'magnify' + on__offset: app.update_index(root.data_index, self.index) - MDTextField: - id: search_field - hint_text: 'Search' - canvas.before: - Color: - rgba: (0,0,0,1) + canvas.before: + Color: + rgba: C('FFFFFF33') + Rectangle: + pos: self.pos + size: self.size -: - id: spinner - size_hint: None, None - size: dp(46), dp(46) - pos_hint: {'center_x': 0.5, 'center_y': 0.5} - active: False + Line: + rectangle: self.pos + self.size -: - size_hint_y: None - height: dp(56) - spacing: '10dp' - pos_hint: {'center_x':0.45, 'center_y': .1} + Button: + text: 'delete ({}:{})'.format(root.text, root.data_index) + on_press: app.delete(root.data_index) - Widget: + Button: + text: root.text + on_press: app.getInboxMessageDetail(self.text) - 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() + Button: + text: 'archive' + on_press: app.archive(root.data_index) +: + name: 'inbox' + RecycleView: + data: root.data + viewclass: 'SwipeButton' + do_scroll_x: False + scroll_timeout: 100 -: - size_hint_y: None - height: content.height + RecycleBoxLayout: + id:rc + orientation: 'vertical' + size_hint_y: None + height: self.minimum_height + default_size_hint: 1, None + canvas.before: + Color: + rgba: 0,0,0, 1 + Rectangle: + pos: self.pos + size: self.size - MDCardSwipeLayerBox: - padding: "8dp" +: + name: 'sent' + RecycleView: + data: root.data + viewclass: 'SwipeButton' + do_scroll_x: False + scroll_timeout: 100 - MDIconButton: - id: delete_msg - icon: "trash-can" - pos_hint: {"center_y": .5} - md_bg_color: (1, 0, 0, 1) - disabled: True + RecycleBoxLayout: + id:rc + orientation: 'vertical' + size_hint_y: None + height: self.minimum_height + default_size_hint: 1, None + canvas.before: + Color: + rgba: 0,0,0, 1 + Rectangle: + pos: self.pos + size: self.size - MDCardSwipeFrontBox: +: + name: 'trash' + RecycleView: + data: root.data + viewclass: 'SwipeButton' + do_scroll_x: False + scroll_timeout: 100 - TwoLineAvatarIconListItem: - id: content - text: root.text - _no_ripple_effect: True + RecycleBoxLayout: + id:rc + orientation: 'vertical' + size_hint_y: None + height: self.minimum_height + default_size_hint: 1, None + canvas.before: + Color: + rgba: 0,0,0, 1 + Rectangle: + pos: self.pos + size: self.size - AvatarSampleWidget: - id: avater_img - source: None +: + name: 'dialog' + Label: + text:"I have a good dialox box" + color: 0,0,0,1 +: + name: 'test' + Label: + text:"I am in test" + color: 0,0,0,1 - TimeTagRightSampleWidget: - id: time_tag - text: '' - font_size: "11sp" - font_style: "Caption" - size: [120, 140] if app.app_platform == "android" else [64, 80] +: + name: 'create' + GridLayout: + rows: 5 + cols: 1 + padding: 60,60,60,60 + spacing: 50 + BoxLayout: + size_hint_y: None + height: '32dp' + Label: + text: 'FROM' + color: 0,0,0,1 + Spinner: + size_hint: 1,1 + pos_hint: {"x":0,"top":1.} + pos: 10,10 + id: spinner_id + text: app.showmeaddresses(name='text') + values: app.showmeaddresses(name='values') + BoxLayout: + size_hint_y: None + height: '32dp' + Label: + text: 'TO' + color: 0,0,0,1 + TextInput: + id: recipent + hint_text: 'To' -: - size_hint_y: None - height: content.height + BoxLayout: + size_hint_y: None + height: '32dp' + Label: + text: 'SUBJECT' + color: 0,0,0,1 + TextInput: + id: subject + hint_text: 'SUBJECT' - MDCardSwipeLayerBox: - padding: "8dp" + BoxLayout: + size_hint_y: None + height: '32dp' + Label: + text: 'BODY' + color: 0,0,0,1 + TextInput: + id: message + multiline:True + size_hint: 1,2 - MDIconButton: - id: delete_msg - icon: "trash-can" - pos_hint: {"center_y": .5} - md_bg_color: (1, 0, 0, 1) - disabled: True + Button: + text: 'send' + size_hint_y: 0.1 + size_hint_x: 0.2 + height: '32dp' + pos_hint: {'x': .5, 'y': 0.1} + on_press: root.send() + Button: + text: 'cancel' + size_hint_y: 0.1 + size_hint_x: 0.2 + height: '32dp' + pos_hint: {'x': .72, 'y': 0.1} + on_press: root.cancel() - MDCardSwipeFrontBox: +: + name: 'newidentity' + GridLayout: + padding: '120dp' + cols: 1 + Label: + text:"""Here you may generate as many addresses as you like. Indeed, creating and abandoning addresses is encouraged.""" + line_height:1.5 + text_size:(700,None) + color: 0,0,0,1 + BoxLayout: + CheckBox: + canvas.before: + Color: + rgb: 1,0,0 + Ellipse: + pos:self.center_x-8, self.center_y-8 + size:[16,16] + group: "money" + id:chk + text:"use a random number generator to make an address" + on_active: + root.checked = self.text + active:root.is_active - TwoLineAvatarIconListItem: - id: content - text: root.text - _no_ripple_effect: True + Label: + text: "use a random number generator to make an address" + color: 0,0,0,1 + BoxLayout: + CheckBox: + canvas.before: + Color: + rgb: 1,0,0 + Ellipse: + pos:self.center_x-8, self.center_y-8 + size:[16,16] + group: "money" + id:chk + text:"use a pseudo number generator to make an address" + on_active: + root.checked = self.text + active:not root.is_active + Label: + text: "use a pseudo number generator to make an address" + color: 0,0,0,1 + Label: + color: 0,0,0,1 + size_hint_x: .35 + markup: True + text: "[b]{}[/b]".format("Randomly generated addresses") + BoxLayout: + size_hint_y: None + height: '32dp' + Label: + text: "Label (not shown to anyone except you)" + color: 0,0,0,1 + BoxLayout: + size_hint_y: None + height: '32dp' + TextInput: + id: label - AvatarSampleWidget: - id: avater_img - source: None + Button: + text: 'Cancel' + size_hint_y: 0.1 + size_hint_x: 0.3 + height: '32dp' + pos_hint: {'x': .1, 'y': 0.1} + Button: + text: 'Ok' + size_hint_y: 0.1 + size_hint_x: 0.3 + height: '32dp' + pos_hint: {'x': .5, 'y': 0.1} + on_press: root.generateaddress() - 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] +: + name: 'page' + Label: + text: 'I am on description of my email yooooo' + color: 0,0,0,1 + +: + name: 'add_sucess' + Label: + text: 'Successfully created a new bit address' + color: 0,0,0,1 diff --git a/src/bitmessagekivy/mpybit.py b/src/bitmessagekivy/mpybit.py index 8c3ab4ab..3f9b198b 100644 --- a/src/bitmessagekivy/mpybit.py +++ b/src/bitmessagekivy/mpybit.py @@ -1,488 +1,392 @@ -# pylint: disable=too-many-public-methods, unused-variable, too-many-ancestors -# pylint: disable=no-name-in-module, too-few-public-methods, unused-argument -# pylint: disable=attribute-defined-outside-init, too-many-instance-attributes - -""" -Bitmessage android(mobile) interface -""" - +import kivy_helper_search import os -import logging -import sys -from functools import partial -from PIL import Image as PilImage +import queues +import shutdown +import state +import time -from kivy.clock import Clock +from kivy.app import App from kivy.lang import Builder -from kivy.core.window import Window -from kivy.uix.boxlayout import BoxLayout +from kivy.properties import BooleanProperty +from kivy.clock import Clock +from navigationdrawer import NavigationDrawer +from kivy.properties import ObjectProperty, StringProperty, ListProperty +from kivy.uix.screenmanager import Screen +from kivy.uix.textinput import TextInput +from kivymd.theming import ThemeManager +from kivymd.toolbar import Toolbar +from bmconfigparser import BMConfigParser +from helper_ackPayload import genAckPayload +from addresses import decodeAddress, addBMIfNotPresent +from helper_sql import sqlExecute -from kivymd.app import MDApp -from kivymd.uix.label import MDLabel -from kivymd.uix.dialog import MDDialog -from kivymd.uix.list import ( - IRightBodyTouch -) -from kivymd.uix.button import MDRaisedButton -from kivymd.uix.bottomsheet import MDCustomBottomSheet -from kivymd.uix.filemanager import MDFileManager - -from pybitmessage.bitmessagekivy.kivy_state import KivyStateVariables -from pybitmessage.bitmessagekivy.base_navigation import ( - BaseLanguage, BaseNavigationItem, BaseNavigationDrawerDivider, - BaseNavigationDrawerSubheader, BaseContentNavigationDrawer, - BaseIdentitySpinner -) -from pybitmessage.bmconfigparser import config # noqa: F401 -from pybitmessage.bitmessagekivy import identiconGeneration -from pybitmessage.bitmessagekivy.get_platform import platform -from pybitmessage.bitmessagekivy.baseclass.common import toast, load_image_path, get_identity_list -from pybitmessage.bitmessagekivy.load_kivy_screens_data import load_screen_json - -from pybitmessage.bitmessagekivy.baseclass.popup import ( - AddAddressPopup, AppClosingPopup, AddressChangingLoader -) -from pybitmessage.bitmessagekivy.baseclass.login import * # noqa: F401, F403 -from pybitmessage.bitmessagekivy.uikivysignaler import UIkivySignaler - -from pybitmessage.mockbm.helper_startup import loadConfig, total_encrypted_messages_per_month - -logger = logging.getLogger('default') +statusIconColor = 'red' -class Lang(BaseLanguage): - """UI Language""" +class NavigateApp(App, TextInput): + """Application uses kivy in which base Class of Navigate App inherits from the App class.""" - -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 + theme_cls = ThemeManager() + nav_drawer = ObjectProperty() 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')) + """Return a main_widget as a root widget. - 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 = .85 if platform == 'android' else .8 - self.add_popup = MDDialog( - title='Add contact', - type="custom", - size_hint=(width, .23), - content_cls=AddAddressPopup(), - buttons=[ - MDRaisedButton( - text="Save", - on_release=self.savecontact, - ), - MDRaisedButton( - text="Cancel", - on_release=self.close_pop, - ), - MDRaisedButton( - text="Scan QR code", - on_release=self.scan_qr_code, - ), - ], - ) - self.add_popup.auto_dismiss = False - self.add_popup.open() - - def scan_qr_code(self, instance): - """this method is used for showing QR code scanner""" - if self.is_camara_attached(): - self.add_popup.dismiss() - self.root.ids.id_scanscreen.get_screen(self.root.ids.scr_mngr.current, self.add_popup) - self.root.ids.scr_mngr.current = 'scanscreen' - else: - alert_text = ( - 'Currently this feature is not avaialbe!' if platform == 'android' else 'Camera is not available!') - self.add_popup.dismiss() - toast(alert_text) - - def is_camara_attached(self): - """This method is for checking the camera is available or not""" - self.root.ids.id_scanscreen.check_camera() - is_available = self.root.ids.id_scanscreen.camera_available - return is_available - - def savecontact(self, instance): - """Method is used for saving contacts""" - popup_obj = self.add_popup.content_cls - label = popup_obj.ids.label.text.strip() - address = popup_obj.ids.address.text.strip() - popup_obj.ids.label.focus = not label - # default focus on address field - popup_obj.ids.address.focus = label or not address - - def close_pop(self, instance): - """Close the popup""" - self.add_popup.dismiss() - toast('Canceled') - - def loadMyAddressScreen(self, action): - """loadMyAddressScreen method spin the loader""" - if len(self.root.ids.id_myaddress.children) <= 2: - self.root.ids.id_myaddress.children[0].active = action - else: - self.root.ids.id_myaddress.children[1].active = action - - def load_screen(self, instance): - """This method is used for loading screen on every click""" - if instance.text == 'Inbox': - self.root.ids.scr_mngr.current = 'inbox' - self.root.ids.id_inbox.children[1].active = True - elif instance.text == 'Trash': - self.root.ids.scr_mngr.current = 'trash' - try: - self.root.ids.id_trash.children[1].active = True - except Exception as e: - self.root.ids.id_trash.children[0].children[1].active = True - Clock.schedule_once(partial(self.load_screen_callback, instance), 1) - - def load_screen_callback(self, instance, dt=0): - """This method is rotating loader for few seconds""" - if instance.text == 'Inbox': - self.root.ids.id_inbox.ids.ml.clear_widgets() - self.root.ids.id_inbox.loadMessagelist(self.kivy_state_obj.selected_address) - self.root.ids.id_inbox.children[1].active = False - elif instance.text == 'Trash': - self.root.ids.id_trash.clear_widgets() - self.root.ids.id_trash.add_widget(self.data_screen_dict['Trash'].Trash()) - try: - self.root.ids.id_trash.children[1].active = False - except Exception as e: - self.root.ids.id_trash.children[0].children[1].active = False - - @staticmethod - def get_enabled_addresses(): - """Getting list of all the enabled addresses""" - addresses = [addr for addr in config.addresses() - if config.getboolean(str(addr), 'enabled')] - return addresses - - @staticmethod - def format_address(address): - """Formatting address""" - return " ({0})".format(address) - - @staticmethod - def format_label(label): - """Formatting label""" - if label: - f_name = label.split() - truncate_string = '...' - f_name_max_length = 15 - formatted_label = f_name[0][:14].capitalize() + truncate_string if len( - f_name[0]) > f_name_max_length else f_name[0].capitalize() - return formatted_label - return '' - - @staticmethod - def format_address_and_label(address=None): - """Getting formatted address information""" - if not address: - try: - address = NavigateApp.get_enabled_addresses()[0] - except IndexError: - return '' - return "{0}{1}".format( - NavigateApp.format_label(config.get(address, "label")), - NavigateApp.format_address(address) - ) - - def getDefaultAccData(self, instance): - """Getting Default Account Data""" - if self.identity_list: - self.kivy_state_obj.selected_address = first_addr = self.identity_list[0] - return first_addr - return 'Select Address' + An application can be built if you return a widget on build(), or if you set + self.root. + """ + main_widget = Builder.load_file( + os.path.join(os.path.dirname(__file__), 'main.kv')) + self.nav_drawer = Navigator() + return main_widget def getCurrentAccountData(self, text): - """Get Current Address Account Data""" - if text != '': - if os.path.exists(os.path.join( - self.image_dir, 'default_identicon', '{}.png'.format(text)) - ): - self.load_selected_Image(text) - else: - self.set_identicon(text) - self.root.ids.content_drawer.ids.reset_image.opacity = 0 - self.root.ids.content_drawer.ids.reset_image.disabled = True - address_label = self.format_address_and_label(text) - self.root_window.children[1].ids.toolbar.title = address_label - self.kivy_state_obj.selected_address = text - AddressChangingLoader().open() - for nav_obj in self.root.ids.content_drawer.children[ - 0].children[0].children[0].children: - nav_obj.active = True if nav_obj.text == 'Inbox' else False - self.fileManagerSetting() - Clock.schedule_once(self.setCurrentAccountData, 0.5) + """Get Current Address Account Data.""" + state.association = text + self.root.ids.sc1.clear_widgets() + self.root.ids.sc2.clear_widgets() + self.root.ids.sc3.clear_widgets() + self.root.ids.sc1.add_widget(Inbox()) + self.root.ids.sc2.add_widget(Sent()) + self.root.ids.sc3.add_widget(Trash()) + self.root.ids.toolbar.title = BMConfigParser().get( + state.association, 'label') + '({})'.format(state.association) + Inbox() + Sent() + Trash() - def setCurrentAccountData(self, dt=0): - """This method set the current accout data on all the screens""" - self.root.ids.id_inbox.ids.ml.clear_widgets() - self.root.ids.id_inbox.loadMessagelist(self.kivy_state_obj.selected_address) - - self.root.ids.id_sent.ids.ml.clear_widgets() - self.root.ids.id_sent.children[2].children[2].ids.search_field.text = '' - self.root.ids.id_sent.loadSent(self.kivy_state_obj.selected_address) - - def fileManagerSetting(self): - """This method is for file manager setting""" - if not self.root.ids.content_drawer.ids.file_manager.opacity and \ - self.root.ids.content_drawer.ids.file_manager.disabled: - self.root.ids.content_drawer.ids.file_manager.opacity = 1 - self.root.ids.content_drawer.ids.file_manager.disabled = False - - def on_request_close(self, *args): # pylint: disable=no-self-use - """This method is for app closing request""" - AppClosingPopup().open() - return True - - def clear_composer(self): - """If slow down, the new composer edit screen""" - self.set_navbar_for_composer() - composer_obj = self.root.ids.id_create.children[1].ids - composer_obj.ti.text = '' - composer_obj.composer_dropdown.text = 'Select' - composer_obj.txt_input.text = '' - composer_obj.subject.text = '' - composer_obj.body.text = '' - self.kivy_state_obj.in_composer = True - self.kivy_state_obj = False - - def set_navbar_for_composer(self): - """Clearing toolbar data when composer open""" - self.root.ids.toolbar.left_action_items = [ - ['arrow-left', lambda x: self.back_press()]] - self.root.ids.toolbar.right_action_items = [ - ['refresh', - lambda x: self.root.ids.id_create.children[1].reset_composer()], - ['send', - lambda x: self.root.ids.id_create.children[1].send(self)]] - - def set_identicon(self, text): - """Show identicon in address spinner""" - img = identiconGeneration.generate(text) - self.root.ids.content_drawer.ids.top_box.children[0].texture = (img.texture) - - # pylint: disable=import-outside-toplevel - def file_manager_open(self): - """This method open the file manager of local system""" - if not self.kivy_state_obj.file_manager: - self.file_manager = MDFileManager( - exit_manager=self.exit_manager, - select_path=self.select_path, - ext=['.png', '.jpg'] - ) - self.file_manager.previous = False - self.file_manager.current_path = '/' - if platform == 'android': - # pylint: disable=import-error - from android.permissions import request_permissions, Permission, check_permission - if check_permission(Permission.WRITE_EXTERNAL_STORAGE) and \ - check_permission(Permission.READ_EXTERNAL_STORAGE): - self.file_manager.show(os.getenv('EXTERNAL_STORAGE')) - self.kivy_state_obj.manager_open = True - else: - request_permissions([Permission.WRITE_EXTERNAL_STORAGE, Permission.READ_EXTERNAL_STORAGE]) - else: - self.file_manager.show(os.environ["HOME"]) - self.kivy_state_obj.manager_open = True - - def select_path(self, path): - """This method is used to set the select image""" - try: - newImg = PilImage.open(path).resize((300, 300)) - if platform == 'android': - android_path = os.path.join( - os.path.join(os.environ['ANDROID_PRIVATE'], 'app', 'images', 'kivy') - ) - if not os.path.exists(os.path.join(android_path, 'default_identicon')): - os.makedirs(os.path.join(android_path, 'default_identicon')) - newImg.save(os.path.join(android_path, 'default_identicon', '{}.png'.format( - self.kivy_state_obj.selected_address)) - ) - else: - if not os.path.exists(os.path.join(self.image_dir, 'default_identicon')): - os.makedirs(os.path.join(self.image_dir, 'default_identicon')) - newImg.save(os.path.join(self.image_dir, 'default_identicon', '{0}.png'.format( - self.kivy_state_obj.selected_address)) - ) - self.load_selected_Image(self.kivy_state_obj.selected_address) - toast('Image changed') - except Exception: - toast('Exit') - self.exit_manager() - - def exit_manager(self, *args): - """Called when the user reaches the root of the directory tree.""" - self.kivy_state_obj.manager_open = False - self.file_manager.close() - - def load_selected_Image(self, curerentAddr): - """This method load the selected image on screen""" - top_box_obj = self.root.ids.content_drawer.ids.top_box.children[0] - top_box_obj.source = os.path.join(self.image_dir, 'default_identicon', '{0}.png'.format(curerentAddr)) - self.root.ids.content_drawer.ids.reset_image.opacity = 1 - self.root.ids.content_drawer.ids.reset_image.disabled = False - top_box_obj.reload() - - def rest_default_avatar_img(self): - """set default avatar generated image""" - self.set_identicon(self.kivy_state_obj.selected_address) - img_path = os.path.join( - self.image_dir, 'default_identicon', '{}.png'.format(self.kivy_state_obj.selected_address) - ) - if os.path.exists(img_path): - os.remove(img_path) - self.root.ids.content_drawer.ids.reset_image.opacity = 0 - self.root.ids.content_drawer.ids.reset_image.disabled = True - toast('Avatar reset') - - def get_default_logo(self, instance): - """Getting default logo image""" - if self.identity_list: - first_addr = self.identity_list[0] - if config.getboolean(str(first_addr), 'enabled'): - if os.path.exists( - os.path.join( - self.image_dir, 'default_identicon', '{}.png'.format(first_addr) - ) - ): - return os.path.join( - self.image_dir, 'default_identicon', '{}.png'.format(first_addr) - ) - else: - img = identiconGeneration.generate(first_addr) - instance.texture = img.texture - return - return os.path.join(self.image_dir, 'drawer_logo1.png') + def say_exit(self): + """Exit the application as uses shutdown PyBitmessage.""" + print("**************************EXITING FROM APPLICATION*****************************") + App.get_running_app().stop() + shutdown.doCleanShutdown() @staticmethod - def have_any_address(): - """Checking existance of any address""" - if config.addresses(): - return True - return False + def showmeaddresses(name="text"): + """Show the addresses in spinner to make as dropdown.""" + if name == "text": + return BMConfigParser().addresses()[0] + elif name == "values": + return BMConfigParser().addresses() - 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 update_index(self, data_index, index): + """Update index after archieve message to trash.""" + if self.root.ids.scr_mngr.current == 'inbox': + self.root.ids.sc1.data[data_index]['index'] = index + elif self.root.ids.scr_mngr.current == 'sent': + self.root.ids.sc2.data[data_index]['index'] = index + elif self.root.ids.scr_mngr.current == 'trash': + self.root.ids.sc3.data[data_index]['index'] = index - 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 delete(self, data_index): + """It will make delete using remove function.""" + print("delete {}".format(data_index)) + self._remove(data_index) - 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 archive(self, data_index): + """It will make archieve using remove function.""" + print("archive {}".format(data_index)) + self._remove(data_index) - def set_toolbar_for_QrCode(self): - """This method is use for setting Qr code toolbar.""" - self.root.ids.toolbar.left_action_items = [ - ['arrow-left', lambda x: self.back_press()]] - self.root.ids.toolbar.right_action_items = [] + def _remove(self, data_index): + """It will remove message by resetting the values in recycleview data.""" + if self.root.ids.scr_mngr.current == 'inbox': + self.root.ids.sc1.data.pop(data_index) + self.root.ids.sc1.data = [{ + 'data_index': i, + 'index': d['index'], + 'height': d['height'], + 'text': d['text']} + for i, d in enumerate(self.root.ids.sc1.data) + ] + elif self.root.ids.scr_mngr.current == 'sent': + self.root.ids.sc2.data.pop(data_index) + self.root.ids.sc2.data = [{ + 'data_index': i, + 'index': d['index'], + 'height': d['height'], + 'text': d['text']} + for i, d in enumerate(self.root.ids.sc2.data) + ] + elif self.root.ids.scr_mngr.current == 'trash': + self.root.ids.sc3.data.pop(data_index) + self.root.ids.sc3.data = [{ + 'data_index': i, + 'index': d['index'], + 'height': d['height'], + 'text': d['text']} + for i, d in enumerate(self.root.ids.sc3.data) + ] - 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 getInboxMessageDetail(self, instance): + """It will get message detail after make selected message description.""" + try: + self.root.ids.scr_mngr.current = 'page' + except AttributeError: + self.parent.manager.current = 'page' + print('Message Clicked {}'.format(instance)) - 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) + @staticmethod + def getCurrentAccount(): + """It uses to get current account label.""" + return BMConfigParser().get(state.association, 'label') + '({})'.format(state.association) -class PaymentMethodLayout(BoxLayout): - """PaymentMethodLayout class for kivy Ui""" +class Navigator(NavigationDrawer): + """Navigator class uses NavigationDrawer. + + It is an UI panel that shows our app's main navigation menu + It is hidden when not in use, but appears when the user swipes + a finger from the left edge of the screen or, when at the top + level of the app, the user touches the drawer icon in the app bar + """ + + image_source = StringProperty('images/qidenticon_two.png') + title = StringProperty('Navigation') + + +class Inbox(Screen): + """Inbox Screen uses screen to show widgets of screens.""" + + data = ListProperty() + + def __init__(self, *args, **kwargs): + super(Inbox, self).__init__(*args, **kwargs) + if state.association == '': + state.association = Navigator().ids.btn.text + Clock.schedule_once(self.init_ui, 0) + + def init_ui(self, dt=0): + """Clock Schdule for method inbox accounts.""" + self.inboxaccounts() + print(dt) + + def inboxaccounts(self): + """Load inbox accounts.""" + account = state.association + self.loadMessagelist(account, 'All', '') + + def loadMessagelist(self, account, where="", what=""): + """Load Inbox list for inbox messages.""" + xAddress = "toaddress" + queryreturn = kivy_helper_search.search_sql( + xAddress, account, 'inbox', where, what, False) + if queryreturn: + self.data = [{ + 'data_index': i, + 'index': 1, + 'height': 48, + 'text': row[4]} + for i, row in enumerate(queryreturn) + ] + else: + self.data = [{ + 'data_index': 1, + 'index': 1, + 'height': 48, + 'text': "yet no message for this account!!!!!!!!!!!!!"} + ] + + +class Page(Screen): + pass + + +class AddressSuccessful(Screen): + pass + + +class Sent(Screen): + """Sent Screen uses screen to show widgets of screens.""" + + data = ListProperty() + + def __init__(self, *args, **kwargs): + super(Sent, self).__init__(*args, **kwargs) + if state.association == '': + state.association = Navigator().ids.btn.text + Clock.schedule_once(self.init_ui, 0) + + def init_ui(self, dt=0): + """Clock Schdule for method sent accounts.""" + self.sentaccounts() + print(dt) + + def sentaccounts(self): + """Load sent accounts.""" + account = state.association + self.loadSent(account, 'All', '') + + def loadSent(self, account, where="", what=""): + """Load Sent list for Sent messages.""" + xAddress = 'fromaddress' + queryreturn = kivy_helper_search.search_sql( + xAddress, account, "sent", where, what, False) + if queryreturn: + self.data = [{ + 'data_index': i, + 'index': 1, + 'height': 48, + 'text': row[2]} + for i, row in enumerate(queryreturn) + ] + else: + self.data = [{ + 'data_index': 1, + 'index': 1, + 'height': 48, + 'text': "yet no message for this account!!!!!!!!!!!!!"} + ] + + +class Trash(Screen): + """Trash Screen uses screen to show widgets of screens.""" + + data = ListProperty() + + def __init__(self, *args, **kwargs): + super(Trash, self).__init__(*args, **kwargs) + if state.association == '': + state.association = Navigator().ids.btn.text + Clock.schedule_once(self.init_ui, 0) + + def init_ui(self, dt=0): + """Clock Schdule for method inbox accounts.""" + self.inboxaccounts() + print(dt) + + def inboxaccounts(self): + """Load inbox accounts.""" + account = state.association + self.loadTrashlist(account, 'All', '') + + def loadTrashlist(self, account, where="", what=""): + """Load Trash list for trashed messages.""" + xAddress = "toaddress" + queryreturn = kivy_helper_search.search_sql( + xAddress, account, 'trash', where, what, False) + if queryreturn: + self.data = [{ + 'data_index': i, + 'index': 1, + 'height': 48, + 'text': row[4]} + for i, row in enumerate(queryreturn) + ] + else: + self.data = [{ + 'data_index': 1, + 'index': 1, + 'height': 48, + 'text': "yet no message for this account!!!!!!!!!!!!!"} + ] + + +class Dialog(Screen): + """Dialog Screen uses screen to show widgets of screens.""" + + pass + + +class Test(Screen): + """Test Screen uses screen to show widgets of screens.""" + + pass + + +class Create(Screen): + """Create Screen uses screen to show widgets of screens.""" + + def __init__(self, *args, **kwargs): + super(Create, self).__init__(*args, **kwargs) + + def send(self): + """Send message from one address to another.""" + fromAddress = self.ids.spinner_id.text + # For now we are using static address i.e we are not using recipent field value. + toAddress = "BM-2cWyUfBdY2FbgyuCb7abFZ49JYxSzUhNFe" + message = self.ids.message.text + subject = self.ids.subject.text + encoding = 3 + print("message: ", self.ids.message.text) + sendMessageToPeople = True + if sendMessageToPeople: + if toAddress != '': + status, addressVersionNumber, streamNumber, ripe = decodeAddress( + toAddress) + if status == 'success': + toAddress = addBMIfNotPresent(toAddress) + + if addressVersionNumber > 4 or addressVersionNumber <= 1: + print("addressVersionNumber > 4 or addressVersionNumber <= 1") + if streamNumber > 1 or streamNumber == 0: + print("streamNumber > 1 or streamNumber == 0") + if statusIconColor == 'red': + print("shared.statusIconColor == 'red'") + stealthLevel = BMConfigParser().safeGetInt( + 'bitmessagesettings', 'ackstealthlevel') + ackdata = genAckPayload(streamNumber, stealthLevel) + t = () + sqlExecute( + '''INSERT INTO sent VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''', + '', + toAddress, + ripe, + fromAddress, + subject, + message, + ackdata, + int(time.time()), + int(time.time()), + 0, + 'msgqueued', + 0, + 'sent', + encoding, + BMConfigParser().getint('bitmessagesettings', 'ttl')) + toLabel = '' + queues.workerQueue.put(('sendmessage', toAddress)) + print("sqlExecute successfully ##### ##################") + self.ids.message.text = '' + self.ids.spinner_id.text = '' + self.ids.subject.text = '' + self.ids.recipent.text = '' + return None + + +class NewIdentity(Screen): + """Create new address for PyBitmessage.""" + + is_active = BooleanProperty(False) + checked = StringProperty("") + # self.manager.parent.ids.create.children[0].source = 'images/plus-4-xxl.png' + + def generateaddress(self): + """Generate new address.""" + if self.checked == 'use a random number generator to make an address': + queues.apiAddressGeneratorReturnQueue.queue.clear() + streamNumberForAddress = 1 + label = self.ids.label.text + eighteenByteRipe = False + nonceTrialsPerByte = 1000 + payloadLengthExtraBytes = 1000 + + queues.addressGeneratorQueue.put(( + 'createRandomAddress', + 4, streamNumberForAddress, + label, 1, "", eighteenByteRipe, + nonceTrialsPerByte, + payloadLengthExtraBytes) + ) + self.manager.current = 'add_sucess' if __name__ == '__main__': diff --git a/src/bitmessagekivy/screens_data.json b/src/bitmessagekivy/screens_data.json deleted file mode 100644 index 974ef1c4..00000000 --- a/src/bitmessagekivy/screens_data.json +++ /dev/null @@ -1,83 +0,0 @@ -{ - "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 deleted file mode 100644 index e69de29b..00000000 diff --git a/src/bitmessagekivy/tests/common.py b/src/bitmessagekivy/tests/common.py deleted file mode 100644 index 553fa020..00000000 --- a/src/bitmessagekivy/tests/common.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -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 deleted file mode 100644 index 940b3e14..00000000 --- a/src/bitmessagekivy/tests/sampleData/keys.dat +++ /dev/null @@ -1,59 +0,0 @@ -[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 deleted file mode 100644 index 41ba8a28..00000000 --- a/src/bitmessagekivy/tests/sampleData/knownnodes.dat +++ /dev/null @@ -1,110 +0,0 @@ -[ - { - "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 deleted file mode 100644 index 7150b42b..00000000 Binary files a/src/bitmessagekivy/tests/sampleData/messages.dat and /dev/null differ diff --git a/src/bitmessagekivy/tests/telenium_process.py b/src/bitmessagekivy/tests/telenium_process.py deleted file mode 100644 index 209287e2..00000000 --- a/src/bitmessagekivy/tests/telenium_process.py +++ /dev/null @@ -1,126 +0,0 @@ -""" - 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 deleted file mode 100644 index e61fef2a..00000000 --- a/src/bitmessagekivy/tests/test_addressbook.py +++ /dev/null @@ -1,155 +0,0 @@ -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 deleted file mode 100644 index b08e0f29..00000000 --- a/src/bitmessagekivy/tests/test_allmail_message.py +++ /dev/null @@ -1,35 +0,0 @@ -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 deleted file mode 100644 index 98464bf3..00000000 --- a/src/bitmessagekivy/tests/test_chat_screen.py +++ /dev/null @@ -1,26 +0,0 @@ -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 deleted file mode 100644 index 691a9d46..00000000 --- a/src/bitmessagekivy/tests/test_create_random_address.py +++ /dev/null @@ -1,129 +0,0 @@ -""" - 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 deleted file mode 100644 index e876f418..00000000 --- a/src/bitmessagekivy/tests/test_draft_message.py +++ /dev/null @@ -1,158 +0,0 @@ -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 deleted file mode 100644 index 6d25553c..00000000 --- a/src/bitmessagekivy/tests/test_filemanager.py +++ /dev/null @@ -1,68 +0,0 @@ -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 deleted file mode 100644 index 619daf25..00000000 --- a/src/bitmessagekivy/tests/test_load_screen_data_file.py +++ /dev/null @@ -1,21 +0,0 @@ - -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 deleted file mode 100644 index 78740b79..00000000 --- a/src/bitmessagekivy/tests/test_myaddress_screen.py +++ /dev/null @@ -1,187 +0,0 @@ -from .telenium_process import TeleniumTestProcess -from .common import ordered - - -data = [ - 'BM-2cWmjntZ47WKEUtocrdvs19y5CivpKoi1h', - 'BM-2cVpswZo8rWLXDVtZEUNcDQvnvHJ6TLRYr' -] - - -class MyAddressScreen(TeleniumTestProcess): - """MyAddress Screen Functionality Testing""" - @ordered - def test_myaddress_screen(self): - """Open MyAddress Screen""" - # Checking current Screen(Inbox screen) - self.assert_wait_no_except('//ScreenManager[@current]', timeout=15, value='inbox') - # Method to open side navbar - self.open_side_navbar() - # this is for scrolling Nav drawer - self.drag("//NavigationItem[@text=\"Sent\"]", "//NavigationItem[@text=\"Inbox\"]") - # assert for checking scroll function - self.assertCheckScrollDown('//ContentNavigationDrawer//ScrollView[0]', timeout=10) - # Checking My address label on side nav bar - self.assertExists('//NavigationItem[@text=\"My addresses\"]', timeout=5) - # this is for opening setting screen - self.cli.wait_click('//NavigationItem[@text=\"My addresses\"]', timeout=5) - self.assertExists('//MDNavigationDrawer[@status~=\"closed\"]', timeout=5) - # Checking current screen - self.assertExists("//MyAddress[@name~=\"myaddress\"]", timeout=5) - - @ordered - def test_disable_address(self): - """Disable Addresses""" - # Dragging for loading addreses - self.drag( - '//MyAddress//MDList[0]/CustomTwoLineAvatarIconListItem[@text=\"test2\"]', - '//MyAddress//MDList[0]/CustomTwoLineAvatarIconListItem[@text=\"test1\"]' - ) - self.assertCheckScrollDown('//ContentNavigationDrawer//ScrollView[0]', timeout=10) - # Checking list of Addresses - self.assertExists("//MyAddress//CustomTwoLineAvatarIconListItem", timeout=5) - # Checking the Toggle button is rendered on addresses - self.assertExists("//MyAddress//CustomTwoLineAvatarIconListItem//ToggleBtn", timeout=5) - # Clicking on the Toggle button of first address to make it disable - self.assertExists( - "//MyAddress//CustomTwoLineAvatarIconListItem[@text=\"test2\"]//ToggleBtn[@active=true]", - timeout=5 - ) - # Clicking on Toggle button of first address - self.cli.wait_click( - "//MyAddress//CustomTwoLineAvatarIconListItem[@text=\"test2\"]//ToggleBtn/Thumb", - timeout=5 - ) - # Checking the address is disabled - self.assertExists( - "//MyAddress//CustomTwoLineAvatarIconListItem[@text=\"test2\"]//ToggleBtn[@active=false]", - timeout=5 - ) - # CLICKING ON DISABLE ACCOUNT TO OPEN POPUP - self.cli.wait_click("//MyAddress//CustomTwoLineAvatarIconListItem[@text=\"test2\"]", timeout=5) - # Checking the popup is Opened - self.assertExists( - '//MDDialog[@text=\"Address is not currently active. Please click on Toggle button to active it.\"]', - timeout=5 - ) - # Clicking on 'Ok' Button To Dismiss the popup - self.cli.wait_click('//MDFlatButton[@text=\"Ok\"]', timeout=5) - # Checking the address is disabled - self.assertExists( - "//MyAddress//CustomTwoLineAvatarIconListItem[@text=\"test2\"]//ToggleBtn[@active=false]", - timeout=5 - ) - - @ordered - def test_show_Qrcode(self): - """Show the Qr code of selected address""" - # Checking the current screen is MyAddress - self.assertExists("//MyAddress[@name~=\"myaddress\"]", timeout=5) - # Checking first label - self.assertExists( - '//MyAddress//MDList[0]/CustomTwoLineAvatarIconListItem[1][@text=\"test1\"]', - timeout=5 - ) - # Checking second label - self.assertExists( - '//MyAddress//MDList[0]/CustomTwoLineAvatarIconListItem[0][@text=\"test2\"]', - timeout=5 - ) - self.assertExists( - "//MyAddress//CustomTwoLineAvatarIconListItem[@text=\"test2\"]//ToggleBtn[@active=false]", - timeout=5 - ) - self.assertExists( - "//MyAddress//CustomTwoLineAvatarIconListItem[@text=\"test1\"]//ToggleBtn[@active=true]", - timeout=5 - ) - # Click on Address to open popup - self.cli.wait_click('//MDList[0]/CustomTwoLineAvatarIconListItem[@text=\"test1\"]', timeout=5) - # Check the Popup is opened - self.assertExists('//MyaddDetailPopup//MDLabel[@text=\"Show QR code\"]', timeout=5) - # Cick on 'Show QR code' button to view QR Code - self.cli.wait_click('//MyaddDetailPopup//MDLabel[@text=\"Show QR code\"]', timeout=5) - # Check Current screen is QR Code screen - self.assertExists("//ShowQRCode[@name~=\"showqrcode\"]", timeout=5) - # Check BACK button - self.assertExists('//ActionTopAppBarButton[@icon~=\"arrow-left\"]', timeout=5) - # Click on BACK button - self.cli.wait_click('//ActionTopAppBarButton[@icon~=\"arrow-left\"]', timeout=5) - # Checking current screen(My Address) after BACK press - self.assertExists("//MyAddress[@name~=\"myaddress\"]", timeout=5) - # Checking first label - self.assertExists( - '//MyAddress//MDList[0]/CustomTwoLineAvatarIconListItem[1][@text=\"test1\"]', - timeout=5 - ) - # Checking second label - self.assertExists( - '//MyAddress//MDList[0]/CustomTwoLineAvatarIconListItem[0][@text=\"test2\"]', - timeout=5 - ) - self.cli.sleep(0.3) - - @ordered - def test_enable_address(self): - """Test to enable the disabled address""" - # Checking list of Addresses - self.assertExists("//MyAddress//CustomTwoLineAvatarIconListItem", timeout=5) - # Check the thumb button on address - self.assertExists( - "//MyAddress//CustomTwoLineAvatarIconListItem[@text=\"test2\"]//ToggleBtn/Thumb", - timeout=5 - ) - # Checking the address is disabled - self.assertExists( - "//MyAddress//CustomTwoLineAvatarIconListItem[@text=\"test2\"]//ToggleBtn[@active=false]", - timeout=5 - ) - self.cli.sleep(0.3) - # Clicking on toggle button to enable the address - self.cli.wait_click( - "//MyAddress//CustomTwoLineAvatarIconListItem[@text=\"test2\"]//ToggleBtn/Thumb", - timeout=10 - ) - # Checking the address is enabled - self.assertExists( - "//MyAddress//CustomTwoLineAvatarIconListItem[@text=\"test2\"]//ToggleBtn[@active=true]", - timeout=10 - ) - - @ordered - def test_send_message_from(self): - """Send Message From Send Message From Button""" - # this is for scrolling Myaddress screen - self.drag( - '//MyAddress//MDList[0]/CustomTwoLineAvatarIconListItem[@text=\"test2\"]', - '//MyAddress//MDList[0]/CustomTwoLineAvatarIconListItem[@text=\"test1\"]' - ) - # Checking the addresses - self.assertExists( - '//MyAddress//MDList[0]/CustomTwoLineAvatarIconListItem[@text=\"test1\"]', - timeout=5 - ) - # Click on Address to open popup - self.cli.wait_click('//MDList[0]/CustomTwoLineAvatarIconListItem[@text=\"test1\"]', timeout=5) - # Checking Popup Opened - self.assertExists('//MyaddDetailPopup//MDLabel[@text=\"Send message from\"]', timeout=5) - # Click on Send Message Button to redirect Create Screen - self.cli.wait_click('//MyaddDetailPopup//MDRaisedButton[0]/MDLabel[0]', timeout=5) - # Checking Current screen(Create) - self.assertExists("//Create[@name~=\"create\"]", timeout=5) - # Entering Receiver Address - self.cli.setattr( - '//DropDownWidget/ScrollView[0]//MyTextInput[0]', "text", data[1]) - # Checking Receiver Address filled or not - self.assertNotEqual('//DropDownWidget//MyTextInput[0]', '') - # ADD SUBJECT - self.cli.setattr('//DropDownWidget/ScrollView[0]//MyMDTextField[0]', 'text', 'Hey this is Demo Subject') - # Checking Subject Field is Entered - self.assertNotEqual('//DropDownWidget/ScrollView[0]//MyMDTextField[0]', '') - # ADD MESSAGE BODY - self.cli.setattr( - '//DropDownWidget/ScrollView[0]//ScrollView[0]/MDTextField[0]', - 'text', 'Hey,i am sending message directly from MyAddress book' - ) - # Checking Message body is Entered - self.assertNotEqual('//DropDownWidget/ScrollView[0]//ScrollView[0]/MDTextField[@text]', '') diff --git a/src/bitmessagekivy/tests/test_network_screen.py b/src/bitmessagekivy/tests/test_network_screen.py deleted file mode 100644 index ca398dc1..00000000 --- a/src/bitmessagekivy/tests/test_network_screen.py +++ /dev/null @@ -1,50 +0,0 @@ -# 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 deleted file mode 100644 index 42309001..00000000 --- a/src/bitmessagekivy/tests/test_payment_subscription.py +++ /dev/null @@ -1,70 +0,0 @@ -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 deleted file mode 100644 index a7fd576b..00000000 --- a/src/bitmessagekivy/tests/test_sent_message.py +++ /dev/null @@ -1,133 +0,0 @@ -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 deleted file mode 100644 index 4f3a0a59..00000000 --- a/src/bitmessagekivy/tests/test_setting_screen.py +++ /dev/null @@ -1,38 +0,0 @@ -# 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 deleted file mode 100644 index d7cdb467..00000000 --- a/src/bitmessagekivy/tests/test_trash_message.py +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index 6f73247e..00000000 --- a/src/bitmessagekivy/uikivysignaler.py +++ /dev/null @@ -1,38 +0,0 @@ -""" - 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 ab131a4c..d6cb289b 100755 --- a/src/bitmessagemain.py +++ b/src/bitmessagemain.py @@ -1,9 +1,9 @@ -#!/usr/bin/env python +#!/usr/bin/python2.7 """ The PyBitmessage startup script """ # Copyright (c) 2012-2016 Jonathan Warren -# Copyright (c) 2012-2022 The Bitmessage developers +# Copyright (c) 2012-2020 The Bitmessage developers # Distributed under the MIT/X11 software license. See the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. @@ -12,41 +12,130 @@ The PyBitmessage startup script 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 ctypes import getopt import multiprocessing # Used to capture a Ctrl-C keypress so that Bitmessage can shutdown gracefully. import signal +import socket import threading import time import traceback +from struct import pack import defaults -# Network subsystem -import network +import shared import shutdown import state - -from testmode_init import populate_api_test_data -from bmconfigparser import config +from bmconfigparser import BMConfigParser from debug import logger # this should go before any threads from helper_startup import ( - adjustHalfOpenConnectionsLimit, fixSocket, start_proxyconfig) + isOurOperatingSystemLimitedToHavingVeryFewHalfOpenConnections, + start_proxyconfig +) from inventory import Inventory +from knownnodes import readKnownNodes +# Network objects and threads +from network import ( + BMConnectionPool, Dandelion, AddrThread, AnnounceThread, BMNetworkThread, + InvThread, ReceiveQueueThread, DownloadThread, UploadThread +) from singleinstance import singleinstance # Synchronous threads from threads import ( - set_thread_name, printLock, - addressGenerator, objectProcessor, singleCleaner, singleWorker, sqlThread) + set_thread_name, addressGenerator, objectProcessor, singleCleaner, + singleWorker, sqlThread +) + + +def connectToStream(streamNumber): + """Connect to a stream""" + state.streamsInWhichIAmParticipating.append(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 + + BMConnectionPool().connectToStream(streamNumber) + + +def _fixSocket(): + 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 signal_handler(signum, frame): @@ -68,7 +157,7 @@ def signal_handler(signum, frame): 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: + if shared.thisapp.daemon or not state.enableGUI: shutdown.doCleanShutdown() else: print('# Thread: %s(%d)' % (thread.name, thread.ident)) @@ -85,9 +174,9 @@ class Main(object): def start(self): """Start main application""" # pylint: disable=too-many-statements,too-many-branches,too-many-locals - fixSocket() - adjustHalfOpenConnectionsLimit() + _fixSocket() + config = BMConfigParser() daemon = config.safeGetBoolean('bitmessagesettings', 'daemon') try: @@ -122,8 +211,6 @@ class Main(object): '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') @@ -146,10 +233,10 @@ class Main(object): ' \'-c\' as a commandline argument.' ) # is the application already running? If yes then exit. - state.thisapp = singleinstance("", daemon) + shared.thisapp = singleinstance("", daemon) if daemon: - with printLock: + with shared.printLock: print('Running as a daemon. Send TERM signal to end.') self.daemonize() @@ -157,6 +244,13 @@ class Main(object): set_thread_name("PyBitmessage") + state.dandelion = config.safeGetInt('network', 'dandelion') + # dandelion requires outbound connections, without them, + # stem objects will get stuck forever + if state.dandelion and not config.safeGetBoolean( + 'bitmessagesettings', 'sendoutgoingconnections'): + state.dandelion = 0 + if state.testmode or config.safeGetBoolean( 'bitmessagesettings', 'extralowdifficulty'): defaults.networkDefaultProofOfWorkNonceTrialsPerByte = int( @@ -164,15 +258,11 @@ class Main(object): defaults.networkDefaultPayloadLengthExtraBytes = int( defaults.networkDefaultPayloadLengthExtraBytes / 100) - # 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 + readKnownNodes() + + # Not needed if objproc is disabled + if state.enableObjProc: - if state.enableObjProc: # Not needed if objproc is disabled # Start the address generation thread addressGeneratorThread = addressGenerator() # close the main program even if there are threads left @@ -185,13 +275,19 @@ class Main(object): singleWorkerThread.daemon = True singleWorkerThread.start() - # 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() + # Start the SQL thread + sqlLookup = sqlThread() + # DON'T close the main program even if there are threads left. + # The closeEvent should command this thread to exit gracefully. + sqlLookup.daemon = False + sqlLookup.start() + + Inventory() # init + # init, needs to be early because other thread may access it early + Dandelion() + + # Enable object processor and SMTP only if objproc enabled + if state.enableObjProc: # SMTP delivery thread if daemon and config.safeGet( @@ -207,6 +303,25 @@ class Main(object): smtpServerThread = smtpServer() smtpServerThread.start() + # Start the thread that calculates POWs + objectProcessorThread = objectProcessor() + # DON'T close the main program even the thread remains. + # This thread checks the shutdown variable after processing + # each object. + objectProcessorThread.daemon = False + objectProcessorThread.start() + + # Start the cleanerThread + singleCleanerThread = singleCleaner() + # close the main program even if there are threads left + singleCleanerThread.daemon = True + singleCleanerThread.start() + + # Not needed if objproc disabled + if state.enableObjProc: + shared.reloadMyAddressHashes() + shared.reloadBroadcastSendersForWhichImWatching() + # API is also objproc dependent if config.safeGetBoolean('bitmessagesettings', 'apienabled'): import api # pylint: disable=relative-import @@ -215,23 +330,42 @@ class Main(object): singleAPIThread.daemon = True singleAPIThread.start() - # Start the cleanerThread - singleCleanerThread = singleCleaner() - # close the main program even if there are threads left - singleCleanerThread.daemon = True - singleCleanerThread.start() - # start network components if networking is enabled if state.enableNetwork: start_proxyconfig() - network.start(config, state) + BMConnectionPool() + asyncoreThread = BMNetworkThread() + asyncoreThread.daemon = True + asyncoreThread.start() + for i in range(config.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() + state.uploadThread = UploadThread() + state.uploadThread.daemon = True + state.uploadThread.start() + + connectToStream(1) if config.safeGetBoolean('bitmessagesettings', 'upnp'): import upnp upnpThread = upnp.uPnPThread() upnpThread.start() else: - network.connectionpool.pool.connectToStream(1) + # Populate with hardcoded value (same as connectToStream above) + state.streamsInWhichIAmParticipating.append(1) if not daemon and state.enableGUI: if state.curses: @@ -240,39 +374,36 @@ class Main(object): print('Running with curses') import bitmessagecurses bitmessagecurses.runwrapper() + elif state.kivy: + config.remove_option('bitmessagesettings', 'dontconnect') + from bitmessagekivy.mpybit import NavigateApp + NavigateApp().run() else: import bitmessageqt bitmessageqt.run() else: config.remove_option('bitmessagesettings', 'dontconnect') - if state.testmode: - populate_api_test_data() - if daemon: while state.shutdown == 0: time.sleep(1) if ( - state.testmode - and time.time() - state.last_api_response >= 30 + 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 - + # pylint: disable=relative-import + from tests import core as test_core test_core_result = test_core.run() + state.enableGUI = True self.stop() test_core.cleanup() - sys.exit(not test_core_result.wasSuccessful()) + sys.exit( + 'Core tests failed!' + if test_core_result.errors or test_core_result.failures + else 0 + ) @staticmethod def daemonize(): @@ -282,7 +413,7 @@ class Main(object): try: if os.fork(): # unlock - state.thisapp.cleanup() + shared.thisapp.cleanup() # wait until grandchild ready while True: time.sleep(1) @@ -292,7 +423,7 @@ class Main(object): pass else: parentPid = os.getpid() - state.thisapp.lock() # relock + shared.thisapp.lock() # relock os.umask(0) try: @@ -303,7 +434,7 @@ class Main(object): try: if os.fork(): # unlock - state.thisapp.cleanup() + shared.thisapp.cleanup() # wait until child ready while True: time.sleep(1) @@ -312,14 +443,14 @@ class Main(object): # fork not implemented pass else: - state.thisapp.lock() # relock - state.thisapp.lockPid = None # indicate we're the final child + shared.thisapp.lock() # relock + shared.thisapp.lockPid = None # indicate we're the final child sys.stdout.flush() sys.stderr.flush() if not sys.platform.startswith('win'): - si = open(os.devnull, 'r') - so = open(os.devnull, 'a+') - se = open(os.devnull, 'a+', 0) + si = file(os.devnull, 'r') + so = file(os.devnull, 'a+') + se = file(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()) @@ -352,7 +483,7 @@ All parameters are optional. @staticmethod def stop(): """Stop main application""" - with printLock: + with shared.printLock: print('Stopping Bitmessage Deamon.') shutdown.doCleanShutdown() @@ -360,11 +491,11 @@ All parameters are optional. @staticmethod def getApiAddress(): """This function returns API address and port""" - if not config.safeGetBoolean( + if not BMConfigParser().safeGetBoolean( 'bitmessagesettings', 'apienabled'): return None - address = config.get('bitmessagesettings', 'apiinterface') - port = config.getint('bitmessagesettings', 'apiport') + address = BMConfigParser().get('bitmessagesettings', 'apiinterface') + port = BMConfigParser().getint('bitmessagesettings', 'apiport') return {'address': address, 'port': port} diff --git a/src/bitmessageqt/__init__.py b/src/bitmessageqt/__init__.py index 1b1a7885..440d36b2 100644 --- a/src/bitmessageqt/__init__.py +++ b/src/bitmessageqt/__init__.py @@ -7,7 +7,6 @@ import locale import os import random import string -import subprocess import sys import textwrap import threading @@ -18,31 +17,29 @@ from sqlite3 import register_adapter 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 config +from bmconfigparser import BMConfigParser import namecoin from messageview import MessageView from migrationwizard import Ui_MigrationWizard from foldertree import ( AccountMixin, Ui_FolderWidget, Ui_AddressWidget, Ui_SubscriptionWidget, MessageList_AddressWidget, MessageList_SubjectWidget, - Ui_AddressBookWidgetItemLabel, Ui_AddressBookWidgetItemAddress, - MessageList_TimeWidget) + Ui_AddressBookWidgetItemLabel, Ui_AddressBookWidgetItemAddress) import settingsmixin import support +from helper_ackPayload import genAckPayload from helper_sql import sqlQuery, sqlExecute, sqlExecuteChunked, sqlStoredProcedure -import helper_addressbook import helper_search import l10n from utils import str_broadcast_subscribers, avatarize +from account import ( + getSortedAccounts, getSortedSubscriptions, accountClass, BMAccount, + GatewayAccount, MailchuckAccount, AccountColor) import dialogs from network.stats import pendingDownload, pendingUpload from uisignaler import UISignaler @@ -50,11 +47,11 @@ import paths from proofofwork import getPowType import queues import shutdown +import state from statusbar import BMStatusBar import sound # This is needed for tray icon import bitmessage_icons_rc # noqa:F401 pylint: disable=unused-import -import helper_sent try: from plugins.plugin import get_plugin, get_plugins @@ -62,9 +59,6 @@ except ImportError: get_plugins = False -is_windows = sys.platform.startswith('win') - - # TODO: rewrite def powQueueSize(): """Returns the size of queues.workerQueue including current unfinished work""" @@ -78,15 +72,6 @@ def powQueueSize(): return queue_len -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 - - class MyForm(settingsmixin.SMainWindow): # the maximum frequency of message sounds in seconds @@ -128,8 +113,6 @@ class MyForm(settingsmixin.SMainWindow): 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, @@ -140,7 +123,7 @@ class MyForm(settingsmixin.SMainWindow): langs = [l10n.getWindowsLocale(lang)] for lang in langs: try: - l10n.setlocale(lang) + 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: @@ -165,10 +148,9 @@ class MyForm(settingsmixin.SMainWindow): 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( @@ -232,19 +214,19 @@ class MyForm(settingsmixin.SMainWindow): 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 @@ -284,7 +266,7 @@ class MyForm(settingsmixin.SMainWindow): 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': []} @@ -334,7 +316,7 @@ class MyForm(settingsmixin.SMainWindow): 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 @@ -371,7 +353,7 @@ class MyForm(settingsmixin.SMainWindow): 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): # Actions @@ -400,7 +382,7 @@ class MyForm(settingsmixin.SMainWindow): 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): # Actions @@ -431,13 +413,13 @@ class MyForm(settingsmixin.SMainWindow): treeWidget.header().setSortIndicator( 0, QtCore.Qt.AscendingOrder) # init dictionary - + db = getSortedSubscriptions(True) for address in db: for folder in folders: - if folder not in db[address]: + if not folder in db[address]: db[address][folder] = {} - + if treeWidget.isSortingEnabled(): treeWidget.setSortingEnabled(False) @@ -449,8 +431,8 @@ class MyForm(settingsmixin.SMainWindow): toAddress = widget.address else: toAddress = None - - if toAddress not in db: + + if not toAddress in db: treeWidget.takeTopLevelItem(i) # no increment continue @@ -480,16 +462,10 @@ class MyForm(settingsmixin.SMainWindow): 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: @@ -501,22 +477,23 @@ class MyForm(settingsmixin.SMainWindow): j += 1 widget.setUnreadCount(unread) i += 1 - + 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( @@ -524,13 +501,13 @@ class MyForm(settingsmixin.SMainWindow): # init dictionary db = {} enabled = {} - - for toAddress in config.addresses(True): - isEnabled = config.getboolean( + + for toAddress in getSortedAccounts(): + isEnabled = BMConfigParser().getboolean( toAddress, 'enabled') - isChan = config.safeGetBoolean( + isChan = BMConfigParser().safeGetBoolean( toAddress, 'chan') - isMaillinglist = config.safeGetBoolean( + isMaillinglist = BMConfigParser().safeGetBoolean( toAddress, 'mailinglist') if treeWidget == self.ui.treeWidgetYourIdentities: @@ -543,16 +520,12 @@ class MyForm(settingsmixin.SMainWindow): 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 @@ -565,10 +538,10 @@ class MyForm(settingsmixin.SMainWindow): db[None]["sent"] = 0 db[None]["trash"] = 0 enabled[None] = True - + if treeWidget.isSortingEnabled(): treeWidget.setSortingEnabled(False) - + widgets = {} i = 0 while i < treeWidget.topLevelItemCount(): @@ -577,8 +550,8 @@ class MyForm(settingsmixin.SMainWindow): toAddress = widget.address else: toAddress = None - - if toAddress not in db: + + if not toAddress in db: treeWidget.takeTopLevelItem(i) # no increment continue @@ -587,9 +560,8 @@ class MyForm(settingsmixin.SMainWindow): 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: @@ -605,13 +577,13 @@ class MyForm(settingsmixin.SMainWindow): 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]) @@ -621,12 +593,12 @@ class MyForm(settingsmixin.SMainWindow): 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): @@ -643,20 +615,20 @@ class MyForm(settingsmixin.SMainWindow): # Ask the user if we may delete their old version 1 addresses if they # have any. - for addressInKeysFile in config.addresses(): + for addressInKeysFile in getSortedAccounts(): 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: - config.remove_section(addressInKeysFile) - config.save() + BMConfigParser().remove_section(addressInKeysFile) + BMConfigParser().save() + + self.updateStartOnLogon() self.change_translation() @@ -758,18 +730,20 @@ class MyForm(settingsmixin.SMainWindow): 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( @@ -779,12 +753,9 @@ class MyForm(settingsmixin.SMainWindow): 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( @@ -828,30 +799,21 @@ class MyForm(settingsmixin.SMainWindow): self.rerenderComboBoxSendFrom() self.rerenderComboBoxSendFromBroadcast() - + # Put the TTL slider in the correct spot - TTL = config.getint('bitmessagesettings', 'ttl') + TTL = BMConfigParser().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.ui.updateNetworkSwitchMenuLabel() - - self._firstrun = config.safeGetBoolean( - 'bitmessagesettings', 'dontconnect') self._contact_selected = None @@ -865,34 +827,32 @@ class MyForm(settingsmixin.SMainWindow): 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 + # 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" - settings = QtCore.QSettings( + self.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 + self.settings.remove("PyBitmessage") + 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 def updateTTL(self, sliderPosition): TTL = int(sliderPosition ** 3.199 + 3600) self.updateHumanFriendlyTTLDescription(TTL) - config.set('bitmessagesettings', 'ttl', str(TTL)) - config.save() + BMConfigParser().set('bitmessagesettings', 'ttl', str(TTL)) + BMConfigParser().save() def updateHumanFriendlyTTLDescription(self, TTL): numberOfHours = int(round(TTL / (60*60))) @@ -909,13 +869,7 @@ class MyForm(settingsmixin.SMainWindow): 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) @@ -949,7 +903,7 @@ class MyForm(settingsmixin.SMainWindow): self.appIndicatorShowOrHideWindow() def appIndicatorSwitchQuietMode(self): - config.set( + BMConfigParser().set( 'bitmessagesettings', 'showtraynotifications', str(not self.actionQuiet.isChecked()) ) @@ -1009,30 +963,40 @@ class MyForm(settingsmixin.SMainWindow): Switch unread for item of msgid and related items in other STableWidgets "All Accounts" and "Chans" """ - status = widget.item(row, 0).unread - if status != unread: - return - - widgets = [self.ui.tableWidgetInbox, self.ui.tableWidgetInboxChans] - rrow = None + related = [self.ui.tableWidgetInbox, self.ui.tableWidgetInboxChans] try: - widgets.remove(widget) - related = widgets.pop() + related.remove(widget) + related = related.pop() except ValueError: - pass + rrow = None + related = [] 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 range(related.rowCount()): - if related.item(rrow, 3).data() == msgid: + for rrow in xrange(related.rowCount()): + if msgid == str(related.item(rrow, 3).data( + QtCore.Qt.UserRole).toPyObject()): break + else: + rrow = None - for col in range(widget.columnCount()): - widget.item(row, col).setUnread(not status) - if rrow: - related.item(rrow, col).setUnread(not status) + 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) # Here we need to update unread count for: # - all widgets if there is no args @@ -1117,46 +1081,43 @@ class MyForm(settingsmixin.SMainWindow): if sortingEnabled: tableWidget.setSortingEnabled(False) tableWidget.insertRow(0) - for i, item in enumerate(items): - tableWidget.setItem(0, i, item) + for i in range(len(items)): + tableWidget.setItem(0, i, items[i]) if sortingEnabled: tableWidget.setSortingEnabled(True) - def addMessageListItemSent( - self, tableWidget, toAddress, fromAddress, subject, - status, ackdata, lastactiontime - ): - acct = accountClass(fromAddress) or BMAccount(fromAddress) + def addMessageListItemSent(self, tableWidget, toAddress, fromAddress, subject, status, ackdata, lastactiontime): + acct = accountClass(fromAddress) + if acct is None: + acct = 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.") @@ -1167,64 +1128,58 @@ class MyForm(settingsmixin.SMainWindow): 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)) - - 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)] + 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) self.addMessageListItem(tableWidget, items) - return acct - def addMessageListItemInbox( - self, tableWidget, toAddress, fromAddress, subject, - msgid, received, read - ): + def addMessageListItemInbox(self, tableWidget, msgfolder, msgid, toAddress, fromAddress, subject, received, read): + font = QtGui.QFont() + font.setBold(True) if toAddress == str_broadcast_subscribers: acct = accountClass(fromAddress) else: - acct = accountClass(toAddress) or accountClass(fromAddress) + acct = accountClass(toAddress) + if acct is None: + acct = accountClass(fromAddress) if acct is None: acct = BMAccount(fromAddress) acct.parseMessage(toAddress, fromAddress, subject, "") - - 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) - ] + + 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) self.addMessageListItem(tableWidget, items) - return acct # Load Sent items from database @@ -1239,40 +1194,35 @@ class MyForm(settingsmixin.SMainWindow): xAddress = 'both' else: tableWidget.setColumnHidden(0, False) - tableWidget.setColumnHidden(1, bool(account)) + if account is None: + tableWidget.setColumnHidden(1, False) + else: + tableWidget.setColumnHidden(1, True) xAddress = 'fromaddress' - queryreturn = helper_search.search_sql( - xAddress, account, "sent", where, what, False) + tableWidget.setUpdatesEnabled(False) + tableWidget.setSortingEnabled(False) + tableWidget.setRowCount(0) + queryreturn = helper_search.search_sql(xAddress, account, "sent", where, what, False) for row in queryreturn: - self.addMessageListItemSent(tableWidget, *row) + toAddress, fromAddress, subject, status, ackdata, lastactiontime = row + self.addMessageListItemSent(tableWidget, toAddress, fromAddress, subject, status, ackdata, lastactiontime) tableWidget.horizontalHeader().setSortIndicator( 3, QtCore.Qt.DescendingOrder) tableWidget.setSortingEnabled(True) - tableWidget.horizontalHeaderItem(3).setText( - _translate("MainWindow", "Sent")) + tableWidget.horizontalHeaderItem(3).setText(_translate("MainWindow", "Sent", None)) tableWidget.setUpdatesEnabled(True) # Load messages from database file - def loadMessagelist( - self, tableWidget, account, folder="inbox", where="", what="", - unreadOnly=False - ): - tableWidget.setUpdatesEnabled(False) - tableWidget.setSortingEnabled(False) - tableWidget.setRowCount(0) - + def loadMessagelist(self, tableWidget, account, folder="inbox", where="", what="", unreadOnly = False): 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: @@ -1282,21 +1232,21 @@ class MyForm(settingsmixin.SMainWindow): tableWidget.setColumnHidden(0, False) tableWidget.setColumnHidden(1, False) - queryreturn = helper_search.search_sql( - xAddress, account, folder, where, what, unreadOnly) + tableWidget.setUpdatesEnabled(False) + tableWidget.setSortingEnabled(False) + tableWidget.setRowCount(0) + queryreturn = helper_search.search_sql(xAddress, account, folder, where, what, unreadOnly) + for row in queryreturn: - toAddress, fromAddress, subject, _, msgid, received, read = row - self.addMessageListItemInbox( - tableWidget, toAddress, fromAddress, subject, - msgid, received, read) + msgfolder, msgid, toAddress, fromAddress, subject, received, read = row + self.addMessageListItemInbox(tableWidget, msgfolder, msgid, toAddress, fromAddress, subject, received, read) tableWidget.horizontalHeader().setSortIndicator( 3, QtCore.Qt.DescendingOrder) tableWidget.setSortingEnabled(True) tableWidget.selectRow(0) - tableWidget.horizontalHeaderItem(3).setText( - _translate("MainWindow", "Received")) + tableWidget.horizontalHeaderItem(3).setText(_translate("MainWindow", "Received", None)) tableWidget.setUpdatesEnabled(True) # create application indicator @@ -1320,7 +1270,7 @@ class MyForm(settingsmixin.SMainWindow): # show bitmessage self.actionShow = QtGui.QAction(_translate( "MainWindow", "Show Bitmessage"), m, checkable=True) - self.actionShow.setChecked(not config.getboolean( + self.actionShow.setChecked(not BMConfigParser().getboolean( 'bitmessagesettings', 'startintray')) self.actionShow.triggered.connect(self.appIndicatorShowOrHideWindow) if not sys.platform[0:3] == 'win': @@ -1329,7 +1279,7 @@ class MyForm(settingsmixin.SMainWindow): # quiet mode self.actionQuiet = QtGui.QAction(_translate( "MainWindow", "Quiet Mode"), m, checkable=True) - self.actionQuiet.setChecked(not config.getboolean( + self.actionQuiet.setChecked(not BMConfigParser().getboolean( 'bitmessagesettings', 'showtraynotifications')) self.actionQuiet.triggered.connect(self.appIndicatorSwitchQuietMode) m.addAction(self.actionQuiet) @@ -1452,11 +1402,9 @@ class MyForm(settingsmixin.SMainWindow): 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 @@ -1525,9 +1473,9 @@ class MyForm(settingsmixin.SMainWindow): 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, QtGui.QTableWidget)): - folder = self.getCurrentFolder() + if isinstance(focus, MessageView) or isinstance(focus, QtGui.QTableWidget): if folder == "sent": self.on_action_SentTrash() else: @@ -1563,18 +1511,17 @@ class MyForm(settingsmixin.SMainWindow): self.ui.lineEditTo.setFocus() event.ignore() elif event.key() == QtCore.Qt.Key_F: - try: - self.getCurrentSearchLine(retObj=True).setFocus() - except AttributeError: - pass + searchline = self.getCurrentSearchLine(retObj=True) + if searchline: + searchline.setFocus() event.ignore() if not event.isAccepted(): return if isinstance(focus, MessageView): return MessageView.keyPressEvent(focus, event) - if isinstance(focus, QtGui.QTableWidget): + elif isinstance(focus, QtGui.QTableWidget): return QtGui.QTableWidget.keyPressEvent(focus, event) - if isinstance(focus, QtGui.QTreeWidget): + elif isinstance(focus, QtGui.QTreeWidget): return QtGui.QTreeWidget.keyPressEvent(focus, event) # menu button 'manage keys' @@ -1585,81 +1532,36 @@ class MyForm(settingsmixin.SMainWindow): # 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 \n" - "It 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 \nIt 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 \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) + 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) if reply == QtGui.QMessageBox.Yes: - openKeysFile() + shared.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): @@ -1717,10 +1619,9 @@ class MyForm(settingsmixin.SMainWindow): dialog = dialogs.ConnectDialog(self) if dialog.exec_(): if dialog.radioButtonConnectNow.isChecked(): - self.ui.updateNetworkSwitchMenuLabel(False) - config.remove_option( + BMConfigParser().remove_option( 'bitmessagesettings', 'dontconnect') - config.save() + BMConfigParser().save() elif dialog.radioButtonConfigureNetwork.isChecked(): self.click_actionSettings() else: @@ -1745,7 +1646,7 @@ class MyForm(settingsmixin.SMainWindow): self.ui.blackwhitelist.init_blacklist_popup_menu(False) if event.type() == QtCore.QEvent.WindowStateChange: if self.windowState() & QtCore.Qt.WindowMinimized: - if config.getboolean('bitmessagesettings', 'minimizetotray') and not 'darwin' in sys.platform: + if BMConfigParser().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 @@ -1761,65 +1662,68 @@ class MyForm(settingsmixin.SMainWindow): connected = False def setStatusIcon(self, color): - _notifications_enabled = not config.getboolean( + # print 'setting status icon color' + _notifications_enabled = not BMConfigParser().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) - proxy = config.safeGet( - 'bitmessagesettings', 'socksproxytype', 'none') - if proxy == 'none' and not config.safeGetBoolean( - 'bitmessagesettings', 'upnp'): + if not BMConfigParser().safeGetBoolean('bitmessagesettings', 'upnp') and \ + BMConfigParser().get('bitmessagesettings', 'socksproxytype') == "none": 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") - return + 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 - 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-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.actionStatus is not None: - self.actionStatus.setText(_translate( - "MainWindow", "Connected")) - self.setTrayIconFile("can-icon-24px-%s.png" % color) + if self.actionStatus is not None: + self.actionStatus.setText(_translate( + "MainWindow", "Connected")) + self.setTrayIconFile("can-icon-24px-green.png") def initTrayIcon(self, iconFileName, app): self.currentTrayIconFileName = iconFileName @@ -1831,7 +1735,7 @@ class MyForm(settingsmixin.SMainWindow): 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" @@ -1843,8 +1747,7 @@ class MyForm(settingsmixin.SMainWindow): rect = fontMetrics.boundingRect(txt) # margins that we add in the top-right corner marginX = 2 - # it looks like -2 is also ok due to the error of metric - marginY = 0 + marginY = 0 # it looks like -2 is also ok due to the error of metric # if it renders too wide we need to change it to a plus symbol if rect.width() > 20: txt = "+" @@ -1884,18 +1787,11 @@ class MyForm(settingsmixin.SMainWindow): 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()): @@ -1904,9 +1800,7 @@ class MyForm(settingsmixin.SMainWindow): 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( @@ -1917,26 +1811,22 @@ class MyForm(settingsmixin.SMainWindow): 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() + toAddress = sent.item( + i, 0).data(QtCore.Qt.UserRole) + tableAckdata = sent.item( + i, 3).data(QtCore.Qt.UserRole).toPyObject() 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( @@ -1953,7 +1843,8 @@ class MyForm(settingsmixin.SMainWindow): ): i = None for i in range(inbox.rowCount()): - if msgid == inbox.item(i, 3).data(): + if msgid == \ + inbox.item(i, 3).data(QtCore.Qt.UserRole).toPyObject(): break else: continue @@ -1982,16 +1873,12 @@ class MyForm(settingsmixin.SMainWindow): os._exit(0) def rerenderMessagelistFromLabels(self): - for messagelist in (self.ui.tableWidgetInbox, - self.ui.tableWidgetInboxChans, - self.ui.tableWidgetInboxSubscriptions): + for messagelist in (self.ui.tableWidgetInbox, self.ui.tableWidgetInboxChans, self.ui.tableWidgetInboxSubscriptions): for i in range(messagelist.rowCount()): messagelist.item(i, 1).setLabel() def rerenderMessagelistToLabels(self): - for messagelist in (self.ui.tableWidgetInbox, - self.ui.tableWidgetInboxChans, - self.ui.tableWidgetInboxSubscriptions): + for messagelist in (self.ui.tableWidgetInbox, self.ui.tableWidgetInboxChans, self.ui.tableWidgetInboxSubscriptions): for i in range(messagelist.rowCount()): messagelist.item(i, 0).setLabel() @@ -2020,9 +1907,10 @@ class MyForm(settingsmixin.SMainWindow): label, address = row newRows[address] = [label, AccountMixin.SUBSCRIPTION] # chans - for address in config.addresses(True): + addresses = getSortedAccounts() + for address in addresses: account = accountClass(address) - if (account.type == AccountMixin.CHAN and config.safeGetBoolean(address, 'enabled')): + if (account.type == AccountMixin.CHAN and BMConfigParser().safeGetBoolean(address, 'enabled')): newRows[address] = [account.getLabel(), AccountMixin.CHAN] # normal accounts queryreturn = sqlQuery('SELECT * FROM addressbook') @@ -2031,13 +1919,11 @@ class MyForm(settingsmixin.SMainWindow): newRows[address] = [label, AccountMixin.NORMAL] completerList = [] - for address in sorted( - oldRows, key=lambda x: oldRows[x][2], reverse=True - ): - try: - completerList.append( - newRows.pop(address)[0] + " <" + address + ">") - except KeyError: + 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: self.ui.tableWidgetAddressBook.removeRow(oldRows[address][2]) for address in newRows: addRow(address, newRows[address][0], newRows[address][1]) @@ -2053,16 +1939,11 @@ class MyForm(settingsmixin.SMainWindow): 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("") @@ -2115,14 +1996,11 @@ class MyForm(settingsmixin.SMainWindow): acct = accountClass(fromAddress) - # 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. + 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. for toAddress in toAddressesList: if toAddress != '': # label plus address @@ -2135,26 +2013,19 @@ class MyForm(settingsmixin.SMainWindow): 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) - config.set(fromAddress, 'label', email) - config.set(fromAddress, 'gateway', 'mailchuck') - config.save() + BMConfigParser().set(fromAddress, 'label', email) + BMConfigParser().set(fromAddress, 'gateway', 'mailchuck') + BMConfigParser().save() self.updateStatusBar(_translate( "MainWindow", "Error: Your account wasn't registered at" @@ -2164,7 +2035,8 @@ class MyForm(settingsmixin.SMainWindow): ).arg(email) ) return - status, addressVersionNumber, streamNumber = decodeAddress(toAddress)[:3] + status, addressVersionNumber, streamNumber, ripe = decodeAddress( + toAddress) if status != 'success': try: toAddress = unicode(toAddress, 'utf-8', 'ignore') @@ -2239,27 +2111,15 @@ class MyForm(settingsmixin.SMainWindow): 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 state.statusIconColor == 'red': + if shared.statusIconColor == 'red': self.updateStatusBar(_translate( "MainWindow", "Warning: You are currently not connected." @@ -2267,9 +2127,29 @@ class MyForm(settingsmixin.SMainWindow): " send the message but it won\'t send until" " you connect.") ) - ackdata = helper_sent.insert( - toAddress=toAddress, fromAddress=fromAddress, - subject=subject, message=message, encoding=encoding) + 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') + ) + toLabel = '' queryreturn = sqlQuery('''select label from addressbook where address=?''', toAddress) @@ -2303,16 +2183,31 @@ class MyForm(settingsmixin.SMainWindow): # 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 - - # 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) + 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) toLabel = str_broadcast_subscribers - + self.displayNewSentMessage( toAddress, toLabel, fromAddress, subject, message, ackdata) @@ -2358,20 +2253,18 @@ class MyForm(settingsmixin.SMainWindow): self.ui.tabWidgetSend.setCurrentIndex( self.ui.tabWidgetSend.indexOf( self.ui.sendBroadcast - if config.safeGetBoolean(str(address), 'mailinglist') + if BMConfigParser().safeGetBoolean(str(address), 'mailinglist') else self.ui.sendDirect )) def rerenderComboBoxSendFrom(self): self.ui.comboBoxSendFrom.clear() - 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') + 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') if isEnabled and not isMaillinglist: - label = unicode(config.get(addressInKeysFile, 'label'), 'utf-8', 'ignore').strip() + label = unicode(BMConfigParser().get(addressInKeysFile, 'label'), 'utf-8', 'ignore').strip() if label == "": label = addressInKeysFile self.ui.comboBoxSendFrom.addItem(avatarize(addressInKeysFile), label, addressInKeysFile) @@ -2390,12 +2283,12 @@ class MyForm(settingsmixin.SMainWindow): def rerenderComboBoxSendFromBroadcast(self): self.ui.comboBoxSendFromBroadcast.clear() - for addressInKeysFile in config.addresses(True): - isEnabled = config.getboolean( - addressInKeysFile, 'enabled') - isChan = config.safeGetBoolean(addressInKeysFile, 'chan') + 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') if isEnabled and not isChan: - label = unicode(config.get(addressInKeysFile, 'label'), 'utf-8', 'ignore').strip() + label = unicode(BMConfigParser().get(addressInKeysFile, 'label'), 'utf-8', 'ignore').strip() if label == "": label = addressInKeysFile self.ui.comboBoxSendFromBroadcast.addItem(avatarize(addressInKeysFile), label, addressInKeysFile) @@ -2415,88 +2308,54 @@ class MyForm(settingsmixin.SMainWindow): # 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(message) + + self.addMessageListItemSent(sent, toAddress, fromAddress, subject, "msgqueued", ackdata, time.time()) + self.getAccountTextedit(acct).setPlainText(unicode(message, 'utf-8', 'replace')) sent.setCurrentCell(0, 0) - def displayNewInboxMessage( - self, inventoryHash, toAddress, fromAddress, subject, message): - acct = accountClass( - fromAddress if toAddress == str_broadcast_subscribers - else toAddress - ) + def displayNewInboxMessage(self, inventoryHash, toAddress, fromAddress, subject, message): + if toAddress == str_broadcast_subscribers: + acct = accountClass(fromAddress) + else: + acct = accountClass(toAddress) inbox = self.getAccountMessagelist(acct) - ret = treeWidget = None + ret = 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 - if not helper_search.check_match( - toAddress, fromAddress, subject, message, - self.getCurrentSearchOption(tab), - self.getCurrentSearchLine(tab) - ): - continue 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 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) if ret is None: acct.parseMessage(toAddress, fromAddress, subject, "") else: acct = ret + # pylint:disable=undefined-loop-variable self.propagateUnreadCount(widget=treeWidget if ret else None) - if config.safeGetBoolean( + if BMConfigParser().getboolean( 'bitmessagesettings', 'showtraynotifications'): self.notifierShow( _translate("MainWindow", "New Message"), @@ -2504,22 +2363,16 @@ class MyForm(settingsmixin.SMainWindow): 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 irrespective 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 irespective of # whether it's in current message list or not self.indicatorUpdate(True, to_label=acct.toLabel) - - 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 + # 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_() def click_pushButtonAddAddressBook(self, dialog=None): if not dialog: @@ -2542,15 +2395,15 @@ class MyForm(settingsmixin.SMainWindow): )) return - 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." - )) + 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() def addSubscription(self, address, label): # This should be handled outside of this function, for error displaying @@ -2598,7 +2451,7 @@ class MyForm(settingsmixin.SMainWindow): )) def click_pushButtonStatusIcon(self): - dialogs.IconGlossaryDialog(self, config=config).exec_() + dialogs.IconGlossaryDialog(self, config=BMConfigParser()).exec_() def click_actionHelp(self): dialogs.HelpDialog(self).exec_() @@ -2625,10 +2478,10 @@ class MyForm(settingsmixin.SMainWindow): def on_action_SpecialAddressBehaviorDialog(self): """Show SpecialAddressBehaviorDialog""" - dialogs.SpecialAddressBehaviorDialog(self, config) + dialogs.SpecialAddressBehaviorDialog(self, BMConfigParser()) def on_action_EmailGatewayDialog(self): - dialog = dialogs.EmailGatewayDialog(self, config=config) + dialog = dialogs.EmailGatewayDialog(self, config=BMConfigParser()) # For Modal dialogs dialog.exec_() try: @@ -2672,11 +2525,17 @@ class MyForm(settingsmixin.SMainWindow): if idCount == 0: return + font = QtGui.QFont() + font.setBold(False) + msgids = [] for i in range(0, idCount): - msgids.append(tableWidget.item(i, 3).data()) - for col in xrange(tableWidget.columnCount()): - tableWidget.item(i, col).setUnread(False) + 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) markread = sqlExecuteChunked( "UPDATE inbox SET read = 1 WHERE msgid IN({0}) AND read=0", @@ -2690,7 +2549,7 @@ class MyForm(settingsmixin.SMainWindow): dialogs.NewAddressDialog(self) def network_switch(self): - dontconnect_option = not config.safeGetBoolean( + dontconnect_option = not BMConfigParser().safeGetBoolean( 'bitmessagesettings', 'dontconnect') reply = QtGui.QMessageBox.question( self, _translate("MainWindow", "Disconnecting") @@ -2705,9 +2564,9 @@ class MyForm(settingsmixin.SMainWindow): QtGui.QMessageBox.Cancel) if reply != QtGui.QMessageBox.Yes: return - config.set( + BMConfigParser().set( 'bitmessagesettings', 'dontconnect', str(dontconnect_option)) - config.save() + BMConfigParser().save() self.ui.updateNetworkSwitchMenuLabel(dontconnect_option) self.ui.pushButtonFetchNamecoinID.setHidden( @@ -2744,8 +2603,10 @@ class MyForm(settingsmixin.SMainWindow): ) + "\n\n" + _translate( "MainWindow", "Wait until these tasks finish?"), - QtGui.QMessageBox.Yes | QtGui.QMessageBox.No - | QtGui.QMessageBox.Cancel, QtGui.QMessageBox.Cancel) + QtGui.QMessageBox.Yes | QtGui.QMessageBox.No | + QtGui.QMessageBox.Cancel, + QtGui.QMessageBox.Cancel + ) if reply == QtGui.QMessageBox.No: waitForPow = False elif reply == QtGui.QMessageBox.Cancel: @@ -2762,14 +2623,16 @@ class MyForm(settingsmixin.SMainWindow): " synchronisation finishes?", None, QtCore.QCoreApplication.CodecForTr, pendingDownload() ), - QtGui.QMessageBox.Yes | QtGui.QMessageBox.No - | QtGui.QMessageBox.Cancel, QtGui.QMessageBox.Cancel) + QtGui.QMessageBox.Yes | QtGui.QMessageBox.No | + QtGui.QMessageBox.Cancel, + QtGui.QMessageBox.Cancel + ) if reply == QtGui.QMessageBox.Yes: self.wait = waitForSync = True elif reply == QtGui.QMessageBox.Cancel: return - if state.statusIconColor == 'red' and not config.safeGetBoolean( + if shared.statusIconColor == 'red' and not BMConfigParser().safeGetBoolean( 'bitmessagesettings', 'dontconnect'): reply = QtGui.QMessageBox.question( self, _translate("MainWindow", "Not connected"), @@ -2779,8 +2642,10 @@ class MyForm(settingsmixin.SMainWindow): " 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) + QtGui.QMessageBox.Yes | QtGui.QMessageBox.No | + QtGui.QMessageBox.Cancel, + QtGui.QMessageBox.Cancel + ) if reply == QtGui.QMessageBox.Yes: waitForConnection = True self.wait = waitForSync = True @@ -2795,7 +2660,7 @@ class MyForm(settingsmixin.SMainWindow): if waitForConnection: self.updateStatusBar(_translate( "MainWindow", "Waiting for network connection...")) - while state.statusIconColor == 'red': + while shared.statusIconColor == 'red': time.sleep(0.5) QtCore.QCoreApplication.processEvents( QtCore.QEventLoop.AllEvents, 1000 @@ -2887,7 +2752,6 @@ class MyForm(settingsmixin.SMainWindow): QtCore.QEventLoop.AllEvents, 1000 ) shutdown.doCleanShutdown() - self.updateStatusBar(_translate( "MainWindow", "Stopping notifications... %1%").arg(90)) self.tray.hide() @@ -2896,21 +2760,20 @@ class MyForm(settingsmixin.SMainWindow): "MainWindow", "Shutdown imminent... %1%").arg(100)) logger.info("Shutdown complete") - self.close() - # FIXME: rewrite loops with timer instead - if self.wait: - self.destroy() - app.quit() + super(MyForm, myapp).close() + # return + sys.exit() + # window close event def closeEvent(self, event): - """window close event""" - event.ignore() - trayonclose = config.safeGetBoolean( + self.appIndicatorHide() + + trayonclose = BMConfigParser().safeGetBoolean( 'bitmessagesettings', 'trayonclose') - if trayonclose: - self.appIndicatorHide() - else: - # custom quit method + + event.ignore() + if not trayonclose: + # quit the application self.quit() def on_action_InboxMessageForceHtml(self): @@ -2949,7 +2812,8 @@ class MyForm(settingsmixin.SMainWindow): # modified = 0 for row in tableWidget.selectedIndexes(): currentRow = row.row() - msgid = tableWidget.item(currentRow, 3).data() + msgid = str(tableWidget.item( + currentRow, 3).data(QtCore.Qt.UserRole).toPyObject()) msgids.add(msgid) # if not tableWidget.item(currentRow, 0).unread: # modified += 1 @@ -2974,14 +2838,14 @@ class MyForm(settingsmixin.SMainWindow): # Format predefined text on message reply. def quoted_text(self, message): - 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) + 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) def quote_line(line): # Do quote empty lines. if line == '' or line.isspace(): @@ -2994,20 +2858,18 @@ class MyForm(settingsmixin.SMainWindow): 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 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 + 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) else: box.setCurrentIndex(0) @@ -3039,7 +2901,8 @@ class MyForm(settingsmixin.SMainWindow): acct = accountClass(toAddressAtCurrentInboxRow) fromAddressAtCurrentInboxRow = tableWidget.item( currentInboxRow, column_from).address - msgid = tableWidget.item(currentInboxRow, 3).data() + msgid = str(tableWidget.item( + currentInboxRow, 3).data(QtCore.Qt.UserRole).toPyObject()) queryreturn = sqlQuery( "SELECT message FROM inbox WHERE msgid=?", msgid ) or sqlQuery("SELECT message FROM sent WHERE ackdata=?", msgid) @@ -3061,7 +2924,7 @@ class MyForm(settingsmixin.SMainWindow): self.ui.tabWidgetSend.indexOf(self.ui.sendDirect) ) # toAddressAtCurrentInboxRow = fromAddressAtCurrentInboxRow - elif not config.has_section(toAddressAtCurrentInboxRow): + elif not BMConfigParser().has_section(toAddressAtCurrentInboxRow): QtGui.QMessageBox.information( self, _translate("MainWindow", "Address is gone"), _translate( @@ -3069,7 +2932,7 @@ class MyForm(settingsmixin.SMainWindow): "Bitmessage cannot find your address %1. Perhaps you" " removed it?" ).arg(toAddressAtCurrentInboxRow), QtGui.QMessageBox.Ok) - elif not config.getboolean( + elif not BMConfigParser().getboolean( toAddressAtCurrentInboxRow, 'enabled'): QtGui.QMessageBox.information( self, _translate("MainWindow", "Address disabled"), @@ -3122,7 +2985,7 @@ class MyForm(settingsmixin.SMainWindow): quotedText = self.quoted_text( unicode(messageAtCurrentInboxRow, 'utf-8', 'replace')) widget['message'].setPlainText(quotedText) - if acct.subject[0:3] in ('Re:', 'RE:'): + if acct.subject[0:3] in ['Re:', 'RE:']: widget['subject'].setText( tableWidget.item(currentInboxRow, 2).label) else: @@ -3138,6 +3001,7 @@ class MyForm(settingsmixin.SMainWindow): 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( @@ -3151,6 +3015,7 @@ class MyForm(settingsmixin.SMainWindow): 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( @@ -3159,8 +3024,7 @@ class MyForm(settingsmixin.SMainWindow): queryreturn = sqlQuery('''select * from blacklist where address=?''', addressAtCurrentInboxRow) if queryreturn == []: - label = "\"" + tableWidget.item(currentInboxRow, 2).subject + "\" in " + config.get( - recipientAddress, "label") + label = "\"" + tableWidget.item(currentInboxRow, 2).subject + "\" in " + BMConfigParser().get(recipientAddress, "label") sqlExecute('''INSERT INTO blacklist VALUES (?,?, ?)''', label, addressAtCurrentInboxRow, True) @@ -3175,28 +3039,23 @@ class MyForm(settingsmixin.SMainWindow): "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 = messageList.item(row, 3).data() + inventoryHash = str(messageList.item(row, 3).data( + QtCore.Qt.UserRole).toPyObject()) messageList.removeRow(row) elif inventoryHash is not None: for i in range(messageList.rowCount() - 1, -1, -1): - if messageList.item(i, 3).data() == inventoryHash: + if messageList.item(i, 3).data(QtCore.Qt.UserRole).toPyObject() == inventoryHash: messageList.removeRow(i) elif ackData is not None: for i in range(messageList.rowCount() - 1, -1, -1): - if messageList.item(i, 3).data() == ackData: + if messageList.item(i, 3).data(QtCore.Qt.UserRole).toPyObject() == ackData: messageList.removeRow(i) # Send item on the Inbox tab to trash @@ -3206,25 +3065,24 @@ class MyForm(settingsmixin.SMainWindow): return currentRow = 0 folder = self.getCurrentFolder() - shifted = QtGui.QApplication.queryKeyboardModifiers() \ - & QtCore.Qt.ShiftModifier - tableWidget.setUpdatesEnabled(False) - inventoryHashesToTrash = set() + shifted = QtGui.QApplication.queryKeyboardModifiers() & QtCore.Qt.ShiftModifier + tableWidget.setUpdatesEnabled(False); + inventoryHashesToTrash = [] # 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): - inventoryHashesToTrash.add( - tableWidget.item(r.topRow() + i, 3).data()) + 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) 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) sqlExecuteChunked( ("DELETE FROM inbox" if folder == "trash" or shifted else - "UPDATE inbox SET folder='trash', read=1") + + "UPDATE inbox SET folder='trash'") + " WHERE msgid IN ({0})", idCount, *inventoryHashesToTrash) tableWidget.selectRow(0 if currentRow == 0 else currentRow - 1) tableWidget.setUpdatesEnabled(True) @@ -3237,23 +3095,22 @@ class MyForm(settingsmixin.SMainWindow): return currentRow = 0 tableWidget.setUpdatesEnabled(False) - inventoryHashesToTrash = set() + inventoryHashesToTrash = [] # 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): - inventoryHashesToTrash.add( - tableWidget.item(r.topRow() + i, 3).data()) + 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) 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) 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() @@ -3271,7 +3128,8 @@ class MyForm(settingsmixin.SMainWindow): subjectAtCurrentInboxRow = '' # Retrieve the message data out of the SQL database - msgid = tableWidget.item(currentInboxRow, 3).data() + msgid = str(tableWidget.item( + currentInboxRow, 3).data(QtCore.Qt.UserRole).toPyObject()) queryreturn = sqlQuery( '''select message from inbox where msgid=?''', msgid) if queryreturn != []: @@ -3279,11 +3137,7 @@ class MyForm(settingsmixin.SMainWindow): 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: @@ -3303,7 +3157,8 @@ class MyForm(settingsmixin.SMainWindow): shifted = QtGui.QApplication.queryKeyboardModifiers() & QtCore.Qt.ShiftModifier while tableWidget.selectedIndexes() != []: currentRow = tableWidget.selectedIndexes()[0].row() - ackdataToTrash = tableWidget.item(currentRow, 3).data() + ackdataToTrash = str(tableWidget.item( + currentRow, 3).data(QtCore.Qt.UserRole).toPyObject()) sqlExecute( "DELETE FROM sent" if folder == "trash" or shifted else "UPDATE sent SET folder='trash'" @@ -3526,13 +3381,13 @@ class MyForm(settingsmixin.SMainWindow): 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: @@ -3550,16 +3405,18 @@ class MyForm(settingsmixin.SMainWindow): 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: @@ -3576,18 +3433,24 @@ class MyForm(settingsmixin.SMainWindow): if messagelist: currentRow = messagelist.currentRow() if currentRow >= 0: - return messagelist.item(currentRow, 3).data() + 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 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: @@ -3603,28 +3466,33 @@ class MyForm(settingsmixin.SMainWindow): 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): - return ( - messagelistList[currentIndex] if retObj - else messagelistList[currentIndex].text().toUtf8().data()) + if retObj: + return messagelistList[currentIndex] + else: + return messagelistList[currentIndex].text().toUtf8().data() + else: + return None 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() + return messagelistList[currentIndex].currentText().toUtf8().data() + else: + return None # Group of functions for the Your Identities dialog box def getCurrentItem(self, treeWidget=None): @@ -3681,12 +3549,12 @@ class MyForm(settingsmixin.SMainWindow): " delete the channel?" ), QtGui.QMessageBox.Yes | QtGui.QMessageBox.No ) == QtGui.QMessageBox.Yes: - config.remove_section(str(account.address)) + BMConfigParser().remove_section(str(account.address)) else: return else: return - config.save() + BMConfigParser().save() shared.reloadMyAddressHashes() self.rerenderAddressBook() self.rerenderComboBoxSendFrom() @@ -3702,8 +3570,8 @@ class MyForm(settingsmixin.SMainWindow): account.setEnabled(True) def enableIdentity(self, address): - config.set(address, 'enabled', 'true') - config.save() + BMConfigParser().set(address, 'enabled', 'true') + BMConfigParser().save() shared.reloadMyAddressHashes() self.rerenderAddressBook() @@ -3714,8 +3582,8 @@ class MyForm(settingsmixin.SMainWindow): account.setEnabled(False) def disableIdentity(self, address): - config.set(str(address), 'enabled', 'false') - config.save() + BMConfigParser().set(str(address), 'enabled', 'false') + BMConfigParser().save() shared.reloadMyAddressHashes() self.rerenderAddressBook() @@ -3728,11 +3596,12 @@ class MyForm(settingsmixin.SMainWindow): tableWidget = self.getCurrentMessagelist() currentColumn = tableWidget.currentColumn() currentRow = tableWidget.currentRow() - currentFolder = self.getCurrentFolder() - if currentColumn not in (0, 1, 2): # to, from, subject - currentColumn = 0 if currentFolder == "sent" else 1 - - if currentFolder == "sent": + if currentColumn not in [0, 1, 2]: # to, from, subject + if self.getCurrentFolder() == "sent": + currentColumn = 0 + else: + currentColumn = 1 + if self.getCurrentFolder() == "sent": myAddress = tableWidget.item(currentRow, 1).data(QtCore.Qt.UserRole) otherAddress = tableWidget.item(currentRow, 0).data(QtCore.Qt.UserRole) else: @@ -3740,23 +3609,23 @@ class MyForm(settingsmixin.SMainWindow): 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( @@ -3766,36 +3635,19 @@ class MyForm(settingsmixin.SMainWindow): 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'] - - 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'] + # 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'} 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): @@ -3806,34 +3658,28 @@ class MyForm(settingsmixin.SMainWindow): 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: @@ -4019,7 +3865,8 @@ class MyForm(settingsmixin.SMainWindow): # Check to see if this item is toodifficult and display an additional # menu option (Force Send) if it is. if currentRow >= 0: - ackData = self.ui.tableWidgetInbox.item(currentRow, 3).data() + ackData = str(self.ui.tableWidgetInbox.item( + currentRow, 3).data(QtCore.Qt.UserRole).toPyObject()) queryreturn = sqlQuery('''SELECT status FROM sent where ackdata=?''', ackData) for row in queryreturn: status, = row @@ -4030,27 +3877,25 @@ class MyForm(settingsmixin.SMainWindow): def inboxSearchLineEditUpdated(self, text): # dynamic search for too short text is slow - text = text.toUtf8() - if 0 < len(text) < 3: + if len(str(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, text) + self.loadMessagelist(messagelist, account, folder, searchOption, str(text)) def inboxSearchLineEditReturnPressed(self): logger.debug("Search return pressed") searchLine = self.getCurrentSearchLine() messagelist = self.getCurrentMessagelist() - if messagelist and len(str(searchLine)) < 3: + if len(str(searchLine)) < 3: searchOption = self.getCurrentSearchOption() account = self.getCurrentAccount() folder = self.getCurrentFolder() - self.loadMessagelist( - messagelist, account, folder, searchOption, searchLine) + self.loadMessagelist(messagelist, account, folder, searchOption, searchLine) + if messagelist: messagelist.setFocus() def treeWidgetItemClicked(self): @@ -4073,12 +3918,10 @@ class MyForm(settingsmixin.SMainWindow): if column != 0: return # only account names of normal addresses (no chans/mailinglists) - if (not isinstance(item, Ui_AddressWidget)) or \ - (not self.getCurrentTreeWidget()) or \ - self.getCurrentTreeWidget().currentItem() is None: + if (not isinstance(item, Ui_AddressWidget)) or (not self.getCurrentTreeWidget()) or self.getCurrentTreeWidget().currentItem() is None: return # not visible - if (not self.getCurrentItem()) or (not isinstance(self.getCurrentItem(), Ui_AddressWidget)): + if (not self.getCurrentItem()) or (not isinstance (self.getCurrentItem(), Ui_AddressWidget)): return # only currently selected item if item.address != self.getCurrentAccount(): @@ -4086,7 +3929,7 @@ class MyForm(settingsmixin.SMainWindow): # "All accounts" can't be renamed if item.type == AccountMixin.ALL: return - + newLabel = unicode(item.text(0), 'utf-8', 'ignore') oldLabel = item.defaultLabel() @@ -4111,12 +3954,12 @@ class MyForm(settingsmixin.SMainWindow): 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=?''' % ( @@ -4177,15 +4020,12 @@ class MyForm(settingsmixin.SMainWindow): self.rerenderAddressBook() def updateStatusBar(self, data): - try: - message, option = data - except ValueError: + if type(data) is tuple or type(data) is list: + option = data[1] + message = data[0] + else: option = 0 message = data - except TypeError: - logger.debug( - 'Invalid argument for updateStatusBar!', exc_info=True) - if message != "": logger.info('Status bar: ' + message) @@ -4200,17 +4040,20 @@ class MyForm(settingsmixin.SMainWindow): # Check to see whether we can connect to namecoin. # Hide the 'Fetch Namecoin ID' button if we can't. - if config.safeGetBoolean( - 'bitmessagesettings', 'dontconnect' + 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') + '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 \ @@ -4220,11 +4063,20 @@ class MyForm(settingsmixin.SMainWindow): obj.loadSettings() +# 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 BitmessageQtApplication(QtGui.QApplication): +class MySingleApplication(QtGui.QApplication): """ Listener to allow our Qt form to get focus when another instance of the application is open. @@ -4236,29 +4088,9 @@ class BitmessageQtApplication(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(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))) + super(MySingleApplication, self).__init__(*argv) + id = MySingleApplication.uuid self.server = None self.is_running = False @@ -4286,8 +4118,6 @@ class BitmessageQtApplication(QtGui.QApplication): 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() @@ -4300,30 +4130,34 @@ class BitmessageQtApplication(QtGui.QApplication): def init(): global app if not app: - app = BitmessageQtApplication(sys.argv) + app = MySingleApplication(sys.argv) return app def run(): global myapp app = init() + 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 config.get('bitmessagesettings', 'mailchuck') < 1: -# myapp.showMigrationWizard(config.get('bitmessagesettings', 'mailchuck')) +# if BMConfigParser().get('bitmessagesettings', 'mailchuck') < 1: +# myapp.showMigrationWizard(BMConfigParser().get('bitmessagesettings', 'mailchuck')) # except: # myapp.showMigrationWizard(0) - + # only show after wizards and connect dialogs have completed - if not config.getboolean('bitmessagesettings', 'startintray'): + if not BMConfigParser().getboolean('bitmessagesettings', 'startintray'): myapp.show() - QtCore.QTimer.singleShot( - 30000, lambda: myapp.setStatusIcon(state.statusIconColor)) - app.exec_() + sys.exit(app.exec_()) diff --git a/src/bitmessageqt/about.ui b/src/bitmessageqt/about.ui index 49bd4eca..7073bbd1 100644 --- a/src/bitmessageqt/about.ui +++ b/src/bitmessageqt/about.ui @@ -46,7 +46,7 @@ - <html><head/><body><p>Copyright © 2012-2016 Jonathan Warren<br/>Copyright © 2012-2022 The Bitmessage Developers</p></body></html> + <html><head/><body><p>Copyright © 2012-2016 Jonathan Warren<br/>Copyright © 2012-2020 The Bitmessage Developers</p></body></html> Qt::AlignLeft diff --git a/src/bitmessageqt/account.py b/src/bitmessageqt/account.py index 8c82c6f6..50ea3548 100644 --- a/src/bitmessageqt/account.py +++ b/src/bitmessageqt/account.py @@ -18,13 +18,32 @@ from PyQt4 import QtGui import queues from addresses import decodeAddress -from bmconfigparser import config +from bmconfigparser import BMConfigParser from helper_ackPayload import genAckPayload from helper_sql import sqlQuery, sqlExecute from .foldertree import AccountMixin from .utils import str_broadcast_subscribers +def getSortedAccounts(): + """Get a sorted list of configSections""" + + 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 + + def getSortedSubscriptions(count=False): """ Actually return a grouped dictionary rather than a sorted list @@ -61,7 +80,7 @@ def getSortedSubscriptions(count=False): def accountClass(address): """Return a BMAccount for the address""" - if not config.has_section(address): + if not BMConfigParser().has_section(address): # .. todo:: This BROADCAST section makes no sense if address == str_broadcast_subscribers: subscription = BroadcastAccount(address) @@ -74,7 +93,7 @@ def accountClass(address): return NoAccount(address) return subscription try: - gateway = config.get(address, "gateway") + gateway = BMConfigParser().get(address, "gateway") for _, cls in inspect.getmembers(sys.modules[__name__], inspect.isclass): if issubclass(cls, GatewayAccount) and cls.gatewayName == gateway: return cls(address) @@ -95,9 +114,9 @@ class AccountColor(AccountMixin): # pylint: disable=too-few-public-methods if address_type is None: if address is None: self.type = AccountMixin.ALL - elif config.safeGetBoolean(self.address, 'mailinglist'): + elif BMConfigParser().safeGetBoolean(self.address, 'mailinglist'): self.type = AccountMixin.MAILINGLIST - elif config.safeGetBoolean(self.address, 'chan'): + elif BMConfigParser().safeGetBoolean(self.address, 'chan'): self.type = AccountMixin.CHAN elif sqlQuery( '''select label from subscriptions where address=?''', self.address): @@ -114,10 +133,10 @@ class BMAccount(object): def __init__(self, address=None): self.address = address self.type = AccountMixin.NORMAL - if config.has_section(address): - if config.safeGetBoolean(self.address, 'chan'): + if BMConfigParser().has_section(address): + if BMConfigParser().safeGetBoolean(self.address, 'chan'): self.type = AccountMixin.CHAN - elif config.safeGetBoolean(self.address, 'mailinglist'): + elif BMConfigParser().safeGetBoolean(self.address, 'mailinglist'): self.type = AccountMixin.MAILINGLIST elif self.address == str_broadcast_subscribers: self.type = AccountMixin.BROADCAST @@ -131,7 +150,7 @@ class BMAccount(object): """Get a label for this bitmessage account""" if address is None: address = self.address - label = config.safeGet(address, 'label', address) + label = BMConfigParser().safeGet(address, 'label', address) queryreturn = sqlQuery( '''select label from addressbook where address=?''', address) if queryreturn != []: @@ -197,7 +216,7 @@ class GatewayAccount(BMAccount): # pylint: disable=unused-variable status, addressVersionNumber, streamNumber, ripe = decodeAddress(self.toAddress) - stealthLevel = config.safeGetInt('bitmessagesettings', 'ackstealthlevel') + stealthLevel = BMConfigParser().safeGetInt('bitmessagesettings', 'ackstealthlevel') ackdata = genAckPayload(streamNumber, stealthLevel) sqlExecute( '''INSERT INTO sent VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''', @@ -216,7 +235,7 @@ class GatewayAccount(BMAccount): 'sent', # folder 2, # encodingtype # not necessary to have a TTL higher than 2 days - min(config.getint('bitmessagesettings', 'ttl'), 86400 * 2) + min(BMConfigParser().getint('bitmessagesettings', 'ttl'), 86400 * 2) ) queues.workerQueue.put(('sendmessage', self.toAddress)) diff --git a/src/bitmessageqt/address_dialogs.py b/src/bitmessageqt/address_dialogs.py index bf571041..60c10369 100644 --- a/src/bitmessageqt/address_dialogs.py +++ b/src/bitmessageqt/address_dialogs.py @@ -1,7 +1,9 @@ """ -Dialogs that work with BM address. +src/bitmessageqt/address_dialogs.py +=================================== + """ -# pylint: disable=attribute-defined-outside-init,too-few-public-methods,relative-import +# pylint: disable=attribute-defined-outside-init import hashlib @@ -9,15 +11,16 @@ from PyQt4 import QtCore, QtGui import queues import widgets -import state -from account import AccountMixin, GatewayAccount, MailchuckAccount, accountClass +from account import AccountMixin, GatewayAccount, MailchuckAccount, accountClass, getSortedAccounts from addresses import addBMIfNotPresent, decodeAddress, encodeVarint -from bmconfigparser import config as global_config +from inventory import Inventory +from retranslateui import RetranslateMixin from tr import _translate class AddressCheckMixin(object): """Base address validation class for QT UI""" + # pylint: disable=too-few-public-methods def __init__(self): self.valid = False @@ -30,9 +33,7 @@ class AddressCheckMixin(object): pass def addressChanged(self, QString): - """ - Address validation callback, performs validation and gives feedback - """ + """Address validation callback, performs validation and gives feedback""" status, addressVersion, streamNumber, ripe = decodeAddress( str(QString)) self.valid = status == 'success' @@ -101,8 +102,8 @@ class AddressDataDialog(QtGui.QDialog, AddressCheckMixin): super(AddressDataDialog, self).accept() -class AddAddressDialog(AddressDataDialog): - """QDialog for adding a new address""" +class AddAddressDialog(AddressDataDialog, RetranslateMixin): + """QDialog for adding a new address, with validation and translation""" def __init__(self, parent=None, address=None): super(AddAddressDialog, self).__init__(parent) @@ -112,8 +113,8 @@ class AddAddressDialog(AddressDataDialog): self.lineEditAddress.setText(address) -class NewAddressDialog(QtGui.QDialog): - """QDialog for generating a new address""" +class NewAddressDialog(QtGui.QDialog, RetranslateMixin): + """QDialog for generating a new address, with translation""" def __init__(self, parent=None): super(NewAddressDialog, self).__init__(parent) @@ -121,7 +122,7 @@ class NewAddressDialog(QtGui.QDialog): # Let's fill out the 'existing address' combo box with addresses # from the 'Your Identities' tab. - for address in global_config.addresses(True): + for address in getSortedAccounts(): self.radioButtonExisting.click() self.comboBoxExisting.addItem(address) self.groupBoxDeterministic.setHidden(True) @@ -174,8 +175,8 @@ class NewAddressDialog(QtGui.QDialog): )) -class NewSubscriptionDialog(AddressDataDialog): - """QDialog for subscribing to an address""" +class NewSubscriptionDialog(AddressDataDialog, RetranslateMixin): + """QDialog for subscribing to an address, with validation and translation""" def __init__(self, parent=None): super(NewSubscriptionDialog, self).__init__(parent) @@ -190,13 +191,13 @@ class NewSubscriptionDialog(AddressDataDialog): " broadcasts." )) else: - state.Inventory.flush() + Inventory().flush() doubleHashOfAddressData = hashlib.sha512(hashlib.sha512( - encodeVarint(addressVersion) - + encodeVarint(streamNumber) + ripe + encodeVarint(addressVersion) + + encodeVarint(streamNumber) + ripe ).digest()).digest() tag = doubleHashOfAddressData[32:] - self.recent = state.Inventory.by_type_and_tag(3, tag) + self.recent = Inventory().by_type_and_tag(3, tag) count = len(self.recent) if count == 0: self.checkBoxDisplayMessagesAlreadyInInventory.setText( @@ -217,8 +218,8 @@ class NewSubscriptionDialog(AddressDataDialog): )) -class RegenerateAddressesDialog(QtGui.QDialog): - """QDialog for regenerating deterministic addresses""" +class RegenerateAddressesDialog(QtGui.QDialog, RetranslateMixin): + """QDialog for regenerating deterministic addresses, with translation""" def __init__(self, parent=None): super(RegenerateAddressesDialog, self).__init__(parent) widgets.load('regenerateaddresses.ui', self) @@ -226,12 +227,10 @@ class RegenerateAddressesDialog(QtGui.QDialog): QtGui.QWidget.resize(self, QtGui.QWidget.sizeHint(self)) -class SpecialAddressBehaviorDialog(QtGui.QDialog): - """ - QDialog for special address behaviour (e.g. mailing list functionality) - """ +class SpecialAddressBehaviorDialog(QtGui.QDialog, RetranslateMixin): + """QDialog for special address behaviour (e.g. mailing list functionality), with translation""" - def __init__(self, parent=None, config=global_config): + def __init__(self, parent=None, config=None): super(SpecialAddressBehaviorDialog, self).__init__(parent) widgets.load('specialaddressbehavior.ui', self) self.address = parent.getCurrentAccount() @@ -257,7 +256,11 @@ class SpecialAddressBehaviorDialog(QtGui.QDialog): self.radioButtonBehaviorMailingList.click() else: self.radioButtonBehaveNormalAddress.click() - mailingListName = config.safeGet(self.address, 'mailinglistname', '') + try: + mailingListName = config.get( + self.address, 'mailinglistname') + except: + mailingListName = '' self.lineEditMailingListName.setText( unicode(mailingListName, 'utf-8') ) @@ -291,9 +294,9 @@ class SpecialAddressBehaviorDialog(QtGui.QDialog): self.parent.rerenderMessagelistToLabels() -class EmailGatewayDialog(QtGui.QDialog): - """QDialog for email gateway control""" - def __init__(self, parent, config=global_config, account=None): +class EmailGatewayDialog(QtGui.QDialog, RetranslateMixin): + """QDialog for email gateway control, with translation""" + def __init__(self, parent, config=None, account=None): super(EmailGatewayDialog, self).__init__(parent) widgets.load('emailgateway.ui', self) self.parent = parent diff --git a/src/bitmessageqt/addressvalidator.py b/src/bitmessageqt/addressvalidator.py index dc61b41c..f9de70a2 100644 --- a/src/bitmessageqt/addressvalidator.py +++ b/src/bitmessageqt/addressvalidator.py @@ -1,30 +1,14 @@ -""" -Address validator module. -""" -# pylint: disable=too-many-branches,too-many-arguments - +from PyQt4 import QtGui from Queue import Empty -from PyQt4 import QtGui - from addresses import decodeAddress, addBMIfNotPresent -from bmconfigparser import config +from account import getSortedAccounts from queues import apiAddressGeneratorReturnQueue, addressGeneratorQueue from tr import _translate from utils import str_chan - -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""" +class AddressPassPhraseValidatorMixin(): + def setParams(self, passPhraseObject=None, addressObject=None, feedBackObject=None, buttonBox=None, addressMandatory=True): self.addressObject = addressObject self.passPhraseObject = passPhraseObject self.feedBackObject = feedBackObject @@ -35,7 +19,6 @@ class AddressPassPhraseValidatorMixin(object): 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) @@ -46,14 +29,11 @@ class AddressPassPhraseValidatorMixin(object): 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) @@ -66,13 +46,12 @@ class AddressPassPhraseValidatorMixin(object): 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 None + return while True: try: @@ -81,30 +60,25 @@ class AddressPassPhraseValidatorMixin(object): if gotOne: break else: - return None + return else: gotOne = True - if not addressGeneratorReturnValue: + if len(addressGeneratorReturnValue) == 0: 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 - return QtGui.QValidator.Intermediate + else: + return QtGui.QValidator.Intermediate def validate(self, s, pos): - """Top level validator method""" if self.addressObject is None: address = None else: @@ -125,21 +99,15 @@ class AddressPassPhraseValidatorMixin(object): if self.addressMandatory or address is not None: # check if address already exists: - if address in config.addresses(): + if address in getSortedAccounts(): 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.")) @@ -154,28 +122,23 @@ class AddressPassPhraseValidatorMixin(object): if address is None: addressGeneratorQueue.put(('createChan', 4, 1, str_chan + ' ' + str(passPhrase), passPhrase, False)) else: - addressGeneratorQueue.put( - ('joinChan', addBMIfNotPresent(address), - "{} {}".format(str_chan, passPhrase), passPhrase, False)) + addressGeneratorQueue.put(('joinChan', addBMIfNotPresent(address), str_chan + ' ' + str(passPhrase), passPhrase, False)) if self.buttonBox.button(QtGui.QDialogButtonBox.Ok).hasFocus(): return (self.returnValid(), pos) - return (QtGui.QValidator.Intermediate, pos) + else: + 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 bee8fd57..30d054d0 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 config +from bmconfigparser import BMConfigParser from foldertree import AddressBookCompleter from messageview import MessageView from messagecompose import MessageCompose @@ -24,28 +24,24 @@ except AttributeError: 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) @@ -61,8 +57,7 @@ class Ui_MainWindow(object): self.tabWidget.setMinimumSize(QtCore.QSize(0, 0)) self.tabWidget.setBaseSize(QtCore.QSize(0, 0)) font = QtGui.QFont() - base_size = QtGui.QApplication.instance().font().pointSize() - font.setPointSize(int(base_size * 0.75)) + font.setPointSize(9) self.tabWidget.setFont(font) self.tabWidget.setTabPosition(QtGui.QTabWidget.North) self.tabWidget.setTabShape(QtGui.QTabWidget.Rounded) @@ -80,9 +75,7 @@ class Ui_MainWindow(object): 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) @@ -111,7 +104,6 @@ class Ui_MainWindow(object): 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) @@ -183,9 +175,7 @@ class Ui_MainWindow(object): 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() @@ -386,9 +376,7 @@ class Ui_MainWindow(object): 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) @@ -415,8 +403,8 @@ class Ui_MainWindow(object): 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) @@ -467,9 +455,7 @@ class Ui_MainWindow(object): 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")) @@ -489,9 +475,7 @@ class Ui_MainWindow(object): 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) @@ -520,7 +504,6 @@ class Ui_MainWindow(object): 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) @@ -571,14 +554,12 @@ class Ui_MainWindow(object): 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 config.get('bitmessagesettings', 'blackwhitelist') == 'white': + if BMConfigParser().get('bitmessagesettings', 'blackwhitelist') == 'white': self.blackwhitelist.radioButtonWhitelist.click() self.blackwhitelist.rerenderBlackWhiteList() @@ -680,7 +661,7 @@ class Ui_MainWindow(object): def updateNetworkSwitchMenuLabel(self, dontconnect=None): if dontconnect is None: - dontconnect = config.safeGetBoolean( + dontconnect = BMConfigParser().safeGetBoolean( 'bitmessagesettings', 'dontconnect') self.actionNetworkSwitch.setText( _translate("MainWindow", "Go online", None) @@ -718,24 +699,19 @@ class Ui_MainWindow(object): self.label_3.setText(_translate("MainWindow", "Subject:", None)) self.label_2.setText(_translate("MainWindow", "From:", None)) self.label.setText(_translate("MainWindow", "To:", None)) - self.tabWidgetSend.setTabText( - self.tabWidgetSend.indexOf(self.sendDirect), _translate("MainWindow", "Send ordinary Message", None) - ) + #self.textEditMessage.setHtml("") + 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.tabWidgetSend.setTabText( - self.tabWidgetSend.indexOf(self.sendBroadcast), - _translate("MainWindow", "Send Message to your Subscribers", None) - ) + #self.textEditMessageBroadcast.setHtml("") + 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(config.getint('bitmessagesettings', 'ttl') / 60 / 60) + hours = int(BMConfigParser().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)) @@ -743,9 +719,10 @@ class Ui_MainWindow(object): 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", "From", None)) - self.inboxSearchOptionSubscriptions.setItemText(2, _translate("MainWindow", "Subject", None)) - self.inboxSearchOptionSubscriptions.setItemText(3, _translate("MainWindow", "Message", 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.tableWidgetInboxSubscriptions.setSortingEnabled(True) item = self.tableWidgetInboxSubscriptions.horizontalHeaderItem(0) item.setText(_translate("MainWindow", "To", None)) @@ -755,10 +732,7 @@ class Ui_MainWindow(object): item.setText(_translate("MainWindow", "Subject", None)) item = self.tableWidgetInboxSubscriptions.horizontalHeaderItem(3) item.setText(_translate("MainWindow", "Received", None)) - self.tabWidget.setTabText( - self.tabWidget.indexOf(self.subscriptions), - _translate("MainWindow", "Subscriptions", None) - ) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.subscriptions), _translate("MainWindow", "Subscriptions", None)) self.treeWidgetChans.headerItem().setText(0, _translate("MainWindow", "Chans", None)) self.pushButtonAddChan.setText(_translate("MainWindow", "Add Chan", None)) self.inboxSearchLineEditChans.setPlaceholderText(_translate("MainWindow", "Search", None)) @@ -778,15 +752,9 @@ class Ui_MainWindow(object): item.setText(_translate("MainWindow", "Received", None)) self.tabWidget.setTabText(self.tabWidget.indexOf(self.chans), _translate("MainWindow", "Chans", None)) self.blackwhitelist.retranslateUi() - self.tabWidget.setTabText( - self.tabWidget.indexOf(self.blackwhitelist), - _translate("blacklist", "Blacklist", None) - ) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.blackwhitelist), _translate("blacklist", "Blacklist", None)) self.networkstatus.retranslateUi() - self.tabWidget.setTabText( - self.tabWidget.indexOf(self.networkstatus), - _translate("networkstatus", "Network Status", None) - ) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.networkstatus), _translate("networkstatus", "Network Status", None)) self.menuFile.setTitle(_translate("MainWindow", "File", None)) self.menuSettings.setTitle(_translate("MainWindow", "Settings", None)) self.menuHelp.setTitle(_translate("MainWindow", "Help", None)) @@ -799,20 +767,19 @@ class Ui_MainWindow(object): self.actionSupport.setText(_translate("MainWindow", "Contact support", None)) self.actionAbout.setText(_translate("MainWindow", "About", None)) self.actionSettings.setText(_translate("MainWindow", "Settings", None)) - self.actionRegenerateDeterministicAddresses.setText( - _translate("MainWindow", "Regenerate deterministic addresses", None) - ) + self.actionRegenerateDeterministicAddresses.setText(_translate("MainWindow", "Regenerate deterministic addresses", None)) self.actionDeleteAllTrashedMessages.setText(_translate("MainWindow", "Delete all trashed messages", None)) self.actionJoinChan.setText(_translate("MainWindow", "Join / Create chan", None)) - self.updateNetworkSwitchMenuLabel() +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 093f23d8..3897bddc 100644 --- a/src/bitmessageqt/blacklist.py +++ b/src/bitmessageqt/blacklist.py @@ -2,7 +2,7 @@ from PyQt4 import QtCore, QtGui import widgets from addresses import addBMIfNotPresent -from bmconfigparser import config +from bmconfigparser import BMConfigParser from dialogs import AddAddressDialog from helper_sql import sqlExecute, sqlQuery from queues import UISignalQueue @@ -39,17 +39,17 @@ class Blacklist(QtGui.QWidget, RetranslateMixin): "rerenderBlackWhiteList()"), self.rerenderBlackWhiteList) def click_radioButtonBlacklist(self): - if config.get('bitmessagesettings', 'blackwhitelist') == 'white': - config.set('bitmessagesettings', 'blackwhitelist', 'black') - config.save() + if BMConfigParser().get('bitmessagesettings', 'blackwhitelist') == 'white': + BMConfigParser().set('bitmessagesettings', 'blackwhitelist', 'black') + BMConfigParser().save() # self.tableWidgetBlacklist.clearContents() self.tableWidgetBlacklist.setRowCount(0) self.rerenderBlackWhiteList() def click_radioButtonWhitelist(self): - if config.get('bitmessagesettings', 'blackwhitelist') == 'black': - config.set('bitmessagesettings', 'blackwhitelist', 'white') - config.save() + if BMConfigParser().get('bitmessagesettings', 'blackwhitelist') == 'black': + BMConfigParser().set('bitmessagesettings', 'blackwhitelist', 'white') + BMConfigParser().save() # self.tableWidgetBlacklist.clearContents() self.tableWidgetBlacklist.setRowCount(0) self.rerenderBlackWhiteList() @@ -65,7 +65,7 @@ class Blacklist(QtGui.QWidget, RetranslateMixin): # address book. The user cannot add it again or else it will # cause problems when updating and deleting the entry. t = (address,) - if config.get('bitmessagesettings', 'blackwhitelist') == 'black': + if BMConfigParser().get('bitmessagesettings', 'blackwhitelist') == 'black': sql = '''select * from blacklist where address=?''' else: sql = '''select * from whitelist where address=?''' @@ -83,7 +83,7 @@ class Blacklist(QtGui.QWidget, RetranslateMixin): self.tableWidgetBlacklist.setItem(0, 1, newItem) self.tableWidgetBlacklist.setSortingEnabled(True) t = (str(self.NewBlacklistDialogInstance.lineEditLabel.text().toUtf8()), address, True) - if config.get('bitmessagesettings', 'blackwhitelist') == 'black': + if BMConfigParser().get('bitmessagesettings', 'blackwhitelist') == 'black': sql = '''INSERT INTO blacklist VALUES (?,?,?)''' else: sql = '''INSERT INTO whitelist VALUES (?,?,?)''' @@ -158,12 +158,12 @@ class Blacklist(QtGui.QWidget, RetranslateMixin): def rerenderBlackWhiteList(self): tabs = self.parent().parent() - if config.get('bitmessagesettings', 'blackwhitelist') == 'black': + if BMConfigParser().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 = config.get('bitmessagesettings', 'blackwhitelist') + listType = BMConfigParser().get('bitmessagesettings', 'blackwhitelist') if listType == 'black': queryreturn = sqlQuery('''SELECT label, address, enabled FROM blacklist''') else: @@ -195,7 +195,7 @@ class Blacklist(QtGui.QWidget, RetranslateMixin): currentRow, 0).text().toUtf8() addressAtCurrentRow = self.tableWidgetBlacklist.item( currentRow, 1).text() - if config.get('bitmessagesettings', 'blackwhitelist') == 'black': + if BMConfigParser().get('bitmessagesettings', 'blackwhitelist') == 'black': sqlExecute( '''DELETE FROM blacklist WHERE label=? AND address=?''', str(labelAtCurrentRow), str(addressAtCurrentRow)) @@ -224,7 +224,7 @@ class Blacklist(QtGui.QWidget, RetranslateMixin): currentRow, 0).setTextColor(QtGui.QApplication.palette().text().color()) self.tableWidgetBlacklist.item( currentRow, 1).setTextColor(QtGui.QApplication.palette().text().color()) - if config.get('bitmessagesettings', 'blackwhitelist') == 'black': + if BMConfigParser().get('bitmessagesettings', 'blackwhitelist') == 'black': sqlExecute( '''UPDATE blacklist SET enabled=1 WHERE address=?''', str(addressAtCurrentRow)) @@ -241,7 +241,7 @@ class Blacklist(QtGui.QWidget, RetranslateMixin): currentRow, 0).setTextColor(QtGui.QColor(128, 128, 128)) self.tableWidgetBlacklist.item( currentRow, 1).setTextColor(QtGui.QColor(128, 128, 128)) - if config.get('bitmessagesettings', 'blackwhitelist') == 'black': + if BMConfigParser().get('bitmessagesettings', 'blackwhitelist') == 'black': sqlExecute( '''UPDATE blacklist SET enabled=0 WHERE address=?''', str(addressAtCurrentRow)) else: diff --git a/src/bitmessageqt/dialogs.py b/src/bitmessageqt/dialogs.py index dc31e266..c667edb1 100644 --- a/src/bitmessageqt/dialogs.py +++ b/src/bitmessageqt/dialogs.py @@ -1,7 +1,8 @@ """ -Custom dialog classes +src/bitmessageqt/dialogs.py +=========================== """ -# pylint: disable=too-few-public-methods + from PyQt4 import QtGui import paths @@ -12,6 +13,7 @@ from address_dialogs import ( SpecialAddressBehaviorDialog ) from newchandialog import NewChanDialog +from retranslateui import RetranslateMixin from settings import SettingsDialog from tr import _translate from version import softwareVersion @@ -25,7 +27,7 @@ __all__ = [ ] -class AboutDialog(QtGui.QDialog): +class AboutDialog(QtGui.QDialog, RetranslateMixin): """The `About` dialog""" def __init__(self, parent=None): super(AboutDialog, self).__init__(parent) @@ -45,7 +47,7 @@ class AboutDialog(QtGui.QDialog): try: self.label_2.setText( self.label_2.text().replace( - '2022', str(last_commit.get('time').year) + '2020', str(last_commit.get('time').year) )) except AttributeError: pass @@ -53,7 +55,7 @@ class AboutDialog(QtGui.QDialog): self.setFixedSize(QtGui.QWidget.sizeHint(self)) -class IconGlossaryDialog(QtGui.QDialog): +class IconGlossaryDialog(QtGui.QDialog, RetranslateMixin): """The `Icon Glossary` dialog, explaining the status icon colors""" def __init__(self, parent=None, config=None): super(IconGlossaryDialog, self).__init__(parent) @@ -69,7 +71,7 @@ class IconGlossaryDialog(QtGui.QDialog): self.setFixedSize(QtGui.QWidget.sizeHint(self)) -class HelpDialog(QtGui.QDialog): +class HelpDialog(QtGui.QDialog, RetranslateMixin): """The `Help` dialog""" def __init__(self, parent=None): super(HelpDialog, self).__init__(parent) @@ -77,7 +79,7 @@ class HelpDialog(QtGui.QDialog): self.setFixedSize(QtGui.QWidget.sizeHint(self)) -class ConnectDialog(QtGui.QDialog): +class ConnectDialog(QtGui.QDialog, RetranslateMixin): """The `Connect` dialog""" def __init__(self, parent=None): super(ConnectDialog, self).__init__(parent) diff --git a/src/bitmessageqt/foldertree.py b/src/bitmessageqt/foldertree.py index c50b7d3d..2e7e735f 100644 --- a/src/bitmessageqt/foldertree.py +++ b/src/bitmessageqt/foldertree.py @@ -1,14 +1,14 @@ """ -Folder tree and messagelist widgets definitions. +src/bitmessageqt/foldertree.py +============================== """ -# pylint: disable=too-many-arguments,bad-super-call -# pylint: disable=attribute-defined-outside-init +# pylint: disable=too-many-arguments,bad-super-call,attribute-defined-outside-init from cgi import escape from PyQt4 import QtCore, QtGui -from bmconfigparser import config +from bmconfigparser import BMConfigParser from helper_sql import sqlExecute, sqlQuery from settingsmixin import SettingsMixin from tr import _translate @@ -20,8 +20,6 @@ _translate("MainWindow", "new") _translate("MainWindow", "sent") _translate("MainWindow", "trash") -TimestampRole = QtCore.Qt.UserRole + 1 - class AccountMixin(object): """UI-related functionality for accounts""" @@ -106,9 +104,9 @@ class AccountMixin(object): if self.address is None: self.type = self.ALL self.setFlags(self.flags() & ~QtCore.Qt.ItemIsEditable) - elif config.safeGetBoolean(self.address, 'chan'): + elif BMConfigParser().safeGetBoolean(self.address, 'chan'): self.type = self.CHAN - elif config.safeGetBoolean(self.address, 'mailinglist'): + elif BMConfigParser().safeGetBoolean(self.address, 'mailinglist'): self.type = self.MAILINGLIST elif sqlQuery( '''select label from subscriptions where address=?''', self.address): @@ -125,7 +123,7 @@ class AccountMixin(object): AccountMixin.CHAN, AccountMixin.MAILINGLIST): try: retval = unicode( - config.get(self.address, 'label'), 'utf-8') + BMConfigParser().get(self.address, 'label'), 'utf-8') except Exception: queryreturn = sqlQuery( '''select label from addressbook where address=?''', self.address) @@ -237,7 +235,7 @@ class Ui_AddressWidget(BMTreeWidgetItem, SettingsMixin): else: try: return unicode( - config.get(self.address, 'label'), + BMConfigParser().get(self.address, 'label'), 'utf-8', 'ignore') except: return unicode(self.address, 'utf-8') @@ -263,13 +261,13 @@ class Ui_AddressWidget(BMTreeWidgetItem, SettingsMixin): """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: - config.set( + BMConfigParser().set( str(self.address), 'label', str(value.toString().toUtf8()) if isinstance(value, QtCore.QVariant) else value.encode('utf-8') ) - config.save() + BMConfigParser().save() return super(Ui_AddressWidget, self).setData(column, role, value) def setAddress(self, address): @@ -336,14 +334,13 @@ class Ui_SubscriptionWidget(Ui_AddressWidget): class BMTableWidgetItem(QtGui.QTableWidgetItem, SettingsMixin): """A common abstract class for Table widget item""" - def __init__(self, label=None, unread=False): + def __init__(self, parent=None, label=None, unread=False): super(QtGui.QTableWidgetItem, self).__init__() self.setLabel(label) self.setUnread(unread) self._setup() - - def _setup(self): - self.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) + if parent is not None: + parent.append(self) def setLabel(self, label): """Set object label""" @@ -356,7 +353,7 @@ class BMTableWidgetItem(QtGui.QTableWidgetItem, SettingsMixin): def data(self, role): """Return object data (QT UI)""" if role in ( - QtCore.Qt.DisplayRole, QtCore.Qt.EditRole, QtCore.Qt.ToolTipRole + QtCore.Qt.DisplayRole, QtCore.Qt.EditRole, QtCore.Qt.ToolTipRole ): return self.label elif role == QtCore.Qt.FontRole: @@ -370,9 +367,7 @@ 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 @@ -382,7 +377,7 @@ class BMAddressWidget(BMTableWidgetItem, AccountMixin): if role == QtCore.Qt.ToolTipRole: return self.label + " (" + self.address + ")" elif role == QtCore.Qt.DecorationRole: - if config.safeGetBoolean( + if BMConfigParser().safeGetBoolean( 'bitmessagesettings', 'useidenticons'): return avatarize(self.address or self.label) elif role == QtCore.Qt.ForegroundRole: @@ -392,9 +387,14 @@ class BMAddressWidget(BMTableWidgetItem, AccountMixin): class MessageList_AddressWidget(BMAddressWidget): """Address item in a messagelist""" - def __init__(self, address=None, label=None, unread=False): + def __init__(self, parent, address=None, label=None, unread=False): self.setAddress(address) - super(MessageList_AddressWidget, self).__init__(label, unread) + super(MessageList_AddressWidget, self).__init__(parent, label, unread) + + def _setup(self): + self.isEnabled = True + self.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) + self.setType() def setLabel(self, label=None): """Set label""" @@ -408,7 +408,7 @@ class MessageList_AddressWidget(BMAddressWidget): AccountMixin.CHAN, AccountMixin.MAILINGLIST): try: newLabel = unicode( - config.get(self.address, 'label'), + BMConfigParser().get(self.address, 'label'), 'utf-8', 'ignore') except: queryreturn = sqlQuery( @@ -443,9 +443,12 @@ class MessageList_AddressWidget(BMAddressWidget): class MessageList_SubjectWidget(BMTableWidgetItem): """Message list subject item""" - def __init__(self, subject=None, label=None, unread=False): + def __init__(self, parent, subject=None, label=None, unread=False): self.setSubject(subject) - super(MessageList_SubjectWidget, self).__init__(label, unread) + super(MessageList_SubjectWidget, self).__init__(parent, label, unread) + + def _setup(self): + self.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) def setSubject(self, subject): """Set subject""" @@ -466,37 +469,6 @@ class MessageList_SubjectWidget(BMTableWidgetItem): 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): """Addressbook item""" # pylint: disable=unused-argument @@ -521,9 +493,9 @@ class Ui_AddressBookWidgetItem(BMAddressWidget): AccountMixin.NORMAL, AccountMixin.MAILINGLIST, AccountMixin.CHAN): try: - config.get(self.address, 'label') - config.set(self.address, 'label', self.label) - config.save() + BMConfigParser().get(self.address, 'label') + BMConfigParser().set(self.address, 'label', self.label) + BMConfigParser().save() except: sqlExecute('''UPDATE addressbook set label=? WHERE address=?''', self.label, self.address) elif self.type == AccountMixin.SUBSCRIPTION: @@ -546,8 +518,8 @@ class Ui_AddressBookWidgetItem(BMAddressWidget): class Ui_AddressBookWidgetItemLabel(Ui_AddressBookWidgetItem): """Addressbook label item""" def __init__(self, address, label, acc_type): - self.address = address super(Ui_AddressBookWidgetItemLabel, self).__init__(label, acc_type) + self.address = address def data(self, role): """Return object data""" @@ -558,8 +530,9 @@ class Ui_AddressBookWidgetItemLabel(Ui_AddressBookWidgetItem): class Ui_AddressBookWidgetItemAddress(Ui_AddressBookWidgetItem): """Addressbook address item""" def __init__(self, address, label, acc_type): - self.address = address super(Ui_AddressBookWidgetItemAddress, self).__init__(address, acc_type) + self.address = address + self.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) def data(self, role): """Return object data""" diff --git a/src/bitmessageqt/languagebox.py b/src/bitmessageqt/languagebox.py index 34f96b02..9032cc42 100644 --- a/src/bitmessageqt/languagebox.py +++ b/src/bitmessageqt/languagebox.py @@ -1,47 +1,34 @@ -"""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): - """LanguageBox class for Qt UI""" - languageName = { - "system": "System Settings", "eo": "Esperanto", - "en_pirate": "Pirate English" - } - - def __init__(self, parent=None): + 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): - """Populates drop down list with all available languages.""" self.clear() - localesPath = os.path.join(paths.codePath(), 'translations') - 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] - if localeShort in LanguageBox.languageName: - self.addItem( - LanguageBox.languageName[localeShort], localeShort) - else: - locale = QtCore.QLocale(localeShort) - self.addItem( - locale.nativeLanguageName() or localeShort, localeShort) + 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)) - configuredLocale = config.safeGet( + if localeShort in LanguageBox.languageName: + self.addItem(LanguageBox.languageName[localeShort], localeShort) + elif locale.nativeLanguageName() == "": + self.addItem(localeShort, localeShort) + else: + self.addItem(locale.nativeLanguageName(), localeShort) + + configuredLocale = BMConfigParser().safeGet( 'bitmessagesettings', 'userlocale', "system") for i in range(self.count()): if self.itemData(i) == configuredLocale: diff --git a/src/bitmessageqt/messagecompose.py b/src/bitmessageqt/messagecompose.py index c51282f8..f7d5dac3 100644 --- a/src/bitmessageqt/messagecompose.py +++ b/src/bitmessageqt/messagecompose.py @@ -1,37 +1,23 @@ -""" -Message editor with a wheel zoom functionality -""" -# pylint: disable=bad-continuation - from PyQt4 import QtCore, QtGui - class MessageCompose(QtGui.QTextEdit): - """Editor class with wheel zoom functionality""" - def __init__(self, parent=0): + + def __init__(self, parent = 0): super(MessageCompose, self).__init__(parent) - self.setAcceptRichText(False) + self.setAcceptRichText(False) # we'll deal with this later when we have a new message format self.defaultFontPointSize = self.currentFont().pointSize() - + def wheelEvent(self, event): - """Mouse wheel scroll event handler""" - 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: 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 13ea16f9..45f3a79a 100644 --- a/src/bitmessageqt/messageview.py +++ b/src/bitmessageqt/messageview.py @@ -1,14 +1,12 @@ """ -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 +src/bitmessageqt/messageview.py +=============================== """ from PyQt4 import QtCore, QtGui from safehtmlparser import SafeHTMLParser -from tr import _translate class MessageView(QtGui.QTextBrowser): @@ -51,12 +49,11 @@ class MessageView(QtGui.QTextBrowser): """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(_translate( - "MainWindow", "Zoom level %1%").arg(str(zoom))) + QtGui.QApplication.activeWindow().statusBar().showMessage( + QtGui.QApplication.translate("MainWindow", "Zoom level %1%").arg(str(zoom))) def setWrappingWidth(self, width=None): """Set word-wrapping width""" diff --git a/src/bitmessageqt/networkstatus.py b/src/bitmessageqt/networkstatus.py index 5d669f39..6fbf5df6 100644 --- a/src/bitmessageqt/networkstatus.py +++ b/src/bitmessageqt/networkstatus.py @@ -1,16 +1,20 @@ """ -Network status tab widget definition. +src/bitmessageqt/networkstatus.py +================================= + """ import time from PyQt4 import QtCore, QtGui +import knownnodes import l10n import network.stats -import state +import shared import widgets -from network import connectionpool, knownnodes +from inventory import Inventory +from network import BMConnectionPool from retranslateui import RetranslateMixin from tr import _translate from uisignaler import UISignaler @@ -30,6 +34,8 @@ class NetworkStatus(QtGui.QWidget, RetranslateMixin): 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 @@ -49,7 +55,7 @@ class NetworkStatus(QtGui.QWidget, RetranslateMixin): def startUpdate(self): """Start a timer to update counters every 2 seconds""" - state.Inventory.numberOfInventoryLookupsPerformed = 0 + Inventory().numberOfInventoryLookupsPerformed = 0 self.runEveryTwoSeconds() self.timer.start(2000) # milliseconds @@ -90,8 +96,8 @@ class NetworkStatus(QtGui.QWidget, RetranslateMixin): "Object(s) to be synced: %n", None, QtCore.QCoreApplication.CodecForTr, - network.stats.pendingDownload() - + network.stats.pendingUpload())) + network.stats.pendingDownload() + + network.stats.pendingUpload())) def updateNumberOfMessagesProcessed(self): """Update the counter for number of processed messages""" @@ -102,7 +108,7 @@ class NetworkStatus(QtGui.QWidget, RetranslateMixin): "Processed %n person-to-person message(s).", None, QtCore.QCoreApplication.CodecForTr, - state.numberOfMessagesProcessed)) + shared.numberOfMessagesProcessed)) def updateNumberOfBroadcastsProcessed(self): """Update the counter for the number of processed broadcasts""" @@ -113,7 +119,7 @@ class NetworkStatus(QtGui.QWidget, RetranslateMixin): "Processed %n broadcast message(s).", None, QtCore.QCoreApplication.CodecForTr, - state.numberOfBroadcastsProcessed)) + shared.numberOfBroadcastsProcessed)) def updateNumberOfPubkeysProcessed(self): """Update the counter for the number of processed pubkeys""" @@ -124,7 +130,7 @@ class NetworkStatus(QtGui.QWidget, RetranslateMixin): "Processed %n public key(s).", None, QtCore.QCoreApplication.CodecForTr, - state.numberOfPubkeysProcessed)) + shared.numberOfPubkeysProcessed)) def updateNumberOfBytes(self): """ @@ -148,16 +154,16 @@ class NetworkStatus(QtGui.QWidget, RetranslateMixin): # pylint: disable=too-many-branches,undefined-variable if outbound: try: - c = connectionpool.pool.outboundConnections[destination] + c = BMConnectionPool().outboundConnections[destination] except KeyError: if add: return else: try: - c = connectionpool.pool.inboundConnections[destination] + c = BMConnectionPool().inboundConnections[destination] except KeyError: try: - c = connectionpool.pool.inboundConnections[destination.host] + c = BMConnectionPool().inboundConnections[destination.host] except KeyError: if add: return @@ -201,7 +207,7 @@ class NetworkStatus(QtGui.QWidget, RetranslateMixin): 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: + if len(BMConnectionPool().inboundConnections) == 0: self.window().setStatusIcon('yellow') for i in range(self.tableWidgetConnectionCount.rowCount()): if self.tableWidgetConnectionCount.item(i, 0).data(QtCore.Qt.UserRole).toPyObject() != destination: @@ -219,30 +225,21 @@ class NetworkStatus(QtGui.QWidget, RetranslateMixin): # 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': + if self.tableWidgetConnectionCount.rowCount() and shared.statusIconColor == 'red': self.window().setStatusIcon('yellow') - elif self.tableWidgetConnectionCount.rowCount() == 0 and state.statusIconColor != "red": + elif self.tableWidgetConnectionCount.rowCount() == 0 and shared.statusIconColor != "red": self.window().setStatusIcon('red') # timer driven def runEveryTwoSeconds(self): """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 + str(Inventory().numberOfInventoryLookupsPerformed / 2))) + Inventory().numberOfInventoryLookupsPerformed = 0 self.updateNumberOfBytes() self.updateNumberOfObjectsToBeSynced() def retranslateUi(self): - """Conventional Qt Designer method for dynamic l10n""" super(NetworkStatus, self).retranslateUi() - 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() + self.labelStartupTime.setText(_translate("networkstatus", "Since startup on %1").arg( + l10n.formatTimestamp(self.startup))) diff --git a/src/bitmessageqt/newaddressdialog.ui b/src/bitmessageqt/newaddressdialog.ui index 8b5276cc..a9eda5c3 100644 --- a/src/bitmessageqt/newaddressdialog.ui +++ b/src/bitmessageqt/newaddressdialog.ui @@ -309,10 +309,9 @@ The 'Random Number' option is selected by default but deterministic addresses ha - newaddresslabel - buttonBox - radioButtonDeterministicAddress radioButtonRandomAddress + radioButtonDeterministicAddress + newaddresslabel radioButtonMostAvailable radioButtonExisting comboBoxExisting @@ -320,6 +319,7 @@ 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 new file mode 100644 index 00000000..2311239c --- /dev/null +++ b/src/bitmessageqt/newaddresswizard.py @@ -0,0 +1,354 @@ +#!/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 c0629cd7..8db486c1 100644 --- a/src/bitmessageqt/newchandialog.py +++ b/src/bitmessageqt/newchandialog.py @@ -9,13 +9,13 @@ from PyQt4 import QtCore, QtGui import widgets from addresses import addBMIfNotPresent from addressvalidator import AddressValidator, PassPhraseValidator -from queues import ( - addressGeneratorQueue, apiAddressGeneratorReturnQueue, UISignalQueue) +from queues import UISignalQueue, addressGeneratorQueue, apiAddressGeneratorReturnQueue +from retranslateui import RetranslateMixin from tr import _translate from utils import str_chan -class NewChanDialog(QtGui.QDialog): +class NewChanDialog(QtGui.QDialog, RetranslateMixin): """The `New Chan` dialog""" def __init__(self, parent=None): super(NewChanDialog, self).__init__(parent) diff --git a/src/bitmessageqt/retranslateui.py b/src/bitmessageqt/retranslateui.py index c7676f77..e9d5bb3a 100644 --- a/src/bitmessageqt/retranslateui.py +++ b/src/bitmessageqt/retranslateui.py @@ -13,8 +13,6 @@ class RetranslateMixin(object): 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 d408d2c7..edacd4bb 100644 --- a/src/bitmessageqt/safehtmlparser.py +++ b/src/bitmessageqt/safehtmlparser.py @@ -65,8 +65,6 @@ class SafeHTMLParser(HTMLParser): 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""" @@ -94,7 +92,7 @@ class SafeHTMLParser(HTMLParser): if url.scheme not in self.src_schemes: val = "" self.sanitised += " " + quote_plus(attr) - if val is not None: + if not (val is None): self.sanitised += "=\"" + val + "\"" if inspect.stack()[1][3] == "handle_startendtag": self.sanitised += "/" diff --git a/src/bitmessageqt/settings.py b/src/bitmessageqt/settings.py index eeb507c7..bab27fbb 100644 --- a/src/bitmessageqt/settings.py +++ b/src/bitmessageqt/settings.py @@ -1,27 +1,23 @@ -""" -This module setting file is for settings -""" import ConfigParser import os import sys -import tempfile -import six from PyQt4 import QtCore, QtGui import debug import defaults +import knownnodes import namecoin import openclpow import paths import queues +import shared import state +import tempfile import widgets -from bmconfigparser import config as config_obj +from bmconfigparser import BMConfigParser 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 @@ -32,7 +28,7 @@ def getSOCKSProxyType(config): result = ConfigParser.SafeConfigParser.get( config, 'bitmessagesettings', 'socksproxytype') except (ConfigParser.NoSectionError, ConfigParser.NoOptionError): - return None + return else: if result.lower() in ('', 'none', 'false'): result = None @@ -41,37 +37,25 @@ def getSOCKSProxyType(config): 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.config = BMConfigParser() 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.comboBoxProxyType.addItem(ep.name) self.lineEditMaxOutboundConnections.setValidator( QtGui.QIntValidator(0, 8, self.lineEditMaxOutboundConnections)) @@ -88,15 +72,6 @@ class SettingsDialog(QtGui.QDialog): 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( @@ -136,10 +111,13 @@ class SettingsDialog(QtGui.QDialog): tempfile.NamedTemporaryFile( dir=paths.lookupExeFolder(), delete=True ).close() # should autodelete - except Exception: + except: self.checkBoxPortableMode.setDisabled(True) if 'darwin' in sys.platform: + self.checkBoxStartOnLogon.setDisabled(True) + self.checkBoxStartOnLogon.setText(_translate( + "MainWindow", "Start-on-login not yet supported on your OS.")) self.checkBoxMinimizeToTray.setDisabled(True) self.checkBoxMinimizeToTray.setText(_translate( "MainWindow", @@ -148,19 +126,15 @@ class SettingsDialog(QtGui.QDialog): self.checkBoxShowTrayNotifications.setText(_translate( "MainWindow", "Tray notifications not yet supported on your OS.")) - - if not sys.platform.startswith('win') and not self.parent.desktop: + elif 'linux' in sys.platform: 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( @@ -174,26 +148,6 @@ class SettingsDialog(QtGui.QDialog): else self.comboBoxProxyType.findText(self._proxy_type)) self.comboBoxProxyTypeChanged(self.comboBoxProxyType.currentIndex()) - if self._proxy_type: - for node, info in six.iteritems( - knownnodes.knownNodes.get( - min(connectionpool.pool.streams), []) - ): - if ( - node.host.endswith('.onion') and len(node.host) > 22 - and not info.get('self') - ): - break - else: - if self.checkBoxOnionOnly.isChecked(): - self.checkBoxOnionOnly.setText( - self.checkBoxOnionOnly.text() + ", " + _translate( - "MainWindow", "may cause connection problems!")) - self.checkBoxOnionOnly.setStyleSheet( - "QCheckBox { color : red; }") - else: - self.checkBoxOnionOnly.setEnabled(False) - self.lineEditSocksHostname.setText( config.get('bitmessagesettings', 'sockshostname')) self.lineEditSocksPort.setText(str( @@ -334,18 +288,6 @@ class SettingsDialog(QtGui.QDialog): 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 @@ -372,20 +314,6 @@ class SettingsDialog(QtGui.QDialog): 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) @@ -395,8 +323,7 @@ class SettingsDialog(QtGui.QDialog): self.lineEditTCPPort.text()): self.config.set( 'bitmessagesettings', 'port', str(self.lineEditTCPPort.text())) - if not self.config.safeGetBoolean( - 'bitmessagesettings', 'dontconnect'): + if not self.config.safeGetBoolean('bitmessagesettings', 'dontconnect'): self.net_restart_needed = True if self.checkBoxUPnP.isChecked() != self.config.safeGetBoolean( @@ -409,27 +336,10 @@ class SettingsDialog(QtGui.QDialog): 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': + if self._proxy_type and shared.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() @@ -454,11 +364,8 @@ class SettingsDialog(QtGui.QDialog): self.lineEditSocksPassword.text())) self.config.set('bitmessagesettings', 'sockslisten', str( self.checkBoxSocksListen.isChecked())) - if ( - self.checkBoxOnionOnly.isChecked() - and not self.config.safeGetBoolean( - 'bitmessagesettings', 'onionservicesonly') - ): + 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())) @@ -501,14 +408,14 @@ class SettingsDialog(QtGui.QDialog): self.config.set( 'bitmessagesettings', 'defaultnoncetrialsperbyte', str(int( - float(self.lineEditTotalDifficulty.text()) - * defaults.networkDefaultProofOfWorkNonceTrialsPerByte))) + 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))) + float(self.lineEditSmallMessageDifficulty.text()) * + defaults.networkDefaultPayloadLengthExtraBytes))) if self.comboBoxOpenCL.currentText().toUtf8() != self.config.safeGet( 'bitmessagesettings', 'opencl'): @@ -520,38 +427,40 @@ class SettingsDialog(QtGui.QDialog): acceptableDifficultyChanged = False if ( - float(self.lineEditMaxAcceptableTotalDifficulty.text()) >= 1 - or float(self.lineEditMaxAcceptableTotalDifficulty.text()) == 0 + float(self.lineEditMaxAcceptableTotalDifficulty.text()) >= 1 or + float(self.lineEditMaxAcceptableTotalDifficulty.text()) == 0 ): if self.config.get( - 'bitmessagesettings', 'maxacceptablenoncetrialsperbyte' + 'bitmessagesettings', 'maxacceptablenoncetrialsperbyte' ) != str(int( - float(self.lineEditMaxAcceptableTotalDifficulty.text()) - * defaults.networkDefaultProofOfWorkNonceTrialsPerByte)): + 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)) + float(self.lineEditMaxAcceptableTotalDifficulty.text()) * + defaults.networkDefaultProofOfWorkNonceTrialsPerByte)) ) if ( - float(self.lineEditMaxAcceptableSmallMessageDifficulty.text()) >= 1 - or float(self.lineEditMaxAcceptableSmallMessageDifficulty.text()) == 0 + float(self.lineEditMaxAcceptableSmallMessageDifficulty.text()) >= 1 or + float(self.lineEditMaxAcceptableSmallMessageDifficulty.text()) == 0 ): if self.config.get( - 'bitmessagesettings', 'maxacceptablepayloadlengthextrabytes' + 'bitmessagesettings', 'maxacceptablepayloadlengthextrabytes' ) != str(int( - float(self.lineEditMaxAcceptableSmallMessageDifficulty.text()) - * defaults.networkDefaultPayloadLengthExtraBytes)): + 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)) + float(self.lineEditMaxAcceptableSmallMessageDifficulty.text()) * + defaults.networkDefaultPayloadLengthExtraBytes)) ) if acceptableDifficultyChanged: # It might now be possible to send msgs which were previously @@ -573,7 +482,7 @@ class SettingsDialog(QtGui.QDialog): # default behavior. The input is blank/blank self.config.set('bitmessagesettings', 'stopresendingafterxdays', '') self.config.set('bitmessagesettings', 'stopresendingafterxmonths', '') - state.maximumLengthOfTimeToBotherResendingMessages = float('inf') + shared.maximumLengthOfTimeToBotherResendingMessages = float('inf') stopResendingDefaults = True try: @@ -588,9 +497,9 @@ class SettingsDialog(QtGui.QDialog): months = 0.0 if days >= 0 and months >= 0 and not stopResendingDefaults: - state.maximumLengthOfTimeToBotherResendingMessages = \ + shared.maximumLengthOfTimeToBotherResendingMessages = \ days * 24 * 60 * 60 + months * 60 * 60 * 24 * 365 / 12 - if state.maximumLengthOfTimeToBotherResendingMessages < 432000: + 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( @@ -607,7 +516,7 @@ class SettingsDialog(QtGui.QDialog): 'bitmessagesettings', 'stopresendingafterxdays', '0') self.config.set( 'bitmessagesettings', 'stopresendingafterxmonths', '0') - state.maximumLengthOfTimeToBotherResendingMessages = 0.0 + shared.maximumLengthOfTimeToBotherResendingMessages = 0.0 else: self.config.set( 'bitmessagesettings', 'stopresendingafterxdays', str(days)) @@ -629,8 +538,8 @@ class SettingsDialog(QtGui.QDialog): self.parent.updateStartOnLogon() if ( - state.appdata != paths.lookupExeFolder() - and self.checkBoxPortableMode.isChecked() + state.appdata != paths.lookupExeFolder() and + self.checkBoxPortableMode.isChecked() ): # If we are NOT using portable mode now but the user selected # that we should... @@ -648,12 +557,12 @@ class SettingsDialog(QtGui.QDialog): try: os.remove(previousAppdataLocation + 'debug.log') os.remove(previousAppdataLocation + 'debug.log.1') - except Exception: + except: pass if ( - state.appdata == paths.lookupExeFolder() - and not self.checkBoxPortableMode.isChecked() + state.appdata == paths.lookupExeFolder() and + not self.checkBoxPortableMode.isChecked() ): # If we ARE using portable mode now but the user selected # that we shouldn't... @@ -671,5 +580,5 @@ class SettingsDialog(QtGui.QDialog): try: os.remove(paths.lookupExeFolder() + 'debug.log') os.remove(paths.lookupExeFolder() + 'debug.log.1') - except Exception: + except: pass diff --git a/src/bitmessageqt/settings.ui b/src/bitmessageqt/settings.ui index e7ce1d71..0ffbf442 100644 --- a/src/bitmessageqt/settings.ui +++ b/src/bitmessageqt/settings.ui @@ -147,32 +147,6 @@ - - - - Custom Style - - - - - - - 100 - 0 - - - - - - - - Font - - - - - - @@ -257,7 +231,7 @@ - + Bandwidth limit @@ -348,7 +322,7 @@ - + Proxy server / Tor @@ -458,14 +432,7 @@ - - - - Announce self by UDP - - - - + Qt::Vertical @@ -1228,11 +1195,5 @@ - - buttonFont - clicked() - settingsDialog - choose_font - diff --git a/src/bitmessageqt/sound.py b/src/bitmessageqt/sound.py index 33b4c500..9c86a9a4 100644 --- a/src/bitmessageqt/sound.py +++ b/src/bitmessageqt/sound.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -"""Sound Module""" # sound type constants SOUND_NONE = 0 @@ -13,12 +12,10 @@ SOUND_CONNECTION_GREEN = 5 # 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 2add604d..65a5acfb 100644 --- a/src/bitmessageqt/statusbar.py +++ b/src/bitmessageqt/statusbar.py @@ -1,12 +1,8 @@ -# pylint: disable=unused-argument -"""Status bar Module""" - +from PyQt4 import QtCore, QtGui +from Queue import Queue from time import time -from PyQt4 import QtGui - class BMStatusBar(QtGui.QStatusBar): - """Status bar with queue and priorities""" duration = 10000 deleteAfter = 60 @@ -17,9 +13,6 @@ class BMStatusBar(QtGui.QStatusBar): 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: @@ -37,3 +30,9 @@ class BMStatusBar(QtGui.QStatusBar): 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 a84affa4..d6d4543d 100644 --- a/src/bitmessageqt/support.py +++ b/src/bitmessageqt/support.py @@ -1,43 +1,33 @@ -"""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 -import network.stats +from foldertree import AccountMixin +from helper_sql import * +from l10n import getTranslationLanguage +from openclpow import openclAvailable, openclEnabled import paths import proofofwork -import queues -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 +import queues +import network.stats +import state 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 = _translate("Support", "PyBitmessage support") -SUPPORT_MY_LABEL = _translate("Support", "My new address") +SUPPORT_LABEL = 'PyBitmessage support' +SUPPORT_MY_LABEL = 'My new address' SUPPORT_SUBJECT = 'Support request' -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. +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. Please describe what you are trying to do: @@ -47,8 +37,7 @@ Please describe what happens instead: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -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: {} @@ -63,53 +52,39 @@ Locale: {} 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 (?,?)', - SUPPORT_LABEL.toUtf8(), SUPPORT_ADDRESS) + sqlExecute('''INSERT INTO addressbook VALUES (?,?)''', str(QtGui.QApplication.translate("Support", SUPPORT_LABEL)), SUPPORT_ADDRESS) myapp.rerenderAddressBook() - def checkHasNormalAddress(): - for address in config.addresses(): + for address in account.getSortedAccounts(): acct = account.accountClass(address) - if acct.type == AccountMixin.NORMAL and config.safeGetBoolean(address, 'enabled'): + if acct.type == AccountMixin.NORMAL and BMConfigParser().safeGetBoolean(address, 'enabled'): return address return False - def createAddressIfNeeded(myapp): if not checkHasNormalAddress(): - queues.addressGeneratorQueue.put(( - 'createRandomAddress', 4, 1, - str(SUPPORT_MY_LABEL.toUtf8()), - 1, "", False, - defaults.networkDefaultProofOfWorkNonceTrialsPerByte, - defaults.networkDefaultPayloadLengthExtraBytes - )) + queues.addressGeneratorQueue.put(('createRandomAddress', 4, 1, str(QtGui.QApplication.translate("Support", SUPPORT_MY_LABEL)), 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(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(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 return myapp.ui.comboBoxSendFrom.setCurrentIndex(addrIndex) myapp.ui.lineEditTo.setText(SUPPORT_ADDRESS) @@ -132,9 +107,8 @@ 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: @@ -142,16 +116,14 @@ def createSupportMessage(myapp): portablemode = "True" if state.appdata == paths.lookupExeFolder() else "False" cpow = "True" if proofofwork.bmpow else "False" openclpow = str( - config.safeGet('bitmessagesettings', 'opencl') + BMConfigParser().safeGet('bitmessagesettings', 'opencl') ) if openclEnabled() else "None" locale = getTranslationLanguage() - socks = getSOCKSProxyType(config) or "N/A" - upnp = config.safeGet('bitmessagesettings', 'upnp', "N/A") + socks = getSOCKSProxyType(BMConfigParser()) or "N/A" + upnp = BMConfigParser().safeGet('bitmessagesettings', 'upnp', "N/A") connectedhosts = len(network.stats.connectedHostsList()) - myapp.ui.textEditMessage.setText(unicode(SUPPORT_MESSAGE, 'utf-8').format( - version, os, architecture, pythonversion, opensslversion, frozen, - portablemode, cpow, openclpow, locale, socks, upnp, connectedhosts)) + 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)) # single msg tab myapp.ui.tabWidgetSend.setCurrentIndex( diff --git a/src/bitmessageqt/tests/__init__.py b/src/bitmessageqt/tests/__init__.py deleted file mode 100644 index a81ddb04..00000000 --- a/src/bitmessageqt/tests/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -"""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 deleted file mode 100644 index cd86c5d6..00000000 --- a/src/bitmessageqt/tests/addressbook.py +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index d3fda8aa..00000000 --- a/src/bitmessageqt/tests/main.py +++ /dev/null @@ -1,71 +0,0 @@ -"""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 deleted file mode 100644 index bad28ed7..00000000 --- a/src/bitmessageqt/tests/settings.py +++ /dev/null @@ -1,78 +0,0 @@ -"""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 deleted file mode 100644 index ba28b73a..00000000 --- a/src/bitmessageqt/tests/support.py +++ /dev/null @@ -1,33 +0,0 @@ -# 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 c23ec3bc..055f9097 100644 --- a/src/bitmessageqt/uisignaler.py +++ b/src/bitmessageqt/uisignaler.py @@ -22,11 +22,8 @@ class UISignaler(QThread): 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': @@ -49,11 +46,7 @@ class UISignaler(QThread): 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': @@ -80,11 +73,7 @@ class UISignaler(QThread): 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 9f849b3b..564dbc8b 100644 --- a/src/bitmessageqt/utils.py +++ b/src/bitmessageqt/utils.py @@ -1,27 +1,24 @@ +from PyQt4 import QtGui import hashlib import os - -from PyQt4 import QtGui - -import state from addresses import addBMIfNotPresent -from bmconfigparser import config +from bmconfigparser import BMConfigParser +import state str_broadcast_subscribers = '[Broadcast subscribers]' str_chan = '[chan]' - def identiconize(address): size = 48 - if not config.getboolean('bitmessagesettings', 'useidenticons'): + if not BMConfigParser().getboolean('bitmessagesettings', 'useidenticons'): return QtGui.QIcon() # If you include another identicon library, please generate an # example identicon with the following md5 hash: # 3fd4bf901b9d4ea1394f0fb358725b28 - identicon_lib = config.safeGet( + identicon_lib = BMConfigParser().safeGet( 'bitmessagesettings', 'identiconlib', 'qidenticon_two_x') # As an 'identiconsuffix' you could put "@bitmessge.ch" or "@bm.addr" @@ -30,41 +27,33 @@ def identiconize(address): # It can be used as a pseudo-password to salt the generation of # the identicons to decrease the risk of attacks where someone creates # an address to mimic someone else's identicon. - identiconsuffix = config.get('bitmessagesettings', 'identiconsuffix') - if identicon_lib[:len('qidenticon')] == 'qidenticon': + identiconsuffix = BMConfigParser().get('bitmessagesettings', 'identiconsuffix') + if (identicon_lib[:len('qidenticon')] == 'qidenticon'): + # print identicon_lib # originally by: # :Author:Shin Adachi # Licesensed under FreeBSD License. # stripped from PIL and uses QT instead (by sendiulo, same license) import qidenticon - 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 + 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 penwidth = 0 - image = qidenticon.render_identicon( - int(icon_hash, 16), size, use_two_colors, opacity, penwidth) + image = qidenticon.render_identicon(int(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': - # Here you could load pydenticon.py - # (just put it in the "src" folder of your Bitmessage source) + # print identicon_lib + # 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: - # 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) + # 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) rendering = idcon_render._render() data = rendering.convert("RGBA").tostring("raw", "RGBA") qim = QtGui.QImage(data, size, size, QtGui.QImage.Format_ARGB32) @@ -73,31 +62,32 @@ 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() - icon_hash = hashlib.md5(addBMIfNotPresent(address)).hexdigest() + hash = hashlib.md5(addBMIfNotPresent(address)).hexdigest() + str_broadcast_subscribers = '[Broadcast subscribers]' if address == str_broadcast_subscribers: # don't hash [Broadcast subscribers] - icon_hash = address - # https://www.riverbankcomputing.com/static/Docs/PyQt4/qimagereader.html#supportedImageFormats + hash = address + # http://pyqt.sourceforge.net/Docs/PyQt4/qimagereader.html#supportedImageFormats + # print QImageReader.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/' + icon_hash + '.' + ext.lower() - upper_hash = state.appdata + 'avatars/' + icon_hash + '.' + ext.upper() + lower_hash = state.appdata + 'avatars/' + hash + '.' + ext.lower() + upper_hash = state.appdata + 'avatars/' + 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/bmconfigparser.py b/src/bmconfigparser.py index abf285ad..328cf0c7 100644 --- a/src/bmconfigparser.py +++ b/src/bmconfigparser.py @@ -2,47 +2,83 @@ BMConfigParser class definition and default configuration settings """ +import ConfigParser import os import shutil -from threading import Event from datetime import datetime -from six import string_types -from six.moves import configparser +import state +from singleton import Singleton -try: - import state -except ImportError: - from pybitmessage import state - -SafeConfigParser = configparser.SafeConfigParser -config_ready = Event() +BMConfigDefaults = { + "bitmessagesettings": { + "maxaddrperstreamsend": 500, + "maxbootstrapconnections": 20, + "maxdownloadrate": 0, + "maxoutboundconnections": 8, + "maxtotalconnections": 200, + "maxuploadrate": 0, + "apiinterface": "127.0.0.1", + "apiport": 8442 + }, + "threads": { + "receive": 3, + }, + "network": { + "bind": '', + "dandelion": 90, + }, + "inventory": { + "storage": "sqlite", + "acceptmismatch": False, + }, + "knownnodes": { + "maxnodes": 20000, + }, + "zlib": { + 'maxsize': 1048576 + } +} -class BMConfigParser(SafeConfigParser): +@Singleton +class BMConfigParser(ConfigParser.SafeConfigParser): """ Singleton class inherited from :class:`ConfigParser.SafeConfigParser` with additional methods specific to bitmessage config. """ # pylint: disable=too-many-ancestors + _temp = {} def set(self, section, option, value=None): if self._optcre is self.OPTCRE or value: - if not isinstance(value, string_types): + if not isinstance(value, basestring): raise TypeError("option values must be strings") if not self.validate(section, option, value): raise ValueError("Invalid value %s" % value) - return SafeConfigParser.set(self, section, option, value) + return ConfigParser.ConfigParser.set(self, section, option, value) - def get(self, section, option, **kwargs): - """Try returning temporary value before using parent get()""" + def get(self, section, option, raw=False, variables=None): + # pylint: disable=arguments-differ try: - return self._temp[section][option] - except KeyError: - pass - return SafeConfigParser.get( - self, section, option, **kwargs) + if section == "bitmessagesettings" and option == "timeformat": + return ConfigParser.ConfigParser.get( + self, section, option, raw, variables) + try: + return self._temp[section][option] + except KeyError: + pass + 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 setTemp(self, section, option, value=None): """Temporary set option to value, not saving.""" @@ -51,71 +87,60 @@ class BMConfigParser(SafeConfigParser): except KeyError: self._temp[section] = {option: value} - def safeGetBoolean(self, section, option): + def safeGetBoolean(self, section, field): """Return value as boolean, False on exceptions""" try: - return self.getboolean(section, option) - except (configparser.NoSectionError, configparser.NoOptionError, + return self.getboolean(section, field) + except (ConfigParser.NoSectionError, ConfigParser.NoOptionError, ValueError, AttributeError): return False - def safeGetInt(self, section, option, default=0): + def safeGetInt(self, section, field, 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.getfloat(section, option) - except (configparser.NoSectionError, configparser.NoOptionError, + return self.getint(section, field) + except (ConfigParser.NoSectionError, ConfigParser.NoOptionError, ValueError, AttributeError): return default def safeGet(self, section, option, default=None): - """ - Return value as is, default on exceptions, None if default missing - """ + """Return value as is, default on exceptions, None if default missing""" try: return self.get(section, option) - except (configparser.NoSectionError, configparser.NoOptionError, + except (ConfigParser.NoSectionError, ConfigParser.NoOptionError, ValueError, AttributeError): return default def items(self, section, raw=False, variables=None): - # pylint: disable=signature-differs """Return section variables as parent, but override the "raw" argument to always True""" - return SafeConfigParser.items(self, section, True, variables) + # pylint: disable=arguments-differ + return ConfigParser.ConfigParser.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): + @staticmethod + def addresses(): """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 + return [ + x for x in BMConfigParser().sections() if x.startswith('BM-')] + + 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 def save(self): """Save the runtime config onto the filesystem""" @@ -128,12 +153,12 @@ class BMConfigParser(SafeConfigParser): shutil.copyfile(fileName, fileNameBak) # The backup succeeded. fileNameExisted = True - except(IOError, Exception): + except (IOError, Exception): # The backup failed. This can happen if the file # didn't exist before. fileNameExisted = False - - with open(fileName, 'w') as configfile: + # write the file + with open(fileName, 'wb') as configfile: self.write(configfile) # delete the backup if fileNameExisted: @@ -156,24 +181,3 @@ class BMConfigParser(SafeConfigParser): if value < 0 or value > 8: return False return True - - def search_addresses(self, address, searched_text): - """Return the searched label of MyAddress""" - return [x for x in [self.get(address, 'label').lower(), - address.lower()] if searched_text in x] - - def disable_address(self, address): - """"Disabling the specific Address""" - self.set(str(address), 'enabled', 'false') - self.save() - - def enable_address(self, address): - """"Enabling the specific Address""" - self.set(address, 'enabled', 'true') - self.save() - - -if not getattr(BMConfigParser, 'read_file', False): - BMConfigParser.read_file = BMConfigParser.readfp - -config = BMConfigParser() # TODO: remove this crutch diff --git a/src/build_osx.py b/src/build_osx.py index d83e9b9b..83d2f280 100644 --- a/src/build_osx.py +++ b/src/build_osx.py @@ -9,8 +9,7 @@ version = os.getenv("PYBITMESSAGEVERSION", "custom") mainscript = ["bitmessagemain.py"] DATA_FILES = [ - ('', ['sslkeys', 'images', 'default.ini']), - ('sql', glob('sql/*.sql')), + ('', ['sslkeys', 'images']), ('bitmsghash', ['bitmsghash/bitmsghash.cl', 'bitmsghash/bitmsghash.so']), ('translations', glob('translations/*.qm')), ('ui', glob('bitmessageqt/*.ui')), diff --git a/packages/android/buildozer.spec b/src/buildozer.spec similarity index 53% rename from packages/android/buildozer.spec rename to src/buildozer.spec index afdd4282..07f9e6b2 100644 --- a/packages/android/buildozer.spec +++ b/src/buildozer.spec @@ -1,19 +1,19 @@ [app] # (str) Title of your application -title = PyBitmessage Mock +title = PyBitmessage # (str) Package name -package.name = pybitmessagemock +package.name = PyBitmessage # (str) Package domain (needed for android/ios packaging) -package.domain = at.bitmessage +package.domain = org.test # (str) Source code where the main.py live -source.dir = ../../src +source.dir = . # (list) Source files to include (let empty to include all the files) -source.include_exts = py,png,jpg,kv,atlas,tflite,sql,json +source.include_exts = py,png,jpg,kv,atlas # (list) List of inclusions using pattern matching #source.include_patterns = assets/*,images/*.png @@ -22,25 +22,29 @@ source.include_exts = py,png,jpg,kv,atlas,tflite,sql,json #source.exclude_exts = spec # (list) List of directory to exclude (let empty to not exclude anything) -#source.exclude_dirs = tests, bin, venv +#source.exclude_dirs = tests, bin # (list) List of exclusions using pattern matching #source.exclude_patterns = license,images/*/*.jpg # (str) Application versioning (method 1) -version = 0.1.1 +version = 0.1 # (str) Application versioning (method 2) # version.regex = __version__ = ['"](.*)['"] # version.filename = %(source.dir)s/main.py # (list) Application requirements -# comma separated e.g. requirements = sqlite3,kivy -requirements = python3,kivy,sqlite3,kivymd==1.0.2,Pillow,opencv,kivy-garden.qrcode,qrcode,typing_extensions,pypng,pyzbar,xcamera,zbarcam +# comma seperated e.g. requirements = sqlite3,kivy +requirements = python2, sqlite3, kivy, openssl # (str) Custom source folders for requirements # Sets custom source for any requirements with recipes # requirements.source.kivy = ../../kivy +#requirements.source.sqlite3 = + +# (list) Garden requirements +#garden_requirements = # (str) Presplash of the application #presplash.filename = %(source.dir)s/data/presplash.png @@ -48,7 +52,7 @@ requirements = python3,kivy,sqlite3,kivymd==1.0.2,Pillow,opencv,kivy-garden.qrco # (str) Icon of the application #icon.filename = %(source.dir)s/data/icon.png -# (str) Supported orientation (one of landscape, sensorLandscape, portrait or all) +# (str) Supported orientation (one of landscape, portrait or all) orientation = portrait # (list) List of service to declare @@ -62,7 +66,8 @@ orientation = portrait # author = © Copyright Info # change the major version of python used by the app -osx.python_version = 3 +#osx.python_version = 2 + # Kivy version to use osx.kivy_version = 1.9.1 @@ -74,67 +79,54 @@ osx.kivy_version = 1.9.1 # (bool) Indicate if the application should be fullscreen or not fullscreen = 0 -# (string) Presplash background color (for android toolchain) +# (string) Presplash background color (for new 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 +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) Android API to use +#android.api = 19 -# (int) Minimum API required. You will need to set the android.ndk_api to be as low as this value. -android.minapi = 21 +# (int) Minimum API required +#android.minapi = 9 + +# (int) Android SDK version to use +#android.sdk = 20 # (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 +#android.ndk = 9c # (bool) Use --private data storage (True) or --dir public storage (False) -android.private_storage = True +#android.private_storage = True # (str) Android NDK directory (if empty, it will be automatically downloaded.) -android.ndk_path = /opt/android/android-ndk +#android.ndk_path = # (str) Android SDK directory (if empty, it will be automatically downloaded.) -android.sdk_path = /opt/android/android-sdk +#android.sdk_path = # (str) ANT directory (if empty, it will be automatically downloaded.) -android.ant_path = /opt/android/apache-ant +#android.ant_path = # (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 = +android.whitelist = /usr/lib/komodo-edit/python/lib/python2.7/lib-dynload/_sqlite3.so + + # (str) Path to a custom whitelist file #android.whitelist_src = @@ -143,7 +135,7 @@ android.ant_path = /opt/android/apache-ant # (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: +# down the build process. Allows wildcards matching, for example: # OUYA-ODK/libs/*.jar #android.add_jars = foo.jar,bar.jar,path/to/more/*.jar @@ -151,32 +143,16 @@ android.ant_path = /opt/android/apache-ant # directory containing the files) #android.add_src = -# (list) Android AAR archives to add +# (list) Android AAR archives to add (currently works only with sdl2_gradle +# bootstrap) #android.add_aars = -# (list) Gradle dependencies to add +# (list) Gradle dependencies to add (currently works only with sdl2_gradle +# bootstrap) #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 +, /home/cis/Downloads/libssl1.0.2_1.0.2l-2+deb9u2_amd64 +# (str) python-for-android branch to use, defaults to stable +#p4a.branch = stable # (str) OUYA Console category. Should be one of GAME or APP # If you leave this blank, OUYA support will not be enabled @@ -188,13 +164,9 @@ android.ant_path = /opt/android/apache-ant # (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 +# (list) Android additionnal 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 @@ -209,52 +181,19 @@ android.ant_path = /opt/android/apache-ant # 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 +# (str) The Android arch to build for, choices: armeabi-v7a, arm64-v8a, x86 +android.arch = armeabi-v7a # # 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 = @@ -267,16 +206,6 @@ android.release_artifact = apk # (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 @@ -284,19 +213,6 @@ android.release_artifact = apk # (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 diff --git a/src/class_addressGenerator.py b/src/class_addressGenerator.py index 33da1371..3df6501f 100644 --- a/src/class_addressGenerator.py +++ b/src/class_addressGenerator.py @@ -1,26 +1,22 @@ """ A thread for creating addresses """ - +import hashlib import time from binascii import hexlify -from six.moves import configparser, queue - import defaults import highlevelcrypto import queues import shared import state +import tr from addresses import decodeAddress, encodeAddress, encodeVarint -from bmconfigparser import config +from bmconfigparser import BMConfigParser +from fallback import RIPEMD160Hash from network import StoppableThread -from tr import _translate - - -class AddressGeneratorException(Exception): - '''Generic AddressGenerator exception''' - pass +from pyelliptic import arithmetic +from pyelliptic.openssl import OpenSSL class addressGenerator(StoppableThread): @@ -29,12 +25,10 @@ class addressGenerator(StoppableThread): name = "addressGenerator" def stopThread(self): - """Tell the thread to stop putting a special command to it's queue""" try: queues.addressGeneratorQueue.put(("stopThread", "data")) - except queue.Full: - self.logger.error('addressGeneratorQueue is Full') - + except: + pass super(addressGenerator, self).stopThread() def run(self): @@ -42,9 +36,8 @@ class addressGenerator(StoppableThread): 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 - + # pylint: disable=too-many-locals, too-many-branches + # pylint: disable=protected-access, too-many-statements while state.shutdown == 0: queueValue = queues.addressGeneratorQueue.get() nonceTrialsPerByte = 0 @@ -68,25 +61,35 @@ class addressGenerator(StoppableThread): command, addressVersionNumber, streamNumber, label, \ numberOfAddressesToMake, deterministicPassphrase, \ eighteenByteRipe = queueValue - - numberOfNullBytesDemandedOnFrontOfRipeHash = \ - config.safeGetInt( - 'bitmessagesettings', - 'numberofnullbytesonaddress', - 2 if eighteenByteRipe else 1 - ) + try: + numberOfNullBytesDemandedOnFrontOfRipeHash = \ + BMConfigParser().getint( + 'bitmessagesettings', + 'numberofnullbytesonaddress' + ) + except: + if eighteenByteRipe: + numberOfNullBytesDemandedOnFrontOfRipeHash = 2 + else: + # the default + numberOfNullBytesDemandedOnFrontOfRipeHash = 1 elif len(queueValue) == 9: command, addressVersionNumber, streamNumber, label, \ numberOfAddressesToMake, deterministicPassphrase, \ eighteenByteRipe, nonceTrialsPerByte, \ payloadLengthExtraBytes = queueValue - - numberOfNullBytesDemandedOnFrontOfRipeHash = \ - config.safeGetInt( - 'bitmessagesettings', - 'numberofnullbytesonaddress', - 2 if eighteenByteRipe else 1 - ) + try: + numberOfNullBytesDemandedOnFrontOfRipeHash = \ + BMConfigParser().getint( + 'bitmessagesettings', + 'numberofnullbytesonaddress' + ) + except: + if eighteenByteRipe: + numberOfNullBytesDemandedOnFrontOfRipeHash = 2 + else: + # the default + numberOfNullBytesDemandedOnFrontOfRipeHash = 1 elif queueValue[0] == 'stopThread': break else: @@ -101,14 +104,14 @@ class addressGenerator(StoppableThread): ' one version %s address which it cannot do.\n', addressVersionNumber) if nonceTrialsPerByte == 0: - nonceTrialsPerByte = config.getint( + nonceTrialsPerByte = BMConfigParser().getint( 'bitmessagesettings', 'defaultnoncetrialsperbyte') if nonceTrialsPerByte < \ defaults.networkDefaultProofOfWorkNonceTrialsPerByte: nonceTrialsPerByte = \ defaults.networkDefaultProofOfWorkNonceTrialsPerByte if payloadLengthExtraBytes == 0: - payloadLengthExtraBytes = config.getint( + payloadLengthExtraBytes = BMConfigParser().getint( 'bitmessagesettings', 'defaultpayloadlengthextrabytes') if payloadLengthExtraBytes < \ defaults.networkDefaultPayloadLengthExtraBytes: @@ -117,7 +120,7 @@ class addressGenerator(StoppableThread): if command == 'createRandomAddress': queues.UISignalQueue.put(( 'updateStatusBar', - _translate( + tr._translate( "MainWindow", "Generating one new address") )) # This next section is a little bit strange. We're going @@ -127,16 +130,21 @@ class addressGenerator(StoppableThread): # the \x00 or \x00\x00 bytes thus making the address shorter. startTime = time.time() numberOfAddressesWeHadToMakeBeforeWeFoundOneWithTheCorrectRipePrefix = 0 - privSigningKey, pubSigningKey = highlevelcrypto.random_keys() + potentialPrivSigningKey = OpenSSL.rand(32) + potentialPubSigningKey = highlevelcrypto.pointMult( + potentialPrivSigningKey) while True: numberOfAddressesWeHadToMakeBeforeWeFoundOneWithTheCorrectRipePrefix += 1 - potentialPrivEncryptionKey, potentialPubEncryptionKey = \ - highlevelcrypto.random_keys() - ripe = highlevelcrypto.to_ripe( - pubSigningKey, potentialPubEncryptionKey) + potentialPrivEncryptionKey = OpenSSL.rand(32) + potentialPubEncryptionKey = highlevelcrypto.pointMult( + potentialPrivEncryptionKey) + sha = hashlib.new('sha512') + sha.update( + potentialPubSigningKey + potentialPubEncryptionKey) + ripe = RIPEMD160Hash(sha.digest()).digest() if ( - ripe[:numberOfNullBytesDemandedOnFrontOfRipeHash] - == b'\x00' * numberOfNullBytesDemandedOnFrontOfRipeHash + ripe[:numberOfNullBytesDemandedOnFrontOfRipeHash] == + '\x00' * numberOfNullBytesDemandedOnFrontOfRipeHash ): break self.logger.info( @@ -156,25 +164,34 @@ class addressGenerator(StoppableThread): address = encodeAddress( addressVersionNumber, streamNumber, ripe) - privSigningKeyWIF = highlevelcrypto.encodeWalletImportFormat( - privSigningKey) - privEncryptionKeyWIF = highlevelcrypto.encodeWalletImportFormat( - potentialPrivEncryptionKey) + # 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) - config.add_section(address) - config.set(address, 'label', label) - config.set(address, 'enabled', 'true') - config.set(address, 'decoy', 'false') - config.set(address, 'noncetrialsperbyte', str( + 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)) - config.set(address, 'payloadlengthextrabytes', str( + BMConfigParser().set(address, 'payloadlengthextrabytes', str( payloadLengthExtraBytes)) - config.set( - address, 'privsigningkey', privSigningKeyWIF.decode()) - config.set( - address, 'privencryptionkey', - privEncryptionKeyWIF.decode()) - config.save() + BMConfigParser().set( + address, 'privsigningkey', privSigningKeyWIF) + BMConfigParser().set( + address, 'privencryptionkey', privEncryptionKeyWIF) + BMConfigParser().save() # The API and the join and create Chan functionality # both need information back from the address generator. @@ -182,7 +199,7 @@ class addressGenerator(StoppableThread): queues.UISignalQueue.put(( 'updateStatusBar', - _translate( + tr._translate( "MainWindow", "Done generating address. Doing work necessary" " to broadcast it...") @@ -197,10 +214,9 @@ class addressGenerator(StoppableThread): queues.workerQueue.put(( 'sendOutOrStoreMyV4Pubkey', address)) - elif command in ( - 'createDeterministicAddresses', 'createChan', - 'getDeterministicAddress', 'joinChan' - ): + elif command == 'createDeterministicAddresses' \ + or command == 'getDeterministicAddress' \ + or command == 'createChan' or command == 'joinChan': if not deterministicPassphrase: self.logger.warning( 'You are creating deterministic' @@ -209,7 +225,7 @@ class addressGenerator(StoppableThread): if command == 'createDeterministicAddresses': queues.UISignalQueue.put(( 'updateStatusBar', - _translate( + tr._translate( "MainWindow", "Generating %1 new addresses." ).arg(str(numberOfAddressesToMake)) @@ -231,22 +247,27 @@ class addressGenerator(StoppableThread): numberOfAddressesWeHadToMakeBeforeWeFoundOneWithTheCorrectRipePrefix = 0 while True: numberOfAddressesWeHadToMakeBeforeWeFoundOneWithTheCorrectRipePrefix += 1 - potentialPrivSigningKey, potentialPubSigningKey = \ - highlevelcrypto.deterministic_keys( - deterministicPassphrase, - encodeVarint(signingKeyNonce)) - potentialPrivEncryptionKey, potentialPubEncryptionKey = \ - highlevelcrypto.deterministic_keys( - deterministicPassphrase, - encodeVarint(encryptionKeyNonce)) - + potentialPrivSigningKey = hashlib.sha512( + deterministicPassphrase + + encodeVarint(signingKeyNonce) + ).digest()[:32] + potentialPrivEncryptionKey = hashlib.sha512( + deterministicPassphrase + + encodeVarint(encryptionKeyNonce) + ).digest()[:32] + potentialPubSigningKey = highlevelcrypto.pointMult( + potentialPrivSigningKey) + potentialPubEncryptionKey = highlevelcrypto.pointMult( + potentialPrivEncryptionKey) signingKeyNonce += 2 encryptionKeyNonce += 2 - ripe = highlevelcrypto.to_ripe( - potentialPubSigningKey, potentialPubEncryptionKey) + sha = hashlib.new('sha512') + sha.update( + potentialPubSigningKey + potentialPubEncryptionKey) + ripe = RIPEMD160Hash(sha.digest()).digest() if ( - ripe[:numberOfNullBytesDemandedOnFrontOfRipeHash] - == b'\x00' * numberOfNullBytesDemandedOnFrontOfRipeHash + ripe[:numberOfNullBytesDemandedOnFrontOfRipeHash] == + '\x00' * numberOfNullBytesDemandedOnFrontOfRipeHash ): break @@ -258,8 +279,8 @@ class addressGenerator(StoppableThread): ' 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. @@ -280,17 +301,26 @@ class addressGenerator(StoppableThread): saveAddressToDisk = False if saveAddressToDisk and live: - privSigningKeyWIF = \ - highlevelcrypto.encodeWalletImportFormat( - potentialPrivSigningKey) - privEncryptionKeyWIF = \ - highlevelcrypto.encodeWalletImportFormat( - potentialPrivEncryptionKey) + # 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: - config.add_section(address) + BMConfigParser().add_section(address) addressAlreadyExists = False - except configparser.DuplicateSectionError: + except: addressAlreadyExists = True if addressAlreadyExists: @@ -300,7 +330,7 @@ class addressGenerator(StoppableThread): ) queues.UISignalQueue.put(( 'updateStatusBar', - _translate( + tr._translate( "MainWindow", "%1 is already in 'Your Identities'." " Not adding it again." @@ -308,24 +338,25 @@ class addressGenerator(StoppableThread): )) else: self.logger.debug('label: %s', label) - config.set(address, 'label', label) - config.set(address, 'enabled', 'true') - config.set(address, 'decoy', 'false') - if command in ('createChan', 'joinChan'): - config.set(address, 'chan', 'true') - config.set( + 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)) - config.set( + BMConfigParser().set( address, 'payloadlengthextrabytes', str(payloadLengthExtraBytes)) - config.set( - address, 'privsigningkey', - privSigningKeyWIF.decode()) - config.set( - address, 'privencryptionkey', - privEncryptionKeyWIF.decode()) - config.save() + BMConfigParser().set( + address, 'privSigningKey', + privSigningKeyWIF) + BMConfigParser().set( + address, 'privEncryptionKey', + privEncryptionKeyWIF) + BMConfigParser().save() queues.UISignalQueue.put(( 'writeNewAddressToTable', @@ -337,10 +368,10 @@ class addressGenerator(StoppableThread): highlevelcrypto.makeCryptor( hexlify(potentialPrivEncryptionKey)) shared.myAddressesByHash[ripe] = address - tag = highlevelcrypto.double_sha512( - encodeVarint(addressVersionNumber) - + encodeVarint(streamNumber) + ripe - )[32:] + tag = hashlib.sha512(hashlib.sha512( + encodeVarint(addressVersionNumber) + + encodeVarint(streamNumber) + ripe + ).digest()).digest()[32:] shared.myAddressesByTag[tag] = address if addressVersionNumber == 3: # If this is a chan address, @@ -353,24 +384,23 @@ class addressGenerator(StoppableThread): 'sendOutOrStoreMyV4Pubkey', address)) queues.UISignalQueue.put(( 'updateStatusBar', - _translate( + tr._translate( "MainWindow", "Done generating address") )) elif saveAddressToDisk and not live \ - and not config.has_section(address): + and not BMConfigParser().has_section(address): listOfNewAddressesToSendOutThroughTheAPI.append( address) # Done generating addresses. - if command in ( - 'createDeterministicAddresses', 'createChan', 'joinChan' - ): + if command == 'createDeterministicAddresses' \ + or command == 'joinChan' or command == 'createChan': queues.apiAddressGeneratorReturnQueue.put( listOfNewAddressesToSendOutThroughTheAPI) elif command == 'getDeterministicAddress': queues.apiAddressGeneratorReturnQueue.put(address) else: - raise AddressGeneratorException( - "Error in the addressGenerator thread. Thread was" - + " given a command it could not understand: " + command) + raise Exception( + "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 974631cb..824580c2 100644 --- a/src/class_objectProcessor.py +++ b/src/class_objectProcessor.py @@ -6,33 +6,35 @@ processes the network objects # pylint: disable=too-many-branches,too-many-statements import hashlib import logging -import os import random -import subprocess # nosec B404 import threading import time from binascii import hexlify +from subprocess import call # nosec import helper_bitcoin import helper_inbox import helper_msgcoding import helper_sent import highlevelcrypto +import knownnodes import l10n import protocol import queues import shared import state +import tr from addresses import ( - decodeAddress, decodeVarint, + calculateInventoryHash, 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 bmconfigparser import BMConfigParser +from fallback import RIPEMD160Hash +from helper_ackPayload import genAckPayload +from helper_sql import SqlBulkExecute, sqlExecute, sqlQuery +from network import bmproto from network.node import Peer -from tr import _translate +# pylint: disable=too-many-locals, too-many-return-statements, too-many-branches, too-many-statements logger = logging.getLogger('default') @@ -45,24 +47,21 @@ class objectProcessor(threading.Thread): def __init__(self): threading.Thread.__init__(self, name="objectProcessor") random.seed() - if sql_ready.wait(sql_timeout) is False: - logger.fatal('SQL thread is not started in %s sec', sql_timeout) - os._exit(1) # pylint: disable=protected-access - shared.reloadMyAddressHashes() - shared.reloadBroadcastSendersForWhichImWatching() # It may be the case that the last time Bitmessage was running, # the user closed it before it finished processing everything in the # objectProcessorQueue. Assuming that Bitmessage wasn't closed # forcefully, it should have saved the data in the queue into the # objectprocessorqueue table. Let's pull it out. queryreturn = sqlQuery( - 'SELECT objecttype, data FROM objectprocessorqueue') - for objectType, data in queryreturn: + '''SELECT objecttype, data FROM objectprocessorqueue''') + for row in queryreturn: + objectType, data = row queues.objectProcessorQueue.put((objectType, data)) - sqlExecute('DELETE FROM objectprocessorqueue') + sqlExecute('''DELETE FROM objectprocessorqueue''') logger.debug( 'Loaded %s objects from disk into the objectProcessorQueue.', len(queryreturn)) + self._ack_obj = bmproto.BMStringParser() self.successfullyDecryptMessageTimings = [] def run(self): @@ -102,7 +101,7 @@ class objectProcessor(threading.Thread): 'The object is too big after decompression (stopped' ' decompressing at %ib, your configured limit %ib).' ' Ignoring', - e.size, config.safeGetInt('zlib', 'maxsize')) + e.size, BMConfigParser().safeGetInt("zlib", "maxsize")) except varintDecodeError as e: logger.debug( 'There was a problem with a varint while processing an' @@ -133,6 +132,7 @@ class objectProcessor(threading.Thread): @staticmethod def checkackdata(data): """Checking Acknowledgement of message received or not?""" + # pylint: disable=protected-access # Let's check whether this is a message acknowledgement bound for us. if len(data) < 32: return @@ -140,19 +140,22 @@ class objectProcessor(threading.Thread): # bypass nonce and time, retain object type/version/stream + body readPosition = 16 - if data[readPosition:] in state.ackdataForWhichImWatching: + if data[readPosition:] in shared.ackdataForWhichImWatching: logger.info('This object is an acknowledgement bound for me.') - del state.ackdataForWhichImWatching[data[readPosition:]] + del shared.ackdataForWhichImWatching[data[readPosition:]] sqlExecute( - "UPDATE sent SET status='ackreceived', lastactiontime=?" - " WHERE ackdata=?", int(time.time()), data[readPosition:]) + 'UPDATE sent SET status=?, lastactiontime=?' + ' WHERE ackdata=?', + 'ackreceived', int(time.time()), data[readPosition:]) queues.UISignalQueue.put(( - 'updateSentItemStatusByAckdata', ( + 'updateSentItemStatusByAckdata', + ( data[readPosition:], - _translate( + tr._translate( "MainWindow", "Acknowledgement of the message received %1" - ).arg(l10n.formatTimestamp())) + ).arg(l10n.formatTimestamp()) + ) )) else: logger.debug('This object is not an acknowledgement bound for me.') @@ -173,7 +176,6 @@ class objectProcessor(threading.Thread): return peer = Peer(host, port) with knownnodes.knownNodesLock: - # FIXME: adjust expirestime knownnodes.addKnownNode( stream, peer, is_self=state.ownAddresses.get(peer)) @@ -181,9 +183,10 @@ class objectProcessor(threading.Thread): def processgetpubkey(data): """Process getpubkey object""" if len(data) > 200: - return logger.info( + logger.info( 'getpubkey is abnormally long. Sanity check failed.' ' Ignoring object.') + return readPosition = 20 # bypass the nonce, time, and object type requestedAddressVersionNumber, addressVersionLength = decodeVarint( data[readPosition:readPosition + 10]) @@ -193,25 +196,29 @@ class objectProcessor(threading.Thread): readPosition += streamNumberLength if requestedAddressVersionNumber == 0: - return logger.debug( + logger.debug( 'The requestedAddressVersionNumber of the pubkey request' ' is zero. That doesn\'t make any sense. Ignoring it.') - if requestedAddressVersionNumber == 1: - return logger.debug( + return + elif requestedAddressVersionNumber == 1: + logger.debug( 'The requestedAddressVersionNumber of the pubkey request' ' is 1 which isn\'t supported anymore. Ignoring it.') - if requestedAddressVersionNumber > 4: - return logger.debug( + return + elif requestedAddressVersionNumber > 4: + logger.debug( 'The requestedAddressVersionNumber of the pubkey request' ' is too high. Can\'t understand. Ignoring it.') + return myAddress = '' if requestedAddressVersionNumber <= 3: requestedHash = data[readPosition:readPosition + 20] if len(requestedHash) != 20: - return logger.debug( + 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)) @@ -221,9 +228,10 @@ class objectProcessor(threading.Thread): elif requestedAddressVersionNumber >= 4: requestedTag = data[readPosition:readPosition + 32] if len(requestedTag) != 32: - return logger.debug( + 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)) @@ -235,31 +243,35 @@ class objectProcessor(threading.Thread): return if decodeAddress(myAddress)[1] != requestedAddressVersionNumber: - return logger.warning( + 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 if decodeAddress(myAddress)[2] != streamNumber: - return logger.warning( + 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( + 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.') - lastPubkeySendTime = config.safeGetInt( + return + lastPubkeySendTime = BMConfigParser().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( + 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' @@ -274,7 +286,7 @@ class objectProcessor(threading.Thread): def processpubkey(self, data): """Process a pubkey object""" pubkeyProcessingStartTime = time.time() - state.numberOfPubkeysProcessed += 1 + shared.numberOfPubkeysProcessed += 1 queues.UISignalQueue.put(( 'updateNumberOfPubkeysProcessed', 'no data')) readPosition = 20 # bypass the nonce, time, and object type @@ -285,34 +297,41 @@ class objectProcessor(threading.Thread): data[readPosition:readPosition + 10]) readPosition += varintLength if addressVersion == 0: - return logger.debug( + logger.debug( '(Within processpubkey) addressVersion of 0 doesn\'t' ' make sense.') + return if addressVersion > 4 or addressVersion == 1: - return logger.info( + logger.info( 'This version of Bitmessage cannot handle version %s' ' addresses.', addressVersion) + return if addressVersion == 2: # sanity check. This is the minimum possible length. if len(data) < 146: - return logger.debug( + logger.debug( '(within processpubkey) payloadLength less than 146.' ' Sanity check failed.') + return readPosition += 4 - pubSigningKey = '\x04' + data[readPosition:readPosition + 64] + publicSigningKey = 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 - pubEncryptionKey = '\x04' + data[readPosition:readPosition + 64] - if len(pubEncryptionKey) < 65: - return logger.debug( + publicEncryptionKey = data[readPosition:readPosition + 64] + if len(publicEncryptionKey) < 64: + logger.debug( 'publicEncryptionKey length less than 64. Sanity check' ' failed.') + return readPosition += 64 # The data we'll store in the pubkeys table. dataToStore = data[20:readPosition] - ripe = highlevelcrypto.to_ripe(pubSigningKey, pubEncryptionKey) + sha = hashlib.new('sha512') + sha.update( + '\x04' + publicSigningKey + '\x04' + publicEncryptionKey) + ripe = RIPEMD160Hash(sha.digest()).digest() if logger.isEnabledFor(logging.DEBUG): logger.debug( @@ -320,7 +339,7 @@ class objectProcessor(threading.Thread): '\nripe %s\npublicSigningKey in hex: %s' '\npublicEncryptionKey in hex: %s', addressVersion, streamNumber, hexlify(ripe), - hexlify(pubSigningKey), hexlify(pubEncryptionKey) + hexlify(publicSigningKey), hexlify(publicEncryptionKey) ) address = encodeAddress(addressVersion, streamNumber, ripe) @@ -350,15 +369,15 @@ class objectProcessor(threading.Thread): ' Sanity check failed.') return readPosition += 4 - pubSigningKey = '\x04' + data[readPosition:readPosition + 64] + publicSigningKey = '\x04' + data[readPosition:readPosition + 64] readPosition += 64 - pubEncryptionKey = '\x04' + data[readPosition:readPosition + 64] + publicEncryptionKey = '\x04' + data[readPosition:readPosition + 64] readPosition += 64 - specifiedNonceTrialsPerByteLength = decodeVarint( - data[readPosition:readPosition + 10])[1] + _, specifiedNonceTrialsPerByteLength = decodeVarint( + data[readPosition:readPosition + 10]) readPosition += specifiedNonceTrialsPerByteLength - specifiedPayloadLengthExtraBytesLength = decodeVarint( - data[readPosition:readPosition + 10])[1] + _, specifiedPayloadLengthExtraBytesLength = decodeVarint( + data[readPosition:readPosition + 10]) readPosition += specifiedPayloadLengthExtraBytesLength endOfSignedDataPosition = readPosition # The data we'll store in the pubkeys table. @@ -369,13 +388,15 @@ class objectProcessor(threading.Thread): signature = data[readPosition:readPosition + signatureLength] if highlevelcrypto.verify( data[8:endOfSignedDataPosition], - signature, hexlify(pubSigningKey)): + signature, hexlify(publicSigningKey)): logger.debug('ECDSA verify passed (within processpubkey)') else: logger.warning('ECDSA verify failed (within processpubkey)') return - ripe = highlevelcrypto.to_ripe(pubSigningKey, pubEncryptionKey) + sha = hashlib.new('sha512') + sha.update(publicSigningKey + publicEncryptionKey) + ripe = RIPEMD160Hash(sha.digest()).digest() if logger.isEnabledFor(logging.DEBUG): logger.debug( @@ -383,7 +404,7 @@ class objectProcessor(threading.Thread): '\nripe %s\npublicSigningKey in hex: %s' '\npublicEncryptionKey in hex: %s', addressVersion, streamNumber, hexlify(ripe), - hexlify(pubSigningKey), hexlify(pubEncryptionKey) + hexlify(publicSigningKey), hexlify(publicEncryptionKey) ) address = encodeAddress(addressVersion, streamNumber, ripe) @@ -408,17 +429,19 @@ class objectProcessor(threading.Thread): if addressVersion == 4: if len(data) < 350: # sanity check. - return logger.debug( + logger.debug( '(within processpubkey) payloadLength less than 350.' ' Sanity check failed.') + return tag = data[readPosition:readPosition + 32] if tag not in state.neededPubkeys: - return logger.info( + logger.info( 'We don\'t need this v4 pubkey. We didn\'t ask for it.') + return # Let us try to decrypt the pubkey - toAddress = state.neededPubkeys[tag][0] + toAddress, _ = state.neededPubkeys[tag] if protocol.decryptAndCheckPubkeyPayload(data, toAddress) == \ 'successful': # At this point we know that we have been waiting on this @@ -427,29 +450,32 @@ class objectProcessor(threading.Thread): self.possibleNewPubkey(toAddress) # Display timing data + timeRequiredToProcessPubkey = time.time( + ) - pubkeyProcessingStartTime logger.debug( 'Time required to process this pubkey: %s', - time.time() - pubkeyProcessingStartTime) + timeRequiredToProcessPubkey) def processmsg(self, data): """Process a message object""" messageProcessingStartTime = time.time() - state.numberOfMessagesProcessed += 1 + shared.numberOfMessagesProcessed += 1 queues.UISignalQueue.put(( 'updateNumberOfMessagesProcessed', 'no data')) readPosition = 20 # bypass the nonce, time, and object type msgVersion, msgVersionLength = decodeVarint( data[readPosition:readPosition + 9]) if msgVersion != 1: - return logger.info( + logger.info( 'Cannot understand message versions other than one.' ' Ignoring message.') + return readPosition += msgVersionLength streamNumberAsClaimedByMsg, streamNumberAsClaimedByMsgLength = \ decodeVarint(data[readPosition:readPosition + 9]) readPosition += streamNumberAsClaimedByMsgLength - inventoryHash = highlevelcrypto.calculateInventoryHash(data) + inventoryHash = calculateInventoryHash(data) initialDecryptionSuccessful = False # This is not an acknowledgement bound for me. See if it is a message @@ -457,7 +483,7 @@ class objectProcessor(threading.Thread): for key, cryptorObject in sorted( shared.myECCryptorObjects.items(), - key=lambda x: random.random()): # nosec B311 + key=lambda x: random.random()): try: # continue decryption attempts to avoid timing attacks if initialDecryptionSuccessful: @@ -472,14 +498,15 @@ class objectProcessor(threading.Thread): logger.info( 'EC decryption successful using key associated' ' with ripe hash: %s.', hexlify(key)) - except Exception: # nosec B110 + except Exception: pass if not initialDecryptionSuccessful: # This is not a message bound for me. - return logger.info( + logger.info( 'Length of time program spent failing to decrypt this' ' message: %s seconds.', time.time() - messageProcessingStartTime) + return # This is a message bound for me. # Look up my address based on the RIPE hash. @@ -489,17 +516,20 @@ class objectProcessor(threading.Thread): decodeVarint(decryptedData[readPosition:readPosition + 10]) readPosition += sendersAddressVersionNumberLength if sendersAddressVersionNumber == 0: - return logger.info( + logger.info( 'Cannot understand sendersAddressVersionNumber = 0.' ' Ignoring message.') + return if sendersAddressVersionNumber > 4: - return logger.info( + logger.info( 'Sender\'s address version number %s not yet supported.' ' Ignoring message.', sendersAddressVersionNumber) + return if len(decryptedData) < 170: - return logger.info( + logger.info( 'Length of the unencrypted data is unreasonably short.' ' Sanity check failed. Ignoring message.') + return sendersStreamNumber, sendersStreamNumberLength = decodeVarint( decryptedData[readPosition:readPosition + 10]) if sendersStreamNumber == 0: @@ -528,7 +558,7 @@ class objectProcessor(threading.Thread): # for later use. endOfThePublicKeyPosition = readPosition if toRipe != decryptedData[readPosition:readPosition + 20]: - return logger.info( + logger.info( 'The original sender of this message did not send it to' ' you. Someone is attempting a Surreptitious Forwarding' ' Attack.\nSee: ' @@ -537,6 +567,7 @@ class objectProcessor(threading.Thread): hexlify(toRipe), hexlify(decryptedData[readPosition:readPosition + 20]) ) + return readPosition += 20 messageEncodingType, messageEncodingTypeLength = decodeVarint( decryptedData[readPosition:readPosition + 10]) @@ -545,6 +576,7 @@ class objectProcessor(threading.Thread): 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]) @@ -564,7 +596,8 @@ class objectProcessor(threading.Thread): if not highlevelcrypto.verify( signedData, signature, hexlify(pubSigningKey)): - return logger.debug('ECDSA verify failed') + logger.debug('ECDSA verify failed') + return logger.debug('ECDSA verify passed') if logger.isEnabledFor(logging.DEBUG): logger.debug( @@ -579,10 +612,13 @@ class objectProcessor(threading.Thread): helper_bitcoin.calculateTestnetAddressFromPubkey(pubSigningKey) ) # Used to detect and ignore duplicate messages in our inbox - sigHash = highlevelcrypto.double_sha512(signature)[32:] + sigHash = hashlib.sha512( + hashlib.sha512(signature).digest()).digest()[32:] # calculate the fromRipe. - ripe = highlevelcrypto.to_ripe(pubSigningKey, pubEncryptionKey) + sha = hashlib.new('sha512') + sha.update(pubSigningKey + pubEncryptionKey) + ripe = RIPEMD160Hash(sha.digest()).digest() fromAddress = encodeAddress( sendersAddressVersionNumber, sendersStreamNumber, ripe) @@ -609,25 +645,26 @@ class objectProcessor(threading.Thread): # If the toAddress version number is 3 or higher and not one of # my chan addresses: if decodeAddress(toAddress)[1] >= 3 \ - and not config.safeGetBoolean(toAddress, 'chan'): + and not BMConfigParser().safeGetBoolean(toAddress, 'chan'): # If I'm not friendly with this person: if not shared.isAddressInMyAddressBookSubscriptionsListOrWhitelist( fromAddress): - requiredNonceTrialsPerByte = config.getint( + requiredNonceTrialsPerByte = BMConfigParser().getint( toAddress, 'noncetrialsperbyte') - requiredPayloadLengthExtraBytes = config.getint( + requiredPayloadLengthExtraBytes = BMConfigParser().getint( toAddress, 'payloadlengthextrabytes') if not protocol.isProofOfWorkSufficient( data, requiredNonceTrialsPerByte, requiredPayloadLengthExtraBytes): - return logger.info( + logger.info( 'Proof of work in msg is insufficient only because' ' it does not meet our higher requirement.') + return # 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( + if BMConfigParser().get( 'bitmessagesettings', 'blackwhitelist') == 'black': queryreturn = sqlQuery( "SELECT label FROM blacklist where address=? and enabled='1'", @@ -645,7 +682,10 @@ class objectProcessor(threading.Thread): 'Message ignored because address not in whitelist.') blockMessage = True - # toLabel = config.safeGet(toAddress, 'label', toAddress) + toLabel = BMConfigParser().get(toAddress, 'label') + if toLabel == '': + toLabel = toAddress + try: decodedMessage = helper_msgcoding.MsgDecode( messageEncodingType, message) @@ -671,19 +711,25 @@ class objectProcessor(threading.Thread): # If we are behaving as an API then we might need to run an # outside command to let some program know that a new message # has arrived. - if config.safeGetBoolean( + if BMConfigParser().safeGetBoolean( 'bitmessagesettings', 'apienabled'): - apiNotifyPath = config.safeGet( - 'bitmessagesettings', 'apinotifypath') - if apiNotifyPath: - subprocess.call([apiNotifyPath, "newMessage"]) # nosec B603 + try: + apiNotifyPath = BMConfigParser().get( + 'bitmessagesettings', 'apinotifypath') + except: + apiNotifyPath = '' + if apiNotifyPath != '': + call([apiNotifyPath, "newMessage"]) # Let us now check and see whether our receiving address is # behaving as a mailing list - if config.safeGetBoolean(toAddress, 'mailinglist') \ + if BMConfigParser().safeGetBoolean(toAddress, 'mailinglist') \ and messageEncodingType != 0: - mailingListName = config.safeGet( - toAddress, 'mailinglistname', '') + try: + mailingListName = BMConfigParser().get( + toAddress, 'mailinglistname') + except: + mailingListName = '' # Let us send out this message as a broadcast subject = self.addMailingListNameToSubject( subject, mailingListName) @@ -699,14 +745,32 @@ class objectProcessor(threading.Thread): # 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]' + streamNumber = decodeAddress(fromAddress)[2] - ackdata = helper_sent.insert( - fromAddress=fromAddress, - status='broadcastqueued', - subject=subject, - message=message, - encoding=messageEncodingType) + ackdata = genAckPayload(streamNumber, 0) + 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', ( @@ -718,18 +782,12 @@ class objectProcessor(threading.Thread): # 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') + self.ackDataHasAValidHeader(ackData) and not blockMessage and + messageEncodingType != 0 and + not BMConfigParser().safeGetBoolean(toAddress, 'dontsendack') and + not BMConfigParser().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)) + self._ack_obj.send_data(ackData[24:]) # Display timing data timeRequiredToAttemptToDecryptMessage = time.time( @@ -750,21 +808,22 @@ class objectProcessor(threading.Thread): def processbroadcast(self, data): """Process a broadcast object""" messageProcessingStartTime = time.time() - state.numberOfBroadcastsProcessed += 1 + shared.numberOfBroadcastsProcessed += 1 queues.UISignalQueue.put(( 'updateNumberOfBroadcastsProcessed', 'no data')) - inventoryHash = highlevelcrypto.calculateInventoryHash(data) + inventoryHash = 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: - return logger.info( + 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 cleartextStreamNumber, cleartextStreamNumberLength = decodeVarint( data[readPosition:readPosition + 10]) readPosition += cleartextStreamNumberLength @@ -778,7 +837,7 @@ class objectProcessor(threading.Thread): initialDecryptionSuccessful = False for key, cryptorObject in sorted( shared.MyECSubscriptionCryptorObjects.items(), - key=lambda x: random.random()): # nosec B311 + key=lambda x: random.random()): try: # continue decryption attempts to avoid timing attacks if initialDecryptionSuccessful: @@ -801,10 +860,11 @@ class objectProcessor(threading.Thread): 'cryptorObject.decrypt Exception:', exc_info=True) if not initialDecryptionSuccessful: # This is not a broadcast I am interested in. - return logger.debug( + logger.debug( 'Length of time program spent failing to decrypt this' ' v4 broadcast: %s seconds.', time.time() - messageProcessingStartTime) + return elif broadcastVersion == 5: embeddedTag = data[readPosition:readPosition + 32] readPosition += 32 @@ -819,9 +879,10 @@ class objectProcessor(threading.Thread): decryptedData = cryptorObject.decrypt(data[readPosition:]) logger.debug('EC decryption successful') except Exception: - return logger.debug( + logger.debug( 'Broadcast version %s decryption Unsuccessful.', broadcastVersion) + return # At this point this is a broadcast I have decrypted and am # interested in. readPosition = 0 @@ -829,29 +890,32 @@ class objectProcessor(threading.Thread): decryptedData[readPosition:readPosition + 9]) if broadcastVersion == 4: if sendersAddressVersion < 2 or sendersAddressVersion > 3: - return logger.warning( + 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 elif broadcastVersion == 5: if sendersAddressVersion < 4: - return logger.info( + 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 readPosition += sendersAddressVersionLength sendersStream, sendersStreamLength = decodeVarint( decryptedData[readPosition:readPosition + 9]) if sendersStream != cleartextStreamNumber: - return logger.info( + 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 readPosition += sendersStreamLength readPosition += 4 sendersPubSigningKey = '\x04' + \ @@ -875,27 +939,30 @@ class objectProcessor(threading.Thread): requiredPayloadLengthExtraBytes) endOfPubkeyPosition = readPosition - calculatedRipe = highlevelcrypto.to_ripe( - sendersPubSigningKey, sendersPubEncryptionKey) + sha = hashlib.new('sha512') + sha.update(sendersPubSigningKey + sendersPubEncryptionKey) + calculatedRipe = RIPEMD160Hash(sha.digest()).digest() if broadcastVersion == 4: if toRipe != calculatedRipe: - return logger.info( + logger.info( 'The encryption key used to encrypt this message' ' doesn\'t match the keys inbedded in the message' ' itself. Ignoring message.' ) + return elif broadcastVersion == 5: - calculatedTag = highlevelcrypto.double_sha512( - encodeVarint(sendersAddressVersion) - + encodeVarint(sendersStream) + calculatedRipe - )[32:] + calculatedTag = hashlib.sha512(hashlib.sha512( + encodeVarint(sendersAddressVersion) + + encodeVarint(sendersStream) + calculatedRipe + ).digest()).digest()[32:] if calculatedTag != embeddedTag: - return logger.debug( + 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 messageEncodingType, messageEncodingTypeLength = decodeVarint( decryptedData[readPosition:readPosition + 9]) if messageEncodingType == 0: @@ -919,7 +986,8 @@ class objectProcessor(threading.Thread): return logger.debug('ECDSA verify passed') # Used to detect and ignore duplicate messages in our inbox - sigHash = highlevelcrypto.double_sha512(signature)[32:] + sigHash = hashlib.sha512( + hashlib.sha512(signature).digest()).digest()[32:] fromAddress = encodeAddress( sendersAddressVersion, sendersStream, calculatedRipe) @@ -938,6 +1006,10 @@ class objectProcessor(threading.Thread): # and send it. self.possibleNewPubkey(fromAddress) + fromAddress = encodeAddress( + sendersAddressVersion, sendersStream, calculatedRipe) + logger.debug('fromAddress: %s', fromAddress) + try: decodedMessage = helper_msgcoding.MsgDecode( messageEncodingType, message) @@ -960,11 +1032,14 @@ class objectProcessor(threading.Thread): # If we are behaving as an API then we might need to run an # outside command to let some program know that a new message # has arrived. - if config.safeGetBoolean('bitmessagesettings', 'apienabled'): - apiNotifyPath = config.safeGet( - 'bitmessagesettings', 'apinotifypath') - if apiNotifyPath: - subprocess.call([apiNotifyPath, "newBroadcast"]) # nosec B603 + if BMConfigParser().safeGetBoolean('bitmessagesettings', 'apienabled'): + try: + apiNotifyPath = BMConfigParser().get( + 'bitmessagesettings', 'apinotifypath') + except: + apiNotifyPath = '' + if apiNotifyPath != '': + call([apiNotifyPath, "newBroadcast"]) # Display timing data logger.info( @@ -980,7 +1055,7 @@ class objectProcessor(threading.Thread): # 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:] + _, addressVersion, streamNumber, ripe = decodeAddress(address) if addressVersion <= 3: if address in state.neededPubkeys: del state.neededPubkeys[address] @@ -993,10 +1068,10 @@ class objectProcessor(threading.Thread): # Let us create the tag from the address and see if we were waiting # for it. elif addressVersion >= 4: - tag = highlevelcrypto.double_sha512( + tag = hashlib.sha512(hashlib.sha512( encodeVarint(addressVersion) + encodeVarint(streamNumber) + ripe - )[32:] + ).digest()).digest()[32:] if tag in state.neededPubkeys: del state.neededPubkeys[tag] self.sendMessages(address) @@ -1026,7 +1101,7 @@ class objectProcessor(threading.Thread): magic, command, payloadLength, checksum = protocol.Header.unpack( ackData[:protocol.Header.size]) - if magic != protocol.magic: + if magic != 0xE9BEB4D9: logger.info('Ackdata magic bytes were wrong. Not sending ackData.') return False payload = ackData[protocol.Header.size:] diff --git a/src/class_singleCleaner.py b/src/class_singleCleaner.py index 06153dcf..b9fe3d1c 100644 --- a/src/class_singleCleaner.py +++ b/src/class_singleCleaner.py @@ -23,19 +23,15 @@ import gc import os import time +import knownnodes import queues +import shared import state -from bmconfigparser import config +import tr +from bmconfigparser import BMConfigParser 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 +from inventory import Inventory +from network import BMConnectionPool, StoppableThread class singleCleaner(StoppableThread): @@ -48,45 +44,47 @@ class singleCleaner(StoppableThread): gc.disable() timeWeLastClearedInventoryAndPubkeysTables = 0 try: - state.maximumLengthOfTimeToBotherResendingMessages = ( - config.getfloat( - 'bitmessagesettings', 'stopresendingafterxdays') + shared.maximumLengthOfTimeToBotherResendingMessages = ( + float(BMConfigParser().get( + 'bitmessagesettings', 'stopresendingafterxdays')) * 24 * 60 * 60 ) + ( - config.getfloat( - 'bitmessagesettings', 'stopresendingafterxmonths') + float(BMConfigParser().get( + 'bitmessagesettings', 'stopresendingafterxmonths')) * (60 * 60 * 24 * 365) / 12) - except: # noqa:E722 + except: # Either the user hasn't set stopresendingafterxdays and # stopresendingafterxmonths yet or the options are missing # from the config file. - state.maximumLengthOfTimeToBotherResendingMessages = float('inf') + shared.maximumLengthOfTimeToBotherResendingMessages = float('inf') + + # initial wait + if state.shutdown == 0: + self.stop.wait(singleCleaner.cycleLength) while state.shutdown == 0: - self.stop.wait(self.cycleLength) queues.UISignalQueue.put(( 'updateStatusBar', 'Doing housekeeping (Flushing inventory in memory to disk...)' )) - state.Inventory.flush() + 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. # FIXME redundant? - if state.thisapp.daemon or not state.enableGUI: + if shared.thisapp.daemon or not state.enableGUI: queues.UISignalQueue.queue.clear() - - tick = int(time.time()) - if timeWeLastClearedInventoryAndPubkeysTables < tick - 7380: - timeWeLastClearedInventoryAndPubkeysTables = tick - state.Inventory.clean() + if timeWeLastClearedInventoryAndPubkeysTables < \ + int(time.time()) - 7380: + timeWeLastClearedInventoryAndPubkeysTables = int(time.time()) + Inventory().clean() queues.workerQueue.put(('sendOnionPeerObj', '')) # pubkeys sqlExecute( "DELETE FROM pubkeys WHERE time?)", - tick, - tick - state.maximumLengthOfTimeToBotherResendingMessages + int(time.time()), int(time.time()) + - shared.maximumLengthOfTimeToBotherResendingMessages ) - for toAddress, ackData, status in queryreturn: + for row in queryreturn: + if len(row) < 2: + self.logger.error( + 'Something went wrong in the singleCleaner thread:' + ' a query did not return the requested fields. %r', + row + ) + self.stop.wait(3) + break + toAddress, ackData, status = row if status == 'awaitingpubkey': self.resendPubkeyRequest(toAddress) elif status == 'msgsent': @@ -107,9 +114,9 @@ class singleCleaner(StoppableThread): try: # Cleanup knownnodes and handle possible severe exception # while writing it to disk - if state.enableNetwork: - knownnodes.cleanupKnownNodes(connectionpool.pool) + knownnodes.cleanupKnownNodes() except Exception as err: + # pylint: disable=protected-access if "Errno 28" in str(err): self.logger.fatal( '(while writing knownnodes to disk)' @@ -117,19 +124,18 @@ class singleCleaner(StoppableThread): ) queues.UISignalQueue.put(( 'alert', - (_translate("MainWindow", "Disk full"), - _translate( + (tr._translate("MainWindow", "Disk full"), + tr._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 + if shared.thisapp.daemon or not state.enableGUI: + os._exit(1) # inv/object tracking - for connection in connectionpool.pool.connections(): + for connection in BMConnectionPool().connections(): connection.clean() # discovery tracking @@ -144,6 +150,9 @@ class singleCleaner(StoppableThread): gc.collect() + if state.shutdown == 0: + self.stop.wait(singleCleaner.cycleLength) + def resendPubkeyRequest(self, address): """Resend pubkey request for address""" self.logger.debug( @@ -156,19 +165,16 @@ class singleCleaner(StoppableThread): # 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: + except: 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) + '''UPDATE sent SET status='msgqueued' WHERE toaddress=?''', + address) queues.workerQueue.put(('sendmessage', '')) def resendMsg(self, ackdata): @@ -178,8 +184,8 @@ class singleCleaner(StoppableThread): ' to our msg. Sending again.' ) sqlExecute( - "UPDATE sent SET status = 'msgqueued'" - " WHERE ackdata = ? AND folder = 'sent'", ackdata) + '''UPDATE sent SET status='msgqueued' WHERE ackdata=?''', + ackdata) queues.workerQueue.put(('sendmessage', '')) queues.UISignalQueue.put(( 'updateStatusBar', diff --git a/src/class_singleWorker.py b/src/class_singleWorker.py index f79d9240..6d7514d4 100644 --- a/src/class_singleWorker.py +++ b/src/class_singleWorker.py @@ -16,7 +16,6 @@ import defaults import helper_inbox import helper_msgcoding import helper_random -import helper_sql import highlevelcrypto import l10n import proofofwork @@ -25,11 +24,13 @@ import queues import shared import state import tr -from addresses import decodeAddress, decodeVarint, encodeVarint -from bmconfigparser import config +from addresses import ( + calculateInventoryHash, decodeAddress, decodeVarint, encodeVarint +) +from bmconfigparser import BMConfigParser from helper_sql import sqlExecute, sqlQuery -from network import knownnodes, StoppableThread, invQueue -from six.moves import configparser, queue +from inventory import Inventory +from network import StoppableThread def sizeof_fmt(num, suffix='h/s'): @@ -47,8 +48,6 @@ class singleWorker(StoppableThread): def __init__(self): super(singleWorker, self).__init__(name="singleWorker") - self.digestAlg = config.safeGet( - 'bitmessagesettings', 'digestalg', 'sha256') proofofwork.init() def stopThread(self): @@ -56,15 +55,15 @@ class singleWorker(StoppableThread): try: queues.workerQueue.put(("stopThread", "data")) - except queue.Full: - self.logger.error('workerQueue is Full') + except: + pass super(singleWorker, self).stopThread() def run(self): # pylint: disable=attribute-defined-outside-init - while not helper_sql.sql_ready.wait(1.0) and state.shutdown == 0: - self.stop.wait(1.0) + while not state.sqlReady and state.shutdown == 0: + self.stop.wait(2) if state.shutdown > 0: return @@ -72,16 +71,18 @@ class singleWorker(StoppableThread): queryreturn = sqlQuery( '''SELECT DISTINCT toaddress FROM sent''' ''' WHERE (status='awaitingpubkey' AND folder='sent')''') - for toAddress, in queryreturn: - toAddressVersionNumber, toStreamNumber, toRipe = \ - decodeAddress(toAddress)[1:] + for row in queryreturn: + toAddress, = row + # toStatus + _, toAddressVersionNumber, toStreamNumber, toRipe = \ + decodeAddress(toAddress) if toAddressVersionNumber <= 3: state.neededPubkeys[toAddress] = 0 elif toAddressVersionNumber >= 4: - doubleHashOfAddressData = highlevelcrypto.double_sha512( - encodeVarint(toAddressVersionNumber) - + encodeVarint(toStreamNumber) + toRipe - ) + doubleHashOfAddressData = hashlib.sha512(hashlib.sha512( + encodeVarint(toAddressVersionNumber) + + encodeVarint(toStreamNumber) + toRipe + ).digest()).digest() # Note that this is the first half of the sha512 hash. privEncryptionKey = doubleHashOfAddressData[:32] tag = doubleHashOfAddressData[32:] @@ -93,34 +94,25 @@ class singleWorker(StoppableThread): hexlify(privEncryptionKey)) ) - # Initialize the state.ackdataForWhichImWatching data structure + # Initialize the shared.ackdataForWhichImWatching data structure queryreturn = sqlQuery( - '''SELECT ackdata FROM sent WHERE status = 'msgsent' AND folder = 'sent' ''') + '''SELECT ackdata FROM sent WHERE status = 'msgsent' ''') for row in queryreturn: ackdata, = row self.logger.info('Watching for ackdata %s', hexlify(ackdata)) - state.ackdataForWhichImWatching[ackdata] = 0 + shared.ackdataForWhichImWatching[ackdata] = 0 # Fix legacy (headerless) watched ackdata to include header - for oldack in state.ackdataForWhichImWatching: + for oldack in shared.ackdataForWhichImWatching: if len(oldack) == 32: # attach legacy header, always constant (msg/1/1) newack = '\x00\x00\x00\x02\x01\x01' + oldack - state.ackdataForWhichImWatching[newack] = 0 + shared.ackdataForWhichImWatching[newack] = 0 sqlExecute( - '''UPDATE sent SET ackdata=? WHERE ackdata=? AND folder = 'sent' ''', + 'UPDATE sent SET ackdata=? WHERE ackdata=?', newack, 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 + del shared.ackdataForWhichImWatching[oldack] # give some time for the GUI to start # before we start on existing POW tasks. @@ -146,38 +138,38 @@ class singleWorker(StoppableThread): if command == 'sendmessage': try: self.sendMsg() - except: # noqa:E722 - self.logger.warning("sendMsg didn't work") + except: + pass elif command == 'sendbroadcast': try: self.sendBroadcast() - except: # noqa:E722 - self.logger.warning("sendBroadcast didn't work") + except: + pass elif command == 'doPOWForMyV2Pubkey': try: self.doPOWForMyV2Pubkey(data) - except: # noqa:E722 - self.logger.warning("doPOWForMyV2Pubkey didn't work") + except: + pass elif command == 'sendOutOrStoreMyV3Pubkey': try: self.sendOutOrStoreMyV3Pubkey(data) - except: # noqa:E722 - self.logger.warning("sendOutOrStoreMyV3Pubkey didn't work") + except: + pass elif command == 'sendOutOrStoreMyV4Pubkey': try: self.sendOutOrStoreMyV4Pubkey(data) - except: # noqa:E722 - self.logger.warning("sendOutOrStoreMyV4Pubkey didn't work") + except: + pass elif command == 'sendOnionPeerObj': try: self.sendOnionPeerObj(data) - except: # noqa:E722 - self.logger.warning("sendOnionPeerObj didn't work") + except: + pass elif command == 'resetPoW': try: proofofwork.resetPoW() - except: # noqa:E722 - self.logger.warning("proofofwork.resetPoW didn't work") + except: + pass elif command == 'stopThread': self.busy = 0 return @@ -192,19 +184,15 @@ class singleWorker(StoppableThread): self.logger.info("Quitting...") def _getKeysForAddress(self, address): - 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 + privSigningKeyBase58 = BMConfigParser().get( + address, 'privsigningkey') + privEncryptionKeyBase58 = BMConfigParser().get( + address, 'privencryptionkey') - privSigningKeyHex = hexlify(highlevelcrypto.decodeWalletImportFormat( - privSigningKeyBase58.encode())) - privEncryptionKeyHex = hexlify( - highlevelcrypto.decodeWalletImportFormat( - privEncryptionKeyBase58.encode())) + privSigningKeyHex = hexlify(shared.decodeWalletImportFormat( + privSigningKeyBase58)) + privEncryptionKeyHex = hexlify(shared.decodeWalletImportFormat( + privEncryptionKeyBase58)) # The \x04 on the beginning of the public keys are not sent. # This way there is only one acceptable way to encode @@ -222,11 +210,11 @@ class singleWorker(StoppableThread): log_time=False): target = 2 ** 64 / ( defaults.networkDefaultProofOfWorkNonceTrialsPerByte * ( - len(payload) + 8 - + defaults.networkDefaultPayloadLengthExtraBytes + (( + len(payload) + 8 + + defaults.networkDefaultPayloadLengthExtraBytes + (( TTL * ( - len(payload) + 8 - + defaults.networkDefaultPayloadLengthExtraBytes + len(payload) + 8 + + defaults.networkDefaultPayloadLengthExtraBytes )) / (2 ** 16)) )) initialHash = hashlib.sha512(payload).digest() @@ -245,8 +233,8 @@ class singleWorker(StoppableThread): 'PoW took %.1f seconds, speed %s.', delta, sizeof_fmt(nonce / delta) ) - except: # noqa:E722 # NameError - self.logger.warning("Proof of Work exception") + except: # NameError + pass payload = pack('>Q', nonce) + payload return payload @@ -255,7 +243,9 @@ class singleWorker(StoppableThread): message once it is done with the POW""" # Look up my stream number based on my address hash myAddress = shared.myAddressesByHash[adressHash] - addressVersionNumber, streamNumber = decodeAddress(myAddress)[1:3] + # status + _, 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)) @@ -268,15 +258,15 @@ class singleWorker(StoppableThread): payload += protocol.getBitfield(myAddress) try: - pubSigningKey, pubEncryptionKey = self._getKeysForAddress( - myAddress)[2:] - except ValueError: - return - except Exception: # pylint:disable=broad-exception-caught + # privSigningKeyHex, privEncryptionKeyHex + _, _, pubSigningKey, pubEncryptionKey = \ + self._getKeysForAddress(myAddress) + except Exception as err: self.logger.error( 'Error within doPOWForMyV2Pubkey. Could not read' ' the keys from the keys.dat file for a requested' - ' address. %s\n', exc_info=True) + ' address. %s\n', err + ) return payload += pubSigningKey + pubEncryptionKey @@ -285,26 +275,24 @@ class singleWorker(StoppableThread): payload = self._doPOWDefaults( payload, TTL, log_prefix='(For pubkey message)') - inventoryHash = highlevelcrypto.calculateInventoryHash(payload) + inventoryHash = calculateInventoryHash(payload) objectType = 1 - state.Inventory[inventoryHash] = ( + Inventory()[inventoryHash] = ( objectType, streamNumber, payload, embeddedTime, '') self.logger.info( 'broadcasting inv with hash: %s', hexlify(inventoryHash)) - invQueue.put((streamNumber, inventoryHash)) + queues.invQueue.put((streamNumber, inventoryHash)) queues.UISignalQueue.put(('updateStatusBar', '')) try: - config.set( + BMConfigParser().set( myAddress, 'lastpubkeysendtime', str(int(time.time()))) - config.save() - except configparser.NoSectionError: + BMConfigParser().save() + except: # 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") def sendOutOrStoreMyV3Pubkey(self, adressHash): """ @@ -314,11 +302,10 @@ class singleWorker(StoppableThread): """ try: myAddress = shared.myAddressesByHash[adressHash] - except KeyError: - self.logger.warning( # The address has been deleted. - "Can't find %s in myAddressByHash", hexlify(adressHash)) + except: + # The address has been deleted. return - if config.safeGetBoolean(myAddress, 'chan'): + if BMConfigParser().safeGetBoolean(myAddress, 'chan'): self.logger.info('This is a chan address. Not sending pubkey.') return _, addressVersionNumber, streamNumber, adressHash = decodeAddress( @@ -348,24 +335,22 @@ class singleWorker(StoppableThread): # , privEncryptionKeyHex privSigningKeyHex, _, pubSigningKey, pubEncryptionKey = \ self._getKeysForAddress(myAddress) - except ValueError: - return - except Exception: # pylint:disable=broad-exception-caught + except Exception as err: self.logger.error( 'Error within sendOutOrStoreMyV3Pubkey. Could not read' ' the keys from the keys.dat file for a requested' - ' address. %s\n', exc_info=True) + ' address. %s\n', err + ) return payload += pubSigningKey + pubEncryptionKey - payload += encodeVarint(config.getint( + payload += encodeVarint(BMConfigParser().getint( myAddress, 'noncetrialsperbyte')) - payload += encodeVarint(config.getint( + payload += encodeVarint(BMConfigParser().getint( myAddress, 'payloadlengthextrabytes')) - signature = highlevelcrypto.sign( - payload, privSigningKeyHex, self.digestAlg) + signature = highlevelcrypto.sign(payload, privSigningKeyHex) payload += encodeVarint(len(signature)) payload += signature @@ -373,26 +358,24 @@ class singleWorker(StoppableThread): payload = self._doPOWDefaults( payload, TTL, log_prefix='(For pubkey message)') - inventoryHash = highlevelcrypto.calculateInventoryHash(payload) + inventoryHash = calculateInventoryHash(payload) objectType = 1 - state.Inventory[inventoryHash] = ( + Inventory()[inventoryHash] = ( objectType, streamNumber, payload, embeddedTime, '') self.logger.info( 'broadcasting inv with hash: %s', hexlify(inventoryHash)) - invQueue.put((streamNumber, inventoryHash)) + queues.invQueue.put((streamNumber, inventoryHash)) queues.UISignalQueue.put(('updateStatusBar', '')) try: - config.set( + BMConfigParser().set( myAddress, 'lastpubkeysendtime', str(int(time.time()))) - config.save() - except configparser.NoSectionError: + BMConfigParser().save() + except: # 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") def sendOutOrStoreMyV4Pubkey(self, myAddress): """ @@ -400,10 +383,10 @@ class singleWorker(StoppableThread): 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): + if not BMConfigParser().has_section(myAddress): # The address has been deleted. return - if config.safeGetBoolean(myAddress, 'chan'): + if shared.BMConfigParser().safeGetBoolean(myAddress, 'chan'): self.logger.info('This is a chan address. Not sending pubkey.') return _, addressVersionNumber, streamNumber, addressHash = decodeAddress( @@ -422,20 +405,19 @@ class singleWorker(StoppableThread): # , privEncryptionKeyHex privSigningKeyHex, _, pubSigningKey, pubEncryptionKey = \ self._getKeysForAddress(myAddress) - except ValueError: - return - except Exception: # pylint:disable=broad-exception-caught + except Exception as err: self.logger.error( 'Error within sendOutOrStoreMyV4Pubkey. Could not read' ' the keys from the keys.dat file for a requested' - ' address. %s\n', exc_info=True) + ' address. %s\n', err + ) return dataToEncrypt += pubSigningKey + pubEncryptionKey - dataToEncrypt += encodeVarint(config.getint( + dataToEncrypt += encodeVarint(BMConfigParser().getint( myAddress, 'noncetrialsperbyte')) - dataToEncrypt += encodeVarint(config.getint( + dataToEncrypt += encodeVarint(BMConfigParser().getint( myAddress, 'payloadlengthextrabytes')) # When we encrypt, we'll use a hash of the data @@ -445,13 +427,14 @@ class singleWorker(StoppableThread): # unencrypted, the pubkey with part of the hash so that nodes # know which pubkey object to try to decrypt # when they want to send a message. - doubleHashOfAddressData = highlevelcrypto.double_sha512( - encodeVarint(addressVersionNumber) - + encodeVarint(streamNumber) + addressHash - ) + doubleHashOfAddressData = hashlib.sha512(hashlib.sha512( + encodeVarint(addressVersionNumber) + + encodeVarint(streamNumber) + addressHash + ).digest()).digest() payload += doubleHashOfAddressData[32:] # the tag signature = highlevelcrypto.sign( - payload + dataToEncrypt, privSigningKeyHex, self.digestAlg) + payload + dataToEncrypt, privSigningKeyHex + ) dataToEncrypt += encodeVarint(len(signature)) dataToEncrypt += signature @@ -464,9 +447,9 @@ class singleWorker(StoppableThread): payload = self._doPOWDefaults( payload, TTL, log_prefix='(For pubkey message)') - inventoryHash = highlevelcrypto.calculateInventoryHash(payload) + inventoryHash = calculateInventoryHash(payload) objectType = 1 - state.Inventory[inventoryHash] = ( + Inventory()[inventoryHash] = ( objectType, streamNumber, payload, embeddedTime, doubleHashOfAddressData[32:] ) @@ -474,12 +457,12 @@ class singleWorker(StoppableThread): self.logger.info( 'broadcasting inv with hash: %s', hexlify(inventoryHash)) - invQueue.put((streamNumber, inventoryHash)) + queues.invQueue.put((streamNumber, inventoryHash)) queues.UISignalQueue.put(('updateStatusBar', '')) try: - config.set( + BMConfigParser().set( myAddress, 'lastpubkeysendtime', str(int(time.time()))) - config.save() + BMConfigParser().save() except Exception as err: self.logger.error( 'Error: Couldn\'t add the lastpubkeysendtime' @@ -500,9 +483,9 @@ class singleWorker(StoppableThread): objectType = protocol.OBJECT_ONIONPEER # FIXME: ideally the objectPayload should be signed objectPayload = encodeVarint(peer.port) + protocol.encodeHost(peer.host) - tag = highlevelcrypto.calculateInventoryHash(objectPayload) + tag = calculateInventoryHash(objectPayload) - if state.Inventory.by_type_and_tag(objectType, tag): + if Inventory().by_type_and_tag(objectType, tag): return # not expired payload = pack('>Q', embeddedTime) @@ -514,15 +497,15 @@ class singleWorker(StoppableThread): 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 + inventoryHash = calculateInventoryHash(payload) + Inventory()[inventoryHash] = ( + objectType, streamNumber, buffer(payload), + embeddedTime, buffer(tag) ) self.logger.info( 'sending inv (within sendOnionPeerObj function) for object: %s', hexlify(inventoryHash)) - invQueue.put((streamNumber, inventoryHash)) + queues.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)""" @@ -530,7 +513,7 @@ class singleWorker(StoppableThread): sqlExecute( '''UPDATE sent SET status='broadcastqueued' ''' - '''WHERE status = 'doingbroadcastpow' AND folder = 'sent' ''') + '''WHERE status = 'doingbroadcastpow' ''') queryreturn = sqlQuery( '''SELECT fromaddress, subject, message, ''' ''' ackdata, ttl, encodingtype FROM sent ''' @@ -553,7 +536,7 @@ class singleWorker(StoppableThread): # , privEncryptionKeyHex privSigningKeyHex, _, pubSigningKey, pubEncryptionKey = \ self._getKeysForAddress(fromaddress) - except ValueError: + except: queues.UISignalQueue.put(( 'updateSentItemStatusByAckdata', ( ackdata, @@ -563,27 +546,11 @@ class singleWorker(StoppableThread): " (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 - if not sqlExecute( - '''UPDATE sent SET status='doingbroadcastpow' ''' - ''' WHERE ackdata=? AND status='broadcastqueued' ''' - ''' AND folder='sent' ''', - ackdata): - continue + sqlExecute( + '''UPDATE sent SET status='doingbroadcastpow' ''' + ''' WHERE ackdata=? AND status='broadcastqueued' ''', + ackdata) # At this time these pubkeys are 65 bytes long # because they include the encoding byte which we won't @@ -608,10 +575,10 @@ class singleWorker(StoppableThread): payload += encodeVarint(streamNumber) if addressVersionNumber >= 4: - doubleHashOfAddressData = highlevelcrypto.double_sha512( - encodeVarint(addressVersionNumber) - + encodeVarint(streamNumber) + ripe - ) + doubleHashOfAddressData = hashlib.sha512(hashlib.sha512( + encodeVarint(addressVersionNumber) + + encodeVarint(streamNumber) + ripe + ).digest()).digest() tag = doubleHashOfAddressData[32:] payload += tag else: @@ -623,9 +590,9 @@ class singleWorker(StoppableThread): dataToEncrypt += protocol.getBitfield(fromaddress) dataToEncrypt += pubSigningKey + pubEncryptionKey if addressVersionNumber >= 3: - dataToEncrypt += encodeVarint(config.getint( + dataToEncrypt += encodeVarint(BMConfigParser().getint( fromaddress, 'noncetrialsperbyte')) - dataToEncrypt += encodeVarint(config.getint( + dataToEncrypt += encodeVarint(BMConfigParser().getint( fromaddress, 'payloadlengthextrabytes')) # message encoding type dataToEncrypt += encodeVarint(encoding) @@ -636,7 +603,7 @@ class singleWorker(StoppableThread): dataToSign = payload + dataToEncrypt signature = highlevelcrypto.sign( - dataToSign, privSigningKeyHex, self.digestAlg) + dataToSign, privSigningKeyHex) dataToEncrypt += encodeVarint(len(signature)) dataToEncrypt += signature @@ -649,8 +616,8 @@ class singleWorker(StoppableThread): # 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] @@ -681,16 +648,16 @@ class singleWorker(StoppableThread): ) continue - inventoryHash = highlevelcrypto.calculateInventoryHash(payload) + inventoryHash = calculateInventoryHash(payload) objectType = 3 - state.Inventory[inventoryHash] = ( + Inventory()[inventoryHash] = ( objectType, streamNumber, payload, embeddedTime, tag) self.logger.info( 'sending inv (within sendBroadcast function)' ' for object: %s', hexlify(inventoryHash) ) - invQueue.put((streamNumber, inventoryHash)) + queues.invQueue.put((streamNumber, inventoryHash)) queues.UISignalQueue.put(( 'updateSentItemStatusByAckdata', ( @@ -704,8 +671,8 @@ class singleWorker(StoppableThread): # Update the status of the message in the 'sent' table to have # a 'broadcastsent' status sqlExecute( - '''UPDATE sent SET msgid=?, status=?, lastactiontime=? ''' - ''' WHERE ackdata=? AND folder='sent' ''', + 'UPDATE sent SET msgid=?, status=?, lastactiontime=?' + ' WHERE ackdata=?', inventoryHash, 'broadcastsent', int(time.time()), ackdata ) @@ -715,8 +682,7 @@ class singleWorker(StoppableThread): # Reset just in case sqlExecute( '''UPDATE sent SET status='msgqueued' ''' - ''' WHERE status IN ('doingpubkeypow', 'doingmsgpow') ''' - ''' AND folder='sent' ''') + ''' WHERE status IN ('doingpubkeypow', 'doingmsgpow')''') queryreturn = sqlQuery( '''SELECT toaddress, fromaddress, subject, message, ''' ''' ackdata, status, ttl, retrynumber, encodingtype FROM ''' @@ -751,13 +717,12 @@ class singleWorker(StoppableThread): # then we won't need an entry in the pubkeys table; # we can calculate the needed pubkey using the private keys # in our keys.dat file. - elif config.has_section(toaddress): - if not sqlExecute( + elif BMConfigParser().has_section(toaddress): + sqlExecute( '''UPDATE sent SET status='doingmsgpow' ''' - ''' WHERE toaddress=? AND status='msgqueued' AND folder='sent' ''', + ''' WHERE toaddress=? AND status='msgqueued' ''', toaddress - ): - continue + ) status = 'doingmsgpow' elif status == 'msgqueued': # Let's see if we already have the pubkey in our pubkeys table @@ -768,12 +733,11 @@ class singleWorker(StoppableThread): # If we have the needed pubkey in the pubkey table already, if queryreturn != []: # set the status of this msg to doingmsgpow - if not sqlExecute( + sqlExecute( '''UPDATE sent SET status='doingmsgpow' ''' - ''' WHERE toaddress=? AND status='msgqueued' AND folder='sent' ''', + ''' WHERE toaddress=? AND status='msgqueued' ''', toaddress - ): - continue + ) status = 'doingmsgpow' # mark the pubkey as 'usedpersonally' so that # we don't delete it later. If the pubkey version @@ -790,10 +754,10 @@ class singleWorker(StoppableThread): if toAddressVersionNumber <= 3: toTag = '' else: - toTag = highlevelcrypto.double_sha512( - encodeVarint(toAddressVersionNumber) - + encodeVarint(toStreamNumber) + toRipe - )[32:] + toTag = hashlib.sha512(hashlib.sha512( + encodeVarint(toAddressVersionNumber) + + encodeVarint(toStreamNumber) + toRipe + ).digest()).digest()[32:] if toaddress in state.neededPubkeys or \ toTag in state.neededPubkeys: # We already sent a request for the pubkey @@ -827,11 +791,11 @@ class singleWorker(StoppableThread): # already contains the toAddress and cryptor # object associated with the tag for this toAddress. if toAddressVersionNumber >= 4: - doubleHashOfToAddressData = \ - highlevelcrypto.double_sha512( - encodeVarint(toAddressVersionNumber) - + encodeVarint(toStreamNumber) + toRipe - ) + doubleHashOfToAddressData = hashlib.sha512( + hashlib.sha512( + encodeVarint(toAddressVersionNumber) + encodeVarint(toStreamNumber) + toRipe + ).digest() + ).digest() # The first half of the sha512 hash. privEncryptionKey = doubleHashOfToAddressData[:32] # The second half of the sha512 hash. @@ -842,7 +806,7 @@ class singleWorker(StoppableThread): hexlify(privEncryptionKey)) ) - for value in state.Inventory.by_type_and_tag(1, toTag): + for value in Inventory().by_type_and_tag(1, toTag): # if valid, this function also puts it # in the pubkeys table. if protocol.decryptAndCheckPubkeyPayload( @@ -856,8 +820,7 @@ class singleWorker(StoppableThread): ''' toaddress=? AND ''' ''' (status='msgqueued' or ''' ''' status='awaitingpubkey' or ''' - ''' status='doingpubkeypow') AND ''' - ''' folder='sent' ''', + ''' status='doingpubkeypow')''', toaddress) del state.neededPubkeys[tag] break @@ -874,7 +837,7 @@ class singleWorker(StoppableThread): sqlExecute( '''UPDATE sent SET ''' ''' status='doingpubkeypow' WHERE ''' - ''' toaddress=? AND status='msgqueued' AND folder='sent' ''', + ''' toaddress=? AND status='msgqueued' ''', toaddress ) queues.UISignalQueue.put(( @@ -900,8 +863,8 @@ class singleWorker(StoppableThread): embeddedTime = int(time.time() + TTL) # if we aren't sending this to ourselves or a chan - if not config.has_section(toaddress): - state.ackdataForWhichImWatching[ackdata] = 0 + if not BMConfigParser().has_section(toaddress): + shared.ackdataForWhichImWatching[ackdata] = 0 queues.UISignalQueue.put(( 'updateSentItemStatusByAckdata', ( ackdata, @@ -951,7 +914,7 @@ class singleWorker(StoppableThread): if protocol.isBitSetWithinBitfield(behaviorBitfield, 30): # if we are Not willing to include the receiver's # RIPE hash on the message.. - if not config.safeGetBoolean( + if not shared.BMConfigParser().safeGetBoolean( 'bitmessagesettings', 'willinglysendtomobile' ): self.logger.info( @@ -1039,13 +1002,13 @@ class singleWorker(StoppableThread): " and %2" ).arg( str( - float(requiredAverageProofOfWorkNonceTrialsPerByte) - / defaults.networkDefaultProofOfWorkNonceTrialsPerByte + float(requiredAverageProofOfWorkNonceTrialsPerByte) / + defaults.networkDefaultProofOfWorkNonceTrialsPerByte ) ).arg( str( - float(requiredPayloadLengthExtraBytes) - / defaults.networkDefaultPayloadLengthExtraBytes + float(requiredPayloadLengthExtraBytes) / + defaults.networkDefaultPayloadLengthExtraBytes ) ) ) @@ -1053,9 +1016,9 @@ class singleWorker(StoppableThread): ) if status != 'forcepow': - maxacceptablenoncetrialsperbyte = config.getint( + maxacceptablenoncetrialsperbyte = BMConfigParser().getint( 'bitmessagesettings', 'maxacceptablenoncetrialsperbyte') - maxacceptablepayloadlengthextrabytes = config.getint( + maxacceptablepayloadlengthextrabytes = BMConfigParser().getint( 'bitmessagesettings', 'maxacceptablepayloadlengthextrabytes') cond1 = maxacceptablenoncetrialsperbyte and \ requiredAverageProofOfWorkNonceTrialsPerByte > maxacceptablenoncetrialsperbyte @@ -1067,7 +1030,7 @@ class singleWorker(StoppableThread): # we are willing to do. sqlExecute( '''UPDATE sent SET status='toodifficult' ''' - ''' WHERE ackdata=? AND folder='sent' ''', + ''' WHERE ackdata=? ''', ackdata) queues.UISignalQueue.put(( 'updateSentItemStatusByAckdata', ( @@ -1078,11 +1041,11 @@ class singleWorker(StoppableThread): " 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.. self.logger.info('Sending a message.') @@ -1091,9 +1054,9 @@ class singleWorker(StoppableThread): behaviorBitfield = protocol.getBitfield(fromaddress) try: - privEncryptionKeyBase58 = config.get( + privEncryptionKeyBase58 = BMConfigParser().get( toaddress, 'privencryptionkey') - except (configparser.NoSectionError, configparser.NoOptionError) as err: + except Exception as err: queues.UISignalQueue.put(( 'updateSentItemStatusByAckdata', ( ackdata, @@ -1111,9 +1074,8 @@ class singleWorker(StoppableThread): ' from the keys.dat file for our own address. %s\n', err) continue - privEncryptionKeyHex = hexlify( - highlevelcrypto.decodeWalletImportFormat( - privEncryptionKeyBase58.encode())) + privEncryptionKeyHex = hexlify(shared.decodeWalletImportFormat( + privEncryptionKeyBase58)) pubEncryptionKeyBase256 = unhexlify(highlevelcrypto.privToPub( privEncryptionKeyHex))[1:] requiredAverageProofOfWorkNonceTrialsPerByte = \ @@ -1142,7 +1104,7 @@ class singleWorker(StoppableThread): privSigningKeyHex, privEncryptionKeyHex, \ pubSigningKey, pubEncryptionKey = self._getKeysForAddress( fromaddress) - except ValueError: + except: queues.UISignalQueue.put(( 'updateSentItemStatusByAckdata', ( ackdata, @@ -1152,20 +1114,6 @@ class singleWorker(StoppableThread): " (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 @@ -1181,9 +1129,9 @@ class singleWorker(StoppableThread): payload += encodeVarint( defaults.networkDefaultPayloadLengthExtraBytes) else: - payload += encodeVarint(config.getint( + payload += encodeVarint(BMConfigParser().getint( fromaddress, 'noncetrialsperbyte')) - payload += encodeVarint(config.getint( + payload += encodeVarint(BMConfigParser().getint( fromaddress, 'payloadlengthextrabytes')) # This hash will be checked by the receiver of the message @@ -1196,7 +1144,7 @@ class singleWorker(StoppableThread): ) payload += encodeVarint(encodedMessage.length) payload += encodedMessage.data - if config.has_section(toaddress): + if BMConfigParser().has_section(toaddress): self.logger.info( 'Not bothering to include ackdata because we are' ' sending to ourselves or a chan.' @@ -1219,8 +1167,7 @@ class singleWorker(StoppableThread): payload += fullAckPayload dataToSign = pack('>Q', embeddedTime) + '\x00\x00\x00\x02' + \ encodeVarint(1) + encodeVarint(toStreamNumber) + payload - signature = highlevelcrypto.sign( - dataToSign, privSigningKeyHex, self.digestAlg) + signature = highlevelcrypto.sign(dataToSign, privSigningKeyHex) payload += encodeVarint(len(signature)) payload += signature @@ -1229,10 +1176,9 @@ class singleWorker(StoppableThread): encrypted = highlevelcrypto.encrypt( payload, "04" + hexlify(pubEncryptionKeyBase256) ) - except: # noqa:E722 - self.logger.warning("highlevelcrypto.encrypt didn't work") + except: sqlExecute( - '''UPDATE sent SET status='badkey' WHERE ackdata=? AND folder='sent' ''', + '''UPDATE sent SET status='badkey' WHERE ackdata=?''', ackdata ) queues.UISignalQueue.put(( @@ -1252,20 +1198,20 @@ class singleWorker(StoppableThread): encryptedPayload += encodeVarint(toStreamNumber) + encrypted target = 2 ** 64 / ( requiredAverageProofOfWorkNonceTrialsPerByte * ( - len(encryptedPayload) + 8 - + requiredPayloadLengthExtraBytes + (( + len(encryptedPayload) + 8 + + requiredPayloadLengthExtraBytes + (( TTL * ( - len(encryptedPayload) + 8 - + requiredPayloadLengthExtraBytes + len(encryptedPayload) + 8 + + requiredPayloadLengthExtraBytes )) / (2 ** 16)) )) self.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 + float(requiredAverageProofOfWorkNonceTrialsPerByte) / + defaults.networkDefaultProofOfWorkNonceTrialsPerByte, + float(requiredPayloadLengthExtraBytes) / + defaults.networkDefaultPayloadLengthExtraBytes ) powStartTime = time.time() @@ -1281,8 +1227,8 @@ class singleWorker(StoppableThread): time.time() - powStartTime, sizeof_fmt(nonce / (time.time() - powStartTime)) ) - except: # noqa:E722 - self.logger.warning("Proof of Work exception") + except: + pass encryptedPayload = pack('>Q', nonce) + encryptedPayload @@ -1298,11 +1244,11 @@ class singleWorker(StoppableThread): ) continue - inventoryHash = highlevelcrypto.calculateInventoryHash(encryptedPayload) + inventoryHash = calculateInventoryHash(encryptedPayload) objectType = 2 - state.Inventory[inventoryHash] = ( + Inventory()[inventoryHash] = ( objectType, toStreamNumber, encryptedPayload, embeddedTime, '') - if config.has_section(toaddress) or \ + if BMConfigParser().has_section(toaddress) or \ not protocol.checkBitfield(behaviorBitfield, protocol.BITFIELD_DOESACK): queues.UISignalQueue.put(( 'updateSentItemStatusByAckdata', ( @@ -1326,11 +1272,11 @@ class singleWorker(StoppableThread): 'Broadcasting inv for my msg(within sendmsg function): %s', hexlify(inventoryHash) ) - invQueue.put((toStreamNumber, inventoryHash)) + queues.invQueue.put((toStreamNumber, inventoryHash)) # Update the sent message in the sent table with the # necessary information. - if config.has_section(toaddress) or \ + if BMConfigParser().has_section(toaddress) or \ not protocol.checkBitfield(behaviorBitfield, protocol.BITFIELD_DOESACK): newStatus = 'msgsentnoackexpected' else: @@ -1339,16 +1285,17 @@ class singleWorker(StoppableThread): sleepTill = int(time.time() + TTL * 1.1) sqlExecute( '''UPDATE sent SET msgid=?, status=?, retrynumber=?, ''' - ''' sleeptill=?, lastactiontime=? WHERE ackdata=? AND folder='sent' ''', + ''' sleeptill=?, lastactiontime=? WHERE ackdata=?''', 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 config.has_section(toaddress): + if BMConfigParser().has_section(toaddress): # Used to detect and ignore duplicate messages in our inbox - sigHash = highlevelcrypto.double_sha512(signature)[32:] + sigHash = hashlib.sha512(hashlib.sha512( + signature).digest()).digest()[32:] t = (inventoryHash, toaddress, fromaddress, subject, int( time.time()), message, 'inbox', encoding, 0, sigHash) helper_inbox.insert(t) @@ -1359,16 +1306,15 @@ class singleWorker(StoppableThread): # If we are behaving as an API then we might need to run an # outside command to let some program know that a new message # has arrived. - if config.safeGetBoolean( + if BMConfigParser().safeGetBoolean( 'bitmessagesettings', 'apienabled'): - - apiNotifyPath = config.safeGet( - 'bitmessagesettings', 'apinotifypath') - - if apiNotifyPath: - # There is no additional risk of remote exploitation or - # privilege escalation - call([apiNotifyPath, "newMessage"]) # nosec B603 + try: + apiNotifyPath = BMConfigParser().get( + 'bitmessagesettings', 'apinotifypath') + except: + apiNotifyPath = '' + if apiNotifyPath != '': + call([apiNotifyPath, "newMessage"]) def requestPubKey(self, toAddress): """Send a getpubkey object""" @@ -1385,7 +1331,7 @@ class singleWorker(StoppableThread): queryReturn = sqlQuery( '''SELECT retrynumber FROM sent WHERE toaddress=? ''' ''' AND (status='doingpubkeypow' OR status='awaitingpubkey') ''' - ''' AND folder='sent' LIMIT 1''', + ''' LIMIT 1''', toAddress ) if not queryReturn: @@ -1405,13 +1351,16 @@ class singleWorker(StoppableThread): # neededPubkeys dictionary. But if we are recovering # from a restart of the client then we have to put it in now. - doubleHashOfAddressData = highlevelcrypto.double_sha512( - encodeVarint(addressVersionNumber) - + encodeVarint(streamNumber) + ripe - ) - privEncryptionKey = doubleHashOfAddressData[:32] + # Note that this is the first half of the sha512 hash. + privEncryptionKey = hashlib.sha512(hashlib.sha512( + encodeVarint(addressVersionNumber) + + encodeVarint(streamNumber) + ripe + ).digest()).digest()[:32] # Note that this is the second half of the sha512 hash. - tag = doubleHashOfAddressData[32:] + tag = hashlib.sha512(hashlib.sha512( + encodeVarint(addressVersionNumber) + + encodeVarint(streamNumber) + ripe + ).digest()).digest()[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. @@ -1441,6 +1390,7 @@ class singleWorker(StoppableThread): 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)) @@ -1454,12 +1404,12 @@ class singleWorker(StoppableThread): payload = self._doPOWDefaults(payload, TTL) - inventoryHash = highlevelcrypto.calculateInventoryHash(payload) + inventoryHash = calculateInventoryHash(payload) objectType = 1 - state.Inventory[inventoryHash] = ( + Inventory()[inventoryHash] = ( objectType, streamNumber, payload, embeddedTime, '') self.logger.info('sending inv (for the getpubkey message)') - invQueue.put((streamNumber, inventoryHash)) + queues.invQueue.put((streamNumber, inventoryHash)) # wait 10% past expiration sleeptill = int(time.time() + TTL * 1.1) @@ -1467,7 +1417,7 @@ class singleWorker(StoppableThread): '''UPDATE sent SET lastactiontime=?, ''' ''' status='awaitingpubkey', retrynumber=?, sleeptill=? ''' ''' WHERE toaddress=? AND (status='doingpubkeypow' OR ''' - ''' status='awaitingpubkey') AND folder='sent' ''', + ''' status='awaitingpubkey') ''', int(time.time()), retryNumber + 1, sleeptill, toAddress) queues.UISignalQueue.put(( diff --git a/src/class_smtpDeliver.py b/src/class_smtpDeliver.py index 9e3b8ab3..4f8422cc 100644 --- a/src/class_smtpDeliver.py +++ b/src/class_smtpDeliver.py @@ -10,7 +10,7 @@ from email.mime.text import MIMEText import queues import state -from bmconfigparser import config +from bmconfigparser import BMConfigParser from network.threads import StoppableThread SMTPDOMAIN = "bmaddr.lan" @@ -22,8 +22,11 @@ class smtpDeliver(StoppableThread): _instance = None def stopThread(self): - """Relay shutdown instruction""" - queues.UISignalQueue.put(("stopThread", "data")) + # pylint: disable=no-member + try: + queues.UISignallerQueue.put(("stopThread", "data")) + except: + pass super(smtpDeliver, self).stopThread() @classmethod @@ -48,7 +51,7 @@ class smtpDeliver(StoppableThread): ackData, message = data elif command == 'displayNewInboxMessage': inventoryHash, toAddress, fromAddress, subject, body = data - dest = config.safeGet("bitmessagesettings", "smtpdeliver", '') + dest = BMConfigParser().safeGet("bitmessagesettings", "smtpdeliver", '') if dest == '': continue try: @@ -59,9 +62,9 @@ class smtpDeliver(StoppableThread): msg['Subject'] = Header(subject, 'utf-8') msg['From'] = fromAddress + '@' + SMTPDOMAIN toLabel = map( - lambda y: config.safeGet(y, "label"), + lambda y: BMConfigParser().safeGet(y, "label"), filter( - lambda x: x == toAddress, config.addresses()) + lambda x: x == toAddress, BMConfigParser().addresses()) ) if toLabel: msg['To'] = "\"%s\" <%s>" % (Header(toLabel[0], 'utf-8'), toAddress + '@' + SMTPDOMAIN) @@ -75,7 +78,7 @@ class smtpDeliver(StoppableThread): 'Delivered via SMTP to %s through %s:%i ...', to, u.hostname, u.port) client.quit() - except: # noqa:E722 + except: self.logger.error('smtp delivery error', exc_info=True) elif command == 'displayNewSentMessage': toAddress, fromLabel, fromAddress, subject, message, ackdata = data diff --git a/src/class_smtpServer.py b/src/class_smtpServer.py index 44ea7c9c..453ca640 100644 --- a/src/class_smtpServer.py +++ b/src/class_smtpServer.py @@ -15,7 +15,7 @@ from email.parser import Parser import queues from addresses import decodeAddress -from bmconfigparser import config +from bmconfigparser import BMConfigParser from helper_ackPayload import genAckPayload from helper_sql import sqlExecute from network.threads import StoppableThread @@ -28,11 +28,6 @@ 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): @@ -51,16 +46,16 @@ class smtpServerChannel(smtpd.SMTPChannel): authstring = arg[6:] try: decoded = base64.b64decode(authstring) - correctauth = "\x00" + config.safeGet( - "bitmessagesettings", "smtpdusername", "") + "\x00" + config.safeGet( + correctauth = "\x00" + BMConfigParser().safeGet( + "bitmessagesettings", "smtpdusername", "") + "\x00" + BMConfigParser().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 SmtpServerChannelException("Auth fail") - except: # noqa:E722 + raise Exception("Auth fail") + except: self.push('501 Authentication fail') def smtp_DATA(self, arg): @@ -78,13 +73,14 @@ class smtpServerPyBitmessage(smtpd.SMTPServer): 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): """Send a bitmessage""" # pylint: disable=arguments-differ streamNumber, ripe = decodeAddress(toAddress)[2:] - stealthLevel = config.safeGetInt('bitmessagesettings', 'ackstealthlevel') + stealthLevel = BMConfigParser().safeGetInt('bitmessagesettings', 'ackstealthlevel') ackdata = genAckPayload(streamNumber, stealthLevel) sqlExecute( '''INSERT INTO sent VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''', @@ -103,7 +99,7 @@ class smtpServerPyBitmessage(smtpd.SMTPServer): 'sent', # folder 2, # encodingtype # not necessary to have a TTL higher than 2 days - min(config.getint('bitmessagesettings', 'ttl'), 86400 * 2) + min(BMConfigParser().getint('bitmessagesettings', 'ttl'), 86400 * 2) ) queues.workerQueue.put(('sendmessage', toAddress)) @@ -113,7 +109,7 @@ class smtpServerPyBitmessage(smtpd.SMTPServer): ret = [] for h in decode_header(self.msg_headers[hdr]): if h[1]: - ret.append(h[0].decode(h[1])) + ret.append(unicode(h[0], h[1])) else: ret.append(h[0].decode("utf-8", errors='replace')) @@ -122,13 +118,14 @@ class smtpServerPyBitmessage(smtpd.SMTPServer): def process_message(self, peer, mailfrom, rcpttos, data): """Process an email""" # pylint: disable=too-many-locals, too-many-branches + # print 'Receiving message from:', peer p = re.compile(".*<([^>]+)>") if not hasattr(self.channel, "auth") or not self.channel.auth: logger.error('Missing or invalid auth') return try: self.msg_headers = Parser().parsestr(data) - except: # noqa:E722 + except: logger.error('Invalid headers') return @@ -136,7 +133,7 @@ class smtpServerPyBitmessage(smtpd.SMTPServer): sender, domain = p.sub(r'\1', mailfrom).split("@") if domain != SMTPDOMAIN: raise Exception("Bad domain %s" % domain) - if sender not in config.addresses(): + if sender not in BMConfigParser().addresses(): raise Exception("Nonexisting user %s" % sender) except Exception as err: logger.debug('Bad envelope from %s: %r', mailfrom, err) @@ -146,7 +143,7 @@ class smtpServerPyBitmessage(smtpd.SMTPServer): sender, domain = msg_from.split("@") if domain != SMTPDOMAIN: raise Exception("Bad domain %s" % domain) - if sender not in config.addresses(): + if sender not in BMConfigParser().addresses(): raise Exception("Nonexisting user %s" % sender) except Exception as err: logger.error('Bad headers from %s: %r', msg_from, err) @@ -154,7 +151,7 @@ class smtpServerPyBitmessage(smtpd.SMTPServer): try: msg_subject = self.decode_header('subject')[0] - except: # noqa:E722 + except: msg_subject = "Subject missing..." msg_tmp = email.message_from_string(data) diff --git a/src/class_sqlThread.py b/src/class_sqlThread.py index 7df9e253..7e9eb6c5 100644 --- a/src/class_sqlThread.py +++ b/src/class_sqlThread.py @@ -9,22 +9,15 @@ import sys import threading import time -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 +import helper_sql +import helper_startup +import paths +import queues +import state +import tr +from bmconfigparser import BMConfigParser +from debug import logger +# pylint: disable=attribute-defined-outside-init,protected-access class sqlThread(threading.Thread): @@ -35,17 +28,12 @@ class sqlThread(threading.Thread): def run(self): # pylint: disable=too-many-locals, too-many-branches, too-many-statements """Process SQL queries from `.helper_sql.sqlSubmitQueue`""" - helper_sql.sql_available = True - config_ready.wait() self.conn = sqlite3.connect(state.appdata + 'messages.dat') self.conn.text_factory = str self.cur = self.conn.cursor() 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,''' @@ -58,7 +46,7 @@ class sqlThread(threading.Thread): self.cur.execute( '''CREATE TABLE subscriptions (label text, address text, enabled bool)''') self.cur.execute( - '''CREATE TABLE addressbook (label text, address text, UNIQUE(address) ON CONFLICT IGNORE)''') + '''CREATE TABLE addressbook (label text, address text)''') self.cur.execute( '''CREATE TABLE blacklist (label text, address text, enabled bool)''') self.cur.execute( @@ -74,7 +62,7 @@ class sqlThread(threading.Thread): '''('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','11')''') + self.cur.execute('''INSERT INTO settings VALUES('version','10')''') self.cur.execute('''INSERT INTO settings VALUES('lastvacuumtime',?)''', ( int(time.time()),)) self.cur.execute( @@ -94,7 +82,7 @@ class sqlThread(threading.Thread): # If the settings version is equal to 2 or 3 then the # sqlThread will modify the pubkeys table and change # the settings version to 4. - settingsversion = config.getint( + settingsversion = BMConfigParser().getint( 'bitmessagesettings', 'settingsversion') # People running earlier versions of PyBitmessage do not have the @@ -126,9 +114,9 @@ class sqlThread(threading.Thread): settingsversion = 4 - config.set( + BMConfigParser().set( 'bitmessagesettings', 'settingsversion', str(settingsversion)) - config.save() + BMConfigParser().save() helper_startup.updateConfig() @@ -336,7 +324,6 @@ class sqlThread(threading.Thread): # 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) @@ -370,11 +357,16 @@ class sqlThread(threading.Thread): 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 '' ;''') - - # replica for loop to update hashed address - self.cur.execute('''UPDATE pubkeys SET address=(enaddr(pubkeys.addressversion, 1, hash)); ''') - + 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: + addressHash, addressVersion = row + address = encodeAddress(addressVersion, 1, hash) + item = '''UPDATE pubkeys SET address=? WHERE hash=?;''' + parameters = (address, addressHash) + self.cur.execute(item, parameters) # Now we can remove the hash field from the pubkeys table. self.cur.execute( '''CREATE TEMPORARY TABLE pubkeys_backup''' @@ -397,25 +389,6 @@ class sqlThread(threading.Thread): ' and removing the hash field.') self.cur.execute('''update settings set value=10 WHERE key='version';''') - # 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! @@ -449,10 +422,10 @@ class sqlThread(threading.Thread): ' sqlThread will now exit.') queues.UISignalQueue.put(( 'alert', ( - _translate( + tr._translate( "MainWindow", "Disk full"), - _translate( + tr._translate( "MainWindow", 'Alert: Your disk or data storage volume is full. Bitmessage will now exit.'), True))) @@ -479,10 +452,10 @@ class sqlThread(threading.Thread): ' sqlThread will now exit.') queues.UISignalQueue.put(( 'alert', ( - _translate( + tr._translate( "MainWindow", "Disk full"), - _translate( + tr._translate( "MainWindow", 'Alert: Your disk or data storage volume is full. Bitmessage will now exit.'), True))) @@ -491,7 +464,7 @@ class sqlThread(threading.Thread): parameters = (int(time.time()),) self.cur.execute(item, parameters) - helper_sql.sql_ready.set() + state.sqlReady = True while True: item = helper_sql.sqlSubmitQueue.get() @@ -505,10 +478,10 @@ class sqlThread(threading.Thread): ' sqlThread will now exit.') queues.UISignalQueue.put(( 'alert', ( - _translate( + tr._translate( "MainWindow", "Disk full"), - _translate( + tr._translate( "MainWindow", 'Alert: Your disk or data storage volume is full. Bitmessage will now exit.'), True))) @@ -530,10 +503,10 @@ class sqlThread(threading.Thread): ' sqlThread will now exit.') queues.UISignalQueue.put(( 'alert', ( - _translate( + tr._translate( "MainWindow", "Disk full"), - _translate( + tr._translate( "MainWindow", 'Alert: Your disk or data storage volume is full. Bitmessage will now exit.'), True))) @@ -556,10 +529,10 @@ class sqlThread(threading.Thread): ' sqlThread will now exit.') queues.UISignalQueue.put(( 'alert', ( - _translate( + tr._translate( "MainWindow", "Disk full"), - _translate( + tr._translate( "MainWindow", 'Alert: Your disk or data storage volume is full. Bitmessage will now exit.'), True))) @@ -583,10 +556,10 @@ class sqlThread(threading.Thread): ' sqlThread will now exit.') queues.UISignalQueue.put(( 'alert', ( - _translate( + tr._translate( "MainWindow", "Disk full"), - _translate( + tr._translate( "MainWindow", 'Alert: Your disk or data storage volume is full. Bitmessage will now exit.'), True))) @@ -594,6 +567,8 @@ class sqlThread(threading.Thread): else: parameters = helper_sql.sqlSubmitQueue.get() rowcount = 0 + # print 'item', item + # print 'parameters', parameters try: self.cur.execute(item, parameters) rowcount = self.cur.rowcount @@ -604,10 +579,10 @@ class sqlThread(threading.Thread): ' sqlThread will now exit.') queues.UISignalQueue.put(( 'alert', ( - _translate( + tr._translate( "MainWindow", "Disk full"), - _translate( + tr._translate( "MainWindow", 'Alert: Your disk or data storage volume is full. Bitmessage will now exit.'), True))) @@ -629,12 +604,3 @@ class sqlThread(threading.Thread): 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 639be123..cab07275 100644 --- a/src/debug.py +++ b/src/debug.py @@ -35,13 +35,12 @@ Logging is thread-safe so you don't have to worry about locks, just import and log. """ +import ConfigParser import logging import logging.config import os import sys -from six.moves import configparser - import helper_startup import state @@ -50,7 +49,7 @@ helper_startup.loadConfig() # 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 +# https://bitmessage.org/forum/index.php/topic,4820.msg11163.html#msg11163 log_level = 'WARNING' @@ -75,7 +74,7 @@ def configureLogging(): False, 'Loaded logger configuration from %s' % logging_config ) - except (OSError, configparser.NoSectionError, KeyError): + except (OSError, ConfigParser.NoSectionError): if os.path.isfile(logging_config): fail_msg = \ 'Failed to load logger configuration from %s, using default' \ @@ -150,7 +149,6 @@ def resetLogging(): # ! - preconfigured, msg = configureLogging() logger = logging.getLogger('default') if msg: diff --git a/src/default.ini b/src/default.ini deleted file mode 100644 index d4420ba5..00000000 --- a/src/default.ini +++ /dev/null @@ -1,46 +0,0 @@ -[bitmessagesettings] -maxaddrperstreamsend = 500 -maxbootstrapconnections = 20 -maxdownloadrate = 0 -maxoutboundconnections = 8 -maxtotalconnections = 200 -maxuploadrate = 0 -apiinterface = 127.0.0.1 -apiport = 8442 -udp = True -port = 8444 -timeformat = %%c -blackwhitelist = black -startonlogon = False -showtraynotifications = True -startintray = False -socksproxytype = none -sockshostname = localhost -socksport = 9050 -socksauthentication = False -socksusername = -sockspassword = -keysencrypted = False -messagesencrypted = False -minimizeonclose = False -replybelow = False -stopresendingafterxdays = -stopresendingafterxmonths = -opencl = - -[threads] -receive = 3 - -[network] -bind = -dandelion = 90 - -[inventory] -storage = sqlite -acceptmismatch = False - -[knownnodes] -maxnodes = 20000 - -[zlib] -maxsize = 1048576 diff --git a/src/depends.py b/src/depends.py index d966d5fe..03e297f1 100755 --- a/src/depends.py +++ b/src/depends.py @@ -3,8 +3,6 @@ Utility functions to check the availability of dependencies and suggest how it may be installed """ -import os -import re import sys # Only really old versions of Python don't have sys.hexversion. We don't @@ -16,9 +14,8 @@ if not hasattr(sys, 'hexversion') or sys.hexversion < 0x20300F0: % sys.version ) -import logging # noqa:E402 -import subprocess # nosec B404 - +import logging +import os from importlib import import_module # We can now use logging so set up a simple configuration @@ -45,7 +42,6 @@ PACKAGE_MANAGER = { "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", @@ -59,7 +55,6 @@ PACKAGES = { "Debian": "python-qt4", "Ubuntu": "python-qt4", "Ubuntu 12": "python-qt4", - "Ubuntu 20": "", "openSUSE": "python-qt", "Fedora": "PyQt4", "Guix": "python2-pyqt@4.11.4", @@ -77,7 +72,6 @@ PACKAGES = { "Debian": "python-msgpack", "Ubuntu": "python-msgpack", "Ubuntu 12": "msgpack-python", - "Ubuntu 20": "", "openSUSE": "python-msgpack-python", "Fedora": "python2-msgpack", "Guix": "python2-msgpack", @@ -92,7 +86,6 @@ PACKAGES = { "Debian": "python-pyopencl", "Ubuntu": "python-pyopencl", "Ubuntu 12": "python-pyopencl", - "Ubuntu 20": "", "Fedora": "python2-pyopencl", "openSUSE": "", "OpenBSD": "", @@ -110,25 +103,11 @@ PACKAGES = { "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, } } @@ -168,8 +147,6 @@ def detectOSRelease(): 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): @@ -219,9 +196,9 @@ def check_sqlite(): 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] + sqlite3.sqlite_version_info[0] * 1000000 + + sqlite3.sqlite_version_info[1] * 1000 + + sqlite3.sqlite_version_info[2] ) conn = None @@ -271,6 +248,7 @@ def check_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'] @@ -280,14 +258,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: # nosec B110 # pylint:disable=bare-except + except: pass openssl_version = None @@ -323,7 +301,7 @@ def check_openssl(): ' OpenSSL 0.9.8b or later with AES, Elliptic Curves (EC),' ' ECDH, and ECDSA enabled.') return False - matches = cflags_regex.findall(openssl_cflags.decode('utf-8', "ignore")) + matches = cflags_regex.findall(openssl_cflags) if matches: logger.error( 'This OpenSSL library is missing the following required' @@ -360,8 +338,10 @@ def check_curses(): logger.error('The curses interface can not be used.') return False + import subprocess + try: - subprocess.check_call(['which', 'dialog']) # nosec B603, B607 + subprocess.check_call(['which', 'dialog']) except subprocess.CalledProcessError: logger.error( 'Curses requires the `dialog` command to be installed as well as' @@ -373,7 +353,7 @@ def check_curses(): # 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')) + logger.info('dialog Utility Version %s', unicode(dialog_util_version)) return True @@ -441,12 +421,8 @@ def check_dependencies(verbose=False, optional=False): if sys.hexversion >= 0x3000000: logger.error( 'PyBitmessage does not support Python 3+. Python 2.7.4' - ' or greater is required. Python 2.7.18 is recommended.') - sys.exit() - - # 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") + ' or greater is required.') + has_all_dependencies = False check_functions = [check_ripemd160, check_sqlite, check_openssl] if optional: @@ -456,7 +432,7 @@ def check_dependencies(verbose=False, optional=False): for check in check_functions: try: has_all_dependencies &= check() - except: # noqa:E722 + except: logger.exception('%s failed unexpectedly.', check.__name__) has_all_dependencies = False diff --git a/src/fallback/__init__.py b/src/fallback/__init__.py index f65999a1..9a8d646f 100644 --- a/src/fallback/__init__.py +++ b/src/fallback/__init__.py @@ -18,11 +18,11 @@ try: hashlib.new('ripemd160') except ValueError: try: - from Crypto.Hash import RIPEMD160 + from Crypto.Hash import RIPEMD except ImportError: RIPEMD160Hash = None else: - RIPEMD160Hash = RIPEMD160.new + RIPEMD160Hash = RIPEMD.RIPEMD160Hash else: def RIPEMD160Hash(data=None): """hashlib based RIPEMD160Hash""" diff --git a/src/helper_ackPayload.py b/src/helper_ackPayload.py index 1c5ddf98..d30f4c0d 100644 --- a/src/helper_ackPayload.py +++ b/src/helper_ackPayload.py @@ -22,26 +22,26 @@ def genAckPayload(streamNumber=1, stealthLevel=0): - level 1: a getpubkey request for a (random) dummy key hash - level 2: a standard message, encrypted to a random pubkey """ - if stealthLevel == 2: # Generate privacy-enhanced payload + if stealthLevel == 2: # Generate privacy-enhanced payload # Generate a dummy privkey and derive the pubkey dummyPubKeyHex = highlevelcrypto.privToPub( - hexlify(highlevelcrypto.randomBytes(32))) + hexlify(helper_random.randomBytes(32))) # Generate a dummy message of random length # (the smallest possible standard-formatted message is 234 bytes) - dummyMessage = highlevelcrypto.randomBytes( + dummyMessage = helper_random.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 = highlevelcrypto.randomBytes(32) + elif stealthLevel == 1: # Basic privacy payload (random getpubkey) + ackdata = helper_random.randomBytes(32) acktype = 0 # getpubkey version = 4 else: # Minimum viable payload (non stealth) - ackdata = highlevelcrypto.randomBytes(32) + ackdata = helper_random.randomBytes(32) acktype = 2 # message version = 1 diff --git a/src/helper_addressbook.py b/src/helper_addressbook.py deleted file mode 100644 index 6d354113..00000000 --- a/src/helper_addressbook.py +++ /dev/null @@ -1,14 +0,0 @@ -""" -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_inbox.py b/src/helper_inbox.py index 555795df..654dd59d 100644 --- a/src/helper_inbox.py +++ b/src/helper_inbox.py @@ -18,16 +18,6 @@ def trash(msgid): queues.UISignalQueue.put(('removeInboxRowByMsgid', msgid)) -def delete(ack_data): - """Permanent delete message from trash""" - sqlExecute("DELETE FROM inbox WHERE msgid = ?", ack_data) - - -def undeleteMessage(msgid): - """Undelte the message""" - sqlExecute('''UPDATE inbox SET folder='inbox' WHERE msgid=?''', msgid) - - def isMessageAlreadyInInbox(sigHash): """Check for previous instances of this message""" queryReturn = sqlQuery( diff --git a/src/helper_msgcoding.py b/src/helper_msgcoding.py index 05fa1c1b..76dad423 100644 --- a/src/helper_msgcoding.py +++ b/src/helper_msgcoding.py @@ -6,7 +6,7 @@ import string import zlib import messagetypes -from bmconfigparser import config +from bmconfigparser import BMConfigParser from debug import logger from tr import _translate @@ -100,11 +100,11 @@ class MsgDecode(object): """Handle extended encoding""" dc = zlib.decompressobj() tmp = "" - while len(tmp) <= config.safeGetInt("zlib", "maxsize"): + while len(tmp) <= BMConfigParser().safeGetInt("zlib", "maxsize"): try: got = dc.decompress( - data, config.safeGetInt("zlib", "maxsize") - + 1 - len(tmp)) + data, BMConfigParser().safeGetInt("zlib", "maxsize") + + 1 - len(tmp)) # EOF if got == "": break @@ -134,7 +134,7 @@ class MsgDecode(object): raise MsgDecodeException("Malformed message") try: msgObj.process() - except: # noqa:E722 + except: raise MsgDecodeException("Malformed message") if msgType == "message": self.subject = msgObj.subject diff --git a/src/helper_random.py b/src/helper_random.py index e6da707e..9a29d5e2 100644 --- a/src/helper_random.py +++ b/src/helper_random.py @@ -1,7 +1,9 @@ """Convenience functions for random operations. Not suitable for security / cryptography operations.""" +import os import random +from pyelliptic.openssl import OpenSSL NoneType = type(None) @@ -11,6 +13,14 @@ def seed(): random.seed() +def randomBytes(n): + """Method randomBytes.""" + try: + return os.urandom(n) + except NotImplementedError: + return OpenSSL.rand(n) + + def randomshuffle(population): """Method randomShuffle. diff --git a/src/helper_search.py b/src/helper_search.py index 9fcb88b5..69acec43 100644 --- a/src/helper_search.py +++ b/src/helper_search.py @@ -1,113 +1,92 @@ -""" -Additional SQL helper for searching messages. -Used by :mod:`.bitmessageqt`. -""" +"""Additional SQL helper for searching messages""" from helper_sql import sqlQuery -from tr import _translate + +try: + from PyQt4 import QtGui + haveQt = True +except ImportError: + haveQt = False -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_translate(context, text): + """Translation wrapper""" + if haveQt: + return QtGui.QApplication.translate(context, text) + return text.lower() - :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] - """ + +def search_sql(xAddress="toaddress", account=None, folder="inbox", where=None, what=None, unreadOnly=False): + """Perform a search in mailbox tables""" # pylint: disable=too-many-arguments, 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' + 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" else: - where = 'toaddress || fromaddress || subject || message' + where = "toaddress || fromaddress || subject || message" + else: + what = None - sqlStatementBase = 'SELECT toaddress, fromaddress, subject, ' + ( - 'status, ackdata, lastactiontime FROM sent ' if folder == 'sent' - else 'folder, msgid, received, read FROM inbox ' - ) + 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 ''' 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: - sqlStatementParts.append('%s LIKE ?' % (where)) + sqlStatementParts.append("folder != ?") + sqlArguments.append("trash") + if what is not None: + sqlStatementParts.append("%s LIKE ?" % (where)) sqlArguments.append(what) if unreadOnly: - sqlStatementParts.append('read = 0') + sqlStatementParts.append("read = 0") if sqlStatementParts: - sqlStatementBase += 'WHERE ' + ' AND '.join(sqlStatementParts) - if folder == 'sent': - sqlStatementBase += ' ORDER BY lastactiontime' + 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): - """ - Check if a single message matches a filter (used when new messages - are added to messagelists) - """ +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)""" # pylint: disable=too-many-arguments - 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 + 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 return True diff --git a/src/helper_sent.py b/src/helper_sent.py index aa76e756..bc3362e8 100644 --- a/src/helper_sent.py +++ b/src/helper_sent.py @@ -2,68 +2,9 @@ Insert values into sent table """ -import time -import uuid -from addresses import decodeAddress -from bmconfigparser import config -from helper_ackPayload import genAckPayload -from helper_sql import sqlExecute, sqlQuery +from helper_sql import sqlExecute -# 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'): +def insert(t): """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 + sqlExecute('''INSERT INTO sent VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''', *t) diff --git a/src/helper_sql.py b/src/helper_sql.py index 8dee9e0c..9b5dc29d 100644 --- a/src/helper_sql.py +++ b/src/helper_sql.py @@ -16,38 +16,26 @@ SQLite objects can only be used from one thread. or isn't thread-safe. """ +import Queue import threading -from six.moves import queue - - -sqlSubmitQueue = queue.Queue() +sqlSubmitQueue = Queue.Queue() """the queue for SQL""" -sqlReturnQueue = queue.Queue() +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""" +sqlLock = threading.Lock() -def sqlQuery(sql_statement, *args): +def sqlQuery(sqlStatement, *args): """ Query sqlite and return results - :param str sql_statement: SQL statement string + :param str sqlStatement: SQL statement string :param list args: SQL query parameters :rtype: list """ - assert sql_available - sql_lock.acquire() - sqlSubmitQueue.put(sql_statement) + sqlLock.acquire() + sqlSubmitQueue.put(sqlStatement) if args == (): sqlSubmitQueue.put('') @@ -56,48 +44,46 @@ def sqlQuery(sql_statement, *args): else: sqlSubmitQueue.put(args) queryreturn, _ = sqlReturnQueue.get() - sql_lock.release() + sqlLock.release() return queryreturn -def sqlExecuteChunked(sql_statement, idCount, *args): +def sqlExecuteChunked(sqlStatement, 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 - total_row_count = 0 - with sql_lock: + totalRowCount = 0 + with sqlLock: 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( - sql_statement.format(','.join('?' * len(chunk_slice))) + sqlStatement.format(','.join('?' * len(chunk_slice))) ) # first static args, and then iterative chunk sqlSubmitQueue.put( args[0:len(args) - idCount] + chunk_slice ) - ret_val = sqlReturnQueue.get() - total_row_count += ret_val[1] + retVal = sqlReturnQueue.get() + totalRowCount += retVal[1] sqlSubmitQueue.put('commit') - return total_row_count + return totalRowCount -def sqlExecute(sql_statement, *args): +def sqlExecute(sqlStatement, *args): """Execute SQL statement (optionally with arguments)""" - assert sql_available - sql_lock.acquire() - sqlSubmitQueue.put(sql_statement) + sqlLock.acquire() + sqlSubmitQueue.put(sqlStatement) if args == (): sqlSubmitQueue.put('') @@ -105,46 +91,32 @@ def sqlExecute(sql_statement, *args): sqlSubmitQueue.put(args) _, rowcount = sqlReturnQueue.get() sqlSubmitQueue.put('commit') - sql_lock.release() + sqlLock.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): """Schedule procName to be run""" - assert sql_available - sql_lock.acquire() + sqlLock.acquire() sqlSubmitQueue.put(procName) - if procName == "exit": - sqlSubmitQueue.task_done() - sqlSubmitQueue.put("terminate") - sql_lock.release() + sqlLock.release() class SqlBulkExecute(object): """This is used when you have to execute the same statement in a cycle.""" def __enter__(self): - sql_lock.acquire() + sqlLock.acquire() return self def __exit__(self, exc_type, value, traceback): sqlSubmitQueue.put('commit') - sql_lock.release() + sqlLock.release() @staticmethod - def execute(sql_statement, *args): + def execute(sqlStatement, *args): """Used for statements that do not return results.""" - assert sql_available - sqlSubmitQueue.put(sql_statement) + sqlSubmitQueue.put(sqlStatement) if args == (): sqlSubmitQueue.put('') diff --git a/src/helper_startup.py b/src/helper_startup.py index 52e1bf7a..9711c339 100644 --- a/src/helper_startup.py +++ b/src/helper_startup.py @@ -3,26 +3,18 @@ Startup operations. """ # pylint: disable=too-many-branches,too-many-statements -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 -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 +import defaults +import helper_random +import paths +import state +from bmconfigparser import BMConfigParser try: from plugins.plugin import get_plugin @@ -39,6 +31,7 @@ StoreConfigFilesInSameDirectoryAsProgramByDefault = False def loadConfig(): """Load the config""" + config = BMConfigParser() if state.appdata: config.read(state.appdata + 'keys.dat') # state.appdata must have been specified as a startup option. @@ -50,12 +43,12 @@ def loadConfig(): ' on startup: %s', state.appdata) else: config.read(paths.lookupExeFolder() + 'keys.dat') - - if config.safeGet('bitmessagesettings', 'settingsversion'): + try: + config.get('bitmessagesettings', 'settingsversion') logger.info('Loading config files from same directory as program.') needToCreateKeysFile = False state.appdata = paths.lookupExeFolder() - else: + except: # Could not load the keys.dat file in the program directory. # Perhaps it is in the appdata directory. state.appdata = paths.lookupAppdataFolder() @@ -70,9 +63,12 @@ def loadConfig(): # This appears to be the first time running the program; there is # no config file (or it cannot be accessed). Create config file. - # config.add_section('bitmessagesettings') - config.read() + config.add_section('bitmessagesettings') config.set('bitmessagesettings', 'settingsversion', '10') + config.set('bitmessagesettings', 'port', '8444') + config.set('bitmessagesettings', 'timeformat', '%%c') + config.set('bitmessagesettings', 'blackwhitelist', 'black') + config.set('bitmessagesettings', 'startonlogon', 'false') if 'linux' in sys.platform: config.set('bitmessagesettings', 'minimizetotray', 'false') # This isn't implimented yet and when True on @@ -80,16 +76,31 @@ def loadConfig(): # running when minimized. else: config.set('bitmessagesettings', 'minimizetotray', 'true') + config.set('bitmessagesettings', 'showtraynotifications', 'true') + config.set('bitmessagesettings', 'startintray', 'false') + config.set('bitmessagesettings', 'socksproxytype', 'none') + config.set('bitmessagesettings', 'sockshostname', 'localhost') + config.set('bitmessagesettings', 'socksport', '9050') + config.set('bitmessagesettings', 'socksauthentication', 'false') + config.set('bitmessagesettings', 'socksusername', '') + config.set('bitmessagesettings', 'sockspassword', '') + config.set('bitmessagesettings', 'keysencrypted', 'false') + config.set('bitmessagesettings', 'messagesencrypted', 'false') config.set( 'bitmessagesettings', 'defaultnoncetrialsperbyte', str(defaults.networkDefaultProofOfWorkNonceTrialsPerByte)) config.set( 'bitmessagesettings', 'defaultpayloadlengthextrabytes', str(defaults.networkDefaultPayloadLengthExtraBytes)) + config.set('bitmessagesettings', 'minimizeonclose', 'false') config.set('bitmessagesettings', 'dontconnect', 'true') + config.set('bitmessagesettings', 'replybelow', 'False') + config.set('bitmessagesettings', 'maxdownloadrate', '0') + config.set('bitmessagesettings', 'maxuploadrate', '0') + # UI setting to stop trying to send messages after X days/months - # config.set('bitmessagesettings', 'stopresendingafterxdays', '') - # config.set('bitmessagesettings', 'stopresendingafterxmonths', '') + config.set('bitmessagesettings', 'stopresendingafterxdays', '') + config.set('bitmessagesettings', 'stopresendingafterxmonths', '') # Are you hoping to add a new option to the keys.dat file? You're in # the right place for adding it to users who install the software for @@ -112,11 +123,11 @@ def loadConfig(): config.save() else: updateConfig() - config_ready.set() def updateConfig(): """Save the config""" + config = BMConfigParser() settingsversion = config.getint('bitmessagesettings', 'settingsversion') if settingsversion == 1: config.set('bitmessagesettings', 'socksproxytype', 'none') @@ -161,9 +172,9 @@ def updateConfig(): # acts as a salt config.set( 'bitmessagesettings', 'identiconsuffix', ''.join( - helper_random.randomchoice( - "123456789ABCDEFGHJKLMNPQRSTUVWXYZ" - "abcdefghijkmnopqrstuvwxyz") for x in range(12)) + 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 @@ -219,8 +230,7 @@ def updateConfig(): config.set( addressInKeysFile, 'payloadlengthextrabytes', str(int(previousSmallMessageDifficulty * 1000))) - except (ValueError, TypeError, configparser.NoSectionError, - configparser.NoOptionError): + except Exception: continue config.set('bitmessagesettings', 'maxdownloadrate', '0') config.set('bitmessagesettings', 'maxuploadrate', '0') @@ -231,17 +241,18 @@ def updateConfig(): 'bitmessagesettings', 'maxacceptablenoncetrialsperbyte') == 0: config.set( 'bitmessagesettings', 'maxacceptablenoncetrialsperbyte', - str(defaults.ridiculousDifficulty - * defaults.networkDefaultProofOfWorkNonceTrialsPerByte) + str(defaults.ridiculousDifficulty * + defaults.networkDefaultProofOfWorkNonceTrialsPerByte) ) - if config.safeGetInt( - 'bitmessagesettings', 'maxacceptablepayloadlengthextrabytes') == 0: + if config.safeGetInt('bitmessagesettings', 'maxacceptablepayloadlengthextrabytes') == 0: config.set( 'bitmessagesettings', 'maxacceptablepayloadlengthextrabytes', - str(defaults.ridiculousDifficulty - * defaults.networkDefaultPayloadLengthExtraBytes) + str(defaults.ridiculousDifficulty * + defaults.networkDefaultPayloadLengthExtraBytes) ) + if not config.has_option('bitmessagesettings', 'onionhostname'): + config.set('bitmessagesettings', 'onionhostname', '') if not config.has_option('bitmessagesettings', 'onionport'): config.set('bitmessagesettings', 'onionport', '8444') if not config.has_option('bitmessagesettings', 'onionbindip'): @@ -265,96 +276,25 @@ def updateConfig(): 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 +def isOurOperatingSystemLimitedToHavingVeryFewHalfOpenConnections(): + """Check for (mainly XP and Vista) limitations""" 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()) - is_limited = ( - StrictVersion("5.1.2600") <= VER_THIS - and StrictVersion("6.0.6000") >= VER_THIS + return ( + StrictVersion("5.1.2600") <= VER_THIS and + StrictVersion("6.0.6000") >= VER_THIS ) - except ValueError: + return False + except Exception: 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() + config = BMConfigParser() proxy_type = config.safeGet('bitmessagesettings', 'socksproxytype') if proxy_type and proxy_type not in ('none', 'SOCKS4a', 'SOCKS5'): try: @@ -366,7 +306,7 @@ def start_proxyconfig(): logger.error( 'Failed to run proxy config plugin %s', proxy_type, exc_info=True) - config.setTemp('bitmessagesettings', 'dontconnect', 'true') + os._exit(0) # pylint: disable=protected-access else: logger.info( 'Started proxy config plugin %s in %s sec', diff --git a/src/highlevelcrypto.py b/src/highlevelcrypto.py index b83da2f3..f392fe4a 100644 --- a/src/highlevelcrypto.py +++ b/src/highlevelcrypto.py @@ -2,118 +2,43 @@ 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. `_ + 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 -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 +import pyelliptic +from bmconfigparser import BMConfigParser +from pyelliptic import OpenSSL +from 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 makeCryptor(privkey): + """Return a private `.pyelliptic.ECC` instance""" + 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 def hexToPubkey(pubkey): """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:] + pubkey_bin = '\x02\xca\x00 ' + pubkey_raw[:32] + '\x00 ' + pubkey_raw[32:] return pubkey_bin +def makePubCryptor(pubkey): + """Return a public `.pyelliptic.ECC` instance""" + pubkey_bin = hexToPubkey(pubkey) + return pyelliptic.ECC(curve='secp256k1', pubkey=pubkey_bin) + + def privToPub(privkey): """Converts hex private key into hex public key""" private_key = a.changebase(privkey, 16, 256, minlen=32) @@ -121,6 +46,63 @@ def privToPub(privkey): return hexlify(public_key) +def encrypt(msg, hexPubkey): + """Encrypts message with hex public key""" + return pyelliptic.ECC(curve='secp256k1').encrypt( + msg, hexToPubkey(hexPubkey)) + + +def decrypt(msg, hexPrivkey): + """Decrypts message with hex private key""" + return makeCryptor(hexPrivkey).decrypt(msg) + + +def decryptFast(msg, cryptor): + """Decrypts message with an existing `.pyelliptic.ECC` object""" + return cryptor.decrypt(msg) + + +def sign(msg, hexPrivkey): + """ + Signs with hex private key using SHA1 or SHA256 depending on + "digestalg" setting + """ + digestAlg = BMConfigParser().safeGet( + 'bitmessagesettings', 'digestalg', '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) + + +def verify(msg, sig, hexPubkey): + """Verifies with hex public key using SHA1 or SHA256""" + # As mentioned above, we must upgrade gracefully to use SHA256. So + # let us check the signature using both SHA1 and SHA256 and if one + # of them passes then we will be satisfied. Eventually this can + # be simplified and we'll only check with SHA256. + try: + # old SHA1 algorithm. + sigVerifyPassed = makePubCryptor(hexPubkey).verify( + sig, msg, digest_alg=OpenSSL.digest_ecdsa_sha1) + except: + sigVerifyPassed = False + if sigVerifyPassed: + # The signature check passed using SHA1 + return True + # The signature check using SHA1 failed. Let us try it with SHA256. + try: + return makePubCryptor(hexPubkey).verify( + sig, msg, digest_alg=OpenSSL.EVP_sha256) + except: + return False + + def pointMult(secret): """ Does an EC point multiplication; turns a private key into a public key. @@ -147,6 +129,9 @@ def pointMult(secret): mb = OpenSSL.create_string_buffer(size) OpenSSL.i2o_ECPublicKey(k, OpenSSL.byref(OpenSSL.pointer(mb))) + OpenSSL.EC_POINT_free(pub_key) + OpenSSL.BN_free(priv_key) + OpenSSL.EC_KEY_free(k) return mb.raw except Exception: @@ -154,85 +139,3 @@ def pointMult(secret): import time traceback.print_exc() time.sleep(0.2) - finally: - OpenSSL.EC_POINT_free(pub_key) - OpenSSL.BN_free(priv_key) - OpenSSL.EC_KEY_free(k) - - -# Encryption - -def makeCryptor(privkey, curve='secp256k1'): - """Return a private `.pyelliptic.ECC` instance""" - private_key = a.changebase(privkey, 16, 256, minlen=32) - public_key = pointMult(private_key) - cryptor = pyelliptic.ECC( - pubkey_x=public_key[1:-32], pubkey_y=public_key[-32:], - raw_privkey=private_key, curve=curve) - return cryptor - - -def makePubCryptor(pubkey): - """Return a public `.pyelliptic.ECC` instance""" - pubkey_bin = hexToPubkey(pubkey) - return pyelliptic.ECC(curve='secp256k1', pubkey=pubkey_bin) - - -def encrypt(msg, hexPubkey): - """Encrypts message with hex public key""" - return pyelliptic.ECC(curve='secp256k1').encrypt( - msg, hexToPubkey(hexPubkey)) - - -def decrypt(msg, hexPrivkey): - """Decrypts message with hex private key""" - return makeCryptor(hexPrivkey).decrypt(msg) - - -def decryptFast(msg, cryptor): - """Decrypts message with an existing `.pyelliptic.ECC` object""" - return cryptor.decrypt(msg) - - -# Signatures - -def _choose_digest_alg(name): - """ - Choose openssl digest constant by name raises ValueError if not appropriate - """ - if name not in ("sha1", "sha256"): - raise ValueError("Unknown digest algorithm %s" % name) - return ( - # SHA1, this will eventually be deprecated - OpenSSL.digest_ecdsa_sha1 if name == "sha1" else OpenSSL.EVP_sha256) - - -def sign(msg, hexPrivkey, digestAlg="sha256"): - """ - Signs with hex private key using SHA1 or SHA256 depending on - *digestAlg* keyword. - """ - return makeCryptor(hexPrivkey).sign( - msg, digest_alg=_choose_digest_alg(digestAlg)) - - -def verify(msg, sig, hexPubkey, digestAlg=None): - """Verifies with hex public key using SHA1 or SHA256""" - # As mentioned above, we must upgrade gracefully to use SHA256. So - # let us check the signature using both SHA1 and SHA256 and if one - # of them passes then we will be satisfied. Eventually this can - # be simplified and we'll only check with SHA256. - if digestAlg is None: - # old SHA1 algorithm. - sigVerifyPassed = verify(msg, sig, hexPubkey, "sha1") - if sigVerifyPassed: - # The signature check passed using SHA1 - return True - # The signature check using SHA1 failed. Let us try it with SHA256. - return verify(msg, sig, hexPubkey, "sha256") - - try: - return makePubCryptor(hexPubkey).verify( - sig, msg, digest_alg=_choose_digest_alg(digestAlg)) - except: - return False diff --git a/src/images/kivy/down-arrow.png b/src/images/kivy/down-arrow.png deleted file mode 100644 index bf3e864c..00000000 Binary files a/src/images/kivy/down-arrow.png and /dev/null differ diff --git a/src/images/kivy/draft-icon.png b/src/images/kivy/draft-icon.png deleted file mode 100644 index 9fc38f31..00000000 Binary files a/src/images/kivy/draft-icon.png and /dev/null differ diff --git a/src/images/kivy/drawer_logo1.png b/src/images/kivy/drawer_logo1.png deleted file mode 100644 index 256f9be6..00000000 Binary files a/src/images/kivy/drawer_logo1.png and /dev/null differ diff --git a/src/images/kivy/loader.gif b/src/images/kivy/loader.gif deleted file mode 100644 index 34ab1943..00000000 Binary files a/src/images/kivy/loader.gif and /dev/null differ diff --git a/src/images/kivy/payment/btc.png b/src/images/kivy/payment/btc.png deleted file mode 100644 index 33302ff8..00000000 Binary files a/src/images/kivy/payment/btc.png and /dev/null differ diff --git a/src/images/kivy/payment/buy.png b/src/images/kivy/payment/buy.png deleted file mode 100644 index 3a63af11..00000000 Binary files a/src/images/kivy/payment/buy.png and /dev/null differ diff --git a/src/images/kivy/payment/buynew1.png b/src/images/kivy/payment/buynew1.png deleted file mode 100644 index f02090f8..00000000 Binary files a/src/images/kivy/payment/buynew1.png and /dev/null differ diff --git a/src/images/kivy/payment/gplay.png b/src/images/kivy/payment/gplay.png deleted file mode 100644 index 69550edd..00000000 Binary files a/src/images/kivy/payment/gplay.png and /dev/null differ diff --git a/src/images/kivy/payment/paypal.png b/src/images/kivy/payment/paypal.png deleted file mode 100644 index f994130d..00000000 Binary files a/src/images/kivy/payment/paypal.png and /dev/null differ diff --git a/src/images/kivy/right-arrow.png b/src/images/kivy/right-arrow.png deleted file mode 100644 index 8f136a77..00000000 Binary files a/src/images/kivy/right-arrow.png and /dev/null differ diff --git a/src/images/kivy/search.png b/src/images/kivy/search.png deleted file mode 100644 index 42a1e45a..00000000 Binary files a/src/images/kivy/search.png and /dev/null differ diff --git a/src/images/kivy/text_images/!.png b/src/images/kivy/text_images/!.png deleted file mode 100644 index bac2f246..00000000 Binary files a/src/images/kivy/text_images/!.png and /dev/null differ diff --git a/src/images/kivy/text_images/0.png b/src/images/kivy/text_images/0.png deleted file mode 100644 index 2b8b63e3..00000000 Binary files a/src/images/kivy/text_images/0.png and /dev/null differ diff --git a/src/images/kivy/text_images/1.png b/src/images/kivy/text_images/1.png deleted file mode 100644 index 3918f6d3..00000000 Binary files a/src/images/kivy/text_images/1.png and /dev/null differ diff --git a/src/images/kivy/text_images/2.png b/src/images/kivy/text_images/2.png deleted file mode 100644 index 0cf202e9..00000000 Binary files a/src/images/kivy/text_images/2.png and /dev/null differ diff --git a/src/images/kivy/text_images/3.png b/src/images/kivy/text_images/3.png deleted file mode 100644 index f9d612dd..00000000 Binary files a/src/images/kivy/text_images/3.png and /dev/null differ diff --git a/src/images/kivy/text_images/4.png b/src/images/kivy/text_images/4.png deleted file mode 100644 index f2ab33e1..00000000 Binary files a/src/images/kivy/text_images/4.png and /dev/null differ diff --git a/src/images/kivy/text_images/5.png b/src/images/kivy/text_images/5.png deleted file mode 100644 index 09d6e56e..00000000 Binary files a/src/images/kivy/text_images/5.png and /dev/null differ diff --git a/src/images/kivy/text_images/6.png b/src/images/kivy/text_images/6.png deleted file mode 100644 index e385a954..00000000 Binary files a/src/images/kivy/text_images/6.png and /dev/null differ diff --git a/src/images/kivy/text_images/7.png b/src/images/kivy/text_images/7.png deleted file mode 100644 index 55fc4f77..00000000 Binary files a/src/images/kivy/text_images/7.png and /dev/null differ diff --git a/src/images/kivy/text_images/8.png b/src/images/kivy/text_images/8.png deleted file mode 100644 index 2a3fa76f..00000000 Binary files a/src/images/kivy/text_images/8.png and /dev/null differ diff --git a/src/images/kivy/text_images/9.png b/src/images/kivy/text_images/9.png deleted file mode 100644 index 81ad9084..00000000 Binary files a/src/images/kivy/text_images/9.png and /dev/null differ diff --git a/src/images/kivy/text_images/A.png b/src/images/kivy/text_images/A.png deleted file mode 100644 index 64ed6110..00000000 Binary files a/src/images/kivy/text_images/A.png and /dev/null differ diff --git a/src/images/kivy/text_images/B.png b/src/images/kivy/text_images/B.png deleted file mode 100644 index 2db56c1f..00000000 Binary files a/src/images/kivy/text_images/B.png and /dev/null differ diff --git a/src/images/kivy/text_images/C.png b/src/images/kivy/text_images/C.png deleted file mode 100644 index 47a4052c..00000000 Binary files a/src/images/kivy/text_images/C.png and /dev/null differ diff --git a/src/images/kivy/text_images/D.png b/src/images/kivy/text_images/D.png deleted file mode 100644 index 2549ffc2..00000000 Binary files a/src/images/kivy/text_images/D.png and /dev/null differ diff --git a/src/images/kivy/text_images/E.png b/src/images/kivy/text_images/E.png deleted file mode 100644 index 5d631611..00000000 Binary files a/src/images/kivy/text_images/E.png and /dev/null differ diff --git a/src/images/kivy/text_images/F.png b/src/images/kivy/text_images/F.png deleted file mode 100644 index 43086f38..00000000 Binary files a/src/images/kivy/text_images/F.png and /dev/null differ diff --git a/src/images/kivy/text_images/G.png b/src/images/kivy/text_images/G.png deleted file mode 100644 index 32d1709d..00000000 Binary files a/src/images/kivy/text_images/G.png and /dev/null differ diff --git a/src/images/kivy/text_images/H.png b/src/images/kivy/text_images/H.png deleted file mode 100644 index 279bd1ce..00000000 Binary files a/src/images/kivy/text_images/H.png and /dev/null differ diff --git a/src/images/kivy/text_images/I.png b/src/images/kivy/text_images/I.png deleted file mode 100644 index c88f048d..00000000 Binary files a/src/images/kivy/text_images/I.png and /dev/null differ diff --git a/src/images/kivy/text_images/J.png b/src/images/kivy/text_images/J.png deleted file mode 100644 index 15331171..00000000 Binary files a/src/images/kivy/text_images/J.png and /dev/null differ diff --git a/src/images/kivy/text_images/K.png b/src/images/kivy/text_images/K.png deleted file mode 100644 index 9afcadd7..00000000 Binary files a/src/images/kivy/text_images/K.png and /dev/null differ diff --git a/src/images/kivy/text_images/L.png b/src/images/kivy/text_images/L.png deleted file mode 100644 index e841b9d9..00000000 Binary files a/src/images/kivy/text_images/L.png and /dev/null differ diff --git a/src/images/kivy/text_images/M.png b/src/images/kivy/text_images/M.png deleted file mode 100644 index 10de35e9..00000000 Binary files a/src/images/kivy/text_images/M.png and /dev/null differ diff --git a/src/images/kivy/text_images/N.png b/src/images/kivy/text_images/N.png deleted file mode 100644 index 2d235d06..00000000 Binary files a/src/images/kivy/text_images/N.png and /dev/null differ diff --git a/src/images/kivy/text_images/O.png b/src/images/kivy/text_images/O.png deleted file mode 100644 index c0cc972a..00000000 Binary files a/src/images/kivy/text_images/O.png and /dev/null differ diff --git a/src/images/kivy/text_images/P.png b/src/images/kivy/text_images/P.png deleted file mode 100644 index 57ec5012..00000000 Binary files a/src/images/kivy/text_images/P.png and /dev/null differ diff --git a/src/images/kivy/text_images/Q.png b/src/images/kivy/text_images/Q.png deleted file mode 100644 index 27ffd18b..00000000 Binary files a/src/images/kivy/text_images/Q.png and /dev/null differ diff --git a/src/images/kivy/text_images/R.png b/src/images/kivy/text_images/R.png deleted file mode 100644 index 090646f5..00000000 Binary files a/src/images/kivy/text_images/R.png and /dev/null differ diff --git a/src/images/kivy/text_images/S.png b/src/images/kivy/text_images/S.png deleted file mode 100644 index 444419cf..00000000 Binary files a/src/images/kivy/text_images/S.png and /dev/null differ diff --git a/src/images/kivy/text_images/T.png b/src/images/kivy/text_images/T.png deleted file mode 100644 index ace7b36b..00000000 Binary files a/src/images/kivy/text_images/T.png and /dev/null differ diff --git a/src/images/kivy/text_images/U.png b/src/images/kivy/text_images/U.png deleted file mode 100644 index a47f326e..00000000 Binary files a/src/images/kivy/text_images/U.png and /dev/null differ diff --git a/src/images/kivy/text_images/V.png b/src/images/kivy/text_images/V.png deleted file mode 100644 index da07d0ac..00000000 Binary files a/src/images/kivy/text_images/V.png and /dev/null differ diff --git a/src/images/kivy/text_images/W.png b/src/images/kivy/text_images/W.png deleted file mode 100644 index a00f9d7c..00000000 Binary files a/src/images/kivy/text_images/W.png and /dev/null differ diff --git a/src/images/kivy/text_images/X.png b/src/images/kivy/text_images/X.png deleted file mode 100644 index be919fc4..00000000 Binary files a/src/images/kivy/text_images/X.png and /dev/null differ diff --git a/src/images/kivy/text_images/Y.png b/src/images/kivy/text_images/Y.png deleted file mode 100644 index 4819bbd1..00000000 Binary files a/src/images/kivy/text_images/Y.png and /dev/null differ diff --git a/src/images/kivy/text_images/Z.png b/src/images/kivy/text_images/Z.png deleted file mode 100644 index 7d1c8e01..00000000 Binary files a/src/images/kivy/text_images/Z.png and /dev/null differ diff --git a/src/images/kivymd_logo.png b/src/images/kivymd_logo.png new file mode 100644 index 00000000..ce39b0d4 Binary files /dev/null and b/src/images/kivymd_logo.png differ diff --git a/src/images/me.jpg b/src/images/me.jpg new file mode 100644 index 00000000..f54c791f Binary files /dev/null and b/src/images/me.jpg differ diff --git a/src/images/ngletteravatar/1.png b/src/images/ngletteravatar/1.png new file mode 100644 index 00000000..9436c7d2 Binary files /dev/null and b/src/images/ngletteravatar/1.png differ diff --git a/src/images/ngletteravatar/12.png b/src/images/ngletteravatar/12.png new file mode 100644 index 00000000..de894b4a Binary files /dev/null and b/src/images/ngletteravatar/12.png differ diff --git a/src/images/ngletteravatar/14.png b/src/images/ngletteravatar/14.png new file mode 100644 index 00000000..42b692dd Binary files /dev/null and b/src/images/ngletteravatar/14.png differ diff --git a/src/images/ngletteravatar/3.png b/src/images/ngletteravatar/3.png new file mode 100644 index 00000000..ad83cef2 Binary files /dev/null and b/src/images/ngletteravatar/3.png differ diff --git a/src/images/ngletteravatar/5.png b/src/images/ngletteravatar/5.png new file mode 100644 index 00000000..3875aace Binary files /dev/null and b/src/images/ngletteravatar/5.png differ diff --git a/src/images/ngletteravatar/56.png b/src/images/ngletteravatar/56.png new file mode 100644 index 00000000..33f038cf Binary files /dev/null and b/src/images/ngletteravatar/56.png differ diff --git a/src/images/ngletteravatar/65.png b/src/images/ngletteravatar/65.png new file mode 100644 index 00000000..fb608098 Binary files /dev/null and b/src/images/ngletteravatar/65.png differ diff --git a/src/images/ngletteravatar/8.png b/src/images/ngletteravatar/8.png new file mode 100644 index 00000000..dd2671e9 Binary files /dev/null and b/src/images/ngletteravatar/8.png differ diff --git a/src/images/ngletteravatar/90.png b/src/images/ngletteravatar/90.png new file mode 100644 index 00000000..7d1770ab Binary files /dev/null and b/src/images/ngletteravatar/90.png differ diff --git a/src/images/ngletteravatar/Galleryr_rcirclelogo_Small.jpg b/src/images/ngletteravatar/Galleryr_rcirclelogo_Small.jpg new file mode 100644 index 00000000..8e3b1c35 Binary files /dev/null and b/src/images/ngletteravatar/Galleryr_rcirclelogo_Small.jpg differ diff --git a/src/images/ngletteravatar/a.png b/src/images/ngletteravatar/a.png new file mode 100644 index 00000000..1c3bb8bf Binary files /dev/null and b/src/images/ngletteravatar/a.png differ diff --git a/src/images/ngletteravatar/b.png b/src/images/ngletteravatar/b.png new file mode 100644 index 00000000..462bf808 Binary files /dev/null and b/src/images/ngletteravatar/b.png differ diff --git a/src/images/ngletteravatar/c.png b/src/images/ngletteravatar/c.png new file mode 100644 index 00000000..180b7edc Binary files /dev/null and b/src/images/ngletteravatar/c.png differ diff --git a/src/images/ngletteravatar/d.png b/src/images/ngletteravatar/d.png new file mode 100644 index 00000000..9e983e4d Binary files /dev/null and b/src/images/ngletteravatar/d.png differ diff --git a/src/images/ngletteravatar/depositphotos_142729281-stock-illustration-letter-l-sign-design-template.jpg b/src/images/ngletteravatar/depositphotos_142729281-stock-illustration-letter-l-sign-design-template.jpg new file mode 100644 index 00000000..7630511f Binary files /dev/null and b/src/images/ngletteravatar/depositphotos_142729281-stock-illustration-letter-l-sign-design-template.jpg differ diff --git a/src/images/ngletteravatar/e.png b/src/images/ngletteravatar/e.png new file mode 100644 index 00000000..60961992 Binary files /dev/null and b/src/images/ngletteravatar/e.png differ diff --git a/src/images/ngletteravatar/g.png b/src/images/ngletteravatar/g.png new file mode 100644 index 00000000..762cc6fa Binary files /dev/null and b/src/images/ngletteravatar/g.png differ diff --git a/src/images/ngletteravatar/h.png b/src/images/ngletteravatar/h.png new file mode 100644 index 00000000..8f752952 Binary files /dev/null and b/src/images/ngletteravatar/h.png differ diff --git a/src/images/ngletteravatar/i.png b/src/images/ngletteravatar/i.png new file mode 100644 index 00000000..a89710ef Binary files /dev/null and b/src/images/ngletteravatar/i.png differ diff --git a/src/images/ngletteravatar/j.png b/src/images/ngletteravatar/j.png new file mode 100644 index 00000000..926de34c Binary files /dev/null and b/src/images/ngletteravatar/j.png differ diff --git a/src/images/ngletteravatar/k.png b/src/images/ngletteravatar/k.png new file mode 100644 index 00000000..77da163e Binary files /dev/null and b/src/images/ngletteravatar/k.png differ diff --git a/src/images/ngletteravatar/l.png b/src/images/ngletteravatar/l.png new file mode 100644 index 00000000..e6112f2a Binary files /dev/null and b/src/images/ngletteravatar/l.png differ diff --git a/src/images/ngletteravatar/m.png b/src/images/ngletteravatar/m.png new file mode 100644 index 00000000..c93554a1 Binary files /dev/null and b/src/images/ngletteravatar/m.png differ diff --git a/src/images/ngletteravatar/n.png b/src/images/ngletteravatar/n.png new file mode 100644 index 00000000..8158a1e4 Binary files /dev/null and b/src/images/ngletteravatar/n.png differ diff --git a/src/images/ngletteravatar/o.png b/src/images/ngletteravatar/o.png new file mode 100644 index 00000000..729a82a6 Binary files /dev/null and b/src/images/ngletteravatar/o.png differ diff --git a/src/images/ngletteravatar/p.png b/src/images/ngletteravatar/p.png new file mode 100644 index 00000000..b3adb0ce Binary files /dev/null and b/src/images/ngletteravatar/p.png differ diff --git a/src/images/ngletteravatar/r.png b/src/images/ngletteravatar/r.png new file mode 100644 index 00000000..1b64b8ee Binary files /dev/null and b/src/images/ngletteravatar/r.png differ diff --git a/src/images/ngletteravatar/s.png b/src/images/ngletteravatar/s.png new file mode 100644 index 00000000..8813d11a Binary files /dev/null and b/src/images/ngletteravatar/s.png differ diff --git a/src/images/ngletteravatar/t.jpg b/src/images/ngletteravatar/t.jpg new file mode 100644 index 00000000..20932aad Binary files /dev/null and b/src/images/ngletteravatar/t.jpg differ diff --git a/src/images/ngletteravatar/u.png b/src/images/ngletteravatar/u.png new file mode 100644 index 00000000..6af53add Binary files /dev/null and b/src/images/ngletteravatar/u.png differ diff --git a/src/images/ngletteravatar/v.png b/src/images/ngletteravatar/v.png new file mode 100644 index 00000000..aaaf191e Binary files /dev/null and b/src/images/ngletteravatar/v.png differ diff --git a/src/images/ngletteravatar/w.png b/src/images/ngletteravatar/w.png new file mode 100644 index 00000000..20ff7ed9 Binary files /dev/null and b/src/images/ngletteravatar/w.png differ diff --git a/src/images/ngletteravatar/x.jpg b/src/images/ngletteravatar/x.jpg new file mode 100644 index 00000000..107f1732 Binary files /dev/null and b/src/images/ngletteravatar/x.jpg differ diff --git a/src/images/ngletteravatar/z.png b/src/images/ngletteravatar/z.png new file mode 100644 index 00000000..efcda8fe Binary files /dev/null and b/src/images/ngletteravatar/z.png differ diff --git a/src/inventory.py b/src/inventory.py index 5b739e84..fc06e455 100644 --- a/src/inventory.py +++ b/src/inventory.py @@ -1,29 +1,25 @@ -"""The Inventory""" +"""The Inventory singleton""" # TODO make this dynamic, and watch out for frozen, like with messagetypes import storage.filesystem import storage.sqlite -from bmconfigparser import config +from bmconfigparser import BMConfigParser +from singleton import Singleton -def create_inventory_instance(backend="sqlite"): +@Singleton +class Inventory(): """ - Create an instance of the inventory class - defined in `storage.`. - """ - return getattr( - getattr(storage, backend), - "{}Inventory".format(backend.title()))() - - -class Inventory: - """ - Inventory class which uses storage backends + Inventory singleton class which uses storage backends to manage the inventory. """ def __init__(self): - self._moduleName = config.safeGet("inventory", "storage") - self._realInventory = create_inventory_instance(self._moduleName) + self._moduleName = BMConfigParser().safeGet("inventory", "storage") + self._inventoryClass = getattr( + getattr(storage, self._moduleName), + "{}Inventory".format(self._moduleName.title()) + ) + self._realInventory = self._inventoryClass() self.numberOfInventoryLookupsPerformed = 0 # cheap inheritance copied from asyncore @@ -43,6 +39,3 @@ class Inventory: # hint for pylint: this is dictionary like object def __getitem__(self, key): return self._realInventory[key] - - def __setitem__(self, key, value): - self._realInventory[key] = value diff --git a/src/kivymd/LICENSE b/src/kivymd/LICENSE new file mode 100644 index 00000000..a17ea136 --- /dev/null +++ b/src/kivymd/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Andrés Rodríguez and KivyMD contributors + +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. \ No newline at end of file diff --git a/src/kivymd/__init__.py b/src/kivymd/__init__.py new file mode 100644 index 00000000..bc07270c --- /dev/null +++ b/src/kivymd/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +import os + +path = os.path.dirname(__file__) +fonts_path = os.path.join(path, "fonts/") +images_path = os.path.join(path, 'images/') diff --git a/src/kivymd/accordion.py b/src/kivymd/accordion.py new file mode 100644 index 00000000..6e816ca6 --- /dev/null +++ b/src/kivymd/accordion.py @@ -0,0 +1,254 @@ +# -*- coding: utf-8 -*- + +from kivy.lang import Builder +from kivy.properties import StringProperty, ListProperty, OptionProperty +from kivy.utils import get_color_from_hex +from kivymd.color_definitions import colors +from kivymd.theming import ThemableBehavior +from kivy.uix.accordion import Accordion, AccordionItem +from kivymd.backgroundcolorbehavior import BackgroundColorBehavior +from kivy.uix.boxlayout import BoxLayout + + +class MDAccordionItemTitleLayout(ThemableBehavior, BackgroundColorBehavior, BoxLayout): + pass + + +class MDAccordion(ThemableBehavior, BackgroundColorBehavior, Accordion): + pass + + +class MDAccordionItem(ThemableBehavior, AccordionItem): + title_theme_color = OptionProperty(None, allownone=True, + options=['Primary', 'Secondary', 'Hint', + 'Error', 'Custom']) + ''' Color theme for title text and icon ''' + + title_color = ListProperty(None, allownone=True) + ''' Color for title text and icon if `title_theme_color` is Custom ''' + + background_color = ListProperty(None, allownone=True) + ''' Color for the background of the accordian item title in rgba format. + ''' + + divider_color = ListProperty(None, allownone=True) + ''' Color for dividers between different titles in rgba format + To remove the divider set a color with an alpha of 0. + ''' + + indicator_color = ListProperty(None, allownone=True) + ''' Color for the indicator on the side of the active item in rgba format + To remove the indicator set a color with an alpha of 0. + ''' + + font_style = OptionProperty( + 'Subhead', options=['Body1', 'Body2', 'Caption', 'Subhead', 'Title', + 'Headline', 'Display1', 'Display2', 'Display3', + 'Display4', 'Button', 'Icon']) + ''' Font style to use for the title text ''' + + title_template = StringProperty('MDAccordionItemTitle') + ''' Template to use for the title ''' + + icon = StringProperty(None,allownone=True) + ''' Icon name to use when this item is expanded ''' + + icon_expanded = StringProperty('chevron-up') + ''' Icon name to use when this item is expanded ''' + + icon_collapsed = StringProperty('chevron-down') + ''' Icon name to use when this item is collapsed ''' + + +Builder.load_string(''' +#:import MDLabel kivymd.label.MDLabel +#:import md_icons kivymd.icon_definitions.md_icons + + +: + canvas.before: + Color: + rgba: self.background_color or self.theme_cls.primary_color + Rectangle: + size:self.size + pos:self.pos + + PushMatrix + Translate: + xy: (dp(2),0) if self.orientation == 'vertical' else (0,dp(2)) + canvas.after: + PopMatrix + Color: + rgba: self.divider_color or self.theme_cls.divider_color + Rectangle: + size:(dp(1),self.height) if self.orientation == 'horizontal' else (self.width,dp(1)) + pos:self.pos + Color: + rgba: [0,0,0,0] if self.collapse else (self.indicator_color or self.theme_cls.accent_color) + Rectangle: + size:(dp(2),self.height) if self.orientation == 'vertical' else (self.width,dp(2)) + pos:self.pos + +[MDAccordionItemTitle@MDAccordionItemTitleLayout]: + padding: '12dp' + spacing: '12dp' + orientation: 'horizontal' if ctx.item.orientation=='vertical' else 'vertical' + canvas: + PushMatrix + Translate: + xy: (-dp(2),0) if ctx.item.orientation == 'vertical' else (0,-dp(2)) + + Color: + rgba: self.background_color or self.theme_cls.primary_color + Rectangle: + size:self.size + pos:self.pos + + canvas.after: + Color: + rgba: [0,0,0,0] if ctx.item.collapse else (ctx.item.indicator_color or self.theme_cls.accent_color) + Rectangle: + size:(dp(2),self.height) if ctx.item.orientation == 'vertical' else (self.width,dp(2)) + pos:self.pos + PopMatrix + MDLabel: + id:_icon + theme_text_color:ctx.item.title_theme_color if ctx.item.icon else 'Custom' + text_color:ctx.item.title_color if ctx.item.icon else [0,0,0,0] + text: md_icons[ctx.item.icon if ctx.item.icon else 'menu'] + font_style:'Icon' + size_hint: (None,1) if ctx.item.orientation == 'vertical' else (1,None) + size: ((self.texture_size[0],1) if ctx.item.orientation == 'vertical' else (1,self.texture_size[1])) \ + if ctx.item.icon else (0,0) + text_size: (self.width, None) if ctx.item.orientation=='vertical' else (None,self.width) + canvas.before: + PushMatrix + Rotate: + angle: 90 if ctx.item.orientation == 'horizontal' else 0 + origin: self.center + canvas.after: + PopMatrix + MDLabel: + id:_label + theme_text_color:ctx.item.title_theme_color + text_color:ctx.item.title_color + text: ctx.item.title + font_style:ctx.item.font_style + text_size: (self.width, None) if ctx.item.orientation=='vertical' else (None,self.width) + canvas.before: + PushMatrix + Rotate: + angle: 90 if ctx.item.orientation == 'horizontal' else 0 + origin: self.center + canvas.after: + PopMatrix + + MDLabel: + id:_expand_icon + theme_text_color:ctx.item.title_theme_color + text_color:ctx.item.title_color + font_style:'Icon' + size_hint: (None,1) if ctx.item.orientation == 'vertical' else (1,None) + size: (self.texture_size[0],1) if ctx.item.orientation == 'vertical' else (1,self.texture_size[1]) + text:md_icons[ctx.item.icon_collapsed if ctx.item.collapse else ctx.item.icon_expanded] + halign: 'right' if ctx.item.orientation=='vertical' else 'center' + #valign: 'middle' if ctx.item.orientation=='vertical' else 'bottom' + canvas.before: + PushMatrix + Rotate: + angle: 90 if ctx.item.orientation == 'horizontal' else 0 + origin:self.center + canvas.after: + PopMatrix + +''') + +if __name__ == '__main__': + from kivy.app import App + from kivymd.theming import ThemeManager + + class AccordionApp(App): + theme_cls = ThemeManager() + + def build(self): + # self.theme_cls.primary_palette = 'Indigo' + return Builder.load_string(""" +#:import MDLabel kivymd.label.MDLabel +#:import MDList kivymd.list.MDList +#:import OneLineListItem kivymd.list.OneLineListItem +BoxLayout: + spacing: '64dp' + MDAccordion: + orientation:'vertical' + MDAccordionItem: + title:'Item 1' + icon: 'home' + ScrollView: + MDList: + OneLineListItem: + text: "Subitem 1" + theme_text_color: 'Custom' + text_color: [1,1,1,1] + OneLineListItem: + text: "Subitem 2" + theme_text_color: 'Custom' + text_color: [1,1,1,1] + OneLineListItem: + text: "Subitem 3" + theme_text_color: 'Custom' + text_color: [1,1,1,1] + MDAccordionItem: + title:'Item 2' + icon: 'globe' + ScrollView: + MDList: + OneLineListItem: + text: "Subitem 4" + theme_text_color: 'Custom' + text_color: [1,1,1,1] + OneLineListItem: + text: "Subitem 5" + theme_text_color: 'Custom' + text_color: [1,1,1,1] + OneLineListItem: + text: "Subitem 6" + theme_text_color: 'Custom' + text_color: [1,1,1,1] + MDAccordionItem: + title:'Item 3' + ScrollView: + MDList: + OneLineListItem: + text: "Subitem 7" + theme_text_color: 'Custom' + text_color: [1,1,1,1] + OneLineListItem: + text: "Subitem 8" + theme_text_color: 'Custom' + text_color: [1,1,1,1] + OneLineListItem: + text: "Subitem 9" + theme_text_color: 'Custom' + text_color: [1,1,1,1] + MDAccordion: + orientation:'horizontal' + MDAccordionItem: + title:'Item 1' + icon: 'home' + MDLabel: + text:'Content 1' + theme_text_color:'Primary' + MDAccordionItem: + title:'Item 2' + MDLabel: + text:'Content 2' + theme_text_color:'Primary' + MDAccordionItem: + title:'Item 3' + MDLabel: + text:'Content 3' + theme_text_color:'Primary' +""") + + + AccordionApp().run() diff --git a/src/kivymd/backgroundcolorbehavior.py b/src/kivymd/backgroundcolorbehavior.py new file mode 100644 index 00000000..bd98f129 --- /dev/null +++ b/src/kivymd/backgroundcolorbehavior.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +from kivy.lang import Builder +from kivy.properties import BoundedNumericProperty, ReferenceListProperty +from kivy.uix.widget import Widget + +Builder.load_string(''' + + canvas: + Color: + rgba: self.background_color + Rectangle: + size: self.size + pos: self.pos +''') + + +class BackgroundColorBehavior(Widget): + r = BoundedNumericProperty(1., min=0., max=1.) + g = BoundedNumericProperty(1., min=0., max=1.) + b = BoundedNumericProperty(1., min=0., max=1.) + a = BoundedNumericProperty(0., min=0., max=1.) + + background_color = ReferenceListProperty(r, g, b, a) diff --git a/src/kivymd/bottomsheet.py b/src/kivymd/bottomsheet.py new file mode 100644 index 00000000..901322b0 --- /dev/null +++ b/src/kivymd/bottomsheet.py @@ -0,0 +1,211 @@ +# -*- coding: utf-8 -*- +''' +Bottom Sheets +============= + +`Material Design spec Bottom Sheets page `_ + +In this module there's the :class:`MDBottomSheet` class which will let you implement your own Material Design Bottom Sheets, and there are two classes called :class:`MDListBottomSheet` and :class:`MDGridBottomSheet` implementing the ones mentioned in the spec. + +Examples +-------- + +.. note:: + + These widgets are designed to be called from Python code only. + +For :class:`MDListBottomSheet`: + +.. code-block:: python + + bs = MDListBottomSheet() + bs.add_item("Here's an item with text only", lambda x: x) + bs.add_item("Here's an item with an icon", lambda x: x, icon='md-cast') + bs.add_item("Here's another!", lambda x: x, icon='md-nfc') + bs.open() + +For :class:`MDListBottomSheet`: + +.. code-block:: python + + bs = MDGridBottomSheet() + bs.add_item("Facebook", lambda x: x, icon_src='./assets/facebook-box.png') + bs.add_item("YouTube", lambda x: x, icon_src='./assets/youtube-play.png') + bs.add_item("Twitter", lambda x: x, icon_src='./assets/twitter.png') + bs.add_item("Da Cloud", lambda x: x, icon_src='./assets/cloud-upload.png') + bs.add_item("Camera", lambda x: x, icon_src='./assets/camera.png') + bs.open() + +API +--- +''' +from kivy.clock import Clock +from kivy.lang import Builder +from kivy.metrics import dp +from kivy.properties import ObjectProperty, StringProperty +from kivy.uix.behaviors import ButtonBehavior +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.floatlayout import FloatLayout +from kivy.uix.gridlayout import GridLayout +from kivy.uix.modalview import ModalView +from kivy.uix.scrollview import ScrollView +from kivymd.backgroundcolorbehavior import BackgroundColorBehavior +from kivymd.label import MDLabel +from kivymd.list import MDList, OneLineListItem, ILeftBody, \ + OneLineIconListItem +from kivymd.theming import ThemableBehavior + +Builder.load_string(''' + + background: 'atlas://data/images/defaulttheme/action_group_disabled' + background_color: 0,0,0,.8 + sv: sv + upper_padding: upper_padding + gl_content: gl_content + ScrollView: + id: sv + do_scroll_x: False + BoxLayout: + size_hint_y: None + orientation: 'vertical' + padding: 0,1,0,0 + height: upper_padding.height + gl_content.height + 1 # +1 to allow overscroll + BsPadding: + id: upper_padding + size_hint_y: None + height: root.height - min(root.width * 9 / 16, gl_content.height) + on_release: root.dismiss() + BottomSheetContent: + id: gl_content + size_hint_y: None + background_color: root.theme_cls.bg_normal + cols: 1 +''') + + +class BsPadding(ButtonBehavior, FloatLayout): + pass + + +class BottomSheetContent(BackgroundColorBehavior, GridLayout): + pass + + +class MDBottomSheet(ThemableBehavior, ModalView): + sv = ObjectProperty() + upper_padding = ObjectProperty() + gl_content = ObjectProperty() + dismiss_zone_scroll = 1000 # Arbitrary high number + + def open(self, *largs): + super(MDBottomSheet, self).open(*largs) + Clock.schedule_once(self.set_dismiss_zone, 0) + + def set_dismiss_zone(self, *largs): + # Scroll to right below overscroll threshold: + self.sv.scroll_y = 1 - self.sv.convert_distance_to_scroll(0, 1)[1] + + # This is a line where m (slope) is 1/6 and b (y-intercept) is 80: + self.dismiss_zone_scroll = self.sv.convert_distance_to_scroll( + 0, (self.height - self.upper_padding.height) * (1 / 6.0) + 80)[ + 1] + # Uncomment next line if the limit should just be half of + # visible content on open (capped by specs to 16 units to width/9: + # self.dismiss_zone_scroll = (self.sv.convert_distance_to_scroll( + # 0, self.height - self.upper_padding.height)[1] * 0.50) + + # Check if user has overscrolled enough to dismiss bottom sheet: + self.sv.bind(on_scroll_stop=self.check_if_scrolled_to_death) + + def check_if_scrolled_to_death(self, *largs): + if self.sv.scroll_y >= 1 + self.dismiss_zone_scroll: + self.dismiss() + + def add_widget(self, widget, index=0): + if type(widget) == ScrollView: + super(MDBottomSheet, self).add_widget(widget, index) + else: + self.gl_content.add_widget(widget,index) + + +Builder.load_string(''' +#:import md_icons kivymd.icon_definitions.md_icons + + font_style: 'Icon' + text: u"{}".format(md_icons[root.icon]) + halign: 'center' + theme_text_color: 'Primary' + valign: 'middle' +''') + + +class ListBSIconLeft(ILeftBody, MDLabel): + icon = StringProperty() + + +class MDListBottomSheet(MDBottomSheet): + mlist = ObjectProperty() + + def __init__(self, **kwargs): + super(MDListBottomSheet, self).__init__(**kwargs) + self.mlist = MDList() + self.gl_content.add_widget(self.mlist) + Clock.schedule_once(self.resize_content_layout, 0) + + def resize_content_layout(self, *largs): + self.gl_content.height = self.mlist.height + + def add_item(self, text, callback, icon=None): + if icon: + item = OneLineIconListItem(text=text, on_release=callback) + item.add_widget(ListBSIconLeft(icon=icon)) + else: + item = OneLineListItem(text=text, on_release=callback) + + item.bind(on_release=lambda x: self.dismiss()) + self.mlist.add_widget(item) + + +Builder.load_string(''' + + orientation: 'vertical' + padding: 0, dp(24), 0, 0 + size_hint_y: None + size: dp(64), dp(96) + BoxLayout: + padding: dp(8), 0, dp(8), dp(8) + size_hint_y: None + height: dp(48) + Image: + source: root.source + MDLabel: + font_style: 'Caption' + theme_text_color: 'Secondary' + text: root.caption + halign: 'center' +''') + + +class GridBSItem(ButtonBehavior, BoxLayout): + source = StringProperty() + + caption = StringProperty() + + +class MDGridBottomSheet(MDBottomSheet): + def __init__(self, **kwargs): + super(MDGridBottomSheet, self).__init__(**kwargs) + self.gl_content.padding = (dp(16), 0, dp(16), dp(24)) + self.gl_content.height = dp(24) + self.gl_content.cols = 3 + + def add_item(self, text, callback, icon_src): + item = GridBSItem( + caption=text, + on_release=callback, + source=icon_src + ) + item.bind(on_release=lambda x: self.dismiss()) + if len(self.gl_content.children) % 3 == 0: + self.gl_content.height += dp(96) + self.gl_content.add_widget(item) diff --git a/src/kivymd/button.py b/src/kivymd/button.py new file mode 100644 index 00000000..75016716 --- /dev/null +++ b/src/kivymd/button.py @@ -0,0 +1,453 @@ +# -*- coding: utf-8 -*- +''' +Buttons +======= + +`Material Design spec, Buttons page `_ + +`Material Design spec, Buttons: Floating Action Button page `_ + +TO-DO: DOCUMENT MODULE +''' +from kivy.clock import Clock +from kivy.lang import Builder +from kivy.metrics import dp +from kivy.utils import get_color_from_hex +from kivy.properties import StringProperty, BoundedNumericProperty, \ + ListProperty, AliasProperty, BooleanProperty, NumericProperty, \ + OptionProperty +from kivy.uix.anchorlayout import AnchorLayout +from kivy.uix.behaviors import ButtonBehavior +from kivy.uix.boxlayout import BoxLayout +from kivy.animation import Animation +from kivymd.backgroundcolorbehavior import BackgroundColorBehavior +from kivymd.ripplebehavior import CircularRippleBehavior, \ + RectangularRippleBehavior +from kivymd.elevationbehavior import ElevationBehavior, \ + RoundElevationBehavior +from kivymd.theming import ThemableBehavior +from kivymd.color_definitions import colors + +Builder.load_string(''' +#:import md_icons kivymd.icon_definitions.md_icons +#:import colors kivymd.color_definitions.colors +#:import MDLabel kivymd.label.MDLabel + + size_hint: (None, None) + size: (dp(48), dp(48)) + padding: dp(12) + theme_text_color: 'Primary' + MDLabel: + id: _label + font_style: 'Icon' + text: u"{}".format(md_icons[root.icon]) + halign: 'center' + theme_text_color: root.theme_text_color + text_color: root.text_color + opposite_colors: root.opposite_colors + valign: 'middle' + + + canvas: + Color: + #rgba: self.background_color if self.state == 'normal' else self._bg_color_down + rgba: self._current_button_color + Rectangle: + size: self.size + pos: self.pos + size_hint: (None, None) + height: dp(36) + width: _label.texture_size[0] + dp(16) + padding: (dp(8), 0) + theme_text_color: 'Custom' + text_color: root.theme_cls.primary_color + MDLabel: + id: _label + text: root._text + font_style: 'Button' + size_hint_x: None + text_size: (None, root.height) + height: self.texture_size[1] + theme_text_color: root.theme_text_color + text_color: root.text_color + valign: 'middle' + halign: 'center' + opposite_colors: root.opposite_colors + +: + canvas: + Clear + Color: + rgba: self.background_color_disabled if self.disabled else \ + (self.background_color if self.state == 'normal' else self.background_color_down) + Rectangle: + size: self.size + pos: self.pos + + anchor_x: 'center' + anchor_y: 'center' + background_color: root.theme_cls.primary_color + background_color_down: root.theme_cls.primary_dark + background_color_disabled: root.theme_cls.divider_color + theme_text_color: 'Primary' + MDLabel: + id: label + font_style: 'Button' + text: root._text + size_hint: None, None + width: root.width + text_size: self.width, None + height: self.texture_size[1] + theme_text_color: root.theme_text_color + text_color: root.text_color + opposite_colors: root.opposite_colors + disabled: root.disabled + halign: 'center' + valign: 'middle' + +: + canvas: + Clear + Color: + rgba: self.background_color_disabled if self.disabled else \ + (self.background_color if self.state == 'normal' else self.background_color_down) + Ellipse: + size: self.size + pos: self.pos + + anchor_x: 'center' + anchor_y: 'center' + background_color: root.theme_cls.accent_color + background_color_down: root.theme_cls.accent_dark + background_color_disabled: root.theme_cls.divider_color + theme_text_color: 'Primary' + MDLabel: + id: label + font_style: 'Icon' + text: u"{}".format(md_icons[root.icon]) + size_hint: None, None + size: dp(24), dp(24) + text_size: self.size + theme_text_color: root.theme_text_color + text_color: root.text_color + opposite_colors: root.opposite_colors + disabled: root.disabled + halign: 'center' + valign: 'middle' +''') + + +class MDIconButton(CircularRippleBehavior, ButtonBehavior, BoxLayout): + icon = StringProperty('circle') + theme_text_color = OptionProperty(None, allownone=True, + options=['Primary', 'Secondary', 'Hint', + 'Error', 'Custom']) + text_color = ListProperty(None, allownone=True) + opposite_colors = BooleanProperty(False) + + +class MDFlatButton(ThemableBehavior, RectangularRippleBehavior, + ButtonBehavior, BackgroundColorBehavior, AnchorLayout): + width = BoundedNumericProperty(dp(64), min=dp(64), max=None, + errorhandler=lambda x: dp(64)) + + text_color = ListProperty() + + text = StringProperty('') + theme_text_color = OptionProperty(None, allownone=True, + options=['Primary', 'Secondary', 'Hint', + 'Error', 'Custom']) + text_color = ListProperty(None, allownone=True) + + _text = StringProperty('') + _bg_color_down = ListProperty([0, 0, 0, 0]) + _current_button_color = ListProperty([0, 0, 0, 0]) + + def __init__(self, **kwargs): + super(MDFlatButton, self).__init__(**kwargs) + self._current_button_color = self.background_color + self._bg_color_down = get_color_from_hex( + colors[self.theme_cls.theme_style]['FlatButtonDown']) + + Clock.schedule_once(lambda x: self.ids._label.bind( + texture_size=self.update_width_on_label_texture)) + + def update_width_on_label_texture(self, instance, value): + self.ids._label.width = value[0] + + def on_text(self, instance, value): + self._text = value.upper() + + def on_touch_down(self, touch): + if touch.is_mouse_scrolling: + return False + elif not self.collide_point(touch.x, touch.y): + return False + elif self in touch.ud: + return False + elif self.disabled: + return False + else: + self.fade_bg = Animation(duration=.2, _current_button_color=get_color_from_hex( + colors[self.theme_cls.theme_style]['FlatButtonDown'])) + self.fade_bg.start(self) + return super(MDFlatButton, self).on_touch_down(touch) + + def on_touch_up(self, touch): + if touch.grab_current is self: + self.fade_bg.stop_property(self, '_current_button_color') + Animation(duration=.05, _current_button_color=self.background_color).start(self) + return super(MDFlatButton, self).on_touch_up(touch) + + +class MDRaisedButton(ThemableBehavior, RectangularRippleBehavior, + ElevationBehavior, ButtonBehavior, + AnchorLayout): + _bg_color_down = ListProperty([]) + background_color = ListProperty() + background_color_down = ListProperty() + background_color_disabled = ListProperty() + theme_text_color = OptionProperty(None, allownone=True, + options=['Primary', 'Secondary', 'Hint', + 'Error', 'Custom']) + text_color = ListProperty(None, allownone=True) + + def _get_bg_color_down(self): + return self._bg_color_down + + def _set_bg_color_down(self, color, alpha=None): + if len(color) == 2: + self._bg_color_down = get_color_from_hex( + colors[color[0]][color[1]]) + if alpha: + self._bg_color_down[3] = alpha + elif len(color) == 4: + self._bg_color_down = color + + background_color_down = AliasProperty(_get_bg_color_down, + _set_bg_color_down, + bind=('_bg_color_down',)) + + _bg_color_disabled = ListProperty([]) + + def _get_bg_color_disabled(self): + return self._bg_color_disabled + + def _set_bg_color_disabled(self, color, alpha=None): + if len(color) == 2: + self._bg_color_disabled = get_color_from_hex( + colors[color[0]][color[1]]) + if alpha: + self._bg_color_disabled[3] = alpha + elif len(color) == 4: + self._bg_color_disabled = color + + background_color_disabled = AliasProperty(_get_bg_color_disabled, + _set_bg_color_disabled, + bind=('_bg_color_disabled',)) + + _elev_norm = NumericProperty(2) + + def _get_elev_norm(self): + return self._elev_norm + + def _set_elev_norm(self, value): + self._elev_norm = value if value <= 12 else 12 + self._elev_raised = (value + 6) if value + 6 <= 12 else 12 + self.elevation = self._elev_norm + + elevation_normal = AliasProperty(_get_elev_norm, _set_elev_norm, + bind=('_elev_norm',)) + + _elev_raised = NumericProperty(8) + + def _get_elev_raised(self): + return self._elev_raised + + def _set_elev_raised(self, value): + self._elev_raised = value if value + self._elev_norm <= 12 else 12 + + elevation_raised = AliasProperty(_get_elev_raised, _set_elev_raised, + bind=('_elev_raised',)) + + text = StringProperty() + + _text = StringProperty() + + def __init__(self, **kwargs): + super(MDRaisedButton, self).__init__(**kwargs) + self.elevation_press_anim = Animation(elevation=self.elevation_raised, + duration=.2, t='out_quad') + self.elevation_release_anim = Animation( + elevation=self.elevation_normal, duration=.2, t='out_quad') + + def on_disabled(self, instance, value): + if value: + self.elevation = 0 + else: + self.elevation = self.elevation_normal + super(MDRaisedButton, self).on_disabled(instance, value) + + def on_touch_down(self, touch): + if not self.disabled: + if touch.is_mouse_scrolling: + return False + if not self.collide_point(touch.x, touch.y): + return False + if self in touch.ud: + return False + Animation.cancel_all(self, 'elevation') + self.elevation_press_anim.start(self) + return super(MDRaisedButton, self).on_touch_down(touch) + + def on_touch_up(self, touch): + if not self.disabled: + if touch.grab_current is not self: + return super(ButtonBehavior, self).on_touch_up(touch) + Animation.cancel_all(self, 'elevation') + self.elevation_release_anim.start(self) + else: + Animation.cancel_all(self, 'elevation') + self.elevation = 0 + return super(MDRaisedButton, self).on_touch_up(touch) + + def on_text(self, instance, text): + self._text = text.upper() + + def on__elev_norm(self, instance, value): + self.elevation_release_anim = Animation(elevation=value, + duration=.2, t='out_quad') + + def on__elev_raised(self, instance, value): + self.elevation_press_anim = Animation(elevation=value, + duration=.2, t='out_quad') + + +class MDFloatingActionButton(ThemableBehavior, CircularRippleBehavior, + RoundElevationBehavior, ButtonBehavior, + AnchorLayout): + _bg_color_down = ListProperty([]) + background_color = ListProperty() + background_color_down = ListProperty() + background_color_disabled = ListProperty() + theme_text_color = OptionProperty(None, allownone=True, + options=['Primary', 'Secondary', 'Hint', + 'Error', 'Custom']) + text_color = ListProperty(None, allownone=True) + + def _get_bg_color_down(self): + return self._bg_color_down + + def _set_bg_color_down(self, color, alpha=None): + if len(color) == 2: + self._bg_color_down = get_color_from_hex( + colors[color[0]][color[1]]) + if alpha: + self._bg_color_down[3] = alpha + elif len(color) == 4: + self._bg_color_down = color + + background_color_down = AliasProperty(_get_bg_color_down, + _set_bg_color_down, + bind=('_bg_color_down',)) + + _bg_color_disabled = ListProperty([]) + + def _get_bg_color_disabled(self): + return self._bg_color_disabled + + def _set_bg_color_disabled(self, color, alpha=None): + if len(color) == 2: + self._bg_color_disabled = get_color_from_hex( + colors[color[0]][color[1]]) + if alpha: + self._bg_color_disabled[3] = alpha + elif len(color) == 4: + self._bg_color_disabled = color + + background_color_disabled = AliasProperty(_get_bg_color_disabled, + _set_bg_color_disabled, + bind=('_bg_color_disabled',)) + icon = StringProperty('android') + + _elev_norm = NumericProperty(6) + + def _get_elev_norm(self): + return self._elev_norm + + def _set_elev_norm(self, value): + self._elev_norm = value if value <= 12 else 12 + self._elev_raised = (value + 6) if value + 6 <= 12 else 12 + self.elevation = self._elev_norm + + elevation_normal = AliasProperty(_get_elev_norm, _set_elev_norm, + bind=('_elev_norm',)) + + # _elev_raised = NumericProperty(12) + _elev_raised = NumericProperty(6) + + def _get_elev_raised(self): + return self._elev_raised + + def _set_elev_raised(self, value): + self._elev_raised = value if value + self._elev_norm <= 12 else 12 + + elevation_raised = AliasProperty(_get_elev_raised, _set_elev_raised, + bind=('_elev_raised',)) + + def __init__(self, **kwargs): + if self.elevation_raised == 0 and self.elevation_normal + 6 <= 12: + self.elevation_raised = self.elevation_normal + 6 + elif self.elevation_raised == 0: + self.elevation_raised = 12 + + super(MDFloatingActionButton, self).__init__(**kwargs) + + self.elevation_press_anim = Animation(elevation=self.elevation_raised, + duration=.2, t='out_quad') + self.elevation_release_anim = Animation( + elevation=self.elevation_normal, duration=.2, t='out_quad') + + def _set_ellipse(self, instance, value): + ellipse = self.ellipse + ripple_rad = self.ripple_rad + + ellipse.size = (ripple_rad, ripple_rad) + ellipse.pos = (self.center_x - ripple_rad / 2., + self.center_y - ripple_rad / 2.) + + def on_disabled(self, instance, value): + super(MDFloatingActionButton, self).on_disabled(instance, value) + if self.disabled: + self.elevation = 0 + else: + self.elevation = self.elevation_normal + + def on_touch_down(self, touch): + if not self.disabled: + if touch.is_mouse_scrolling: + return False + if not self.collide_point(touch.x, touch.y): + return False + if self in touch.ud: + return False + self.elevation_press_anim.stop(self) + self.elevation_press_anim.start(self) + return super(MDFloatingActionButton, self).on_touch_down(touch) + + def on_touch_up(self, touch): + if not self.disabled: + if touch.grab_current is not self: + return super(ButtonBehavior, self).on_touch_up(touch) + self.elevation_release_anim.stop(self) + self.elevation_release_anim.start(self) + return super(MDFloatingActionButton, self).on_touch_up(touch) + + def on_elevation_normal(self, instance, value): + self.elevation = value + + def on_elevation_raised(self, instance, value): + if self.elevation_raised == 0 and self.elevation_normal + 6 <= 12: + self.elevation_raised = self.elevation_normal + 6 + elif self.elevation_raised == 0: + self.elevation_raised = 12 diff --git a/src/kivymd/card.py b/src/kivymd/card.py new file mode 100644 index 00000000..d411644b --- /dev/null +++ b/src/kivymd/card.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +from kivy.lang import Builder +from kivy.properties import BoundedNumericProperty, ReferenceListProperty, ListProperty,BooleanProperty +from kivy.uix.boxlayout import BoxLayout +from kivymd.elevationbehavior import ElevationBehavior +from kivymd.theming import ThemableBehavior +from kivy.metrics import dp +from kivy.uix.widget import Widget + +Builder.load_string(''' + + canvas: + Color: + rgba: self.background_color + RoundedRectangle: + size: self.size + pos: self.pos + radius: [self.border_radius] + Color: + rgba: self.theme_cls.divider_color + a: self.border_color_a + Line: + rounded_rectangle: (self.pos[0],self.pos[1],self.size[0],self.size[1],self.border_radius) + background_color: self.theme_cls.bg_light + + + canvas: + Color: + rgba: self.theme_cls.divider_color + Rectangle: + size: self.size + pos: self.pos +''') + + +class MDSeparator(ThemableBehavior, BoxLayout): + """ A separator line """ + def __init__(self, *args, **kwargs): + super(MDSeparator, self).__init__(*args, **kwargs) + self.on_orientation() + + def on_orientation(self,*args): + self.size_hint = (1, None) if self.orientation == 'horizontal' else (None, 1) + if self.orientation == 'horizontal': + self.height = dp(1) + else: + self.width = dp(1) + + +class MDCard(ThemableBehavior, ElevationBehavior, BoxLayout): + r = BoundedNumericProperty(1., min=0., max=1.) + g = BoundedNumericProperty(1., min=0., max=1.) + b = BoundedNumericProperty(1., min=0., max=1.) + a = BoundedNumericProperty(0., min=0., max=1.) + + border_radius = BoundedNumericProperty(dp(3),min=0) + border_color_a = BoundedNumericProperty(0, min=0., max=1.) + background_color = ReferenceListProperty(r, g, b, a) diff --git a/src/kivymd/color_definitions.py b/src/kivymd/color_definitions.py new file mode 100644 index 00000000..c81bd731 --- /dev/null +++ b/src/kivymd/color_definitions.py @@ -0,0 +1,360 @@ +colors = { + 'Pink': { + '50': 'fce4ec', + '100': 'f8bbd0', + '200': 'f48fb1', + '300': 'f06292', + '400': 'ec407a', + '500': 'e91e63', + '600': 'd81b60', + '700': 'C2185B', + '800': 'ad1457', + '900': '88e4ff', + 'A100': 'ff80ab', + 'A400': 'F50057', + 'A700': 'c51162', + 'A200': 'ff4081' + }, + + 'Blue': { + '200': '90caf9', + '900': '0D47A1', + '600': '1e88e5', + 'A100': '82b1ff', + '300': '64b5f6', + 'A400': '2979ff', + '700': '1976d2', + '50': 'e3f2fd', + 'A700': '2962ff', + '400': '42a5f5', + '100': 'bbdefb', + '800': '1565c0', + 'A200': '448aff', + '500': '2196f3' + }, + + 'Indigo': { + '200': '9fa8da', + '900': '1a237e', + '600': '3949ab', + 'A100': '8c9eff', + '300': '7986cb', + 'A400': '3d5afe', + '700': '303f9f', + '50': 'e8eaf6', + 'A700': '304ffe', + '400': '5c6bc0', + '100': 'c5cae9', + '800': '283593', + 'A200': '536dfe', + '500': '3f51b5' + }, + + 'BlueGrey': { + '200': 'b0bec5', + '900': '263238', + '600': '546e7a', + '300': '90a4ae', + '700': '455a64', + '50': 'eceff1', + '400': '78909c', + '100': 'cfd8dc', + '800': '37474f', + '500': '607d8b' + }, + + 'Brown': { + '200': 'bcaaa4', + '900': '3e2723', + '600': '6d4c41', + '300': 'a1887f', + '700': '5d4037', + '50': 'efebe9', + '400': '8d6e63', + '100': 'd7ccc8', + '800': '4e342e', + '500': '795548' + }, + + 'LightBlue': { + '200': '81d4fa', + '900': '01579B', + '600': '039BE5', + 'A100': '80d8ff', + '300': '4fc3f7', + 'A400': '00B0FF', + '700': '0288D1', + '50': 'e1f5fe', + 'A700': '0091EA', + '400': '29b6f6', + '100': 'b3e5fc', + '800': '0277BD', + 'A200': '40c4ff', + '500': '03A9F4' + }, + + 'Purple': { + '200': 'ce93d8', + '900': '4a148c', + '600': '8e24aa', + 'A100': 'ea80fc', + '300': 'ba68c8', + 'A400': 'D500F9', + '700': '7b1fa2', + '50': 'f3e5f5', + 'A700': 'AA00FF', + '400': 'ab47bc', + '100': 'e1bee7', + '800': '6a1b9a', + 'A200': 'e040fb', + '500': '9c27b0' + }, + + 'Grey': { + '200': 'eeeeee', + '900': '212121', + '600': '757575', + '300': 'e0e0e0', + '700': '616161', + '50': 'fafafa', + '400': 'bdbdbd', + '100': 'f5f5f5', + '800': '424242', + '500': '9e9e9e' + }, + + 'Yellow': { + '200': 'fff59d', + '900': 'f57f17', + '600': 'fdd835', + 'A100': 'ffff8d', + '300': 'fff176', + 'A400': 'FFEA00', + '700': 'fbc02d', + '50': 'fffde7', + 'A700': 'FFD600', + '400': 'ffee58', + '100': 'fff9c4', + '800': 'f9a825', + 'A200': 'FFFF00', + '500': 'ffeb3b' + }, + + 'LightGreen': { + '200': 'c5e1a5', + '900': '33691e', + '600': '7cb342', + 'A100': 'ccff90', + '300': 'aed581', + 'A400': '76FF03', + '700': '689f38', + '50': 'f1f8e9', + 'A700': '64dd17', + '400': '9ccc65', + '100': 'dcedc8', + '800': '558b2f', + 'A200': 'b2ff59', + '500': '8bc34a' + }, + + 'DeepOrange': { + '200': 'ffab91', + '900': 'bf36c', + '600': 'f4511e', + 'A100': 'ff9e80', + '300': 'ff8a65', + 'A400': 'FF3D00', + '700': 'e64a19', + '50': 'fbe9e7', + 'A700': 'DD2C00', + '400': 'ff7043', + '100': 'ffccbc', + '800': 'd84315', + 'A200': 'ff6e40', + '500': 'ff5722' + }, + + 'Green': { + '200': 'a5d6a7', + '900': '1b5e20', + '600': '43a047', + 'A100': 'b9f6ca', + '300': '81c784', + 'A400': '00E676', + '700': '388e3c', + '50': 'e8f5e9', + 'A700': '00C853', + '400': '66bb6a', + '100': 'c8e6c9', + '800': '2e7d32', + 'A200': '69f0ae', + '500': '4caf50' + }, + + 'Red': { + '200': 'ef9a9a', + '900': 'b71c1c', + '600': 'e53935', + 'A100': 'ff8a80', + '300': 'e57373', + 'A400': 'ff1744', + '700': 'd32f2f', + '50': 'ffebee', + 'A700': 'd50000', + '400': 'ef5350', + '100': 'ffcdd2', + '800': 'c62828', + 'A200': 'ff5252', + '500': 'f44336' + }, + + 'Teal': { + '200': '80cbc4', + '900': '004D40', + '600': '00897B', + 'A100': 'a7ffeb', + '300': '4db6ac', + 'A400': '1de9b6', + '700': '00796B', + '50': 'e0f2f1', + 'A700': '00BFA5', + '400': '26a69a', + '100': 'b2dfdb', + '800': '00695C', + 'A200': '64ffda', + '500': '009688' + }, + + 'Orange': { + '200': 'ffcc80', + '900': 'E65100', + '600': 'FB8C00', + 'A100': 'ffd180', + '300': 'ffb74d', + 'A400': 'FF9100', + '700': 'F57C00', + '50': 'fff3e0', + 'A700': 'FF6D00', + '400': 'ffa726', + '100': 'ffe0b2', + '800': 'EF6C00', + 'A200': 'ffab40', + '500': 'FF9800' + }, + + 'Cyan': { + '200': '80deea', + '900': '006064', + '600': '00ACC1', + 'A100': '84ffff', + '300': '4dd0e1', + 'A400': '00E5FF', + '700': '0097A7', + '50': 'e0f7fa', + 'A700': '00B8D4', + '400': '26c6da', + '100': 'b2ebf2', + '800': '00838F', + 'A200': '18ffff', + '500': '00BCD4' + }, + + 'Amber': { + '200': 'ffe082', + '900': 'FF6F00', + '600': 'FFB300', + 'A100': 'ffe57f', + '300': 'ffd54f', + 'A400': 'FFC400', + '700': 'FFA000', + '50': 'fff8e1', + 'A700': 'FFAB00', + '400': 'ffca28', + '100': 'ffecb3', + '800': 'FF8F00', + 'A200': 'ffd740', + '500': 'FFC107' + }, + + 'DeepPurple': { + '200': 'b39ddb', + '900': '311b92', + '600': '5e35b1', + 'A100': 'b388ff', + '300': '9575cd', + 'A400': '651fff', + '700': '512da8', + '50': 'ede7f6', + 'A700': '6200EA', + '400': '7e57c2', + '100': 'd1c4e9', + '800': '4527a0', + 'A200': '7c4dff', + '500': '673ab7' + }, + + 'Lime': { + '200': 'e6ee9c', + '900': '827717', + '600': 'c0ca33', + 'A100': 'f4ff81', + '300': 'dce775', + 'A400': 'C6FF00', + '700': 'afb42b', + '50': 'f9fbe7', + 'A700': 'AEEA00', + '400': 'd4e157', + '100': 'f0f4c3', + '800': '9e9d24', + 'A200': 'eeff41', + '500': 'cddc39' + }, + + 'Light': { + 'StatusBar': 'E0E0E0', + 'AppBar': 'F5F5F5', + 'Background': 'FAFAFA', + 'CardsDialogs': 'FFFFFF', + 'FlatButtonDown': 'cccccc' + }, + + 'Dark': { + 'StatusBar': '000000', + 'AppBar': '212121', + 'Background': '303030', + 'CardsDialogs': '424242', + 'FlatButtonDown': '999999' + } +} + +light_colors = { + 'Pink': ['50' '100', '200', 'A100'], + 'Blue': ['50' '100', '200', '300', '400', 'A100'], + 'Indigo': ['50' '100', '200', 'A100'], + 'BlueGrey': ['50' '100', '200', '300'], + 'Brown': ['50' '100', '200'], + 'LightBlue': ['50' '100', '200', '300', '400', '500', 'A100', 'A200', + 'A400'], + 'Purple': ['50' '100', '200', 'A100'], + 'Grey': ['50' '100', '200', '300', '400', '500'], + 'Yellow': ['50' '100', '200', '300', '400', '500', '600', '700', '800', + '900', 'A100', 'A200', 'A400', 'A700'], + 'LightGreen': ['50' '100', '200', '300', '400', '500', '600', 'A100', + 'A200', 'A400', 'A700'], + 'DeepOrange': ['50' '100', '200', '300', '400', 'A100', 'A200'], + 'Green': ['50' '100', '200', '300', '400', '500', 'A100', 'A200', 'A400', + 'A700'], + 'Red': ['50' '100', '200', '300', 'A100'], + 'Teal': ['50' '100', '200', '300', '400', 'A100', 'A200', 'A400', 'A700'], + 'Orange': ['50' '100', '200', '300', '400', '500', '600', '700', 'A100', + 'A200', 'A400', 'A700'], + 'Cyan': ['50' '100', '200', '300', '400', '500', '600', 'A100', 'A200', + 'A400', 'A700'], + 'Amber': ['50' '100', '200', '300', '400', '500', '600', '700', '800', + '900', 'A100', 'A200', 'A400', 'A700'], + 'DeepPurple': ['50' '100', '200', 'A100'], + 'Lime': ['50' '100', '200', '300', '400', '500', '600', '700', '800', + 'A100', 'A200', 'A400', 'A700'], + 'Dark': [], + 'Light': ['White', 'MainBackground', 'DialogBackground'] +} diff --git a/src/kivymd/date_picker.py b/src/kivymd/date_picker.py new file mode 100644 index 00000000..5194298e --- /dev/null +++ b/src/kivymd/date_picker.py @@ -0,0 +1,325 @@ +# -*- coding: utf-8 -*- +from kivy.lang import Builder +from kivy.uix.modalview import ModalView +from kivymd.label import MDLabel +from kivymd.theming import ThemableBehavior +from kivy.uix.floatlayout import FloatLayout +from kivymd.elevationbehavior import ElevationBehavior +import calendar +from datetime import date +import datetime +from kivy.properties import StringProperty, NumericProperty, ObjectProperty, \ + BooleanProperty +from kivy.uix.anchorlayout import AnchorLayout +from kivy.uix.behaviors import ButtonBehavior +from kivymd.ripplebehavior import CircularRippleBehavior +from kivy.clock import Clock +from kivy.core.window import Window + +Builder.load_string(""" +#:import calendar calendar + + cal_layout: cal_layout + + size_hint: (None, None) + size: [dp(328), dp(484)] if self.theme_cls.device_orientation == 'portrait'\ + else [dp(512), dp(304)] + pos_hint: {'center_x': .5, 'center_y': .5} + canvas: + Color: + rgb: app.theme_cls.primary_color + Rectangle: + size: [dp(328), dp(96)] if self.theme_cls.device_orientation == 'portrait'\ + else [dp(168), dp(304)] + pos: [root.pos[0], root.pos[1] + root.height-dp(96)] if self.theme_cls.device_orientation == 'portrait'\ + else [root.pos[0], root.pos[1] + root.height-dp(304)] + Color: + rgb: app.theme_cls.bg_normal + Rectangle: + size: [dp(328), dp(484)-dp(96)] if self.theme_cls.device_orientation == 'portrait'\ + else [dp(344), dp(304)] + pos: [root.pos[0], root.pos[1] + root.height-dp(96)-(dp(484)-dp(96))]\ + if self.theme_cls.device_orientation == 'portrait' else [root.pos[0]+dp(168), root.pos[1]] #+dp(334) + MDLabel: + id: label_full_date + font_style: 'Display1' + text_color: 1, 1, 1, 1 + theme_text_color: 'Custom' + size_hint: (None, None) + size: [root.width, dp(30)] if root.theme_cls.device_orientation == 'portrait'\ + else [dp(168), dp(30)] + pos: [root.pos[0]+dp(23), root.pos[1] + root.height - dp(74)] \ + if root.theme_cls.device_orientation == 'portrait' \ + else [root.pos[0]+dp(3), root.pos[1] + dp(214)] + line_height: 0.84 + valign: 'middle' + text_size: [root.width, None] if root.theme_cls.device_orientation == 'portrait'\ + else [dp(149), None] + bold: True + text: root.fmt_lbl_date(root.sel_year, root.sel_month, root.sel_day, root.theme_cls.device_orientation) + MDLabel: + id: label_year + font_style: 'Subhead' + text_color: 1, 1, 1, 1 + theme_text_color: 'Custom' + size_hint: (None, None) + size: root.width, dp(30) + pos: (root.pos[0]+dp(23), root.pos[1]+root.height-dp(40)) if root.theme_cls.device_orientation == 'portrait'\ + else (root.pos[0]+dp(16), root.pos[1]+root.height-dp(41)) + valign: 'middle' + text: str(root.sel_year) + GridLayout: + id: cal_layout + cols: 7 + size: (dp(44*7), dp(40*7)) if root.theme_cls.device_orientation == 'portrait'\ + else (dp(46*7), dp(32*7)) + col_default_width: dp(42) if root.theme_cls.device_orientation == 'portrait'\ + else dp(39) + size_hint: (None, None) + padding: (dp(2), 0) if root.theme_cls.device_orientation == 'portrait'\ + else (dp(7), 0) + spacing: (dp(2), 0) if root.theme_cls.device_orientation == 'portrait'\ + else (dp(7), 0) + pos: (root.pos[0]+dp(10), root.pos[1]+dp(60)) if root.theme_cls.device_orientation == 'portrait'\ + else (root.pos[0]+dp(168)+dp(8), root.pos[1]+dp(48)) + MDLabel: + id: label_month_selector + font_style: 'Body2' + text: calendar.month_name[root.month].capitalize() + ' ' + str(root.year) + size_hint: (None, None) + size: root.width, dp(30) + pos: root.pos + theme_text_color: 'Primary' + pos_hint: {'center_x': 0.5, 'center_y': 0.75} if self.theme_cls.device_orientation == 'portrait'\ + else {'center_x': 0.67, 'center_y': 0.915} + valign: "middle" + halign: "center" + MDIconButton: + icon: 'chevron-left' + theme_text_color: 'Secondary' + pos_hint: {'center_x': 0.09, 'center_y': 0.745} if root.theme_cls.device_orientation == 'portrait'\ + else {'center_x': 0.39, 'center_y': 0.925} + on_release: root.change_month('prev') + MDIconButton: + icon: 'chevron-right' + theme_text_color: 'Secondary' + pos_hint: {'center_x': 0.92, 'center_y': 0.745} if root.theme_cls.device_orientation == 'portrait'\ + else {'center_x': 0.94, 'center_y': 0.925} + on_release: root.change_month('next') + MDFlatButton: + pos: root.pos[0]+root.size[0]-dp(72)*2, root.pos[1] + dp(7) + text: "Cancel" + on_release: root.dismiss() + MDFlatButton: + pos: root.pos[0]+root.size[0]-dp(72), root.pos[1] + dp(7) + text: "OK" + on_release: root.ok_click() + + + size_hint: None, None + size: (dp(40), dp(40)) if root.theme_cls.device_orientation == 'portrait'\ + else (dp(32), dp(32)) + MDLabel: + font_style: 'Caption' + theme_text_color: 'Custom' if root.is_today and not root.is_selected else 'Primary' + text_color: root.theme_cls.primary_color + opposite_colors: root.is_selected if root.owner.sel_month == root.owner.month \ + and root.owner.sel_year == root.owner.year and str(self.text) == str(root.owner.sel_day) else False + size_hint_x: None + valign: 'middle' + halign: 'center' + text: root.text + + + font_style: 'Caption' + theme_text_color: 'Secondary' + size: (dp(40), dp(40)) if root.theme_cls.device_orientation == 'portrait'\ + else (dp(32), dp(32)) + size_hint: None, None + text_size: self.size + valign: 'middle' if root.theme_cls.device_orientation == 'portrait' else 'bottom' + halign: 'center' + + + size: (dp(40), dp(40)) if root.theme_cls.device_orientation == 'portrait'\ + else (dp(32), dp(32)) + size_hint: (None, None) + canvas: + Color: + rgba: self.theme_cls.primary_color if self.shown else [0, 0, 0, 0] + Ellipse: + size: (dp(40), dp(40)) if root.theme_cls.device_orientation == 'portrait'\ + else (dp(32), dp(32)) + pos: self.pos if root.theme_cls.device_orientation == 'portrait'\ + else [self.pos[0] + dp(3), self.pos[1]] +""") + + +class DaySelector(ThemableBehavior, AnchorLayout): + shown = BooleanProperty(False) + + def __init__(self, parent): + super(DaySelector, self).__init__() + self.parent_class = parent + self.parent_class.add_widget(self, index=7) + self.selected_widget = None + Window.bind(on_resize=self.move_resize) + + def update(self): + parent = self.parent_class + if parent.sel_month == parent.month and parent.sel_year == parent.year: + self.shown = True + else: + self.shown = False + + def set_widget(self, widget): + self.selected_widget = widget + self.pos = widget.pos + self.move_resize(do_again=True) + self.update() + + def move_resize(self, window=None, width=None, height=None, do_again=True): + self.pos = self.selected_widget.pos + if do_again: + Clock.schedule_once(lambda x: self.move_resize(do_again=False), 0.01) + + +class DayButton(ThemableBehavior, CircularRippleBehavior, ButtonBehavior, + AnchorLayout): + text = StringProperty() + owner = ObjectProperty() + is_today = BooleanProperty(False) + is_selected = BooleanProperty(False) + + def on_release(self): + self.owner.set_selected_widget(self) + + +class WeekdayLabel(MDLabel): + pass + + +class MDDatePicker(FloatLayout, ThemableBehavior, ElevationBehavior, + ModalView): + _sel_day_widget = ObjectProperty() + cal_list = None + cal_layout = ObjectProperty() + sel_year = NumericProperty() + sel_month = NumericProperty() + sel_day = NumericProperty() + day = NumericProperty() + month = NumericProperty() + year = NumericProperty() + today = date.today() + callback = ObjectProperty() + + class SetDateError(Exception): + pass + + def __init__(self, callback, year=None, month=None, day=None, + firstweekday=0, + **kwargs): + self.callback = callback + self.cal = calendar.Calendar(firstweekday) + self.sel_year = year if year else self.today.year + self.sel_month = month if month else self.today.month + self.sel_day = day if day else self.today.day + self.month = self.sel_month + self.year = self.sel_year + self.day = self.sel_day + super(MDDatePicker, self).__init__(**kwargs) + self.selector = DaySelector(parent=self) + self.generate_cal_widgets() + self.update_cal_matrix(self.sel_year, self.sel_month) + self.set_month_day(self.sel_day) + self.selector.update() + + def ok_click(self): + self.callback(date(self.sel_year, self.sel_month, self.sel_day)) + self.dismiss() + + def fmt_lbl_date(self, year, month, day, orientation): + d = datetime.date(int(year), int(month), int(day)) + separator = '\n' if orientation == 'landscape' else ' ' + return d.strftime('%a,').capitalize() + separator + d.strftime( + '%b').capitalize() + ' ' + str(day).lstrip('0') + + def set_date(self, year, month, day): + try: + date(year, month, day) + except Exception as e: + print(e) + if str(e) == "day is out of range for month": + raise self.SetDateError(" Day %s day is out of range for month %s" % (day, month)) + elif str(e) == "month must be in 1..12": + raise self.SetDateError("Month must be between 1 and 12, got %s" % month) + elif str(e) == "year is out of range": + raise self.SetDateError("Year must be between %s and %s, got %s" % + (datetime.MINYEAR, datetime.MAXYEAR, year)) + else: + self.sel_year = year + self.sel_month = month + self.sel_day = day + self.month = self.sel_month + self.year = self.sel_year + self.day = self.sel_day + self.update_cal_matrix(self.sel_year, self.sel_month) + self.set_month_day(self.sel_day) + self.selector.update() + + def set_selected_widget(self, widget): + if self._sel_day_widget: + self._sel_day_widget.is_selected = False + widget.is_selected = True + self.sel_month = int(self.month) + self.sel_year = int(self.year) + self.sel_day = int(widget.text) + self._sel_day_widget = widget + self.selector.set_widget(widget) + + def set_month_day(self, day): + for idx in range(len(self.cal_list)): + if str(day) == str(self.cal_list[idx].text): + self._sel_day_widget = self.cal_list[idx] + self.sel_day = int(self.cal_list[idx].text) + if self._sel_day_widget: + self._sel_day_widget.is_selected = False + self._sel_day_widget = self.cal_list[idx] + self.cal_list[idx].is_selected = True + self.selector.set_widget(self.cal_list[idx]) + + def update_cal_matrix(self, year, month): + try: + dates = [x for x in self.cal.itermonthdates(year, month)] + except ValueError as e: + if str(e) == "year is out of range": + pass + else: + self.year = year + self.month = month + for idx in range(len(self.cal_list)): + if idx >= len(dates) or dates[idx].month != month: + self.cal_list[idx].disabled = True + self.cal_list[idx].text = '' + else: + self.cal_list[idx].disabled = False + self.cal_list[idx].text = str(dates[idx].day) + self.cal_list[idx].is_today = dates[idx] == self.today + self.selector.update() + + def generate_cal_widgets(self): + cal_list = [] + for i in calendar.day_abbr: + self.cal_layout.add_widget(WeekdayLabel(text=i[0].upper())) + for i in range(6 * 7): # 6 weeks, 7 days a week + db = DayButton(owner=self) + cal_list.append(db) + self.cal_layout.add_widget(db) + self.cal_list = cal_list + + def change_month(self, operation): + op = 1 if operation is 'next' else -1 + sl, sy = self.month, self.year + m = 12 if sl + op == 0 else 1 if sl + op == 13 else sl + op + y = sy - 1 if sl + op == 0 else sy + 1 if sl + op == 13 else sy + self.update_cal_matrix(y, m) diff --git a/src/kivymd/dialog.py b/src/kivymd/dialog.py new file mode 100644 index 00000000..cb6b7601 --- /dev/null +++ b/src/kivymd/dialog.py @@ -0,0 +1,176 @@ +# -*- coding: utf-8 -*- + +from kivy.lang import Builder +from kivy.properties import StringProperty, ObjectProperty, ListProperty +from kivy.metrics import dp +from kivy.uix.modalview import ModalView +from kivy.animation import Animation +from kivymd.theming import ThemableBehavior +from kivymd.elevationbehavior import ElevationBehavior +from kivymd.button import MDFlatButton + +Builder.load_string(''' +: + canvas: + Color: + rgba: self.theme_cls.bg_light + Rectangle: + size: self.size + pos: self.pos + + _container: container + _action_area: action_area + elevation: 12 + GridLayout: + cols: 1 + + GridLayout: + cols: 1 + padding: dp(24), dp(24), dp(24), 0 + spacing: dp(20) + MDLabel: + text: root.title + font_style: 'Title' + theme_text_color: 'Primary' + halign: 'left' + valign: 'middle' + size_hint_y: None + text_size: self.width, None + height: self.texture_size[1] + + BoxLayout: + id: container + + AnchorLayout: + anchor_x: 'right' + anchor_y: 'center' + size_hint: 1, None + height: dp(48) + padding: dp(8), dp(8) + spacing: dp(4) + + GridLayout: + id: action_area + rows: 1 + size_hint: None, None if len(root._action_buttons) > 0 else 1 + height: dp(36) if len(root._action_buttons) > 0 else 0 + width: self.minimum_width +''') + + +class MDDialog(ThemableBehavior, ElevationBehavior, ModalView): + title = StringProperty('') + + content = ObjectProperty(None) + + background_color = ListProperty([0, 0, 0, .2]) + + _container = ObjectProperty() + _action_buttons = ListProperty([]) + _action_area = ObjectProperty() + + def __init__(self, **kwargs): + super(MDDialog, self).__init__(**kwargs) + self.bind(_action_buttons=self._update_action_buttons, + auto_dismiss=lambda *x: setattr(self.shadow, 'on_release', + self.shadow.dismiss if self.auto_dismiss else None)) + + def add_action_button(self, text, action=None): + """Add an :class:`FlatButton` to the right of the action area. + + :param icon: Unicode character for the icon + :type icon: str or None + :param action: Function set to trigger when on_release fires + :type action: function or None + """ + button = MDFlatButton(text=text, + size_hint=(None, None), + height=dp(36)) + if action: + button.bind(on_release=action) + button.text_color = self.theme_cls.primary_color + button.background_color = self.theme_cls.bg_light + self._action_buttons.append(button) + + def add_widget(self, widget): + if self._container: + if self.content: + raise PopupException( + 'Popup can have only one widget as content') + self.content = widget + else: + super(MDDialog, self).add_widget(widget) + + def open(self, *largs): + '''Show the view window from the :attr:`attach_to` widget. If set, it + will attach to the nearest window. If the widget is not attached to any + window, the view will attach to the global + :class:`~kivy.core.window.Window`. + ''' + if self._window is not None: + Logger.warning('ModalView: you can only open once.') + return self + # search window + self._window = self._search_window() + if not self._window: + Logger.warning('ModalView: cannot open view, no window found.') + return self + self._window.add_widget(self) + self._window.bind(on_resize=self._align_center, + on_keyboard=self._handle_keyboard) + self.center = self._window.center + self.bind(size=self._align_center) + a = Animation(_anim_alpha=1., d=self._anim_duration) + a.bind(on_complete=lambda *x: self.dispatch('on_open')) + a.start(self) + return self + + def dismiss(self, *largs, **kwargs): + '''Close the view if it is open. If you really want to close the + view, whatever the on_dismiss event returns, you can use the *force* + argument: + :: + + view = ModalView(...) + view.dismiss(force=True) + + When the view is dismissed, it will be faded out before being + removed from the parent. If you don't want animation, use:: + + view.dismiss(animation=False) + + ''' + if self._window is None: + return self + if self.dispatch('on_dismiss') is True: + if kwargs.get('force', False) is not True: + return self + if kwargs.get('animation', True): + Animation(_anim_alpha=0., d=self._anim_duration).start(self) + else: + self._anim_alpha = 0 + self._real_remove_widget() + return self + + def on_content(self, instance, value): + if self._container: + self._container.clear_widgets() + self._container.add_widget(value) + + def on__container(self, instance, value): + if value is None or self.content is None: + return + self._container.clear_widgets() + self._container.add_widget(self.content) + + def on_touch_down(self, touch): + if self.disabled and self.collide_point(*touch.pos): + return True + return super(MDDialog, self).on_touch_down(touch) + + def _update_action_buttons(self, *args): + self._action_area.clear_widgets() + for btn in self._action_buttons: + btn.ids._label.texture_update() + btn.width = btn.ids._label.texture_size[0] + dp(16) + self._action_area.add_widget(btn) diff --git a/src/kivymd/elevationbehavior.py b/src/kivymd/elevationbehavior.py new file mode 100644 index 00000000..19d7985d --- /dev/null +++ b/src/kivymd/elevationbehavior.py @@ -0,0 +1,187 @@ +# -*- coding: utf-8 -*- + +from kivy.app import App +from kivy.lang import Builder +from kivy.properties import (ListProperty, ObjectProperty, NumericProperty) +from kivy.properties import AliasProperty +from kivy.metrics import dp + +Builder.load_string(''' + + canvas.before: + Color: + a: self._soft_shadow_a + Rectangle: + texture: self._soft_shadow_texture + size: self._soft_shadow_size + pos: self._soft_shadow_pos + Color: + a: self._hard_shadow_a + Rectangle: + texture: self._hard_shadow_texture + size: self._hard_shadow_size + pos: self._hard_shadow_pos + Color: + a: 1 + + + canvas.before: + Color: + a: self._soft_shadow_a + Rectangle: + texture: self._soft_shadow_texture + size: self._soft_shadow_size + pos: self._soft_shadow_pos + Color: + a: self._hard_shadow_a + Rectangle: + texture: self._hard_shadow_texture + size: self._hard_shadow_size + pos: self._hard_shadow_pos + Color: + a: 1 +''') + + +class ElevationBehavior(object): + _elevation = NumericProperty(1) + + def _get_elevation(self): + return self._elevation + + def _set_elevation(self, elevation): + try: + self._elevation = elevation + except: + self._elevation = 1 + + elevation = AliasProperty(_get_elevation, _set_elevation, + bind=('_elevation',)) + + _soft_shadow_texture = ObjectProperty() + _soft_shadow_size = ListProperty([0, 0]) + _soft_shadow_pos = ListProperty([0, 0]) + _soft_shadow_a = NumericProperty(0) + _hard_shadow_texture = ObjectProperty() + _hard_shadow_size = ListProperty([0, 0]) + _hard_shadow_pos = ListProperty([0, 0]) + _hard_shadow_a = NumericProperty(0) + + def __init__(self, **kwargs): + super(ElevationBehavior, self).__init__(**kwargs) + self.bind(elevation=self._update_shadow, + pos=self._update_shadow, + size=self._update_shadow) + + def _update_shadow(self, *args): + if self.elevation > 0: + ratio = self.width / (self.height if self.height != 0 else 1) + if ratio > -2 and ratio < 2: + self._shadow = App.get_running_app().theme_cls.quad_shadow + width = soft_width = self.width * 1.9 + height = soft_height = self.height * 1.9 + elif ratio <= -2: + self._shadow = App.get_running_app().theme_cls.rec_st_shadow + ratio = abs(ratio) + if ratio > 5: + ratio = ratio * 22 + else: + ratio = ratio * 11.5 + + width = soft_width = self.width * 1.9 + height = self.height + dp(ratio) + soft_height = self.height + dp(ratio) + dp(self.elevation) * .5 + else: + self._shadow = App.get_running_app().theme_cls.quad_shadow + width = soft_width = self.width * 1.8 + height = soft_height = self.height * 1.8 + # self._shadow = App.get_running_app().theme_cls.rec_shadow + # ratio = abs(ratio) + # if ratio > 5: + # ratio = ratio * 22 + # else: + # ratio = ratio * 11.5 + # + # width = self.width + dp(ratio) + # soft_width = self.width + dp(ratio) + dp(self.elevation) * .9 + # height = soft_height = self.height * 1.9 + + x = self.center_x - width / 2 + soft_x = self.center_x - soft_width / 2 + self._soft_shadow_size = (soft_width, soft_height) + self._hard_shadow_size = (width, height) + + y = self.center_y - soft_height / 2 - dp( + .1 * 1.5 ** self.elevation) + self._soft_shadow_pos = (soft_x, y) + self._soft_shadow_a = 0.1 * 1.1 ** self.elevation + self._soft_shadow_texture = self._shadow.textures[ + str(int(round(self.elevation - 1)))] + + y = self.center_y - height / 2 - dp(.5 * 1.18 ** self.elevation) + self._hard_shadow_pos = (x, y) + self._hard_shadow_a = .4 * .9 ** self.elevation + self._hard_shadow_texture = self._shadow.textures[ + str(int(round(self.elevation)))] + + else: + self._soft_shadow_a = 0 + self._hard_shadow_a = 0 + + +class RoundElevationBehavior(object): + _elevation = NumericProperty(1) + + def _get_elevation(self): + return self._elevation + + def _set_elevation(self, elevation): + try: + self._elevation = elevation + except: + self._elevation = 1 + + elevation = AliasProperty(_get_elevation, _set_elevation, + bind=('_elevation',)) + + _soft_shadow_texture = ObjectProperty() + _soft_shadow_size = ListProperty([0, 0]) + _soft_shadow_pos = ListProperty([0, 0]) + _soft_shadow_a = NumericProperty(0) + _hard_shadow_texture = ObjectProperty() + _hard_shadow_size = ListProperty([0, 0]) + _hard_shadow_pos = ListProperty([0, 0]) + _hard_shadow_a = NumericProperty(0) + + def __init__(self, **kwargs): + super(RoundElevationBehavior, self).__init__(**kwargs) + self._shadow = App.get_running_app().theme_cls.round_shadow + self.bind(elevation=self._update_shadow, + pos=self._update_shadow, + size=self._update_shadow) + + def _update_shadow(self, *args): + if self.elevation > 0: + width = self.width * 2 + height = self.height * 2 + + x = self.center_x - width / 2 + self._soft_shadow_size = (width, height) + + self._hard_shadow_size = (width, height) + + y = self.center_y - height / 2 - dp(.1 * 1.5 ** self.elevation) + self._soft_shadow_pos = (x, y) + self._soft_shadow_a = 0.1 * 1.1 ** self.elevation + self._soft_shadow_texture = self._shadow.textures[ + str(int(round(self.elevation)))] + + y = self.center_y - height / 2 - dp(.5 * 1.18 ** self.elevation) + self._hard_shadow_pos = (x, y) + self._hard_shadow_a = .4 * .9 ** self.elevation + self._hard_shadow_texture = self._shadow.textures[ + str(int(round(self.elevation - 1)))] + + else: + self._soft_shadow_a = 0 + self._hard_shadow_a = 0 diff --git a/src/kivymd/fonts/Material-Design-Iconic-Font.ttf b/src/kivymd/fonts/Material-Design-Iconic-Font.ttf new file mode 100644 index 00000000..5d489fdd Binary files /dev/null and b/src/kivymd/fonts/Material-Design-Iconic-Font.ttf differ diff --git a/src/kivymd/fonts/Roboto-Bold.ttf b/src/kivymd/fonts/Roboto-Bold.ttf new file mode 100644 index 00000000..91ec2122 Binary files /dev/null and b/src/kivymd/fonts/Roboto-Bold.ttf differ diff --git a/src/kivymd/fonts/Roboto-Italic.ttf b/src/kivymd/fonts/Roboto-Italic.ttf new file mode 100644 index 00000000..2041cbc0 Binary files /dev/null and b/src/kivymd/fonts/Roboto-Italic.ttf differ diff --git a/src/kivymd/fonts/Roboto-Light.ttf b/src/kivymd/fonts/Roboto-Light.ttf new file mode 100644 index 00000000..664e1b2f Binary files /dev/null and b/src/kivymd/fonts/Roboto-Light.ttf differ diff --git a/src/kivymd/fonts/Roboto-LightItalic.ttf b/src/kivymd/fonts/Roboto-LightItalic.ttf new file mode 100644 index 00000000..a85444f2 Binary files /dev/null and b/src/kivymd/fonts/Roboto-LightItalic.ttf differ diff --git a/src/kivymd/fonts/Roboto-Medium.ttf b/src/kivymd/fonts/Roboto-Medium.ttf new file mode 100644 index 00000000..aa00de0e Binary files /dev/null and b/src/kivymd/fonts/Roboto-Medium.ttf differ diff --git a/src/kivymd/fonts/Roboto-MediumItalic.ttf b/src/kivymd/fonts/Roboto-MediumItalic.ttf new file mode 100644 index 00000000..b8282055 Binary files /dev/null and b/src/kivymd/fonts/Roboto-MediumItalic.ttf differ diff --git a/src/kivymd/fonts/Roboto-Regular.ttf b/src/kivymd/fonts/Roboto-Regular.ttf new file mode 100644 index 00000000..3e6e2e76 Binary files /dev/null and b/src/kivymd/fonts/Roboto-Regular.ttf differ diff --git a/src/kivymd/fonts/Roboto-Thin.ttf b/src/kivymd/fonts/Roboto-Thin.ttf new file mode 100644 index 00000000..d262d144 Binary files /dev/null and b/src/kivymd/fonts/Roboto-Thin.ttf differ diff --git a/src/kivymd/fonts/Roboto-ThinItalic.ttf b/src/kivymd/fonts/Roboto-ThinItalic.ttf new file mode 100644 index 00000000..b79cb26d Binary files /dev/null and b/src/kivymd/fonts/Roboto-ThinItalic.ttf differ diff --git a/src/kivymd/grid.py b/src/kivymd/grid.py new file mode 100644 index 00000000..db310193 --- /dev/null +++ b/src/kivymd/grid.py @@ -0,0 +1,168 @@ +# coding=utf-8 +from kivy.lang import Builder +from kivy.properties import StringProperty, BooleanProperty, ObjectProperty, \ + NumericProperty, ListProperty, OptionProperty +from kivy.uix.behaviors import ButtonBehavior +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.floatlayout import FloatLayout +from kivymd.ripplebehavior import RectangularRippleBehavior +from kivymd.theming import ThemableBehavior + +Builder.load_string(""" + + _img_widget: img + _img_overlay: img_overlay + _box_overlay: box + AsyncImage: + id: img + allow_stretch: root.allow_stretch + anim_delay: root.anim_delay + anim_loop: root.anim_loop + color: root.img_color + keep_ratio: root.keep_ratio + mipmap: root.mipmap + source: root.source + size_hint_y: 1 if root.overlap else None + x: root.x + y: root.y if root.overlap or root.box_position == 'header' else box.top + BoxLayout: + id: img_overlay + size_hint: img.size_hint + size: img.size + pos: img.pos + BoxLayout: + canvas: + Color: + rgba: root.box_color + Rectangle: + pos: self.pos + size: self.size + id: box + size_hint_y: None + height: dp(68) if root.lines == 2 else dp(48) + x: root.x + y: root.y if root.box_position == 'footer' else root.y + root.height - self.height + + + _img_widget: img + _img_overlay: img_overlay + _box_overlay: box + _box_label: boxlabel + AsyncImage: + id: img + allow_stretch: root.allow_stretch + anim_delay: root.anim_delay + anim_loop: root.anim_loop + color: root.img_color + keep_ratio: root.keep_ratio + mipmap: root.mipmap + source: root.source + size_hint_y: 1 if root.overlap else None + x: root.x + y: root.y if root.overlap or root.box_position == 'header' else box.top + BoxLayout: + id: img_overlay + size_hint: img.size_hint + size: img.size + pos: img.pos + BoxLayout: + canvas: + Color: + rgba: root.box_color + Rectangle: + pos: self.pos + size: self.size + id: box + size_hint_y: None + height: dp(68) if root.lines == 2 else dp(48) + x: root.x + y: root.y if root.box_position == 'footer' else root.y + root.height - self.height + MDLabel: + id: boxlabel + font_style: "Caption" + halign: "center" + text: root.text +""") + + +class Tile(ThemableBehavior, RectangularRippleBehavior, ButtonBehavior, + BoxLayout): + """A simple tile. It does nothing special, just inherits the right behaviors + to work as a building block. + """ + pass + + +class SmartTile(ThemableBehavior, RectangularRippleBehavior, ButtonBehavior, + FloatLayout): + """A tile for more complex needs. + + Includes an image, a container to place overlays and a box that can act + as a header or a footer, as described in the Material Design specs. + """ + + box_color = ListProperty([0, 0, 0, 0.5]) + """Sets the color and opacity for the information box.""" + + box_position = OptionProperty('footer', options=['footer', 'header']) + """Determines wether the information box acts as a header or footer to the + image. + """ + + lines = OptionProperty(1, options=[1, 2]) + """Number of lines in the header/footer. + + As per Material Design specs, only 1 and 2 are valid values. + """ + + overlap = BooleanProperty(True) + """Determines if the header/footer overlaps on top of the image or not""" + + # Img properties + allow_stretch = BooleanProperty(True) + anim_delay = NumericProperty(0.25) + anim_loop = NumericProperty(0) + img_color = ListProperty([1, 1, 1, 1]) + keep_ratio = BooleanProperty(False) + mipmap = BooleanProperty(False) + source = StringProperty() + + _img_widget = ObjectProperty() + _img_overlay = ObjectProperty() + _box_overlay = ObjectProperty() + _box_label = ObjectProperty() + + def reload(self): + self._img_widget.reload() + + def add_widget(self, widget, index=0): + if issubclass(widget.__class__, IOverlay): + self._img_overlay.add_widget(widget, index) + elif issubclass(widget.__class__, IBoxOverlay): + self._box_overlay.add_widget(widget, index) + else: + super(SmartTile, self).add_widget(widget, index) + + +class SmartTileWithLabel(SmartTile): + _box_label = ObjectProperty() + + # MDLabel properties + font_style = StringProperty("Caption") + theme_text_color = StringProperty("") + text = StringProperty("") + """Determines the text for the box footer/header""" + + +class IBoxOverlay(): + """An interface to specify widgets that belong to to the image overlay + in the :class:`SmartTile` widget when added as a child. + """ + pass + + +class IOverlay(): + """An interface to specify widgets that belong to to the image overlay + in the :class:`SmartTile` widget when added as a child. + """ + pass diff --git a/src/kivymd/icon_definitions.py b/src/kivymd/icon_definitions.py new file mode 100644 index 00000000..5b717356 --- /dev/null +++ b/src/kivymd/icon_definitions.py @@ -0,0 +1,1569 @@ +# -*- coding: utf-8 -*- + +# Thanks to Sergey Kupletsky (github.com/zavoloklom) for its Material Design +# Iconic Font, which provides KivyMD's icons. + +# GALLERY HERE: +# https://zavoloklom.github.io/material-design-iconic-font/icons.html + +# LAST UPDATED: version 2.2.0 of Material Design Iconic Font + +md_icons = { + '3d-rotation': u'', + + 'airplane-off': u'', + + 'address': u'', + + 'airplane': u'', + + 'album': u'', + + 'archive': u'', + + 'assignment-account': u'', + + 'assignment-alert': u'', + + 'assignment-check': u'', + + 'assignment-o': u'', + + 'assignment-return': u'', + + 'assignment-returned': u'', + + 'assignment': u'', + + 'attachment-alt': u'', + + 'attachment': u'', + + 'audio': u'', + + 'badge-check': u'', + + 'balance-wallet': u'', + + 'balance': u'', + + 'battery-alert': u'', + + 'battery-flash': u'', + + 'battery-unknown': u'', + + 'battery': u'', + + 'bike': u'', + + 'block-alt': u'', + + 'block': u'', + + 'boat': u'', + + 'book-image': u'', + + 'book': u'', + + 'bookmark-outline': u'', + + 'bookmark': u'', + + 'brush': u'', + + 'bug': u'', + + 'bus': u'', + + 'cake': u'', + + 'car-taxi': u'', + + 'car-wash': u'', + + 'car': u'', + + 'card-giftcard': u'', + + 'card-membership': u'', + + 'card-travel': u'', + + 'card': u'', + + 'case-check': u'', + + 'case-download': u'', + + 'case-play': u'', + + 'case': u'', + + 'cast-connected': u'', + + 'cast': u'', + + 'chart-donut': u'', + + 'chart': u'', + + 'city-alt': u'', + + 'city': u'', + + 'close-circle-o': u'', + + 'close-circle': u'', + + 'close': u'', + + 'cocktail': u'', + + 'code-setting': u'', + + 'code-smartphone': u'', + + 'code': u'', + + 'coffee': u'', + + 'collection-bookmark': u'', + + 'collection-case-play': u'', + + 'collection-folder-image': u'', + + 'collection-image-o': u'', + + 'collection-image': u'', + + 'collection-item-1': u'', + + 'collection-item-2': u'', + + 'collection-item-3': u'', + + 'collection-item-4': u'', + + 'collection-item-5': u'', + + 'collection-item-6': u'', + + 'collection-item-7': u'', + + 'collection-item-8': u'', + + 'collection-item-9-plus': u'', + + 'collection-item-9': u'', + + 'collection-item': u'', + + 'collection-music': u'', + + 'collection-pdf': u'', + + 'collection-plus': u'', + + 'collection-speaker': u'', + + 'collection-text': u'', + + 'collection-video': u'', + + 'compass': u'', + + 'cutlery': u'', + + 'delete': u'', + + 'dialpad': u'', + + 'dns': u'', + + 'drink': u'', + + 'edit': u'', + + 'email-open': u'', + + 'email': u'', + + 'eye-off': u'', + + 'eye': u'', + + 'eyedropper': u'', + + 'favorite-outline': u'', + + 'favorite': u'', + + 'filter-list': u'', + + 'fire': u'', + + 'flag': u'', + + 'flare': u'', + + 'flash-auto': u'', + + 'flash-off': u'', + + 'flash': u'', + + 'flip': u'', + + 'flower-alt': u'', + + 'flower': u'', + + 'font': u'', + + 'fullscreen-alt': u'', + + 'fullscreen-exit': u'', + + 'fullscreen': u'', + + 'functions': u'', + + 'gas-station': u'', + + 'gesture': u'', + + 'globe-alt': u'', + + 'globe-lock': u'', + + 'globe': u'', + + 'graduation-cap': u'', + + 'group': u'', + + 'home': u'', + + 'hospital-alt': u'', + + 'hospital': u'', + + 'hotel': u'', + + 'hourglass-alt': u'', + + 'hourglass-outline': u'', + + 'hourglass': u'', + + 'http': u'', + + 'image-alt': u'', + + 'image-o': u'', + + 'image': u'', + + 'inbox': u'', + + 'invert-colors-off': u'', + + 'invert-colors': u'', + + 'key': u'', + + 'label-alt-outline': u'', + + 'label-alt': u'', + + 'label-heart': u'', + + 'label': u'', + + 'labels': u'', + + 'lamp': u'', + + 'landscape': u'', + + 'layers-off': u'', + + 'layers': u'', + + 'library': u'', + + 'link': u'', + + 'lock-open': u'', + + 'lock-outline': u'', + + 'lock': u'', + + 'mail-reply-all': u'', + + 'mail-reply': u'', + + 'mail-send': u'', + + 'mall': u'', + + 'map': u'', + + 'menu': u'', + + 'money-box': u'', + + 'money-off': u'', + + 'money': u'', + + 'more-vert': u'', + + 'more': u'', + + 'movie-alt': u'', + + 'movie': u'', + + 'nature-people': u'', + + 'nature': u'', + + 'navigation': u'', + + 'open-in-browser': u'', + + 'open-in-new': u'', + + 'palette': u'', + + 'parking': u'', + + 'pin-account': u'', + + 'pin-assistant': u'', + + 'pin-drop': u'', + + 'pin-help': u'', + + 'pin-off': u'', + + 'pin': u'', + + 'pizza': u'', + + 'plaster': u'', + + 'power-setting': u'', + + 'power': u'', + + 'print': u'', + + 'puzzle-piece': u'', + + 'quote': u'', + + 'railway': u'', + + 'receipt': u'', + + 'refresh-alt': u'', + + 'refresh-sync-alert': u'', + + 'refresh-sync-off': u'', + + 'refresh-sync': u'', + + 'refresh': u'', + + 'roller': u'', + + 'ruler': u'', + + 'scissors': u'', + + 'screen-rotation-lock': u'', + + 'screen-rotation': u'', + + 'search-for': u'', + + 'search-in-file': u'', + + 'search-in-page': u'', + + 'search-replace': u'', + + 'search': u'', + + 'seat': u'', + + 'settings-square': u'', + + 'settings': u'', + + 'shape': u'', + + 'shield-check': u'', + + 'shield-security': u'', + + 'shopping-basket': u'', + + 'shopping-cart-plus': u'', + + 'shopping-cart': u'', + + 'sign-in': u'', + + 'sort-amount-asc': u'', + + 'sort-amount-desc': u'', + + 'sort-asc': u'', + + 'sort-desc': u'', + + 'spellcheck': u'', + + 'spinner': u'', + + 'storage': u'', + + 'store-24': u'', + + 'store': u'', + + 'subway': u'', + + 'sun': u'', + + 'tab-unselected': u'', + + 'tab': u'', + + 'tag-close': u'', + + 'tag-more': u'', + + 'tag': u'', + + 'thumb-down': u'', + + 'thumb-up-down': u'', + + 'thumb-up': u'', + + 'ticket-star': u'', + + 'toll': u'', + + 'toys': u'', + + 'traffic': u'', + + 'translate': u'', + + 'triangle-down': u'', + + 'triangle-up': u'', + + 'truck': u'', + + 'turning-sign': u'', + + ' ungroup': u'', + + 'wallpaper': u'', + + 'washing-machine': u'', + + 'window-maximize': u'', + + 'window-minimize': u'', + + 'window-restore': u'', + + 'wrench': u'', + + 'zoom-in': u'', + + 'zoom-out': u'', + + 'alert-circle-o': u'', + + 'alert-circle': u'', + + 'alert-octagon': u'', + + 'alert-polygon': u'', + + 'alert-triangle': u'', + + 'help-outline': u'', + + 'help': u'', + + 'info-outline': u'', + + 'info': u'', + + 'notifications-active': u'', + + 'notifications-add': u'', + + 'notifications-none': u'', + + 'notifications-off': u'', + + 'notifications-paused': u'', + + 'notifications': u'', + + 'account-add': u'', + + 'account-box-mail': u'', + + 'account-box-o': u'', + + 'account-box-phone': u'', + + 'account-box': u'', + + 'account-calendar': u'', + + 'account-circle': u'', + + 'account-o': u'', + + 'account': u'', + + 'accounts-add': u'', + + 'accounts-alt': u'', + + 'accounts-list-alt': u'', + + 'accounts-list': u'', + + 'accounts-outline': u'', + + 'accounts': u'', + + 'face': u'', + + 'female': u'', + + 'male-alt': u'', + + 'male-female': u'', + + 'male': u'', + + 'mood-bad': u'', + + 'mood': u'', + + 'run': u'', + + 'walk': u'', + + 'cloud-box': u'', + + 'cloud-circle': u'', + + 'cloud-done': u'', + + 'cloud-download': u'', + + 'cloud-off': u'', + + 'cloud-outline-alt': u'', + + 'cloud-outline': u'', + + 'cloud-upload': u'', + + 'cloud': u'', + + 'download': u'', + + 'file-plus': u'', + + 'file-text': u'', + + 'file': u'', + + 'folder-outline': u'', + + 'folder-person': u'', + + 'folder-star-alt': u'', + + 'folder-star': u'', + + 'folder': u'', + + 'gif': u'', + + 'upload': u'', + + 'border-all': u'', + + 'border-bottom': u'', + + 'border-clear': u'', + + 'border-color': u'', + + 'border-horizontal': u'', + + 'border-inner': u'', + + 'border-left': u'', + + 'border-outer': u'', + + 'border-right': u'', + + 'border-style': u'', + + 'border-top': u'', + + 'border-vertical': u'', + + 'copy': u'', + + 'crop': u'', + + 'format-align-center': u'', + + 'format-align-justify': u'', + + 'format-align-left': u'', + + 'format-align-right': u'', + + 'format-bold': u'', + + 'format-clear-all': u'', + + 'format-clear': u'', + + 'format-color-fill': u'', + + 'format-color-reset': u'', + + 'format-color-text': u'', + + 'format-indent-decrease': u'', + + 'format-indent-increase': u'', + + 'format-italic': u'', + + 'format-line-spacing': u'', + + 'format-list-bulleted': u'', + + 'format-list-numbered': u'', + + 'format-ltr': u'', + + 'format-rtl': u'', + + 'format-size': u'', + + 'format-strikethrough-s': u'', + + 'format-strikethrough': u'', + + 'format-subject': u'', + + 'format-underlined': u'', + + 'format-valign-bottom': u'', + + 'format-valign-center': u'', + + 'format-valign-top': u'', + + 'redo': u'', + + 'select-all': u'', + + 'space-bar': u'', + + 'text-format': u'', + + 'transform': u'', + + 'undo': u'', + + 'wrap-text': u'', + + 'comment-alert': u'', + + 'comment-alt-text': u'', + + 'comment-alt': u'', + + 'comment-edit': u'', + + 'comment-image': u'', + + 'comment-list': u'', + + 'comment-more': u'', + + 'comment-outline': u'', + + 'comment-text-alt': u'', + + 'comment-text': u'', + + 'comment-video': u'', + + 'comment': u'', + + 'comments': u'', + + 'rm': u'F', + + 'check-all': u'', + + 'check-circle-u': u'', + + 'check-circle': u'', + + 'check-square': u'', + + 'check': u'', + + 'circle-o': u'', + + 'circle': u'', + + 'dot-circle-alt': u'', + + 'dot-circle': u'', + + 'minus-circle-outline': u'', + + 'minus-circle': u'', + + 'minus-square': u'', + + 'minus': u'', + + 'plus-circle-o-duplicate': u'', + + 'plus-circle-o': u'', + + 'plus-circle': u'', + + 'plus-square': u'', + + 'plus': u'', + + 'square-o': u'', + + 'star-circle': u'', + + 'star-half': u'', + + 'star-outline': u'', + + 'star': u'', + + 'bluetooth-connected': u'', + + 'bluetooth-off': u'', + + 'bluetooth-search': u'', + + 'bluetooth-setting': u'', + + 'bluetooth': u'', + + 'camera-add': u'', + + 'camera-alt': u'', + + 'camera-bw': u'', + + 'camera-front': u'', + + 'camera-mic': u'', + + 'camera-party-mode': u'', + + 'camera-rear': u'', + + 'camera-roll': u'', + + 'camera-switch': u'', + + 'camera': u'', + + 'card-alert': u'', + + 'card-off': u'', + + 'card-sd': u'', + + 'card-sim': u'', + + 'desktop-mac': u'', + + 'desktop-windows': u'', + + 'device-hub': u'', + + 'devices-off': u'', + + 'devices': u'', + + 'dock': u'', + + 'floppy': u'', + + 'gamepad': u'', + + 'gps-dot': u'', + + 'gps-off': u'', + + 'gps': u'', + + 'headset-mic': u'', + + 'headset': u'', + + 'input-antenna': u'', + + 'input-composite': u'', + + 'input-hdmi': u'', + + 'input-power': u'', + + 'input-svideo': u'', + + 'keyboard-hide': u'', + + 'keyboard': u'', + + 'laptop-chromebook': u'', + + 'laptop-mac': u'', + + 'laptop': u'', + + 'mic-off': u'', + + 'mic-outline': u'', + + 'mic-setting': u'', + + 'mic': u'', + + 'mouse': u'', + + 'network-alert': u'', + + 'network-locked': u'', + + 'network-off': u'', + + 'network-outline': u'', + + 'network-setting': u'', + + 'network': u'', + + 'phone-bluetooth': u'', + + 'phone-end': u'', + + 'phone-forwarded': u'', + + 'phone-in-talk': u'', + + 'phone-locked': u'', + + 'phone-missed': u'', + + 'phone-msg': u'', + + 'phone-paused': u'', + + 'phone-ring': u'', + + 'phone-setting': u'', + + 'phone-sip': u'', + + 'phone': u'', + + 'portable-wifi-changes': u'', + + 'portable-wifi-off': u'', + + 'portable-wifi': u'', + + 'radio': u'', + + 'reader': u'', + + 'remote-control-alt': u'', + + 'remote-control': u'', + + 'router': u'', + + 'scanner': u'', + + 'smartphone-android': u'', + + 'smartphone-download': u'', + + 'smartphone-erase': u'', + + 'smartphone-info': u'', + + 'smartphone-iphone': u'', + + 'smartphone-landscape-lock': u'', + + 'smartphone-landscape': u'', + + 'smartphone-lock': u'', + + 'smartphone-portrait-lock': u'', + + 'smartphone-ring': u'', + + 'smartphone-setting': u'', + + 'smartphone-setup': u'', + + 'smartphone': u'', + + 'speaker': u'', + + 'tablet-android': u'', + + 'tablet-mac': u'', + + 'tablet': u'', + + 'tv-alt-play': u'', + + 'tv-list': u'', + + 'tv-play': u'', + + 'tv': u'', + + 'usb': u'', + + 'videocam-off': u'', + + 'videocam-switch': u'', + + 'videocam': u'', + + 'watch': u'', + + 'wifi-alt-2': u'', + + 'wifi-alt': u'', + + 'wifi-info': u'', + + 'wifi-lock': u'', + + 'wifi-off': u'', + + 'wifi-outline': u'', + + 'wifi': u'', + + 'arrow-left-bottom': u'', + + 'arrow-left': u'', + + 'arrow-merge': u'', + + 'arrow-missed': u'', + + 'arrow-right-top': u'', + + 'arrow-right': u'', + + 'arrow-split': u'', + + 'arrows': u'', + + 'caret-down-circle': u'', + + 'caret-down': u'', + + 'caret-left-circle': u'', + + 'caret-left': u'', + + 'caret-right-circle': u'', + + 'caret-right': u'', + + 'caret-up-circle': u'', + + 'caret-up': u'', + + 'chevron-down': u'', + + 'chevron-left': u'', + + 'chevron-right': u'', + + 'chevron-up': u'', + + 'forward': u'', + + 'long-arrow-down': u'', + + 'long-arrow-left': u'', + + 'long-arrow-return': u'', + + 'long-arrow-right': u'', + + 'long-arrow-tab': u'', + + 'long-arrow-up': u'', + + 'rotate-ccw': u'', + + 'rotate-cw': u'', + + 'rotate-left': u'', + + 'rotate-right': u'', + + 'square-down': u'', + + 'square-right': u'', + + 'swap-alt': u'', + + 'swap-vertical-circle': u'', + + 'swap-vertical': u'', + + 'swap': u'', + + 'trending-down': u'', + + 'trending-flat': u'', + + 'trending-up': u'', + + 'unfold-less': u'', + + 'unfold-more': u'', + + 'apps': u'', + + 'grid-off': u'', + + 'grid': u'', + + 'view-agenda': u'', + + 'view-array': u'', + + 'view-carousel': u'', + + 'view-column': u'', + + 'view-comfy': u'', + + 'view-compact': u'', + + 'view-dashboard': u'', + + 'view-day': u'', + + 'view-headline': u'', + + 'view-list-alt': u'', + + 'view-list': u'', + + 'view-module': u'', + + 'view-quilt': u'', + + 'view-stream': u'', + + 'view-subtitles': u'', + + 'view-toc': u'', + + 'view-web': u'', + + 'view-week': u'', + + 'widgets': u'', + + 'alarm-check': u'', + + 'alarm-off': u'', + + 'alarm-plus': u'', + + 'alarm-snooze': u'', + + 'alarm': u'', + + 'calendar-alt': u'', + + 'calendar-check': u'', + + 'calendar-close': u'', + + 'calendar-note': u'', + + 'calendar': u'', + + 'time-countdown': u'', + + 'time-interval': u'', + + 'time-restore-setting': u'', + + 'time-restore': u'', + + 'time': u'', + + 'timer-off': u'', + + 'timer': u'', + + 'android-alt': u'', + + 'android': u'', + + 'apple': u'', + + 'behance': u'', + + 'codepen': u'', + + 'dribbble': u'', + + 'dropbox': u'', + + 'evernote': u'', + + 'facebook-box': u'', + + 'facebook': u'', + + 'github-box': u'', + + 'github': u'', + + 'google-drive': u'', + + 'google-earth': u'', + + 'google-glass': u'', + + 'google-maps': u'', + + 'google-pages': u'', + + 'google-play': u'', + + 'google-plus-box': u'', + + 'google-plus': u'', + + 'google': u'', + + 'instagram': u'', + + 'language-css3': u'', + + 'language-html5': u'', + + 'language-javascript': u'', + + 'language-python-alt': u'', + + 'language-python': u'', + + 'lastfm': u'', + + 'linkedin-box': u'', + + 'paypal': u'', + + 'pinterest-box': u'', + + 'pocket': u'', + + 'polymer': u'', + + 'rss': u'', + + 'share': u'', + + 'stackoverflow': u'', + + 'steam-square': u'', + + 'steam': u'', + + 'twitter-box': u'', + + 'twitter': u'', + + 'vk': u'', + + 'wikipedia': u'', + + 'windows': u'', + + '500px': u'', + + '8tracks': u'', + + 'amazon': u'', + + 'blogger': u'', + + 'delicious': u'', + + 'disqus': u'', + + 'flattr': u'', + + 'flickr': u'', + + 'github-alt': u'', + + 'google-old': u'', + + 'linkedin': u'', + + 'odnoklassniki': u'', + + 'outlook': u'', + + 'paypal-alt': u'', + + 'pinterest': u'', + + 'playstation': u'', + + 'reddit': u'', + + 'skype': u'', + + 'slideshare': u'', + + 'soundcloud': u'', + + 'tumblr': u'', + + 'twitch': u'', + + 'vimeo': u'', + + 'whatsapp': u'', + + 'xbox': u'', + + 'yahoo': u'', + + 'youtube-play': u'', + + 'youtube': u'', + + 'aspect-ratio-alt': u'', + + 'aspect-ratio': u'', + + 'blur-circular': u'', + + 'blur-linear': u'', + + 'blur-off': u'', + + 'blur': u'', + + 'brightness-2': u'', + + 'brightness-3': u'', + + 'brightness-4': u'', + + 'brightness-5': u'', + + 'brightness-6': u'', + + 'brightness-7': u'', + + 'brightness-auto': u'', + + 'brightness-setting': u'', + + 'broken-image': u'', + + 'center-focus-strong': u'', + + 'center-focus-weak': u'', + + 'compare': u'', + + 'crop-16-9': u'', + + 'crop-3-2': u'', + + 'crop-5-4': u'', + + 'crop-7-5': u'', + + 'crop-din': u'', + + 'crop-free': u'', + + 'crop-landscape': u'', + + 'crop-portrait': u'', + + 'crop-square': u'', + + 'exposure-alt': u'', + + 'exposure': u'', + + 'filter-b-and-w': u'', + + 'filter-center-focus': u'', + + 'filter-frames': u'', + + 'filter-tilt-shift': u'', + + 'gradient': u'', + + 'grain': u'', + + 'graphic-eq': u'', + + 'hdr-off': u'', + + 'hdr-strong': u'', + + 'hdr-weak': u'', + + 'hdr': u'', + + 'iridescent': u'', + + 'leak-off': u'', + + 'leak': u'', + + 'looks': u'', + + 'loupe': u'', + + 'panorama-horizontal': u'', + + 'panorama-vertical': u'', + + 'panorama-wide-angle': u'', + + 'photo-size-select-large': u'', + + 'photo-size-select-small': u'', + + 'picture-in-picture': u'', + + 'slideshow': u'', + + 'texture': u'', + + 'tonality': u'', + + 'vignette': u'', + + 'wb-auto': u'', + + 'eject-alt': u'', + + 'eject': u'', + + 'equalizer': u'', + + 'fast-forward': u'', + + 'fast-rewind': u'', + + 'forward-10': u'', + + 'forward-30': u'', + + 'forward-5': u'', + + 'hearing': u'', + + 'pause-circle-outline': u'', + + 'pause-circle': u'', + + 'pause': u'', + + 'play-circle-outline': u'', + + 'play-circle': u'', + + 'play': u'', + + 'playlist-audio': u'', + + 'playlist-plus': u'', + + 'repeat-one': u'', + + 'repeat': u'', + + 'replay-10': u'', + + 'replay-30': u'', + + 'replay-5': u'', + + 'replay': u'', + + 'shuffle': u'', + + 'skip-next': u'', + + 'skip-previous': u'', + + 'stop': u'', + + 'surround-sound': u'', + + 'tune': u'', + + 'volume-down': u'', + + 'volume-mute': u'', + + 'volume-off': u'', + + 'volume-up': u'', + + 'n-1-square': u'', + + 'n-2-square': u'', + + 'n-3-square': u'', + + 'n-4-square': u'', + + 'n-5-square': u'', + + 'n-6-square': u'', + + 'neg-1': u'', + + 'neg-2': u'', + + 'plus-1': u'', + + 'plus-2': u'', + + 'sec-10': u'', + + 'sec-3': u'', + + 'zero': u'', + + 'airline-seat-flat-angled': u'', + + 'airline-seat-flat': u'', + + 'airline-seat-individual-suite': u'', + + 'airline-seat-legroom-extra': u'', + + 'airline-seat-legroom-normal': u'', + + 'airline-seat-legroom-reduced': u'', + + 'airline-seat-recline-extra': u'', + + 'airline-seat-recline-normal': u'', + + 'airplay': u'', + + 'closed-caption': u'', + + 'confirmation-number': u'', + + 'developer-board': u'', + + 'disc-full': u'', + + 'explicit': u'', + + 'flight-land': u'', + + 'flight-takeoff': u'', + + 'flip-to-back': u'', + + 'flip-to-front': u'', + + 'group-work': u'', + + 'hd': u'', + + 'hq': u'', + + 'markunread-mailbox': u'', + + 'memory': u'', + + 'nfc': u'', + + 'play-for-work': u'', + + 'power-input': u'', + + 'present-to-all': u'', + + 'satellite': u'', + + 'tap-and-play': u'', + + 'vibration': u'', + + 'voicemail': u'', +} diff --git a/src/kivymd/images/kivymd_512.png b/src/kivymd/images/kivymd_512.png new file mode 100644 index 00000000..7dbae604 Binary files /dev/null and b/src/kivymd/images/kivymd_512.png differ diff --git a/src/kivymd/images/kivymd_logo.png b/src/kivymd/images/kivymd_logo.png new file mode 100644 index 00000000..64d956d4 Binary files /dev/null and b/src/kivymd/images/kivymd_logo.png differ diff --git a/src/kivymd/images/quad_shadow-0.png b/src/kivymd/images/quad_shadow-0.png new file mode 100644 index 00000000..5d64fde5 Binary files /dev/null and b/src/kivymd/images/quad_shadow-0.png differ diff --git a/src/kivymd/images/quad_shadow-1.png b/src/kivymd/images/quad_shadow-1.png new file mode 100644 index 00000000..c0f1e226 Binary files /dev/null and b/src/kivymd/images/quad_shadow-1.png differ diff --git a/src/kivymd/images/quad_shadow-2.png b/src/kivymd/images/quad_shadow-2.png new file mode 100644 index 00000000..44619e56 Binary files /dev/null and b/src/kivymd/images/quad_shadow-2.png differ diff --git a/src/kivymd/images/quad_shadow.atlas b/src/kivymd/images/quad_shadow.atlas new file mode 100644 index 00000000..68e0aad2 --- /dev/null +++ b/src/kivymd/images/quad_shadow.atlas @@ -0,0 +1 @@ +{"quad_shadow-1.png": {"20": [2, 136, 128, 128], "21": [132, 136, 128, 128], "22": [262, 136, 128, 128], "23": [2, 6, 128, 128], "19": [132, 266, 128, 128], "18": [2, 266, 128, 128], "1": [262, 266, 128, 128], "3": [262, 6, 128, 128], "2": [132, 6, 128, 128]}, "quad_shadow-0.png": {"11": [262, 266, 128, 128], "10": [132, 266, 128, 128], "13": [132, 136, 128, 128], "12": [2, 136, 128, 128], "15": [2, 6, 128, 128], "14": [262, 136, 128, 128], "17": [262, 6, 128, 128], "16": [132, 6, 128, 128], "0": [2, 266, 128, 128]}, "quad_shadow-2.png": {"5": [132, 266, 128, 128], "4": [2, 266, 128, 128], "7": [2, 136, 128, 128], "6": [262, 266, 128, 128], "9": [262, 136, 128, 128], "8": [132, 136, 128, 128]}} \ No newline at end of file diff --git a/src/kivymd/images/rec_shadow-0.png b/src/kivymd/images/rec_shadow-0.png new file mode 100644 index 00000000..f02b919a Binary files /dev/null and b/src/kivymd/images/rec_shadow-0.png differ diff --git a/src/kivymd/images/rec_shadow-1.png b/src/kivymd/images/rec_shadow-1.png new file mode 100644 index 00000000..f752fd26 Binary files /dev/null and b/src/kivymd/images/rec_shadow-1.png differ diff --git a/src/kivymd/images/rec_shadow.atlas b/src/kivymd/images/rec_shadow.atlas new file mode 100644 index 00000000..71b0e9d6 --- /dev/null +++ b/src/kivymd/images/rec_shadow.atlas @@ -0,0 +1 @@ +{"rec_shadow-1.png": {"20": [2, 266, 256, 128], "21": [260, 266, 256, 128], "22": [518, 266, 256, 128], "23": [776, 266, 256, 128], "3": [260, 136, 256, 128], "2": [2, 136, 256, 128], "5": [776, 136, 256, 128], "4": [518, 136, 256, 128], "7": [260, 6, 256, 128], "6": [2, 6, 256, 128], "9": [776, 6, 256, 128], "8": [518, 6, 256, 128]}, "rec_shadow-0.png": {"11": [518, 266, 256, 128], "10": [260, 266, 256, 128], "13": [2, 136, 256, 128], "12": [776, 266, 256, 128], "15": [518, 136, 256, 128], "14": [260, 136, 256, 128], "17": [2, 6, 256, 128], "16": [776, 136, 256, 128], "19": [518, 6, 256, 128], "18": [260, 6, 256, 128], "1": [776, 6, 256, 128], "0": [2, 266, 256, 128]}} \ No newline at end of file diff --git a/src/kivymd/images/rec_st_shadow-0.png b/src/kivymd/images/rec_st_shadow-0.png new file mode 100644 index 00000000..887327db Binary files /dev/null and b/src/kivymd/images/rec_st_shadow-0.png differ diff --git a/src/kivymd/images/rec_st_shadow-1.png b/src/kivymd/images/rec_st_shadow-1.png new file mode 100644 index 00000000..759ee652 Binary files /dev/null and b/src/kivymd/images/rec_st_shadow-1.png differ diff --git a/src/kivymd/images/rec_st_shadow-2.png b/src/kivymd/images/rec_st_shadow-2.png new file mode 100644 index 00000000..e9fdaccc Binary files /dev/null and b/src/kivymd/images/rec_st_shadow-2.png differ diff --git a/src/kivymd/images/rec_st_shadow.atlas b/src/kivymd/images/rec_st_shadow.atlas new file mode 100644 index 00000000..d4c24abe --- /dev/null +++ b/src/kivymd/images/rec_st_shadow.atlas @@ -0,0 +1 @@ +{"rec_st_shadow-0.png": {"11": [262, 138, 128, 256], "10": [132, 138, 128, 256], "13": [522, 138, 128, 256], "12": [392, 138, 128, 256], "15": [782, 138, 128, 256], "14": [652, 138, 128, 256], "16": [912, 138, 128, 256], "0": [2, 138, 128, 256]}, "rec_st_shadow-1.png": {"20": [522, 138, 128, 256], "21": [652, 138, 128, 256], "17": [2, 138, 128, 256], "23": [912, 138, 128, 256], "19": [262, 138, 128, 256], "18": [132, 138, 128, 256], "22": [782, 138, 128, 256], "1": [392, 138, 128, 256]}, "rec_st_shadow-2.png": {"3": [132, 138, 128, 256], "2": [2, 138, 128, 256], "5": [392, 138, 128, 256], "4": [262, 138, 128, 256], "7": [652, 138, 128, 256], "6": [522, 138, 128, 256], "9": [912, 138, 128, 256], "8": [782, 138, 128, 256]}} \ No newline at end of file diff --git a/src/kivymd/images/round_shadow-0.png b/src/kivymd/images/round_shadow-0.png new file mode 100644 index 00000000..26d98405 Binary files /dev/null and b/src/kivymd/images/round_shadow-0.png differ diff --git a/src/kivymd/images/round_shadow-1.png b/src/kivymd/images/round_shadow-1.png new file mode 100644 index 00000000..d0f4c0fd Binary files /dev/null and b/src/kivymd/images/round_shadow-1.png differ diff --git a/src/kivymd/images/round_shadow-2.png b/src/kivymd/images/round_shadow-2.png new file mode 100644 index 00000000..d5feef2c Binary files /dev/null and b/src/kivymd/images/round_shadow-2.png differ diff --git a/src/kivymd/images/round_shadow.atlas b/src/kivymd/images/round_shadow.atlas new file mode 100644 index 00000000..f25016dc --- /dev/null +++ b/src/kivymd/images/round_shadow.atlas @@ -0,0 +1 @@ +{"round_shadow-1.png": {"20": [2, 136, 128, 128], "21": [132, 136, 128, 128], "22": [262, 136, 128, 128], "23": [2, 6, 128, 128], "19": [132, 266, 128, 128], "18": [2, 266, 128, 128], "1": [262, 266, 128, 128], "3": [262, 6, 128, 128], "2": [132, 6, 128, 128]}, "round_shadow-0.png": {"11": [262, 266, 128, 128], "10": [132, 266, 128, 128], "13": [132, 136, 128, 128], "12": [2, 136, 128, 128], "15": [2, 6, 128, 128], "14": [262, 136, 128, 128], "17": [262, 6, 128, 128], "16": [132, 6, 128, 128], "0": [2, 266, 128, 128]}, "round_shadow-2.png": {"5": [132, 266, 128, 128], "4": [2, 266, 128, 128], "7": [2, 136, 128, 128], "6": [262, 266, 128, 128], "9": [262, 136, 128, 128], "8": [132, 136, 128, 128]}} \ No newline at end of file diff --git a/src/kivymd/label.py b/src/kivymd/label.py new file mode 100644 index 00000000..844f2a07 --- /dev/null +++ b/src/kivymd/label.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +from kivy.lang import Builder +from kivy.metrics import sp +from kivy.properties import OptionProperty, DictProperty, ListProperty +from kivy.uix.label import Label +from kivymd.material_resources import DEVICE_TYPE +from kivymd.theming import ThemableBehavior + +Builder.load_string(''' + + disabled_color: self.theme_cls.disabled_hint_text_color + text_size: (self.width, None) +''') + + +class MDLabel(ThemableBehavior, Label): + font_style = OptionProperty( + 'Body1', options=['Body1', 'Body2', 'Caption', 'Subhead', 'Title', + 'Headline', 'Display1', 'Display2', 'Display3', + 'Display4', 'Button', 'Icon']) + + # Font, Bold, Mobile size, Desktop size (None if same as Mobile) + _font_styles = DictProperty({'Body1': ['Roboto', False, 14, 13], + 'Body2': ['Roboto', True, 14, 13], + 'Caption': ['Roboto', False, 12, None], + 'Subhead': ['Roboto', False, 16, 15], + 'Title': ['Roboto', True, 20, None], + 'Headline': ['Roboto', False, 24, None], + 'Display1': ['Roboto', False, 34, None], + 'Display2': ['Roboto', False, 45, None], + 'Display3': ['Roboto', False, 56, None], + 'Display4': ['RobotoLight', False, 112, None], + 'Button': ['Roboto', True, 14, None], + 'Icon': ['Icons', False, 24, None]}) + + theme_text_color = OptionProperty(None, allownone=True, + options=['Primary', 'Secondary', 'Hint', + 'Error', 'Custom']) + + text_color = ListProperty(None, allownone=True) + + _currently_bound_property = {} + + def __init__(self, **kwargs): + super(MDLabel, self).__init__(**kwargs) + self.on_theme_text_color(None, self.theme_text_color) + self.on_font_style(None, self.font_style) + self.on_opposite_colors(None, self.opposite_colors) + + def on_font_style(self, instance, style): + info = self._font_styles[style] + self.font_name = info[0] + self.bold = info[1] + if DEVICE_TYPE == 'desktop' and info[3] is not None: + self.font_size = sp(info[3]) + else: + self.font_size = sp(info[2]) + + def on_theme_text_color(self, instance, value): + t = self.theme_cls + op = self.opposite_colors + setter = self.setter('color') + t.unbind(**self._currently_bound_property) + c = {} + if value == 'Primary': + c = {'text_color' if not op else 'opposite_text_color': setter} + t.bind(**c) + self.color = t.text_color if not op else t.opposite_text_color + elif value == 'Secondary': + c = {'secondary_text_color' if not op else + 'opposite_secondary_text_color': setter} + t.bind(**c) + self.color = t.secondary_text_color if not op else \ + t.opposite_secondary_text_color + elif value == 'Hint': + c = {'disabled_hint_text_color' if not op else + 'opposite_disabled_hint_text_color': setter} + t.bind(**c) + self.color = t.disabled_hint_text_color if not op else \ + t.opposite_disabled_hint_text_color + elif value == 'Error': + c = {'error_color': setter} + t.bind(**c) + self.color = t.error_color + elif value == 'Custom': + self.color = self.text_color if self.text_color else (0, 0, 0, 1) + self._currently_bound_property = c + + def on_text_color(self, *args): + if self.theme_text_color == 'Custom': + self.color = self.text_color + + def on_opposite_colors(self, instance, value): + self.on_theme_text_color(self, self.theme_text_color) diff --git a/src/kivymd/list.py b/src/kivymd/list.py new file mode 100644 index 00000000..36162329 --- /dev/null +++ b/src/kivymd/list.py @@ -0,0 +1,531 @@ +# -*- coding: utf-8 -*- +''' +Lists +===== + +`Material Design spec, Lists page `_ + +`Material Design spec, Lists: Controls page `_ + +The class :class:`MDList` in combination with a ListItem like +:class:`OneLineListItem` will create a list that expands as items are added to +it, working nicely with Kivy's :class:`~kivy.uix.scrollview.ScrollView`. + + +Simple examples +--------------- + +Kv Lang: + +.. code-block:: python + + ScrollView: + do_scroll_x: False # Important for MD compliance + MDList: + OneLineListItem: + text: "Single-line item" + TwoLineListItem: + text: "Two-line item" + secondary_text: "Secondary text here" + ThreeLineListItem: + text: "Three-line item" + secondary_text: "This is a multi-line label where you can fit more text than usual" + + +Python: + +.. code-block:: python + + # Sets up ScrollView with MDList, as normally used in Android: + sv = ScrollView() + ml = MDList() + sv.add_widget(ml) + + contacts = ["Paula", "John", "Kate", "Vlad"] + for c in contacts: + ml.add_widget( + OneLineListItem( + text=c + ) + ) + +Advanced usage +-------------- + +Due to the variety in sizes and controls in the MD spec, this module suffers +from a certain level of complexity to keep the widgets compliant, flexible +and performant. + +For this KivyMD provides ListItems that try to cover the most common usecases, +when those are insufficient, there's a base class called :class:`ListItem` +which you can use to create your own ListItems. This documentation will only +cover the provided ones, for custom implementations please refer to this +module's source code. + +Text only ListItems +------------------- + +- :class:`~OneLineListItem` +- :class:`~TwoLineListItem` +- :class:`~ThreeLineListItem` + +These are the simplest ones. The :attr:`~ListItem.text` attribute changes the +text in the most prominent line, while :attr:`~ListItem.secondary_text` +changes the second and third line. + +If there are only two lines, :attr:`~ListItem.secondary_text` will shorten +the text to fit in case it is too long; if a third line is available, it will +instead wrap the text to make use of it. + +ListItems with widget containers +-------------------------------- + +- :class:`~OneLineAvatarListItem` +- :class:`~TwoLineAvatarListItem` +- :class:`~ThreeLineAvatarListItem` +- :class:`~OneLineIconListItem` +- :class:`~TwoLineIconListItem` +- :class:`~ThreeLineIconListItem` +- :class:`~OneLineAvatarIconListItem` +- :class:`~TwoLineAvatarIconListItem` +- :class:`~ThreeLineAvatarIconListItem` + +These widgets will take other widgets that inherit from :class:`~ILeftBody`, +:class:`ILeftBodyTouch`, :class:`~IRightBody` or :class:`~IRightBodyTouch` and +put them in their corresponding container. + +As the name implies, :class:`~ILeftBody` and :class:`~IRightBody` will signal +that the widget goes into the left or right container, respectively. + +:class:`~ILeftBodyTouch` and :class:`~IRightBodyTouch` do the same thing, +except these widgets will also receive touch events that occur within their +surfaces. + +Python example: + +.. code-block:: python + + class ContactPhoto(ILeftBody, AsyncImage): + pass + + class MessageButton(IRightBodyTouch, MDIconButton): + phone_number = StringProperty() + + def on_release(self): + # sample code: + Dialer.send_sms(phone_number, "Hey! What's up?") + pass + + # Sets up ScrollView with MDList, as normally used in Android: + sv = ScrollView() + ml = MDList() + sv.add_widget(ml) + + contacts = [ + ["Annie", "555-24235", "http://myphotos.com/annie.png"], + ["Bob", "555-15423", "http://myphotos.com/bob.png"], + ["Claire", "555-66098", "http://myphotos.com/claire.png"] + ] + + for c in contacts: + item = TwoLineAvatarIconListItem( + text=c[0], + secondary_text=c[1] + ) + item.add_widget(ContactPhoto(source=c[2])) + item.add_widget(MessageButton(phone_number=c[1]) + ml.add_widget(item) + +API +--- +''' + +from kivy.lang import Builder +from kivy.metrics import dp +from kivy.properties import ObjectProperty, StringProperty, NumericProperty, \ + ListProperty, OptionProperty +from kivy.uix.behaviors import ButtonBehavior +from kivy.uix.floatlayout import FloatLayout +from kivy.uix.gridlayout import GridLayout +import kivymd.material_resources as m_res +from kivymd.ripplebehavior import RectangularRippleBehavior +from kivymd.theming import ThemableBehavior + +Builder.load_string(''' +#:import m_res kivymd.material_resources + + cols: 1 + size_hint_y: None + height: self._min_list_height + padding: 0, self._list_vertical_padding + + + size_hint_y: None + canvas: + Color: + rgba: self.theme_cls.divider_color + Line: + points: root.x,root.y, root.x+self.width,root.y + BoxLayout: + id: _text_container + orientation: 'vertical' + pos: root.pos + padding: root._txt_left_pad, root._txt_top_pad, root._txt_right_pad, root._txt_bot_pad + MDLabel: + id: _lbl_primary + text: root.text + font_style: root.font_style + theme_text_color: root.theme_text_color + text_color: root.text_color + size_hint_y: None + height: self.texture_size[1] + MDLabel: + id: _lbl_secondary + text: '' if root._num_lines == 1 else root.secondary_text + font_style: root.secondary_font_style + theme_text_color: root.secondary_theme_text_color + text_color: root.secondary_text_color + size_hint_y: None + height: 0 if root._num_lines == 1 else self.texture_size[1] + shorten: True if root._num_lines == 2 else False + + + BoxLayout: + id: _left_container + size_hint: None, None + x: root.x + dp(16) + y: root.y + root.height/2 - self.height/2 + size: dp(40), dp(40) + + + BoxLayout: + id: _left_container + size_hint: None, None + x: root.x + dp(16) + y: root.y + root.height - root._txt_top_pad - self.height - dp(5) + size: dp(40), dp(40) + + + BoxLayout: + id: _left_container + size_hint: None, None + x: root.x + dp(16) + y: root.y + root.height/2 - self.height/2 + size: dp(48), dp(48) + + + BoxLayout: + id: _left_container + size_hint: None, None + x: root.x + dp(16) + y: root.y + root.height - root._txt_top_pad - self.height - dp(5) + size: dp(48), dp(48) + + + BoxLayout: + id: _right_container + size_hint: None, None + x: root.x + root.width - m_res.HORIZ_MARGINS - self.width + y: root.y + root.height/2 - self.height/2 + size: dp(48), dp(48) + + + BoxLayout: + id: _right_container + size_hint: None, None + x: root.x + root.width - m_res.HORIZ_MARGINS - self.width + y: root.y + root.height/2 - self.height/2 + size: dp(48), dp(48) + + + BoxLayout: + id: _right_container + size_hint: None, None + x: root.x + root.width - m_res.HORIZ_MARGINS - self.width + y: root.y + root.height/2 - self.height/2 + size: dp(48), dp(48) + + + BoxLayout: + id: _right_container + size_hint: None, None + x: root.x + root.width - m_res.HORIZ_MARGINS - self.width + y: root.y + root.height/2 - self.height/2 + size: dp(48), dp(48) + + + BoxLayout: + id: _right_container + size_hint: None, None + x: root.x + root.width - m_res.HORIZ_MARGINS - self.width + y: root.y + root.height - root._txt_top_pad - self.height - dp(5) + size: dp(48), dp(48) +''') + + +class MDList(GridLayout): + '''ListItem container. Best used in conjunction with a + :class:`kivy.uix.ScrollView`. + + When adding (or removing) a widget, it will resize itself to fit its + children, plus top and bottom paddings as described by the MD spec. + ''' + selected = ObjectProperty() + _min_list_height = dp(16) + _list_vertical_padding = dp(8) + + icon = StringProperty() + + def add_widget(self, widget, index=0): + super(MDList, self).add_widget(widget, index) + self.height += widget.height + + def remove_widget(self, widget): + super(MDList, self).remove_widget(widget) + self.height -= widget.height + + +class BaseListItem(ThemableBehavior, RectangularRippleBehavior, + ButtonBehavior, FloatLayout): + '''Base class to all ListItems. Not supposed to be instantiated on its own. + ''' + + text = StringProperty() + '''Text shown in the first line. + + :attr:`text` is a :class:`~kivy.properties.StringProperty` and defaults + to "". + ''' + + text_color = ListProperty(None) + ''' Text color used if theme_text_color is set to 'Custom' ''' + + font_style = OptionProperty( + 'Subhead', options=['Body1', 'Body2', 'Caption', 'Subhead', 'Title', + 'Headline', 'Display1', 'Display2', 'Display3', + 'Display4', 'Button', 'Icon']) + + theme_text_color = StringProperty('Primary',allownone=True) + ''' Theme text color for primary text ''' + + secondary_text = StringProperty() + '''Text shown in the second and potentially third line. + + The text will wrap into the third line if the ListItem's type is set to + \'one-line\'. It can be forced into the third line by adding a \\n + escape sequence. + + :attr:`secondary_text` is a :class:`~kivy.properties.StringProperty` and + defaults to "". + ''' + + secondary_text_color = ListProperty(None) + ''' Text color used for secondary text if secondary_theme_text_color + is set to 'Custom' ''' + + secondary_theme_text_color = StringProperty('Secondary',allownone=True) + ''' Theme text color for secondary primary text ''' + + secondary_font_style = OptionProperty( + 'Body1', options=['Body1', 'Body2', 'Caption', 'Subhead', 'Title', + 'Headline', 'Display1', 'Display2', 'Display3', + 'Display4', 'Button', 'Icon']) + + _txt_left_pad = NumericProperty(dp(16)) + _txt_top_pad = NumericProperty() + _txt_bot_pad = NumericProperty() + _txt_right_pad = NumericProperty(m_res.HORIZ_MARGINS) + _num_lines = 2 + + +class ILeftBody: + '''Pseudo-interface for widgets that go in the left container for + ListItems that support it. + + Implements nothing and requires no implementation, for annotation only. + ''' + pass + + +class ILeftBodyTouch: + '''Same as :class:`~ILeftBody`, but allows the widget to receive touch + events instead of triggering the ListItem's ripple effect + ''' + pass + + +class IRightBody: + '''Pseudo-interface for widgets that go in the right container for + ListItems that support it. + + Implements nothing and requires no implementation, for annotation only. + ''' + pass + + +class IRightBodyTouch: + '''Same as :class:`~IRightBody`, but allows the widget to receive touch + events instead of triggering the ListItem's ripple effect + ''' + pass + + +class ContainerSupport: + '''Overrides add_widget in a ListItem to include support for I*Body + widgets when the appropiate containers are present. + ''' + _touchable_widgets = ListProperty() + + def add_widget(self, widget, index=0): + if issubclass(widget.__class__, ILeftBody): + self.ids['_left_container'].add_widget(widget) + elif issubclass(widget.__class__, ILeftBodyTouch): + self.ids['_left_container'].add_widget(widget) + self._touchable_widgets.append(widget) + elif issubclass(widget.__class__, IRightBody): + self.ids['_right_container'].add_widget(widget) + elif issubclass(widget.__class__, IRightBodyTouch): + self.ids['_right_container'].add_widget(widget) + self._touchable_widgets.append(widget) + else: + return super(BaseListItem, self).add_widget(widget,index) + + def remove_widget(self, widget): + super(BaseListItem, self).remove_widget(widget) + if widget in self._touchable_widgets: + self._touchable_widgets.remove(widget) + + def on_touch_down(self, touch): + if self.propagate_touch_to_touchable_widgets(touch, 'down'): + return + super(BaseListItem, self).on_touch_down(touch) + + def on_touch_move(self, touch, *args): + if self.propagate_touch_to_touchable_widgets(touch, 'move', *args): + return + super(BaseListItem, self).on_touch_move(touch, *args) + + def on_touch_up(self, touch): + if self.propagate_touch_to_touchable_widgets(touch, 'up'): + return + super(BaseListItem, self).on_touch_up(touch) + + def propagate_touch_to_touchable_widgets(self, touch, touch_event, *args): + triggered = False + for i in self._touchable_widgets: + if i.collide_point(touch.x, touch.y): + triggered = True + if touch_event == 'down': + i.on_touch_down(touch) + elif touch_event == 'move': + i.on_touch_move(touch, *args) + elif touch_event == 'up': + i.on_touch_up(touch) + return triggered + + +class OneLineListItem(BaseListItem): + _txt_top_pad = NumericProperty(dp(16)) + _txt_bot_pad = NumericProperty(dp(15)) # dp(20) - dp(5) + _num_lines = 1 + + def __init__(self, **kwargs): + super(OneLineListItem, self).__init__(**kwargs) + self.height = dp(48) + + +class TwoLineListItem(BaseListItem): + _txt_top_pad = NumericProperty(dp(20)) + _txt_bot_pad = NumericProperty(dp(15)) # dp(20) - dp(5) + + def __init__(self, **kwargs): + super(TwoLineListItem, self).__init__(**kwargs) + self.height = dp(72) + + +class ThreeLineListItem(BaseListItem): + _txt_top_pad = NumericProperty(dp(16)) + _txt_bot_pad = NumericProperty(dp(15)) # dp(20) - dp(5) + _num_lines = 3 + + def __init__(self, **kwargs): + super(ThreeLineListItem, self).__init__(**kwargs) + self.height = dp(88) + + +class OneLineAvatarListItem(ContainerSupport, BaseListItem): + _txt_left_pad = NumericProperty(dp(72)) + _txt_top_pad = NumericProperty(dp(20)) + _txt_bot_pad = NumericProperty(dp(19)) # dp(24) - dp(5) + _num_lines = 1 + + def __init__(self, **kwargs): + super(OneLineAvatarListItem, self).__init__(**kwargs) + self.height = dp(56) + + +class TwoLineAvatarListItem(OneLineAvatarListItem): + _txt_top_pad = NumericProperty(dp(20)) + _txt_bot_pad = NumericProperty(dp(15)) # dp(20) - dp(5) + _num_lines = 2 + + def __init__(self, **kwargs): + super(BaseListItem, self).__init__(**kwargs) + self.height = dp(72) + + +class ThreeLineAvatarListItem(ContainerSupport, ThreeLineListItem): + _txt_left_pad = NumericProperty(dp(72)) + + +class OneLineIconListItem(ContainerSupport, OneLineListItem): + _txt_left_pad = NumericProperty(dp(72)) + + +class TwoLineIconListItem(OneLineIconListItem): + _txt_top_pad = NumericProperty(dp(20)) + _txt_bot_pad = NumericProperty(dp(15)) # dp(20) - dp(5) + _num_lines = 2 + + def __init__(self, **kwargs): + super(BaseListItem, self).__init__(**kwargs) + self.height = dp(72) + + +class ThreeLineIconListItem(ContainerSupport, ThreeLineListItem): + _txt_left_pad = NumericProperty(dp(72)) + + +class OneLineRightIconListItem(ContainerSupport, OneLineListItem): + # dp(40) = dp(16) + dp(24): + _txt_right_pad = NumericProperty(dp(40) + m_res.HORIZ_MARGINS) + + +class TwoLineRightIconListItem(OneLineRightIconListItem): + _txt_top_pad = NumericProperty(dp(20)) + _txt_bot_pad = NumericProperty(dp(15)) # dp(20) - dp(5) + _num_lines = 2 + + def __init__(self, **kwargs): + super(BaseListItem, self).__init__(**kwargs) + self.height = dp(72) + + +class ThreeLineRightIconListitem(ContainerSupport, ThreeLineListItem): + # dp(40) = dp(16) + dp(24): + _txt_right_pad = NumericProperty(dp(40) + m_res.HORIZ_MARGINS) + + +class OneLineAvatarIconListItem(OneLineAvatarListItem): + # dp(40) = dp(16) + dp(24): + _txt_right_pad = NumericProperty(dp(40) + m_res.HORIZ_MARGINS) + + +class TwoLineAvatarIconListItem(TwoLineAvatarListItem): + # dp(40) = dp(16) + dp(24): + _txt_right_pad = NumericProperty(dp(40) + m_res.HORIZ_MARGINS) + + +class ThreeLineAvatarIconListItem(ThreeLineAvatarListItem): + # dp(40) = dp(16) + dp(24): + _txt_right_pad = NumericProperty(dp(40) + m_res.HORIZ_MARGINS) diff --git a/src/kivymd/material_resources.py b/src/kivymd/material_resources.py new file mode 100644 index 00000000..46270e5c --- /dev/null +++ b/src/kivymd/material_resources.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +from kivy import platform +from kivy.core.window import Window +from kivy.metrics import dp +from kivymd import fonts_path + +# Feel free to override this const if you're designing for a device such as +# a GNU/Linux tablet. +if platform != "android" and platform != "ios": + DEVICE_TYPE = "desktop" +elif Window.width >= dp(600) and Window.height >= dp(600): + DEVICE_TYPE = "tablet" +else: + DEVICE_TYPE = "mobile" + +if DEVICE_TYPE == "mobile": + MAX_NAV_DRAWER_WIDTH = dp(300) + HORIZ_MARGINS = dp(16) + STANDARD_INCREMENT = dp(56) + PORTRAIT_TOOLBAR_HEIGHT = STANDARD_INCREMENT + LANDSCAPE_TOOLBAR_HEIGHT = STANDARD_INCREMENT - dp(8) +else: + MAX_NAV_DRAWER_WIDTH = dp(400) + HORIZ_MARGINS = dp(24) + STANDARD_INCREMENT = dp(64) + PORTRAIT_TOOLBAR_HEIGHT = STANDARD_INCREMENT + LANDSCAPE_TOOLBAR_HEIGHT = STANDARD_INCREMENT + +TOUCH_TARGET_HEIGHT = dp(48) + +FONTS = [ + { + "name": "Roboto", + "fn_regular": fonts_path + 'Roboto-Regular.ttf', + "fn_bold": fonts_path + 'Roboto-Medium.ttf', + "fn_italic": fonts_path + 'Roboto-Italic.ttf', + "fn_bolditalic": fonts_path + 'Roboto-MediumItalic.ttf' + }, + { + "name": "RobotoLight", + "fn_regular": fonts_path + 'Roboto-Thin.ttf', + "fn_bold": fonts_path + 'Roboto-Light.ttf', + "fn_italic": fonts_path + 'Roboto-ThinItalic.ttf', + "fn_bolditalic": fonts_path + 'Roboto-LightItalic.ttf' + }, + { + "name": "Icons", + "fn_regular": fonts_path + 'Material-Design-Iconic-Font.ttf' + } +] diff --git a/src/kivymd/menu.py b/src/kivymd/menu.py new file mode 100644 index 00000000..f4c96ac8 --- /dev/null +++ b/src/kivymd/menu.py @@ -0,0 +1,192 @@ +# -*- coding: utf-8 -*- +from kivy.animation import Animation +from kivy.clock import Clock +from kivy.core.window import Window +from kivy.lang import Builder +from kivy.garden.recycleview import RecycleView +from kivy.metrics import dp +from kivy.properties import NumericProperty, ListProperty, OptionProperty, \ + StringProperty +from kivy.uix.behaviors import ButtonBehavior +from kivy.uix.boxlayout import BoxLayout +import kivymd.material_resources as m_res +from kivymd.theming import ThemableBehavior + +Builder.load_string(''' +#:import STD_INC kivymd.material_resources.STANDARD_INCREMENT + + size_hint_y: None + height: dp(48) + padding: dp(16), 0 + on_release: root.parent.parent.parent.parent.dismiss() # Horrible, but hey it works + MDLabel: + text: root.text + theme_text_color: 'Primary' + + + size_hint: None, None + width: root.width_mult * STD_INC + key_viewclass: 'viewclass' + key_size: 'height' + + + FloatLayout: + id: fl + MDMenu: + id: md_menu + data: root.items + width_mult: root.width_mult + size_hint: None, None + size: 0,0 + canvas.before: + Color: + rgba: root.theme_cls.bg_light + Rectangle: + size: self.size + pos: self.pos +''') + + +class MDMenuItem(ButtonBehavior, BoxLayout): + text = StringProperty() + + +class MDMenu(RecycleView): + width_mult = NumericProperty(1) + + +class MDDropdownMenu(ThemableBehavior, BoxLayout): + items = ListProperty() + '''See :attr:`~kivy.garden.recycleview.RecycleView.data` + ''' + + width_mult = NumericProperty(1) + '''This number multiplied by the standard increment (56dp on mobile, + 64dp on desktop, determines the width of the menu items. + + If the resulting number were to be too big for the application Window, + the multiplier will be adjusted for the biggest possible one. + ''' + + max_height = NumericProperty() + '''The menu will grow no bigger than this number. + + Set to 0 for no limit. Defaults to 0. + ''' + + border_margin = NumericProperty(dp(4)) + '''Margin between Window border and menu + ''' + + ver_growth = OptionProperty(None, allownone=True, + options=['up', 'down']) + '''Where the menu will grow vertically to when opening + + Set to None to let the widget pick for you. Defaults to None. + ''' + + hor_growth = OptionProperty(None, allownone=True, + options=['left', 'right']) + '''Where the menu will grow horizontally to when opening + + Set to None to let the widget pick for you. Defaults to None. + ''' + + def open(self, *largs): + Window.add_widget(self) + Clock.schedule_once(lambda x: self.display_menu(largs[0]), -1) + + def display_menu(self, caller): + # We need to pick a starting point, see how big we need to be, + # and where to grow to. + + c = caller.to_window(caller.center_x, + caller.center_y) # Starting coords + + # ---ESTABLISH INITIAL TARGET SIZE ESTIMATE--- + target_width = self.width_mult * m_res.STANDARD_INCREMENT + # If we're wider than the Window... + if target_width > Window.width: + # ...reduce our multiplier to max allowed. + target_width = int( + Window.width / m_res.STANDARD_INCREMENT) * m_res.STANDARD_INCREMENT + + target_height = sum([dp(48) for i in self.items]) + # If we're over max_height... + if 0 < self.max_height < target_height: + target_height = self.max_height + + # ---ESTABLISH VERTICAL GROWTH DIRECTION--- + if self.ver_growth is not None: + ver_growth = self.ver_growth + else: + # If there's enough space below us: + if target_height <= c[1] - self.border_margin: + ver_growth = 'down' + # if there's enough space above us: + elif target_height < Window.height - c[1] - self.border_margin: + ver_growth = 'up' + # otherwise, let's pick the one with more space and adjust ourselves + else: + # if there's more space below us: + if c[1] >= Window.height - c[1]: + ver_growth = 'down' + target_height = c[1] - self.border_margin + # if there's more space above us: + else: + ver_growth = 'up' + target_height = Window.height - c[1] - self.border_margin + + if self.hor_growth is not None: + hor_growth = self.hor_growth + else: + # If there's enough space to the right: + if target_width <= Window.width - c[0] - self.border_margin: + hor_growth = 'right' + # if there's enough space to the left: + elif target_width < c[0] - self.border_margin: + hor_growth = 'left' + # otherwise, let's pick the one with more space and adjust ourselves + else: + # if there's more space to the right: + if Window.width - c[0] >= c[0]: + hor_growth = 'right' + target_width = Window.width - c[0] - self.border_margin + # if there's more space to the left: + else: + hor_growth = 'left' + target_width = c[0] - self.border_margin + + if ver_growth == 'down': + tar_y = c[1] - target_height + else: # should always be 'up' + tar_y = c[1] + + if hor_growth == 'right': + tar_x = c[0] + else: # should always be 'left' + tar_x = c[0] - target_width + anim = Animation(x=tar_x, y=tar_y, + width=target_width, height=target_height, + duration=.3, transition='out_quint') + menu = self.ids['md_menu'] + menu.pos = c + anim.start(menu) + + def on_touch_down(self, touch): + if not self.ids['md_menu'].collide_point(*touch.pos): + self.dismiss() + return True + super(MDDropdownMenu, self).on_touch_down(touch) + return True + + def on_touch_move(self, touch): + super(MDDropdownMenu, self).on_touch_move(touch) + return True + + def on_touch_up(self, touch): + super(MDDropdownMenu, self).on_touch_up(touch) + return True + + def dismiss(self): + Window.remove_widget(self) diff --git a/src/kivymd/navigationdrawer.py b/src/kivymd/navigationdrawer.py new file mode 100644 index 00000000..42aa9a62 --- /dev/null +++ b/src/kivymd/navigationdrawer.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +from kivy.animation import Animation +from kivy.lang import Builder +from kivy.properties import StringProperty, ObjectProperty +from kivymd.elevationbehavior import ElevationBehavior +from kivymd.icon_definitions import md_icons +from kivymd.label import MDLabel +from kivymd.list import OneLineIconListItem, ILeftBody, BaseListItem +from kivymd.slidingpanel import SlidingPanel +from kivymd.theming import ThemableBehavior + +Builder.load_string(''' + + canvas: + Color: + rgba: root.theme_cls.divider_color + Line: + points: self.x, self.y, self.x+self.width,self.y + + + _list: list + elevation: 0 + canvas: + Color: + rgba: root.theme_cls.bg_light + Rectangle: + size: root.size + pos: root.pos + NavDrawerToolbar: + title: root.title + opposite_colors: False + title_theme_color: 'Secondary' + background_color: root.theme_cls.bg_light + elevation: 0 + ScrollView: + do_scroll_x: False + MDList: + id: ml + id: list + + + NDIconLabel: + id: _icon + font_style: 'Icon' + theme_text_color: 'Secondary' +''') + + +class NavigationDrawer(SlidingPanel, ThemableBehavior, ElevationBehavior): + title = StringProperty() + + _list = ObjectProperty() + + def add_widget(self, widget, index=0): + if issubclass(widget.__class__, BaseListItem): + self._list.add_widget(widget, index) + widget.bind(on_release=lambda x: self.toggle()) + else: + super(NavigationDrawer, self).add_widget(widget, index) + + def _get_main_animation(self, duration, t, x, is_closing): + a = super(NavigationDrawer, self)._get_main_animation(duration, t, x, + is_closing) + a &= Animation(elevation=0 if is_closing else 5, t=t, duration=duration) + return a + + +class NDIconLabel(ILeftBody, MDLabel): + pass + + +class NavigationDrawerIconButton(OneLineIconListItem): + icon = StringProperty() + + def on_icon(self, instance, value): + self.ids['_icon'].text = u"{}".format(md_icons[value]) diff --git a/src/kivymd/progressbar.py b/src/kivymd/progressbar.py new file mode 100644 index 00000000..6d3a2ca8 --- /dev/null +++ b/src/kivymd/progressbar.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- + +from kivy.lang import Builder +from kivy.properties import ListProperty, OptionProperty, BooleanProperty +from kivy.utils import get_color_from_hex +from kivymd.color_definitions import colors +from kivymd.theming import ThemableBehavior +from kivy.uix.progressbar import ProgressBar + + +Builder.load_string(''' +: + canvas: + Clear + Color: + rgba: self.theme_cls.divider_color + Rectangle: + size: (self.width , dp(4)) if self.orientation == 'horizontal' else (dp(4),self.height) + pos: (self.x, self.center_y - dp(4)) if self.orientation == 'horizontal' \ + else (self.center_x - dp(4),self.y) + + + Color: + rgba: self.theme_cls.primary_color + Rectangle: + size: (self.width*self.value_normalized, sp(4)) if self.orientation == 'horizontal' else (sp(4), \ + self.height*self.value_normalized) + pos: (self.width*(1-self.value_normalized)+self.x if self.reversed else self.x, self.center_y - dp(4)) \ + if self.orientation == 'horizontal' else \ + (self.center_x - dp(4),self.height*(1-self.value_normalized)+self.y if self.reversed else self.y) + +''') + + +class MDProgressBar(ThemableBehavior, ProgressBar): + reversed = BooleanProperty(False) + ''' Reverse the direction the progressbar moves. ''' + + orientation = OptionProperty('horizontal', options=['horizontal', 'vertical']) + ''' Orientation of progressbar''' + + +if __name__ == '__main__': + from kivy.app import App + from kivymd.theming import ThemeManager + + class ProgressBarApp(App): + theme_cls = ThemeManager() + + def build(self): + return Builder.load_string("""#:import MDSlider kivymd.slider.MDSlider +BoxLayout: + orientation:'vertical' + padding: '8dp' + MDSlider: + id:slider + min:0 + max:100 + value: 40 + + MDProgressBar: + value: slider.value + MDProgressBar: + reversed: True + value: slider.value + BoxLayout: + MDProgressBar: + orientation:"vertical" + reversed: True + value: slider.value + + MDProgressBar: + orientation:"vertical" + value: slider.value + +""") + + + ProgressBarApp().run() diff --git a/src/kivymd/ripplebehavior.py b/src/kivymd/ripplebehavior.py new file mode 100644 index 00000000..21dd3463 --- /dev/null +++ b/src/kivymd/ripplebehavior.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- +from kivy.properties import ListProperty, NumericProperty, StringProperty, \ + BooleanProperty +from kivy.animation import Animation +from kivy.graphics import Color, Ellipse, StencilPush, StencilPop, \ + StencilUse, StencilUnUse, Rectangle + + +class CommonRipple(object): + ripple_rad = NumericProperty() + ripple_rad_default = NumericProperty(1) + ripple_post = ListProperty() + ripple_color = ListProperty() + ripple_alpha = NumericProperty(.5) + ripple_scale = NumericProperty(None) + ripple_duration_in_fast = NumericProperty(.3) + # FIXME: These speeds should be calculated based on widget size in dp + ripple_duration_in_slow = NumericProperty(2) + ripple_duration_out = NumericProperty(.3) + ripple_func_in = StringProperty('out_quad') + ripple_func_out = StringProperty('out_quad') + + doing_ripple = BooleanProperty(False) + finishing_ripple = BooleanProperty(False) + fading_out = BooleanProperty(False) + + def on_touch_down(self, touch): + if touch.is_mouse_scrolling: + return False + if not self.collide_point(touch.x, touch.y): + return False + + if not self.disabled: + if self.doing_ripple: + Animation.cancel_all(self, 'ripple_rad', 'ripple_color', + 'rect_color') + self.anim_complete() + self.ripple_rad = self.ripple_rad_default + self.ripple_pos = (touch.x, touch.y) + + if self.ripple_color != []: + pass + elif hasattr(self, 'theme_cls'): + self.ripple_color = self.theme_cls.ripple_color + else: + # If no theme, set Grey 300 + self.ripple_color = [0.8784313725490196, 0.8784313725490196, + 0.8784313725490196, self.ripple_alpha] + self.ripple_color[3] = self.ripple_alpha + + self.lay_canvas_instructions() + self.finish_rad = max(self.width, self.height) * self.ripple_scale + self.start_ripple() + return super(CommonRipple, self).on_touch_down(touch) + + def lay_canvas_instructions(self): + raise NotImplementedError + + def on_touch_move(self, touch, *args): + if not self.collide_point(touch.x, touch.y): + if not self.finishing_ripple and self.doing_ripple: + self.finish_ripple() + return super(CommonRipple, self).on_touch_move(touch, *args) + + def on_touch_up(self, touch): + if self.collide_point(touch.x, touch.y) and self.doing_ripple: + self.finish_ripple() + return super(CommonRipple, self).on_touch_up(touch) + + def start_ripple(self): + if not self.doing_ripple: + anim = Animation( + ripple_rad=self.finish_rad, + t='linear', + duration=self.ripple_duration_in_slow) + anim.bind(on_complete=self.fade_out) + self.doing_ripple = True + anim.start(self) + + def _set_ellipse(self, instance, value): + self.ellipse.size = (self.ripple_rad, self.ripple_rad) + + # Adjust ellipse pos here + + def _set_color(self, instance, value): + self.col_instruction.a = value[3] + + def finish_ripple(self): + if self.doing_ripple and not self.finishing_ripple: + Animation.cancel_all(self, 'ripple_rad') + anim = Animation(ripple_rad=self.finish_rad, + t=self.ripple_func_in, + duration=self.ripple_duration_in_fast) + anim.bind(on_complete=self.fade_out) + self.finishing_ripple = True + anim.start(self) + + def fade_out(self, *args): + rc = self.ripple_color + if not self.fading_out: + Animation.cancel_all(self, 'ripple_color') + anim = Animation(ripple_color=[rc[0], rc[1], rc[2], 0.], + t=self.ripple_func_out, + duration=self.ripple_duration_out) + anim.bind(on_complete=self.anim_complete) + self.fading_out = True + anim.start(self) + + def anim_complete(self, *args): + self.doing_ripple = False + self.finishing_ripple = False + self.fading_out = False + self.canvas.after.clear() + + +class RectangularRippleBehavior(CommonRipple): + ripple_scale = NumericProperty(2.75) + + def lay_canvas_instructions(self): + with self.canvas.after: + StencilPush() + Rectangle(pos=self.pos, size=self.size) + StencilUse() + self.col_instruction = Color(rgba=self.ripple_color) + self.ellipse = \ + Ellipse(size=(self.ripple_rad, self.ripple_rad), + pos=(self.ripple_pos[0] - self.ripple_rad / 2., + self.ripple_pos[1] - self.ripple_rad / 2.)) + StencilUnUse() + Rectangle(pos=self.pos, size=self.size) + StencilPop() + self.bind(ripple_color=self._set_color, + ripple_rad=self._set_ellipse) + + def _set_ellipse(self, instance, value): + super(RectangularRippleBehavior, self)._set_ellipse(instance, value) + self.ellipse.pos = (self.ripple_pos[0] - self.ripple_rad / 2., + self.ripple_pos[1] - self.ripple_rad / 2.) + + +class CircularRippleBehavior(CommonRipple): + ripple_scale = NumericProperty(1) + + def lay_canvas_instructions(self): + with self.canvas.after: + StencilPush() + self.stencil = Ellipse(size=(self.width * self.ripple_scale, + self.height * self.ripple_scale), + pos=(self.center_x - ( + self.width * self.ripple_scale) / 2, + self.center_y - ( + self.height * self.ripple_scale) / 2)) + StencilUse() + self.col_instruction = Color(rgba=self.ripple_color) + self.ellipse = Ellipse(size=(self.ripple_rad, self.ripple_rad), + pos=(self.center_x - self.ripple_rad / 2., + self.center_y - self.ripple_rad / 2.)) + StencilUnUse() + Ellipse(pos=self.pos, size=self.size) + StencilPop() + self.bind(ripple_color=self._set_color, + ripple_rad=self._set_ellipse) + + def _set_ellipse(self, instance, value): + super(CircularRippleBehavior, self)._set_ellipse(instance, value) + if self.ellipse.size[0] > self.width * .6 and not self.fading_out: + self.fade_out() + self.ellipse.pos = (self.center_x - self.ripple_rad / 2., + self.center_y - self.ripple_rad / 2.) diff --git a/src/kivymd/selectioncontrols.py b/src/kivymd/selectioncontrols.py new file mode 100644 index 00000000..b918428a --- /dev/null +++ b/src/kivymd/selectioncontrols.py @@ -0,0 +1,240 @@ +# -*- coding: utf-8 -*- + +from kivy.lang import Builder +from kivy.properties import StringProperty, ListProperty, NumericProperty +from kivy.uix.behaviors import ToggleButtonBehavior +from kivy.uix.label import Label +from kivy.uix.floatlayout import FloatLayout +from kivy.properties import AliasProperty, BooleanProperty +from kivy.metrics import dp, sp +from kivy.animation import Animation +from kivy.utils import get_color_from_hex +from kivymd.color_definitions import colors +from kivymd.icon_definitions import md_icons +from kivymd.theming import ThemableBehavior +from kivymd.elevationbehavior import RoundElevationBehavior +from kivymd.ripplebehavior import CircularRippleBehavior +from kivy.uix.behaviors import ButtonBehavior +from kivy.uix.widget import Widget + +Builder.load_string(''' +: + canvas: + Clear + Color: + rgba: self.color + Rectangle: + texture: self.texture + size: self.texture_size + pos: int(self.center_x - self.texture_size[0] / 2.), int(self.center_y - self.texture_size[1] / 2.) + + text: self._radio_icon if self.group else self._checkbox_icon + font_name: 'Icons' + font_size: sp(24) + color: self.theme_cls.primary_color if self.active else self.theme_cls.secondary_text_color + halign: 'center' + valign: 'middle' + +: + color: 1, 1, 1, 1 + canvas: + Color: + rgba: self.color + Ellipse: + size: self.size + pos: self.pos + +: + canvas.before: + Color: + rgba: self._track_color_disabled if self.disabled else \ + (self._track_color_active if self.active else self._track_color_normal) + Ellipse: + size: dp(16), dp(16) + pos: self.x, self.center_y - dp(8) + angle_start: 180 + angle_end: 360 + Rectangle: + size: self.width - dp(16), dp(16) + pos: self.x + dp(8), self.center_y - dp(8) + Ellipse: + size: dp(16), dp(16) + pos: self.right - dp(16), self.center_y - dp(8) + angle_start: 0 + angle_end: 180 + on_release: thumb.trigger_action() + + Thumb: + id: thumb + size_hint: None, None + size: dp(24), dp(24) + pos: root._thumb_pos + color: root.thumb_color_disabled if root.disabled else \ + (root.thumb_color_down if root.active else root.thumb_color) + elevation: 4 if root.active else 2 + on_release: setattr(root, 'active', not root.active) +''') + + +class MDCheckbox(ThemableBehavior, CircularRippleBehavior, + ToggleButtonBehavior, Label): + active = BooleanProperty(False) + + _checkbox_icon = StringProperty( + u"{}".format(md_icons['square-o'])) + _radio_icon = StringProperty(u"{}".format(md_icons['circle-o'])) + _icon_active = StringProperty(u"{}".format(md_icons['check-square'])) + + def __init__(self, **kwargs): + super(MDCheckbox, self).__init__(**kwargs) + self.register_event_type('on_active') + self.check_anim_out = Animation(font_size=0, duration=.1, t='out_quad') + self.check_anim_in = Animation(font_size=sp(24), duration=.1, + t='out_quad') + self.check_anim_out.bind( + on_complete=lambda *x: self.check_anim_in.start(self)) + + def on_state(self, *args): + if self.state == 'down': + self.check_anim_in.cancel(self) + self.check_anim_out.start(self) + self._radio_icon = u"{}".format(md_icons['dot-circle']) + self._checkbox_icon = u"{}".format(md_icons['check-square']) + self.active = True + else: + self.check_anim_in.cancel(self) + self.check_anim_out.start(self) + self._radio_icon = u"{}".format(md_icons['circle-o']) + self._checkbox_icon = u"{}".format( + md_icons['square-o']) + self.active = False + + def on_active(self, instance, value): + self.state = 'down' if value else 'normal' + + +class Thumb(RoundElevationBehavior, CircularRippleBehavior, ButtonBehavior, + Widget): + ripple_scale = NumericProperty(2) + + def _set_ellipse(self, instance, value): + self.ellipse.size = (self.ripple_rad, self.ripple_rad) + if self.ellipse.size[0] > self.width * 1.5 and not self.fading_out: + self.fade_out() + self.ellipse.pos = (self.center_x - self.ripple_rad / 2., + self.center_y - self.ripple_rad / 2.) + self.stencil.pos = ( + self.center_x - (self.width * self.ripple_scale) / 2, + self.center_y - (self.height * self.ripple_scale) / 2) + + +class MDSwitch(ThemableBehavior, ButtonBehavior, FloatLayout): + active = BooleanProperty(False) + + _thumb_color = ListProperty(get_color_from_hex(colors['Grey']['50'])) + + def _get_thumb_color(self): + return self._thumb_color + + def _set_thumb_color(self, color, alpha=None): + if len(color) == 2: + self._thumb_color = get_color_from_hex(colors[color[0]][color[1]]) + if alpha: + self._thumb_color[3] = alpha + elif len(color) == 4: + self._thumb_color = color + + thumb_color = AliasProperty(_get_thumb_color, _set_thumb_color, + bind=['_thumb_color']) + + _thumb_color_down = ListProperty([1, 1, 1, 1]) + + def _get_thumb_color_down(self): + return self._thumb_color_down + + def _set_thumb_color_down(self, color, alpha=None): + if len(color) == 2: + self._thumb_color_down = get_color_from_hex( + colors[color[0]][color[1]]) + if alpha: + self._thumb_color_down[3] = alpha + else: + self._thumb_color_down[3] = 1 + elif len(color) == 4: + self._thumb_color_down = color + + thumb_color_down = AliasProperty(_get_thumb_color_down, + _set_thumb_color_down, + bind=['_thumb_color_down']) + + _thumb_color_disabled = ListProperty( + get_color_from_hex(colors['Grey']['400'])) + + def _get_thumb_color_disabled(self): + return self._thumb_color_disabled + + def _set_thumb_color_disabled(self, color, alpha=None): + if len(color) == 2: + self._thumb_color_disabled = get_color_from_hex( + colors[color[0]][color[1]]) + if alpha: + self._thumb_color_disabled[3] = alpha + elif len(color) == 4: + self._thumb_color_disabled = color + + thumb_color_down = AliasProperty(_get_thumb_color_disabled, + _set_thumb_color_disabled, + bind=['_thumb_color_disabled']) + + _track_color_active = ListProperty() + _track_color_normal = ListProperty() + _track_color_disabled = ListProperty() + _thumb_pos = ListProperty([0, 0]) + + def __init__(self, **kwargs): + super(MDSwitch, self).__init__(**kwargs) + self.theme_cls.bind(theme_style=self._set_colors, + primary_color=self._set_colors, + primary_palette=self._set_colors) + self._set_colors() + + def _set_colors(self, *args): + self._track_color_normal = self.theme_cls.disabled_hint_text_color + if self.theme_cls.theme_style == 'Dark': + self._track_color_active = self.theme_cls.primary_color + self._track_color_active[3] = .5 + self._track_color_disabled = get_color_from_hex('FFFFFF') + self._track_color_disabled[3] = .1 + self.thumb_color = get_color_from_hex(colors['Grey']['400']) + self.thumb_color_down = get_color_from_hex( + colors[self.theme_cls.primary_palette]['200']) + self.thumb_color_disabled = get_color_from_hex( + colors['Grey']['800']) + else: + self._track_color_active = get_color_from_hex( + colors[self.theme_cls.primary_palette]['200']) + self._track_color_active[3] = .5 + self._track_color_disabled = self.theme_cls.disabled_hint_text_color + self.thumb_color_down = self.theme_cls.primary_color + + def on_pos(self, *args): + if self.active: + self._thumb_pos = (self.right - dp(12), self.center_y - dp(12)) + else: + self._thumb_pos = (self.x - dp(12), self.center_y - dp(12)) + self.bind(active=self._update_thumb) + + def _update_thumb(self, *args): + if self.active: + Animation.cancel_all(self, '_thumb_pos') + anim = Animation( + _thumb_pos=(self.right - dp(12), self.center_y - dp(12)), + duration=.2, + t='out_quad') + else: + Animation.cancel_all(self, '_thumb_pos') + anim = Animation( + _thumb_pos=(self.x - dp(12), self.center_y - dp(12)), + duration=.2, + t='out_quad') + anim.start(self) diff --git a/src/kivymd/slider.py b/src/kivymd/slider.py new file mode 100644 index 00000000..1166bea7 --- /dev/null +++ b/src/kivymd/slider.py @@ -0,0 +1,247 @@ +# -*- coding: utf-8 -*- + +from kivy.lang import Builder +from kivy.properties import StringProperty, ListProperty, NumericProperty,AliasProperty, BooleanProperty +from kivy.utils import get_color_from_hex +from kivy.metrics import dp, sp +from kivymd.color_definitions import colors +from kivymd.theming import ThemableBehavior +from kivy.uix.slider import Slider + + +Builder.load_string(''' +#:import Thumb kivymd.selectioncontrols.Thumb + +: + id: slider + canvas: + Clear + Color: + rgba: self._track_color_disabled if self.disabled else (self._track_color_active if self.active \ + else self._track_color_normal) + Rectangle: + size: (self.width - self.padding*2 - self._offset[0], dp(4)) if self.orientation == 'horizontal' \ + else (dp(4),self.height - self.padding*2 - self._offset[1]) + pos: (self.x + self.padding + self._offset[0], self.center_y - dp(4)) \ + if self.orientation == 'horizontal' else (self.center_x - dp(4),self.y + self.padding + self._offset[1]) + + # If 0 draw circle + Color: + rgba: [0,0,0,0] if not self._is_off else (self._track_color_disabled if self.disabled \ + else (self._track_color_active if self.active else self._track_color_normal)) + Line: + width: 2 + circle: (self.x+self.padding+dp(3),self.center_y-dp(2),8 if self.active else 6 ) \ + if self.orientation == 'horizontal' else (self.center_x-dp(2),self.y+self.padding+dp(3),8 \ + if self.active else 6) + + Color: + rgba: [0,0,0,0] if self._is_off \ + else (self.thumb_color_down if not self.disabled else self._track_color_disabled) + Rectangle: + size: ((self.width-self.padding*2)*self.value_normalized, sp(4)) \ + if slider.orientation == 'horizontal' else (sp(4), (self.height-self.padding*2)*self.value_normalized) + pos: (self.x + self.padding, self.center_y - dp(4)) if self.orientation == 'horizontal' \ + else (self.center_x - dp(4),self.y + self.padding) + Thumb: + id: thumb + size_hint: None, None + size: (dp(12), dp(12)) if root.disabled else ((dp(24), dp(24)) if root.active else (dp(16),dp(16))) + pos: (slider.value_pos[0] - dp(8), slider.center_y - thumb.height/2 - dp(2)) \ + if slider.orientation == 'horizontal' \ + else (slider.center_x - thumb.width/2 - dp(2), slider.value_pos[1]-dp(8)) + color: [0,0,0,0] if slider._is_off else (root._track_color_disabled if root.disabled \ + else root.thumb_color_down) + elevation: 0 if slider._is_off else (4 if root.active else 2) + +''') + + +class MDSlider(ThemableBehavior, Slider): + # If the slider is clicked + active = BooleanProperty(False) + + # Show the "off" ring when set to minimum value + show_off = BooleanProperty(True) + + # Internal state of ring + _is_off = BooleanProperty(False) + + # Internal adjustment to reposition sliders for ring + _offset = ListProperty((0, 0)) + + _thumb_color = ListProperty(get_color_from_hex(colors['Grey']['50'])) + + def _get_thumb_color(self): + return self._thumb_color + + def _set_thumb_color(self, color, alpha=None): + if len(color) == 2: + self._thumb_color = get_color_from_hex(colors[color[0]][color[1]]) + if alpha: + self._thumb_color[3] = alpha + elif len(color) == 4: + self._thumb_color = color + + thumb_color = AliasProperty(_get_thumb_color, _set_thumb_color, + bind=['_thumb_color']) + + _thumb_color_down = ListProperty([1, 1, 1, 1]) + + def _get_thumb_color_down(self): + return self._thumb_color_down + + def _set_thumb_color_down(self, color, alpha=None): + if len(color) == 2: + self._thumb_color_down = get_color_from_hex( + colors[color[0]][color[1]]) + if alpha: + self._thumb_color_down[3] = alpha + else: + self._thumb_color_down[3] = 1 + elif len(color) == 4: + self._thumb_color_down = color + + thumb_color_down = AliasProperty(_get_thumb_color_down, + _set_thumb_color_down, + bind=['_thumb_color_down']) + + _thumb_color_disabled = ListProperty( + get_color_from_hex(colors['Grey']['400'])) + + def _get_thumb_color_disabled(self): + return self._thumb_color_disabled + + def _set_thumb_color_disabled(self, color, alpha=None): + if len(color) == 2: + self._thumb_color_disabled = get_color_from_hex( + colors[color[0]][color[1]]) + if alpha: + self._thumb_color_disabled[3] = alpha + elif len(color) == 4: + self._thumb_color_disabled = color + + thumb_color_down = AliasProperty(_get_thumb_color_disabled, + _set_thumb_color_disabled, + bind=['_thumb_color_disabled']) + + _track_color_active = ListProperty() + _track_color_normal = ListProperty() + _track_color_disabled = ListProperty() + _thumb_pos = ListProperty([0, 0]) + + def __init__(self, **kwargs): + super(MDSlider, self).__init__(**kwargs) + self.theme_cls.bind(theme_style=self._set_colors, + primary_color=self._set_colors, + primary_palette=self._set_colors) + self._set_colors() + + def _set_colors(self, *args): + if self.theme_cls.theme_style == 'Dark': + self._track_color_normal = get_color_from_hex('FFFFFF') + self._track_color_normal[3] = .3 + self._track_color_active = self._track_color_normal + self._track_color_disabled = self._track_color_normal + self.thumb_color = get_color_from_hex(colors['Grey']['400']) + self.thumb_color_down = get_color_from_hex( + colors[self.theme_cls.primary_palette]['200']) + self.thumb_color_disabled = get_color_from_hex( + colors['Grey']['800']) + else: + self._track_color_normal = get_color_from_hex('000000') + self._track_color_normal[3] = 0.26 + self._track_color_active = get_color_from_hex('000000') + self._track_color_active[3] = 0.38 + self._track_color_disabled = get_color_from_hex('000000') + self._track_color_disabled[3] = 0.26 + self.thumb_color_down = self.theme_cls.primary_color + + def on_value_normalized(self, *args): + """ When the value == min set it to "off" state and make slider a ring """ + self._update_is_off() + + def on_show_off(self, *args): + self._update_is_off() + + def _update_is_off(self): + self._is_off = self.show_off and (self.value_normalized == 0) + + def on__is_off(self, *args): + self._update_offset() + + def on_active(self, *args): + self._update_offset() + + def _update_offset(self): + """ Offset is used to shift the sliders so the background color + shows through the off circle. + """ + d = 2 if self.active else 0 + self._offset = (dp(11+d), dp(11+d)) if self._is_off else (0, 0) + + def on_touch_down(self, touch): + if super(MDSlider, self).on_touch_down(touch): + self.active = True + + def on_touch_up(self,touch): + if super(MDSlider, self).on_touch_up(touch): + self.active = False +# thumb = self.ids['thumb'] +# if thumb.collide_point(*touch.pos): +# thumb.on_touch_down(touch) +# thumb.on_touch_up(touch) + +if __name__ == '__main__': + from kivy.app import App + from kivymd.theming import ThemeManager + + class SliderApp(App): + theme_cls = ThemeManager() + + def build(self): + return Builder.load_string(""" +BoxLayout: + orientation:'vertical' + BoxLayout: + size_hint_y:None + height: '48dp' + Label: + text:"Toggle disabled" + color: [0,0,0,1] + CheckBox: + on_press: slider.disabled = not slider.disabled + BoxLayout: + size_hint_y:None + height: '48dp' + Label: + text:"Toggle active" + color: [0,0,0,1] + CheckBox: + on_press: slider.active = not slider.active + BoxLayout: + size_hint_y:None + height: '48dp' + Label: + text:"Toggle show off" + color: [0,0,0,1] + CheckBox: + on_press: slider.show_off = not slider.show_off + + MDSlider: + id:slider + min:0 + max:100 + value: 40 + + MDSlider: + id:slider2 + orientation:"vertical" + min:0 + max:100 + value: 40 + +""") + + + SliderApp().run() diff --git a/src/kivymd/slidingpanel.py b/src/kivymd/slidingpanel.py new file mode 100644 index 00000000..b818505a --- /dev/null +++ b/src/kivymd/slidingpanel.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +from kivy.animation import Animation +from kivy.clock import Clock +from kivy.core.window import Window +from kivy.lang import Builder +from kivy.metrics import dp +from kivy.properties import OptionProperty, NumericProperty, StringProperty, \ + BooleanProperty, ListProperty +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.relativelayout import RelativeLayout + +Builder.load_string(""" +#: import Window kivy.core.window.Window + + orientation: 'vertical' + size_hint_x: None + width: dp(320) + x: -1 * self.width if self.side == 'left' else Window.width + + + canvas: + Color: + rgba: root.color + Rectangle: + size: root.size +""") + + +class PanelShadow(BoxLayout): + color = ListProperty([0, 0, 0, 0]) + + +class SlidingPanel(BoxLayout): + anim_length_close = NumericProperty(0.3) + anim_length_open = NumericProperty(0.3) + animation_t_open = StringProperty('out_sine') + animation_t_close = StringProperty('out_sine') + side = OptionProperty('left', options=['left', 'right']) + + _open = False + + def __init__(self, **kwargs): + super(SlidingPanel, self).__init__(**kwargs) + self.shadow = PanelShadow() + Clock.schedule_once(lambda x: Window.add_widget(self.shadow,89), 0) + Clock.schedule_once(lambda x: Window.add_widget(self,90), 0) + + def toggle(self): + Animation.stop_all(self, 'x') + Animation.stop_all(self.shadow, 'color') + if self._open: + if self.side == 'left': + target_x = -1 * self.width + else: + target_x = Window.width + + sh_anim = Animation(duration=self.anim_length_open, + t=self.animation_t_open, + color=[0, 0, 0, 0]) + sh_anim.start(self.shadow) + self._get_main_animation(duration=self.anim_length_close, + t=self.animation_t_close, + x=target_x, + is_closing=True).start(self) + self._open = False + else: + if self.side == 'left': + target_x = 0 + else: + target_x = Window.width - self.width + Animation(duration=self.anim_length_open, t=self.animation_t_open, + color=[0, 0, 0, 0.5]).start(self.shadow) + self._get_main_animation(duration=self.anim_length_open, + t=self.animation_t_open, + x=target_x, + is_closing=False).start(self) + self._open = True + + def _get_main_animation(self, duration, t, x, is_closing): + return Animation(duration=duration, t=t, x=x) + + def on_touch_down(self, touch): + # Prevents touch events from propagating to anything below the widget. + super(SlidingPanel, self).on_touch_down(touch) + if self.collide_point(*touch.pos) or self._open: + return True + + def on_touch_up(self, touch): + if not self.collide_point(touch.x, touch.y) and self._open: + self.toggle() + return True + super(SlidingPanel, self).on_touch_up(touch) diff --git a/src/kivymd/snackbar.py b/src/kivymd/snackbar.py new file mode 100644 index 00000000..e0ac70e8 --- /dev/null +++ b/src/kivymd/snackbar.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +from collections import deque +from kivy.animation import Animation +from kivy.clock import Clock +from kivy.core.window import Window +from kivy.lang import Builder +from kivy.metrics import dp +from kivy.properties import ObjectProperty, StringProperty, NumericProperty +from kivy.uix.relativelayout import RelativeLayout +from kivymd.material_resources import DEVICE_TYPE + +Builder.load_string(''' +#:import Window kivy.core.window.Window +#:import get_color_from_hex kivy.utils.get_color_from_hex +#:import MDFlatButton kivymd.button.MDFlatButton +#:import MDLabel kivymd.label.MDLabel +#:import DEVICE_TYPE kivymd.material_resources.DEVICE_TYPE +<_SnackbarWidget> + canvas: + Color: + rgb: get_color_from_hex('323232') + Rectangle: + size: self.size + size_hint_y: None + size_hint_x: 1 if DEVICE_TYPE == 'mobile' else None + height: dp(48) if _label.texture_size[1] < dp(30) else dp(80) + width: dp(24) + _label.width + _spacer.width + root.padding_right if root.button_text == '' else dp(24) + \ + _label.width + _spacer.width + _button.width + root.padding_right + top: 0 + x: 0 if DEVICE_TYPE == 'mobile' else Window.width/2 - self.width/2 + BoxLayout: + width: Window.width - root.padding_right - _spacer.width - dp(24) if DEVICE_TYPE == 'mobile' and \ + root.button_text == '' else Window.width - root.padding_right - _button.width - _spacer.width - dp(24) \ + if DEVICE_TYPE == 'mobile' else _label.texture_size[0] if (dp(568) - root.padding_right - _button.width - \ + _spacer.width - _label.texture_size[0] - dp(24)) >= 0 else (dp(568) - root.padding_right - _button.width - \ + _spacer.width - dp(24)) + size_hint_x: None + x: dp(24) + MDLabel: + id: _label + text: root.text + size: self.texture_size + BoxLayout: + id: _spacer + size_hint_x: None + x: _label.right + width: 0 + MDFlatButton: + id: _button + text: root.button_text + size_hint_x: None + x: _spacer.right if root.button_text != '' else root.right + center_y: root.height/2 + on_release: root.button_callback() +''') + + +class _SnackbarWidget(RelativeLayout): + text = StringProperty() + button_text = StringProperty() + button_callback = ObjectProperty() + duration = NumericProperty() + padding_right = NumericProperty(dp(24)) + + def __init__(self, text, duration, button_text='', button_callback=None, + **kwargs): + super(_SnackbarWidget, self).__init__(**kwargs) + self.text = text + self.button_text = button_text + self.button_callback = button_callback + self.duration = duration + self.ids['_label'].text_size = (None, None) + + def begin(self): + if self.button_text == '': + self.remove_widget(self.ids['_button']) + else: + self.ids['_spacer'].width = dp(16) if \ + DEVICE_TYPE == "mobile" else dp(40) + self.padding_right = dp(16) + Window.add_widget(self) + anim = Animation(y=0, duration=.3, t='out_quad') + anim.start(self) + Clock.schedule_once(lambda dt: self.die(), self.duration) + + def die(self): + anim = Animation(top=0, duration=.3, t='out_quad') + anim.bind(on_complete=lambda *args: _play_next(self)) + anim.bind(on_complete=lambda *args: Window.remove_widget(self)) + anim.start(self) + + +queue = deque() +playing = False + + +def make(text, button_text=None, button_callback=None, duration=3): + if button_text is not None and button_callback is not None: + queue.append(_SnackbarWidget(text=text, + button_text=button_text, + button_callback=button_callback, + duration=duration)) + else: + queue.append(_SnackbarWidget(text=text, + duration=duration)) + _play_next() + + +def _play_next(dying_widget=None): + global playing + if (dying_widget or not playing) and len(queue) > 0: + playing = True + queue.popleft().begin() + elif len(queue) == 0: + playing = False diff --git a/src/kivymd/spinner.py b/src/kivymd/spinner.py new file mode 100644 index 00000000..238062db --- /dev/null +++ b/src/kivymd/spinner.py @@ -0,0 +1,149 @@ +# -*- coding: utf-8 -*- + +from kivy.lang import Builder +from kivy.uix.widget import Widget +from kivy.properties import NumericProperty, ListProperty, BooleanProperty +from kivy.animation import Animation +from kivymd.theming import ThemableBehavior +from kivy.clock import Clock + +Builder.load_string(''' +: + canvas.before: + PushMatrix + Rotate: + angle: self._rotation_angle + origin: self.center + canvas: + Color: + rgba: self.color + a: self._alpha + Line: + circle: self.center_x, self.center_y, self.width / 2, \ + self._angle_start, self._angle_end + cap: 'square' + width: dp(2) + canvas.after: + PopMatrix + +''') + + +class MDSpinner(ThemableBehavior, Widget): + """:class:`MDSpinner` is an implementation of the circular progress + indicator in Google's Material Design. + + It can be used either as an indeterminate indicator that loops while + the user waits for something to happen, or as a determinate indicator. + + Set :attr:`determinate` to **True** to activate determinate mode, and + :attr:`determinate_time` to set the duration of the animation. + """ + + determinate = BooleanProperty(False) + """:attr:`determinate` is a :class:`~kivy.properties.BooleanProperty` and + defaults to False + """ + + determinate_time = NumericProperty(2) + """:attr:`determinate_time` is a :class:`~kivy.properties.NumericProperty` + and defaults to 2 + """ + + active = BooleanProperty(True) + """Use :attr:`active` to start or stop the spinner. + + :attr:`active` is a :class:`~kivy.properties.BooleanProperty` and + defaults to True + """ + + color = ListProperty([]) + """:attr:`color` is a :class:`~kivy.properties.ListProperty` and + defaults to 'self.theme_cls.primary_color' + """ + + _alpha = NumericProperty(0) + _rotation_angle = NumericProperty(360) + _angle_start = NumericProperty(0) + _angle_end = NumericProperty(8) + + def __init__(self, **kwargs): + super(MDSpinner, self).__init__(**kwargs) + Clock.schedule_interval(self._update_color, 5) + self.color = self.theme_cls.primary_color + self._alpha_anim_in = Animation(_alpha=1, duration=.8, t='out_quad') + self._alpha_anim_out = Animation(_alpha=0, duration=.3, t='out_quad') + self._alpha_anim_out.bind(on_complete=self._reset) + + if self.determinate: + self._start_determinate() + else: + self._start_loop() + + def _update_color(self, *args): + self.color = self.theme_cls.primary_color + + def _start_determinate(self, *args): + self._alpha_anim_in.start(self) + + _rot_anim = Animation(_rotation_angle=0, + duration=self.determinate_time * .7, + t='out_quad') + _rot_anim.start(self) + + _angle_start_anim = Animation(_angle_end=360, + duration=self.determinate_time, + t='in_out_quad') + _angle_start_anim.bind(on_complete=lambda *x: \ + self._alpha_anim_out.start(self)) + + _angle_start_anim.start(self) + + def _start_loop(self, *args): + if self._alpha == 0: + _rot_anim = Animation(_rotation_angle=0, + duration=2, + t='linear') + _rot_anim.start(self) + + self._alpha = 1 + self._alpha_anim_in.start(self) + _angle_start_anim = Animation(_angle_end=self._angle_end + 270, + duration=.6, + t='in_out_cubic') + _angle_start_anim.bind(on_complete=self._anim_back) + _angle_start_anim.start(self) + + def _anim_back(self, *args): + _angle_back_anim = Animation(_angle_start=self._angle_end - 8, + duration=.6, + t='in_out_cubic') + _angle_back_anim.bind(on_complete=self._start_loop) + + _angle_back_anim.start(self) + + def on__rotation_angle(self, *args): + if self._rotation_angle == 0: + self._rotation_angle = 360 + if not self.determinate: + _rot_anim = Animation(_rotation_angle=0, + duration=2) + _rot_anim.start(self) + + def _reset(self, *args): + Animation.cancel_all(self, '_angle_start', '_rotation_angle', + '_angle_end', '_alpha') + self._angle_start = 0 + self._angle_end = 8 + self._rotation_angle = 360 + self._alpha = 0 + self.active = False + + def on_active(self, *args): + if not self.active: + self._reset() + else: + if self.determinate: + self._start_determinate() + else: + self._start_loop() diff --git a/src/kivymd/tabs.py b/src/kivymd/tabs.py new file mode 100644 index 00000000..c09f21c2 --- /dev/null +++ b/src/kivymd/tabs.py @@ -0,0 +1,303 @@ +# Created on Jul 8, 2016 +# +# The default kivy tab implementation seems like a stupid design to me. The +# ScreenManager is much better. +# +# @author: jrm + +from kivy.properties import StringProperty, DictProperty, ListProperty, \ + ObjectProperty, OptionProperty, BoundedNumericProperty +from kivy.uix.screenmanager import Screen +from kivy.lang import Builder +from kivy.metrics import dp +from kivy.uix.boxlayout import BoxLayout +from kivymd.theming import ThemableBehavior +from kivymd.backgroundcolorbehavior import BackgroundColorBehavior +from kivymd.button import MDFlatButton + +Builder.load_string(""" +: + id: panel + orientation: 'vertical' if panel.tab_orientation in ['top','bottom'] else 'horizontal' + ScrollView: + id: scroll_view + size_hint_y: None + height: panel._tab_display_height[panel.tab_display_mode] + MDTabBar: + id: tab_bar + size_hint_y: None + height: panel._tab_display_height[panel.tab_display_mode] + background_color: panel.tab_color or panel.theme_cls.primary_color + canvas: + # Draw bottom border + Color: + rgba: (panel.tab_border_color or panel.tab_color or panel.theme_cls.primary_dark) + Rectangle: + size: (self.width,dp(2)) + ScreenManager: + id: tab_manager + current: root.current + screens: root.tabs + + +: + canvas: + Color: + rgba: self.panel.tab_color or self.panel.theme_cls.primary_color + Rectangle: + size: self.size + pos: self.pos + + # Draw indicator + Color: + rgba: (self.panel.tab_indicator_color or self.panel.theme_cls.accent_color) if self.tab \ + and self.tab.manager and self.tab.manager.current==self.tab.name else (self.panel.tab_border_color \ + or self.panel.tab_color or self.panel.theme_cls.primary_dark) + Rectangle: + size: (self.width,dp(2)) + pos: self.pos + + size_hint: (None,None) #(1, None) if self.panel.tab_width_mode=='fixed' else (None,None) + width: (_label.texture_size[0] + dp(16)) + padding: (dp(12), 0) + theme_text_color: 'Custom' + text_color: (self.panel.tab_text_color_active or app.theme_cls.bg_light if app.theme_cls.theme_style == "Light" \ + else app.theme_cls.opposite_bg_light) if self.tab and self.tab.manager \ + and self.tab.manager.current==self.tab.name else (self.panel.tab_text_color \ + or self.panel.theme_cls.primary_light) + on_press: + self.tab.dispatch('on_tab_press') + # self.tab.manager.current = self.tab.name + on_release: self.tab.dispatch('on_tab_release') + on_touch_down: self.tab.dispatch('on_tab_touch_down',*args) + on_touch_move: self.tab.dispatch('on_tab_touch_move',*args) + on_touch_up: self.tab.dispatch('on_tab_touch_up',*args) + + + MDLabel: + id: _label + text: root.tab.text if root.panel.tab_display_mode == 'text' else u"{}".format(md_icons[root.tab.icon]) + font_style: 'Button' if root.panel.tab_display_mode == 'text' else 'Icon' + size_hint_x: None# if root.panel.tab_width_mode=='fixed' else 1 + text_size: (None, root.height) + height: self.texture_size[1] + theme_text_color: root.theme_text_color + text_color: root.text_color + valign: 'middle' + halign: 'center' + opposite_colors: root.opposite_colors +""") + + +class MDTabBar(ThemableBehavior, BackgroundColorBehavior, BoxLayout): + pass + + +class MDTabHeader(MDFlatButton): + """ Internal widget for headers based on MDFlatButton""" + + width = BoundedNumericProperty(dp(None), min=dp(72), max=dp(264), errorhandler=lambda x: dp(72)) + tab = ObjectProperty(None) + panel = ObjectProperty(None) + + +class MDTab(Screen): + """ A tab is simply a screen with meta information + that defines the content that goes in the tab header. + """ + __events__ = ('on_tab_touch_down', 'on_tab_touch_move', 'on_tab_touch_up', 'on_tab_press', 'on_tab_release') + + # Tab header text + text = StringProperty("") + + # Tab header icon + icon = StringProperty("circle") + + # Tab dropdown menu items + menu_items = ListProperty() + + # Tab dropdown menu (if you want to customize it) + menu = ObjectProperty(None) + + def __init__(self, **kwargs): + super(MDTab, self).__init__(**kwargs) + self.index = 0 + self.parent_widget = None + self.register_event_type('on_tab_touch_down') + self.register_event_type('on_tab_touch_move') + self.register_event_type('on_tab_touch_up') + self.register_event_type('on_tab_press') + self.register_event_type('on_tab_release') + + def on_leave(self, *args): + self.parent_widget.ids.tab_manager.transition.direction = self.parent_widget.prev_dir + + def on_tab_touch_down(self, *args): + pass + + def on_tab_touch_move(self, *args): + pass + + def on_tab_touch_up(self, *args): + pass + + def on_tab_press(self, *args): + par = self.parent_widget + if par.previous_tab is not self: + par.prev_dir = str(par.ids.tab_manager.transition.direction) + if par.previous_tab.index > self.index: + par.ids.tab_manager.transition.direction = "right" + elif par.previous_tab.index < self.index: + par.ids.tab_manager.transition.direction = "left" + par.ids.tab_manager.current = self.name + par.previous_tab = self + + def on_tab_release(self, *args): + pass + + def __repr__(self): + return "".format(self.name, self.text) + + +class MDTabbedPanel(ThemableBehavior, BackgroundColorBehavior, BoxLayout): + """ A tab panel that is implemented by delegating all tabs + to a ScreenManager. + """ + # If tabs should fill space + tab_width_mode = OptionProperty('stacked', options=['stacked', 'fixed']) + + # Where the tabs go + tab_orientation = OptionProperty('top', options=['top']) # ,'left','bottom','right']) + + # How tabs are displayed + tab_display_mode = OptionProperty('text', options=['text', 'icons']) # ,'both']) + _tab_display_height = DictProperty({'text': dp(46), 'icons': dp(46), 'both': dp(72)}) + + # Tab background color (leave empty for theme color) + tab_color = ListProperty([]) + + # Tab text color in normal state (leave empty for theme color) + tab_text_color = ListProperty([]) + + # Tab text color in active state (leave empty for theme color) + tab_text_color_active = ListProperty([]) + + # Tab indicator color (leave empty for theme color) + tab_indicator_color = ListProperty([]) + + # Tab bar bottom border color (leave empty for theme color) + tab_border_color = ListProperty([]) + + # List of all the tabs so you can dynamically change them + tabs = ListProperty([]) + + # Current tab name + current = StringProperty(None) + + def __init__(self, **kwargs): + super(MDTabbedPanel, self).__init__(**kwargs) + self.previous_tab = None + self.prev_dir = None + self.index = 0 + self._refresh_tabs() + + def on_tab_width_mode(self, *args): + self._refresh_tabs() + + def on_tab_display_mode(self, *args): + self._refresh_tabs() + + def _refresh_tabs(self): + """ Refresh all tabs """ + # if fixed width, use a box layout + if not self.ids: + return + tab_bar = self.ids.tab_bar + tab_bar.clear_widgets() + tab_manager = self.ids.tab_manager + for tab in tab_manager.screens: + tab_header = MDTabHeader(tab=tab, + panel=self, + height=tab_bar.height, + ) + tab_bar.add_widget(tab_header) + + def add_widget(self, widget, **kwargs): + """ Add tabs to the screen or the layout. + :param widget: The widget to add. + """ + d = {} + if isinstance(widget, MDTab): + self.index += 1 + if self.index == 1: + self.previous_tab = widget + widget.index = self.index + widget.parent_widget = self + self.ids.tab_manager.add_widget(widget) + self._refresh_tabs() + else: + super(MDTabbedPanel, self).add_widget(widget) + + def remove_widget(self, widget): + """ Remove tabs from the screen or the layout. + :param widget: The widget to remove. + """ + self.index -= 1 + if isinstance(widget, MDTab): + self.ids.tab_manager.remove_widget(widget) + self._refresh_tabs() + else: + super(MDTabbedPanel, self).remove_widget(widget) + + +if __name__ == '__main__': + from kivy.app import App + from kivymd.theming import ThemeManager + + class TabsApp(App): + theme_cls = ThemeManager() + + def build(self): + from kivy.core.window import Window + Window.size = (540, 720) + # self.theme_cls.theme_style = 'Dark' + + return Builder.load_string(""" +#:import Toolbar kivymd.toolbar.Toolbar +BoxLayout: + orientation:'vertical' + Toolbar: + id: toolbar + title: 'Page title' + background_color: app.theme_cls.primary_color + left_action_items: [['menu', lambda x: '']] + right_action_items: [['search', lambda x: ''],['more-vert',lambda x:'']] + MDTabbedPanel: + id: tab_mgr + tab_display_mode:'icons' + + MDTab: + name: 'music' + text: "Music" # Why are these not set!!! + icon: "playlist-audio" + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: "Here is my music list :)" + halign: 'center' + MDTab: + name: 'movies' + text: 'Movies' + icon: "movie" + + MDLabel: + font_style: 'Body1' + theme_text_color: 'Primary' + text: "Show movies here :)" + halign: 'center' + + +""") + + + TabsApp().run() diff --git a/src/kivymd/textfields.py b/src/kivymd/textfields.py new file mode 100644 index 00000000..18de10e6 --- /dev/null +++ b/src/kivymd/textfields.py @@ -0,0 +1,215 @@ +# -*- coding: utf-8 -*- + +from kivy.lang import Builder +from kivy.uix.textinput import TextInput +from kivy.properties import ObjectProperty, NumericProperty, StringProperty, \ + ListProperty, BooleanProperty +from kivy.metrics import sp, dp +from kivy.animation import Animation +from kivymd.label import MDLabel +from kivymd.theming import ThemableBehavior +from kivy.clock import Clock + +Builder.load_string(''' +: + canvas.before: + Clear + Color: + rgba: self.line_color_normal + Line: + id: "the_line" + points: self.x, self.y + dp(8), self.x + self.width, self.y + dp(8) + width: 1 + dash_length: dp(3) + dash_offset: 2 if self.disabled else 0 + Color: + rgba: self._current_line_color + Rectangle: + size: self._line_width, dp(2) + pos: self.center_x - (self._line_width / 2), self.y + dp(8) + Color: + rgba: self._current_error_color + Rectangle: + texture: self._msg_lbl.texture + size: self._msg_lbl.texture_size + pos: self.x, self.y - dp(8) + Color: + rgba: (self._current_line_color if self.focus and not self.cursor_blink \ + else (0, 0, 0, 0)) + Rectangle: + pos: [int(x) for x in self.cursor_pos] + size: 1, -self.line_height + Color: + #rgba: self._hint_txt_color if not self.text and not self.focus\ + #else (self.line_color_focus if not self.text or self.focus\ + #else self.line_color_normal) + rgba: self._current_hint_text_color + Rectangle: + texture: self._hint_lbl.texture + size: self._hint_lbl.texture_size + pos: self.x, self.y + self._hint_y + Color: + rgba: self.disabled_foreground_color if self.disabled else \ + (self.hint_text_color if not self.text and not self.focus else \ + self.foreground_color) + + font_name: 'Roboto' + foreground_color: app.theme_cls.text_color + font_size: sp(16) + bold: False + padding: 0, dp(16), 0, dp(10) + multiline: False + size_hint_y: None + height: dp(48) +''') + + +class SingleLineTextField(ThemableBehavior, TextInput): + line_color_normal = ListProperty() + line_color_focus = ListProperty() + error_color = ListProperty() + error = BooleanProperty(False) + message = StringProperty("") + message_mode = StringProperty("none") + mode = message_mode + + _hint_txt_color = ListProperty() + _hint_lbl = ObjectProperty() + _hint_lbl_font_size = NumericProperty(sp(16)) + _hint_y = NumericProperty(dp(10)) + _error_label = ObjectProperty() + _line_width = NumericProperty(0) + _hint_txt = StringProperty('') + _current_line_color = line_color_focus + _current_error_color = ListProperty([0.0, 0.0, 0.0, 0.0]) + _current_hint_text_color = _hint_txt_color + + def __init__(self, **kwargs): + Clock.schedule_interval(self._update_color, 5) + self._msg_lbl = MDLabel(font_style='Caption', + theme_text_color='Error', + halign='left', + valign='middle', + text=self.message) + + self._hint_lbl = MDLabel(font_style='Subhead', + halign='left', + valign='middle') + super(SingleLineTextField, self).__init__(**kwargs) + self.line_color_normal = self.theme_cls.divider_color + self.line_color_focus = list(self.theme_cls.primary_color) + self.base_line_color_focus = list(self.theme_cls.primary_color) + self.error_color = self.theme_cls.error_color + + self._hint_txt_color = self.theme_cls.disabled_hint_text_color + self.hint_text_color = (1, 1, 1, 0) + self.cursor_color = self.theme_cls.primary_color + self.bind(message=self._set_msg, + hint_text=self._set_hint, + _hint_lbl_font_size=self._hint_lbl.setter('font_size'), + message_mode=self._set_mode) + self.hint_anim_in = Animation(_hint_y=dp(34), + _hint_lbl_font_size=sp(12), duration=.2, + t='out_quad') + + self.hint_anim_out = Animation(_hint_y=dp(10), + _hint_lbl_font_size=sp(16), duration=.2, + t='out_quad') + + def _update_color(self, *args): + self.line_color_normal = self.theme_cls.divider_color + self.base_line_color_focus = list(self.theme_cls.primary_color) + if not self.focus and not self.error: + self.line_color_focus = self.theme_cls.primary_color + Animation(duration=.2, _current_hint_text_color=self.theme_cls.disabled_hint_text_color).start(self) + if self.mode == "persistent": + Animation(duration=.1, _current_error_color=self.theme_cls.disabled_hint_text_color).start(self) + if self.focus and not self.error: + self.cursor_color = self.theme_cls.primary_color + + def on_hint_text_color(self, instance, color): + self._hint_txt_color = self.theme_cls.disabled_hint_text_color + self.hint_text_color = (1, 1, 1, 0) + + def on_width(self, instance, width): + if self.focus and instance is not None or self.error and instance is not None: + self._line_width = width + self.anim = Animation(_line_width=width, duration=.2, t='out_quad') + self._msg_lbl.width = self.width + self._hint_lbl.width = self.width + + def on_pos(self, *args): + self.hint_anim_in = Animation(_hint_y=dp(34), + _hint_lbl_font_size=sp(12), duration=.2, + t='out_quad') + self.hint_anim_out = Animation(_hint_y=dp(10), + _hint_lbl_font_size=sp(16), duration=.2, + t='out_quad') + + def on_focus(self, *args): + if self.focus: + Animation.cancel_all(self, '_line_width', '_hint_y', + '_hint_lbl_font_size') + if len(self.text) == 0: + self.hint_anim_in.start(self) + if self.error: + Animation(duration=.2, _current_hint_text_color=self.error_color).start(self) + if self.mode == "on_error": + Animation(duration=.2, _current_error_color=self.error_color).start(self) + elif self.mode == "persistent": + Animation(duration=.2, _current_error_color=self.theme_cls.disabled_hint_text_color).start(self) + elif self.mode == "on_focus": + Animation(duration=.2, _current_error_color=self.theme_cls.disabled_hint_text_color).start(self) + else: + pass + elif not self.error: + self.on_width(None, self.width) + self.anim.start(self) + Animation(duration=.2, _current_hint_text_color=self.line_color_focus).start(self) + if self.mode == "on_error": + Animation(duration=.2, _current_error_color=(0, 0, 0, 0)).start(self) + if self.mode == "persistent": + Animation(duration=.2, _current_error_color=self.theme_cls.disabled_hint_text_color).start(self) + elif self.mode == "on_focus": + Animation(duration=.2, _current_error_color=self.theme_cls.disabled_hint_text_color).start(self) + else: + pass + else: + Animation.cancel_all(self, '_line_width', '_hint_y', + '_hint_lbl_font_size') + if len(self.text) == 0: + self.hint_anim_out.start(self) + if not self.error: + self.line_color_focus = self.base_line_color_focus + Animation(duration=.2, _current_line_color=self.line_color_focus, + _current_hint_text_color=self.theme_cls.disabled_hint_text_color).start(self) + if self.mode == "on_error": + Animation(duration=.2, _current_error_color=(0, 0, 0, 0)).start(self) + elif self.mode == "persistent": + Animation(duration=.2, _current_error_color=self.theme_cls.disabled_hint_text_color).start(self) + elif self.mode == "on_focus": + Animation(duration=.2, _current_error_color=(0, 0, 0, 0)).start(self) + + self.on_width(None, 0) + self.anim.start(self) + elif self.error: + Animation(duration=.2, _current_line_color=self.error_color, + _current_hint_text_color=self.error_color).start(self) + if self.mode == "on_error": + Animation(duration=.2, _current_error_color=self.error_color).start(self) + elif self.mode == "persistent": + Animation(duration=.2, _current_error_color=self.theme_cls.disabled_hint_text_color).start(self) + elif self.mode == "on_focus": + Animation(duration=.2, _current_error_color=(0, 0, 0, 0)).start(self) + + def _set_hint(self, instance, text): + self._hint_lbl.text = text + + def _set_msg(self, instance, text): + self._msg_lbl.text = text + self.message = text + + def _set_mode(self, instance, text): + self.mode = text + if self.mode == "persistent": + Animation(duration=.1, _current_error_color=self.theme_cls.disabled_hint_text_color).start(self) diff --git a/src/kivymd/theme_picker.py b/src/kivymd/theme_picker.py new file mode 100644 index 00000000..e5104ce6 --- /dev/null +++ b/src/kivymd/theme_picker.py @@ -0,0 +1,422 @@ +# -*- coding: utf-8 -*- + +from kivy.lang import Builder +from kivy.uix.modalview import ModalView +from kivy.uix.floatlayout import FloatLayout +from kivy.uix.boxlayout import BoxLayout +from kivymd.button import MDFlatButton, MDIconButton +from kivymd.theming import ThemableBehavior +from kivymd.elevationbehavior import ElevationBehavior +from kivy.properties import ObjectProperty, ListProperty +from kivymd.label import MDLabel +from kivy.metrics import dp +from kivy.utils import get_color_from_hex +from kivymd.color_definitions import colors + +Builder.load_string(""" +#:import SingleLineTextField kivymd.textfields.SingleLineTextField +#:import MDTabbedPanel kivymd.tabs.MDTabbedPanel +#:import MDTab kivymd.tabs.MDTab +: + size_hint: (None, None) + size: dp(260), dp(120)+dp(290) + pos_hint: {'center_x': .5, 'center_y': .5} + canvas: + Color: + rgb: app.theme_cls.primary_color + Rectangle: + size: dp(260), dp(120) + pos: root.pos[0], root.pos[1] + root.height-dp(120) + Color: + rgb: app.theme_cls.bg_normal + Rectangle: + size: dp(260), dp(290) + pos: root.pos[0], root.pos[1] + root.height-(dp(120)+dp(290)) + + MDFlatButton: + pos: root.pos[0]+root.size[0]-dp(72), root.pos[1] + dp(10) + text: "Close" + on_release: root.dismiss() + MDLabel: + font_style: "Headline" + text: "Change theme" + size_hint: (None, None) + size: dp(160), dp(50) + pos_hint: {'center_x': 0.5, 'center_y': 0.9} + MDTabbedPanel: + size_hint: (None, None) + size: dp(260), root.height-dp(135) + pos_hint: {'center_x': 0.5, 'center_y': 0.475} + id: tab_panel + tab_display_mode:'text' + + MDTab: + name: 'color' + text: "Theme Color" + BoxLayout: + spacing: dp(4) + size_hint: (None, None) + size: dp(270), root.height # -dp(120) + pos_hint: {'center_x': 0.532, 'center_y': 0.89} + orientation: 'vertical' + BoxLayout: + size_hint: (None, None) + pos_hint: {'center_x': 0.5, 'center_y': 0.5} + size: dp(230), dp(40) + pos: self.pos + halign: 'center' + orientation: 'horizontal' + BoxLayout: + MDIconButton: + size: dp(40), dp(40) + pos: self.pos + size_hint: (None, None) + canvas: + Color: + rgba: root.rgb_hex('Red') + Ellipse: + size: self.size + pos: self.pos + on_release: app.theme_cls.primary_palette = 'Red' + BoxLayout: + MDIconButton: + size: dp(40), dp(40) + pos: self.pos + size_hint: (None, None) + canvas: + Color: + rgba: root.rgb_hex('Pink') + Ellipse: + size: self.size + pos: self.pos + on_release: app.theme_cls.primary_palette = 'Pink' + BoxLayout: + MDIconButton: + size: dp(40), dp(40) + pos: self.pos + size_hint: (None, None) + canvas: + Color: + rgba: root.rgb_hex('Purple') + Ellipse: + size: self.size + pos: self.pos + on_release: app.theme_cls.primary_palette = 'Purple' + BoxLayout: + MDIconButton: + size: dp(40), dp(40) + pos: self.pos + size_hint: (None, None) + canvas: + Color: + rgba: root.rgb_hex('DeepPurple') + Ellipse: + size: self.size + pos: self.pos + on_release: app.theme_cls.primary_palette = 'DeepPurple' + BoxLayout: + size_hint: (None, None) + pos_hint: {'center_x': .5, 'center_y': 0.5} + size: dp(230), dp(40) + pos: self.pos + halign: 'center' + orientation: 'horizontal' + BoxLayout: + MDIconButton: + size: dp(40), dp(40) + pos: self.pos + size_hint: (None, None) + canvas: + Color: + rgba: root.rgb_hex('Indigo') + Ellipse: + size: self.size + pos: self.pos + on_release: app.theme_cls.primary_palette = 'Indigo' + BoxLayout: + MDIconButton: + size: dp(40), dp(40) + pos: self.pos + size_hint: (None, None) + canvas: + Color: + rgba: root.rgb_hex('Blue') + Ellipse: + size: self.size + pos: self.pos + on_release: app.theme_cls.primary_palette = 'Blue' + BoxLayout: + MDIconButton: + size: dp(40), dp(40) + pos: self.pos + size_hint: (None, None) + canvas: + Color: + rgba: root.rgb_hex('LightBlue') + Ellipse: + size: self.size + pos: self.pos + on_release: app.theme_cls.primary_palette = 'LightBlue' + BoxLayout: + MDIconButton: + size: dp(40), dp(40) + pos: self.pos + size_hint: (None, None) + canvas: + Color: + rgba: root.rgb_hex('Cyan') + Ellipse: + size: self.size + pos: self.pos + on_release: app.theme_cls.primary_palette = 'Cyan' + BoxLayout: + size_hint: (None, None) + pos_hint: {'center_x': .5, 'center_y': 0.5} + size: dp(230), dp(40) + pos: self.pos + halign: 'center' + orientation: 'horizontal' + padding: 0, 0, 0, dp(1) + BoxLayout: + MDIconButton: + size: dp(40), dp(40) + pos: self.pos + size_hint: (None, None) + canvas: + Color: + rgba: root.rgb_hex('Teal') + Ellipse: + size: self.size + pos: self.pos + on_release: app.theme_cls.primary_palette = 'Teal' + BoxLayout: + MDIconButton: + size: dp(40), dp(40) + pos: self.pos + size_hint: (None, None) + canvas: + Color: + rgba: root.rgb_hex('Green') + Ellipse: + size: self.size + pos: self.pos + on_release: app.theme_cls.primary_palette = 'Green' + BoxLayout: + MDIconButton: + size: dp(40), dp(40) + pos: self.pos + size_hint: (None, None) + canvas: + Color: + rgba: root.rgb_hex('LightGreen') + Ellipse: + size: self.size + pos: self.pos + on_release: app.theme_cls.primary_palette = 'LightGreen' + BoxLayout: + MDIconButton: + size: dp(40), dp(40) + pos: self.pos + size_hint: (None, None) + canvas: + Color: + rgba: root.rgb_hex('Lime') + Ellipse: + size: self.size + pos: self.pos + on_release: app.theme_cls.primary_palette = 'Lime' + BoxLayout: + size_hint: (None, None) + pos_hint: {'center_x': .5, 'center_y': 0.5} + size: dp(230), dp(40) + pos: self.pos + orientation: 'horizontal' + halign: 'center' + padding: 0, 0, 0, dp(1) + BoxLayout: + MDIconButton: + size: dp(40), dp(40) + pos: self.pos + size_hint: (None, None) + canvas: + Color: + rgba: root.rgb_hex('Yellow') + Ellipse: + size: self.size + pos: self.pos + on_release: app.theme_cls.primary_palette = 'Yellow' + BoxLayout: + MDIconButton: + size: dp(40), dp(40) + pos: self.pos + size_hint: (None, None) + canvas: + Color: + rgba: root.rgb_hex('Amber') + Ellipse: + size: self.size + pos: self.pos + on_release: app.theme_cls.primary_palette = 'Amber' + BoxLayout: + MDIconButton: + size: dp(40), dp(40) + pos: self.pos + size_hint: (None, None) + canvas: + Color: + rgba: root.rgb_hex('Orange') + Ellipse: + size: self.size + pos: self.pos + on_release: app.theme_cls.primary_palette = 'Orange' + BoxLayout: + MDIconButton: + size: dp(40), dp(40) + pos: self.pos + size_hint: (None, None) + canvas: + Color: + rgba: root.rgb_hex('DeepOrange') + Ellipse: + size: self.size + pos: self.pos + on_release: app.theme_cls.primary_palette = 'DeepOrange' + BoxLayout: + size_hint: (None, None) + pos_hint: {'center_x': .5, 'center_y': 0.5} + size: dp(230), dp(40) + #pos: self.pos + orientation: 'horizontal' + padding: 0, 0, 0, dp(1) + BoxLayout: + MDIconButton: + size: dp(40), dp(40) + size_hint: (None, None) + canvas: + Color: + rgba: root.rgb_hex('Brown') + Ellipse: + size: self.size + pos: self.pos + on_release: app.theme_cls.primary_palette = 'Brown' + BoxLayout: + MDIconButton: + size: dp(40), dp(40) + pos: self.pos + size_hint: (None, None) + canvas: + Color: + rgba: root.rgb_hex('Grey') + Ellipse: + size: self.size + pos: self.pos + on_release: app.theme_cls.primary_palette = 'Grey' + BoxLayout: + MDIconButton: + size: dp(40), dp(40) + #pos: self.pos + size_hint: (None, None) + canvas: + Color: + rgba: root.rgb_hex('BlueGrey') + Ellipse: + size: self.size + pos: self.pos + on_release: app.theme_cls.primary_palette = 'BlueGrey' + BoxLayout: + MDIconButton: + size: dp(40), dp(40) + size_hint: (None, None) + canvas: + Color: + rgba: app.theme_cls.bg_normal + Ellipse: + size: self.size + pos: self.pos + disabled: True + + MDTab: + name: 'style' + text: "Theme Style" + BoxLayout: + size_hint: (None, None) + pos_hint: {'center_x': .3, 'center_y': 0.5} + size: self.size + pos: self.pos + halign: 'center' + spacing: dp(10) + BoxLayout: + halign: 'center' + size_hint: (None, None) + size: dp(100), dp(100) + pos: self.pos + pos_hint: {'center_x': .3, 'center_y': 0.5} + MDIconButton: + size: dp(100), dp(100) + pos: self.pos + size_hint: (None, None) + canvas: + Color: + rgba: 1, 1, 1, 1 + Ellipse: + size: self.size + pos: self.pos + Color: + rgba: 0, 0, 0, 1 + Line: + width: 1. + circle: (self.center_x, self.center_y, 50) + on_release: app.theme_cls.theme_style = 'Light' + BoxLayout: + halign: 'center' + size_hint: (None, None) + size: dp(100), dp(100) + MDIconButton: + size: dp(100), dp(100) + pos: self.pos + size_hint: (None, None) + canvas: + Color: + rgba: 0, 0, 0, 1 + Ellipse: + size: self.size + pos: self.pos + on_release: app.theme_cls.theme_style = 'Dark' +""") + + +class MDThemePicker(ThemableBehavior, FloatLayout, ModalView, ElevationBehavior): + # background_color = ListProperty([0, 0, 0, 0]) + time = ObjectProperty() + + def __init__(self, **kwargs): + super(MDThemePicker, self).__init__(**kwargs) + + def rgb_hex(self, col): + return get_color_from_hex(colors[col][self.theme_cls.accent_hue]) + + +if __name__ == "__main__": + from kivy.app import App + from kivymd.theming import ThemeManager + + class ThemePickerApp(App): + theme_cls = ThemeManager() + + def build(self): + main_widget = Builder.load_string(""" +#:import MDRaisedButton kivymd.button.MDRaisedButton +#:import MDThemePicker kivymd.theme_picker.MDThemePicker +FloatLayout: + MDRaisedButton: + size_hint: None, None + pos_hint: {'center_x': .5, 'center_y': .5} + size: 3 * dp(48), dp(48) + center_x: self.parent.center_x + text: 'Open theme picker' + on_release: MDThemePicker().open() + opposite_colors: True +""") + return main_widget + + ThemePickerApp().run() diff --git a/src/kivymd/theming.py b/src/kivymd/theming.py new file mode 100644 index 00000000..3172ee58 --- /dev/null +++ b/src/kivymd/theming.py @@ -0,0 +1,350 @@ +# -*- coding: utf-8 -*- +from kivy.app import App +from kivy.core.text import LabelBase +from kivy.core.window import Window +from kivy.clock import Clock +from kivy.metrics import dp +from kivy.properties import OptionProperty, AliasProperty, ObjectProperty, \ + StringProperty, ListProperty, BooleanProperty +from kivy.uix.widget import Widget +from kivy.utils import get_color_from_hex +from kivy.atlas import Atlas +from kivymd.color_definitions import colors +from kivymd.material_resources import FONTS, DEVICE_TYPE +from kivymd import images_path + +for font in FONTS: + LabelBase.register(**font) + + +class ThemeManager(Widget): + primary_palette = OptionProperty( + 'Blue', + options=['Pink', 'Blue', 'Indigo', 'BlueGrey', 'Brown', + 'LightBlue', + 'Purple', 'Grey', 'Yellow', 'LightGreen', 'DeepOrange', + 'Green', 'Red', 'Teal', 'Orange', 'Cyan', 'Amber', + 'DeepPurple', 'Lime']) + + primary_hue = OptionProperty( + '500', + options=['50', '100', '200', '300', '400', '500', '600', '700', + '800', + '900', 'A100', 'A200', 'A400', 'A700']) + + primary_light_hue = OptionProperty( + '200', + options=['50', '100', '200', '300', '400', '500', '600', '700', + '800', + '900', 'A100', 'A200', 'A400', 'A700']) + + primary_dark_hue = OptionProperty( + '700', + options=['50', '100', '200', '300', '400', '500', '600', '700', + '800', + '900', 'A100', 'A200', 'A400', 'A700']) + + def _get_primary_color(self): + return get_color_from_hex( + colors[self.primary_palette][self.primary_hue]) + + primary_color = AliasProperty(_get_primary_color, + bind=('primary_palette', 'primary_hue')) + + def _get_primary_light(self): + return get_color_from_hex( + colors[self.primary_palette][self.primary_light_hue]) + + primary_light = AliasProperty( + _get_primary_light, bind=('primary_palette', 'primary_light_hue')) + + def _get_primary_dark(self): + return get_color_from_hex( + colors[self.primary_palette][self.primary_dark_hue]) + + primary_dark = AliasProperty(_get_primary_dark, + bind=('primary_palette', 'primary_dark_hue')) + + accent_palette = OptionProperty( + 'Amber', + options=['Pink', 'Blue', 'Indigo', 'BlueGrey', 'Brown', + 'LightBlue', + 'Purple', 'Grey', 'Yellow', 'LightGreen', 'DeepOrange', + 'Green', 'Red', 'Teal', 'Orange', 'Cyan', 'Amber', + 'DeepPurple', 'Lime']) + + accent_hue = OptionProperty( + '500', + options=['50', '100', '200', '300', '400', '500', '600', '700', + '800', + '900', 'A100', 'A200', 'A400', 'A700']) + + accent_light_hue = OptionProperty( + '200', + options=['50', '100', '200', '300', '400', '500', '600', '700', + '800', + '900', 'A100', 'A200', 'A400', 'A700']) + + accent_dark_hue = OptionProperty( + '700', + options=['50', '100', '200', '300', '400', '500', '600', '700', + '800', + '900', 'A100', 'A200', 'A400', 'A700']) + + def _get_accent_color(self): + return get_color_from_hex( + colors[self.accent_palette][self.accent_hue]) + + accent_color = AliasProperty(_get_accent_color, + bind=['accent_palette', 'accent_hue']) + + def _get_accent_light(self): + return get_color_from_hex( + colors[self.accent_palette][self.accent_light_hue]) + + accent_light = AliasProperty(_get_accent_light, + bind=['accent_palette', 'accent_light_hue']) + + def _get_accent_dark(self): + return get_color_from_hex( + colors[self.accent_palette][self.accent_dark_hue]) + + accent_dark = AliasProperty(_get_accent_dark, + bind=['accent_palette', 'accent_dark_hue']) + + theme_style = OptionProperty('Light', options=['Light', 'Dark']) + + def _get_theme_style(self, opposite): + if opposite: + return 'Light' if self.theme_style == 'Dark' else 'Dark' + else: + return self.theme_style + + def _get_bg_darkest(self, opposite=False): + theme_style = self._get_theme_style(opposite) + if theme_style == 'Light': + return get_color_from_hex(colors['Light']['StatusBar']) + elif theme_style == 'Dark': + return get_color_from_hex(colors['Dark']['StatusBar']) + + bg_darkest = AliasProperty(_get_bg_darkest, bind=['theme_style']) + + def _get_op_bg_darkest(self): + return self._get_bg_darkest(True) + + opposite_bg_darkest = AliasProperty(_get_op_bg_darkest, + bind=['theme_style']) + + def _get_bg_dark(self, opposite=False): + theme_style = self._get_theme_style(opposite) + if theme_style == 'Light': + return get_color_from_hex(colors['Light']['AppBar']) + elif theme_style == 'Dark': + return get_color_from_hex(colors['Dark']['AppBar']) + + bg_dark = AliasProperty(_get_bg_dark, bind=['theme_style']) + + def _get_op_bg_dark(self): + return self._get_bg_dark(True) + + opposite_bg_dark = AliasProperty(_get_op_bg_dark, bind=['theme_style']) + + def _get_bg_normal(self, opposite=False): + theme_style = self._get_theme_style(opposite) + if theme_style == 'Light': + return get_color_from_hex(colors['Light']['Background']) + elif theme_style == 'Dark': + return get_color_from_hex(colors['Dark']['Background']) + + bg_normal = AliasProperty(_get_bg_normal, bind=['theme_style']) + + def _get_op_bg_normal(self): + return self._get_bg_normal(True) + + opposite_bg_normal = AliasProperty(_get_op_bg_normal, bind=['theme_style']) + + def _get_bg_light(self, opposite=False): + theme_style = self._get_theme_style(opposite) + if theme_style == 'Light': + return get_color_from_hex(colors['Light']['CardsDialogs']) + elif theme_style == 'Dark': + return get_color_from_hex(colors['Dark']['CardsDialogs']) + + bg_light = AliasProperty(_get_bg_light, bind=['theme_style']) + + def _get_op_bg_light(self): + return self._get_bg_light(True) + + opposite_bg_light = AliasProperty(_get_op_bg_light, bind=['theme_style']) + + def _get_divider_color(self, opposite=False): + theme_style = self._get_theme_style(opposite) + if theme_style == 'Light': + color = get_color_from_hex('000000') + elif theme_style == 'Dark': + color = get_color_from_hex('FFFFFF') + color[3] = .12 + return color + + divider_color = AliasProperty(_get_divider_color, bind=['theme_style']) + + def _get_op_divider_color(self): + return self._get_divider_color(True) + + opposite_divider_color = AliasProperty(_get_op_divider_color, + bind=['theme_style']) + + def _get_text_color(self, opposite=False): + theme_style = self._get_theme_style(opposite) + if theme_style == 'Light': + color = get_color_from_hex('000000') + color[3] = .87 + elif theme_style == 'Dark': + color = get_color_from_hex('FFFFFF') + return color + + text_color = AliasProperty(_get_text_color, bind=['theme_style']) + + def _get_op_text_color(self): + return self._get_text_color(True) + + opposite_text_color = AliasProperty(_get_op_text_color, + bind=['theme_style']) + + def _get_secondary_text_color(self, opposite=False): + theme_style = self._get_theme_style(opposite) + if theme_style == 'Light': + color = get_color_from_hex('000000') + color[3] = .54 + elif theme_style == 'Dark': + color = get_color_from_hex('FFFFFF') + color[3] = .70 + return color + + secondary_text_color = AliasProperty(_get_secondary_text_color, + bind=['theme_style']) + + def _get_op_secondary_text_color(self): + return self._get_secondary_text_color(True) + + opposite_secondary_text_color = AliasProperty(_get_op_secondary_text_color, + bind=['theme_style']) + + def _get_icon_color(self, opposite=False): + theme_style = self._get_theme_style(opposite) + if theme_style == 'Light': + color = get_color_from_hex('000000') + color[3] = .54 + elif theme_style == 'Dark': + color = get_color_from_hex('FFFFFF') + return color + + icon_color = AliasProperty(_get_icon_color, + bind=['theme_style']) + + def _get_op_icon_color(self): + return self._get_icon_color(True) + + opposite_icon_color = AliasProperty(_get_op_icon_color, + bind=['theme_style']) + + def _get_disabled_hint_text_color(self, opposite=False): + theme_style = self._get_theme_style(opposite) + if theme_style == 'Light': + color = get_color_from_hex('000000') + color[3] = .26 + elif theme_style == 'Dark': + color = get_color_from_hex('FFFFFF') + color[3] = .30 + return color + + disabled_hint_text_color = AliasProperty(_get_disabled_hint_text_color, + bind=['theme_style']) + + def _get_op_disabled_hint_text_color(self): + return self._get_disabled_hint_text_color(True) + + opposite_disabled_hint_text_color = AliasProperty( + _get_op_disabled_hint_text_color, bind=['theme_style']) + + # Hardcoded because muh standard + def _get_error_color(self): + return get_color_from_hex(colors['Red']['A700']) + + error_color = AliasProperty(_get_error_color) + + def _get_ripple_color(self): + return self._ripple_color + + def _set_ripple_color(self, value): + self._ripple_color = value + + _ripple_color = ListProperty(get_color_from_hex(colors['Grey']['400'])) + ripple_color = AliasProperty(_get_ripple_color, + _set_ripple_color, + bind=['_ripple_color']) + + def _determine_device_orientation(self, _, window_size): + if window_size[0] > window_size[1]: + self.device_orientation = 'landscape' + elif window_size[1] >= window_size[0]: + self.device_orientation = 'portrait' + + device_orientation = StringProperty('') + + def _get_standard_increment(self): + if DEVICE_TYPE == 'mobile': + if self.device_orientation == 'landscape': + return dp(48) + else: + return dp(56) + else: + return dp(64) + + standard_increment = AliasProperty(_get_standard_increment, + bind=['device_orientation']) + + def _get_horizontal_margins(self): + if DEVICE_TYPE == 'mobile': + return dp(16) + else: + return dp(24) + + horizontal_margins = AliasProperty(_get_horizontal_margins) + + def on_theme_style(self, instance, value): + if hasattr(App.get_running_app(), 'theme_cls') and \ + App.get_running_app().theme_cls == self: + self.set_clearcolor_by_theme_style(value) + + def set_clearcolor_by_theme_style(self, theme_style): + if theme_style == 'Light': + Window.clearcolor = get_color_from_hex( + colors['Light']['Background']) + elif theme_style == 'Dark': + Window.clearcolor = get_color_from_hex( + colors['Dark']['Background']) + + def __init__(self, **kwargs): + super(ThemeManager, self).__init__(**kwargs) + self.rec_shadow = Atlas('{}rec_shadow.atlas'.format(images_path)) + self.rec_st_shadow = Atlas('{}rec_st_shadow.atlas'.format(images_path)) + self.quad_shadow = Atlas('{}quad_shadow.atlas'.format(images_path)) + self.round_shadow = Atlas('{}round_shadow.atlas'.format(images_path)) + Clock.schedule_once(lambda x: self.on_theme_style(0, self.theme_style)) + self._determine_device_orientation(None, Window.size) + Window.bind(size=self._determine_device_orientation) + + +class ThemableBehavior(object): + theme_cls = ObjectProperty(None) + opposite_colors = BooleanProperty(False) + + def __init__(self, **kwargs): + if self.theme_cls is not None: + pass + elif hasattr(App.get_running_app(), 'theme_cls'): + self.theme_cls = App.get_running_app().theme_cls + else: + self.theme_cls = ThemeManager() + super(ThemableBehavior, self).__init__(**kwargs) diff --git a/src/kivymd/time_picker.py b/src/kivymd/time_picker.py new file mode 100644 index 00000000..6de6fc20 --- /dev/null +++ b/src/kivymd/time_picker.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- + +from kivy.lang import Builder +from kivy.uix.modalview import ModalView +from kivy.uix.floatlayout import FloatLayout +from kivymd.theming import ThemableBehavior +from kivymd.elevationbehavior import ElevationBehavior +from kivy.properties import ObjectProperty, ListProperty + +Builder.load_string(""" +#:import MDFlatButton kivymd.button.MDFlatButton +#:import CircularTimePicker kivymd.vendor.circularTimePicker.CircularTimePicker +#:import dp kivy.metrics.dp +: + size_hint: (None, None) + size: [dp(270), dp(335)+dp(95)] + #if root.theme_cls.device_orientation == 'portrait' else [dp(520), dp(325)] + pos_hint: {'center_x': .5, 'center_y': .5} + canvas: + Color: + rgba: self.theme_cls.bg_light + Rectangle: + size: [dp(270), dp(335)] + #if root.theme_cls.device_orientation == 'portrait' else [dp(250), root.height] + pos: [root.pos[0], root.pos[1] + root.height - dp(335) - dp(95)] + #if root.theme_cls.device_orientation == 'portrait' else [root.pos[0]+dp(270), root.pos[1]] + Color: + rgba: self.theme_cls.primary_color + Rectangle: + size: [dp(270), dp(95)] + #if root.theme_cls.device_orientation == 'portrait' else [dp(270), root.height] + pos: [root.pos[0], root.pos[1] + root.height - dp(95)] + #if root.theme_cls.device_orientation == 'portrait' else [root.pos[0], root.pos[1]] + Color: + rgba: self.theme_cls.bg_dark + Ellipse: + size: [dp(220), dp(220)] + #if root.theme_cls.device_orientation == 'portrait' else [dp(195), dp(195)] + pos: root.pos[0]+dp(270)/2-dp(220)/2, root.pos[1] + root.height - (dp(335)/2+dp(95)) - dp(220)/2 + dp(35) + #Color: + #rgba: (1, 0, 0, 1) + #Line: + #width: 4 + #points: dp(270)/2, root.height, dp(270)/2, 0 + CircularTimePicker: + id: time_picker + pos: (dp(270)/2)-(self.width/2), root.height-self.height + size_hint: [.8, .8] + #if root.theme_cls.device_orientation == 'portrait' else [0.35, 0.9] + pos_hint: {'center_x': 0.5, 'center_y': 0.585} + #if root.theme_cls.device_orientation == 'portrait' else {'center_x': 0.75, 'center_y': 0.7} + MDFlatButton: + pos: root.pos[0]+root.size[0]-dp(72)*2, root.pos[1] + dp(10) + text: "Cancel" + on_release: root.close_cancel() + MDFlatButton: + pos: root.pos[0]+root.size[0]-dp(72), root.pos[1] + dp(10) + text: "OK" + on_release: root.close_ok() +""") + + +class MDTimePicker(ThemableBehavior, FloatLayout, ModalView, ElevationBehavior): + # background_color = ListProperty((0, 0, 0, 0)) + time = ObjectProperty() + + def __init__(self, **kwargs): + super(MDTimePicker, self).__init__(**kwargs) + self.current_time = self.ids.time_picker.time + + def set_time(self, time): + try: + self.ids.time_picker.set_time(time) + except AttributeError: + raise TypeError("MDTimePicker._set_time must receive a datetime object, not a \"" + + type(time).__name__ + "\"") + + def close_cancel(self): + self.dismiss() + + def close_ok(self): + self.current_time = self.ids.time_picker.time + self.time = self.current_time + self.dismiss() diff --git a/src/kivymd/toolbar.py b/src/kivymd/toolbar.py new file mode 100644 index 00000000..fc7b146c --- /dev/null +++ b/src/kivymd/toolbar.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +from kivy.clock import Clock +from kivy.lang import Builder +from kivy.metrics import dp +from kivy.properties import ListProperty, StringProperty, OptionProperty +from kivy.uix.boxlayout import BoxLayout +from kivymd.backgroundcolorbehavior import BackgroundColorBehavior +from kivymd.button import MDIconButton +from kivymd.theming import ThemableBehavior +from kivymd.elevationbehavior import ElevationBehavior + +Builder.load_string(''' +#:import m_res kivymd.material_resources + + size_hint_y: None + height: root.theme_cls.standard_increment + background_color: root.background_color + padding: [root.theme_cls.horizontal_margins - dp(12), 0] + opposite_colors: True + elevation: 6 + BoxLayout: + id: left_actions + orientation: 'horizontal' + size_hint_x: None + padding: [0, (self.height - dp(48))/2] + BoxLayout: + padding: dp(12), 0 + MDLabel: + font_style: 'Title' + opposite_colors: root.opposite_colors + theme_text_color: root.title_theme_color + text_color: root.title_color + text: root.title + shorten: True + shorten_from: 'right' + BoxLayout: + id: right_actions + orientation: 'horizontal' + size_hint_x: None + padding: [0, (self.height - dp(48))/2] +''') + + +class Toolbar(ThemableBehavior, ElevationBehavior, BackgroundColorBehavior, + BoxLayout): + left_action_items = ListProperty() + """The icons on the left of the Toolbar. + + To add one, append a list like the following: + + ['icon_name', callback] + + where 'icon_name' is a string that corresponds to an icon definition and + callback is the function called on a touch release event. + """ + + right_action_items = ListProperty() + """The icons on the left of the Toolbar. + + Works the same way as :attr:`left_action_items` + """ + + title = StringProperty() + """The text displayed on the Toolbar.""" + + title_theme_color = OptionProperty(None, allownone=True, + options=['Primary', 'Secondary', 'Hint', + 'Error', 'Custom']) + + title_color = ListProperty(None, allownone=True) + + background_color = ListProperty([0, 0, 0, 1]) + + def __init__(self, **kwargs): + super(Toolbar, self).__init__(**kwargs) + Clock.schedule_once( + lambda x: self.on_left_action_items(0, self.left_action_items)) + Clock.schedule_once( + lambda x: self.on_right_action_items(0, + self.right_action_items)) + + def on_left_action_items(self, instance, value): + self.update_action_bar(self.ids['left_actions'], value) + + def on_right_action_items(self, instance, value): + self.update_action_bar(self.ids['right_actions'], value) + + def update_action_bar(self, action_bar, action_bar_items): + action_bar.clear_widgets() + new_width = 0 + for item in action_bar_items: + new_width += dp(48) + action_bar.add_widget(MDIconButton(icon=item[0], + on_release=item[1], + opposite_colors=True, + text_color=self.title_color, + theme_text_color=self.title_theme_color)) + action_bar.width = new_width diff --git a/src/kivymd/vendor/__init__.py b/src/kivymd/vendor/__init__.py new file mode 100644 index 00000000..9bad5790 --- /dev/null +++ b/src/kivymd/vendor/__init__.py @@ -0,0 +1 @@ +# coding=utf-8 diff --git a/src/kivymd/vendor/circleLayout/LICENSE b/src/kivymd/vendor/circleLayout/LICENSE new file mode 100644 index 00000000..9d6e5b59 --- /dev/null +++ b/src/kivymd/vendor/circleLayout/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Davide Depau + +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/src/kivymd/vendor/circleLayout/README.md b/src/kivymd/vendor/circleLayout/README.md new file mode 100644 index 00000000..6cf54bbe --- /dev/null +++ b/src/kivymd/vendor/circleLayout/README.md @@ -0,0 +1,21 @@ +CircularLayout +============== + +CircularLayout is a special layout that places widgets around a circle. + +See the widget's documentation and the example for more information. + +![Screenshot](screenshot.png) + +size_hint +--------- + +size_hint_x is used as an angle-quota hint (widget with higher +size_hint_x will be farther from each other, and viceversa), while +size_hint_y is used as a widget size hint (widgets with a higher size +hint will be bigger).size_hint_x cannot be None. + +Widgets are all squares, unless you set size_hint_y to None (in that +case you'll be able to specify your own size), and their size is the +difference between the outer and the inner circle's radii. To make the +widgets bigger you can just decrease inner_radius_hint. \ No newline at end of file diff --git a/src/kivymd/vendor/circleLayout/__init__.py b/src/kivymd/vendor/circleLayout/__init__.py new file mode 100644 index 00000000..9d62c99c --- /dev/null +++ b/src/kivymd/vendor/circleLayout/__init__.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +CircularLayout +============== + +CircularLayout is a special layout that places widgets around a circle. + +size_hint +--------- + +size_hint_x is used as an angle-quota hint (widget with higher +size_hint_x will be farther from each other, and vice versa), while +size_hint_y is used as a widget size hint (widgets with a higher size +hint will be bigger).size_hint_x cannot be None. + +Widgets are all squares, unless you set size_hint_y to None (in that +case you'll be able to specify your own size), and their size is the +difference between the outer and the inner circle's radii. To make the +widgets bigger you can just decrease inner_radius_hint. +""" + +from kivy.uix.layout import Layout +from kivy.properties import NumericProperty, ReferenceListProperty, OptionProperty, \ + BoundedNumericProperty, VariableListProperty, AliasProperty +from math import sin, cos, pi, radians + +__all__ = ('CircularLayout') + +try: + xrange(1, 2) +except NameError: + def xrange(first, second, third=None): + if third: + return range(first, second, third) + else: + return range(first, second) + + +class CircularLayout(Layout): + ''' + Circular layout class. See module documentation for more information. + ''' + + padding = VariableListProperty([0, 0, 0, 0]) + '''Padding between the layout box and it's children: [padding_left, + padding_top, padding_right, padding_bottom]. + + padding also accepts a two argument form [padding_horizontal, + padding_vertical] and a one argument form [padding]. + + .. version changed:: 1.7.0 + Replaced NumericProperty with VariableListProperty. + + :attr:`padding` is a :class:`~kivy.properties.VariableListProperty` and + defaults to [0, 0, 0, 0]. + ''' + + start_angle = NumericProperty(0) + '''Angle (in degrees) at which the first widget will be placed. + Start counting angles from the X axis, going counterclockwise. + + :attr:`start_angle` is a :class:`~kivy.properties.NumericProperty` and + defaults to 0 (start from the right). + ''' + + circle_quota = BoundedNumericProperty(360, min=0, max=360) + '''Size (in degrees) of the part of the circumference that will actually + be used to place widgets. + + :attr:`circle_quota` is a :class:`~kivy.properties.BoundedNumericProperty` + and defaults to 360 (all the circumference). + ''' + + direction = OptionProperty("ccw", options=("cw", "ccw")) + '''Direction of widgets in the circle. + + :attr:`direction` is an :class:`~kivy.properties.OptionProperty` and + defaults to 'ccw'. Can be 'ccw' (counterclockwise) or 'cw' (clockwise). + ''' + + outer_radius_hint = NumericProperty(1) + '''Sets the size of the outer circle. A number greater than 1 will make the + widgets larger than the actual widget, a number smaller than 1 will leave + a gap. + + :attr:`outer_radius_hint` is a :class:`~kivy.properties.NumericProperty` and + defaults to 1. + ''' + + inner_radius_hint = NumericProperty(.6) + '''Sets the size of the inner circle. A number greater than + :attr:`outer_radius_hint` will cause glitches. The closest it is to + :attr:`outer_radius_hint`, the smallest will be the widget in the layout. + + :attr:`outer_radius_hint` is a :class:`~kivy.properties.NumericProperty` and + defaults to 1. + ''' + + radius_hint = ReferenceListProperty(inner_radius_hint, outer_radius_hint) + '''Combined :attr:`outer_radius_hint` and :attr:`inner_radius_hint` in a list + for convenience. See their documentation for more details. + + :attr:`radius_hint` is a :class:`~kivy.properties.ReferenceListProperty`. + ''' + + def _get_delta_radii(self): + radius = min(self.width-self.padding[0]-self.padding[2], self.height-self.padding[1]-self.padding[3]) / 2. + outer_r = radius * self.outer_radius_hint + inner_r = radius * self.inner_radius_hint + return outer_r - inner_r + delta_radii = AliasProperty(_get_delta_radii, None, bind=("radius_hint", "padding", "size")) + + def __init__(self, **kwargs): + super(CircularLayout, self).__init__(**kwargs) + + self.bind( + start_angle=self._trigger_layout, + parent=self._trigger_layout, + # padding=self._trigger_layout, + children=self._trigger_layout, + size=self._trigger_layout, + radius_hint=self._trigger_layout, + pos=self._trigger_layout) + + def do_layout(self, *largs): + # optimize layout by preventing looking at the same attribute in a loop + len_children = len(self.children) + if len_children == 0: + return + selfcx = self.center_x + selfcy = self.center_y + direction = self.direction + cquota = radians(self.circle_quota) + start_angle_r = radians(self.start_angle) + padding_left = self.padding[0] + padding_top = self.padding[1] + padding_right = self.padding[2] + padding_bottom = self.padding[3] + padding_x = padding_left + padding_right + padding_y = padding_top + padding_bottom + + radius = min(self.width-padding_x, self.height-padding_y) / 2. + outer_r = radius * self.outer_radius_hint + inner_r = radius * self.inner_radius_hint + middle_r = radius * sum(self.radius_hint) / 2. + delta_r = outer_r - inner_r + + stretch_weight_angle = 0. + for w in self.children: + sha = w.size_hint_x + if sha is None: + raise ValueError("size_hint_x cannot be None in a CircularLayout") + else: + stretch_weight_angle += sha + + sign = +1. + angle_offset = start_angle_r + if direction == 'cw': + angle_offset = 2 * pi - start_angle_r + sign = -1. + + for c in reversed(self.children): + sha = c.size_hint_x + shs = c.size_hint_y + + angle_quota = cquota / stretch_weight_angle * sha + angle = angle_offset + (sign * angle_quota / 2) + angle_offset += sign * angle_quota + + # kived: looking it up, yes. x = cos(angle) * radius + centerx; y = sin(angle) * radius + centery + ccx = cos(angle) * middle_r + selfcx + padding_left - padding_right + ccy = sin(angle) * middle_r + selfcy + padding_bottom - padding_top + + c.center_x = ccx + c.center_y = ccy + if shs: + s = delta_r * shs + c.width = s + c.height = s + +if __name__ == "__main__": + from kivy.app import App + from kivy.uix.button import Button + + class CircLayoutApp(App): + def build(self): + cly = CircularLayout(direction="cw", start_angle=-75, inner_radius_hint=.7, padding="20dp") + + for i in xrange(1, 13): + cly.add_widget(Button(text=str(i), font_size="30dp")) + + return cly + + CircLayoutApp().run() diff --git a/src/kivymd/vendor/circularTimePicker/LICENSE b/src/kivymd/vendor/circularTimePicker/LICENSE new file mode 100644 index 00000000..9d6e5b59 --- /dev/null +++ b/src/kivymd/vendor/circularTimePicker/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Davide Depau + +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/src/kivymd/vendor/circularTimePicker/README.md b/src/kivymd/vendor/circularTimePicker/README.md new file mode 100644 index 00000000..20ac2de9 --- /dev/null +++ b/src/kivymd/vendor/circularTimePicker/README.md @@ -0,0 +1,43 @@ +Circular Date & Time Picker for Kivy +==================================== + +(currently only time, date coming soon) + +Based on [CircularLayout](https://github.com/kivy-garden/garden.circularlayout). +The main aim is to provide a date and time selector similar to the +one found in Android KitKat+. + +![Screenshot](screenshot.png) + +Simple usage +------------ + +Import the widget with + +```python +from kivy.garden.circulardatetimepicker import CircularTimePicker +``` + +then use it! That's it! + +```python +c = CircularTimePicker() +c.bind(time=self.set_time) +root.add_widget(c) +``` + +in Kv language: + +``` +: + BoxLayout: + orientation: "vertical" + + CircularTimePicker + + Button: + text: "Dismiss" + size_hint_y: None + height: "40dp" + on_release: root.dismiss() +``` \ No newline at end of file diff --git a/src/kivymd/vendor/circularTimePicker/__init__.py b/src/kivymd/vendor/circularTimePicker/__init__.py new file mode 100644 index 00000000..fbc73954 --- /dev/null +++ b/src/kivymd/vendor/circularTimePicker/__init__.py @@ -0,0 +1,770 @@ +# -*- coding: utf-8 -*- + +""" +Circular Date & Time Picker for Kivy +==================================== + +(currently only time, date coming soon) + +Based on [CircularLayout](https://github.com/kivy-garden/garden.circularlayout). +The main aim is to provide a date and time selector similar to the +one found in Android KitKat+. + +Simple usage +------------ + +Import the widget with + +```python +from kivy.garden.circulardatetimepicker import CircularTimePicker +``` + +then use it! That's it! + +```python +c = CircularTimePicker() +c.bind(time=self.set_time) +root.add_widget(c) +``` + +in Kv language: + +``` +: + BoxLayout: + orientation: "vertical" + + CircularTimePicker + + Button: + text: "Dismiss" + size_hint_y: None + height: "40dp" + on_release: root.dismiss() +``` +""" + +from kivy.animation import Animation +from kivy.clock import Clock +from kivymd.vendor.circleLayout import CircularLayout +from kivy.graphics import Line, Color, Ellipse +from kivy.lang import Builder +from kivy.properties import NumericProperty, BoundedNumericProperty, \ + ObjectProperty, StringProperty, DictProperty, \ + ListProperty, OptionProperty, BooleanProperty, \ + ReferenceListProperty, AliasProperty +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.label import Label +from kivy.metrics import dp +from kivymd.theming import ThemableBehavior +from math import atan, pi, radians, sin, cos +import sys +import datetime +if sys.version_info[0] > 2: + def xrange(first=None, second=None, third=None): + if third: + return range(first, second, third) + else: + return range(first, second) + + +def map_number(x, in_min, in_max, out_min, out_max): + return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min + + +def rgb_to_hex(*color): + tor = "#" + for col in color: + tor += "{:>02}".format(hex(int(col * 255))[2:]) + return tor + + +Builder.load_string(""" + +: + text_size: self.size + valign: "middle" + halign: "center" + font_size: self.height * self.size_factor + +: + canvas.before: + PushMatrix + Scale: + origin: self.center_x + self.padding[0] - self.padding[2], self.center_y + self.padding[3] - self.padding[1] + x: self.scale + y: self.scale + + canvas.after: + PopMatrix + +: + orientation: "vertical" + spacing: "20dp" + + FloatLayout: + anchor_x: "center" + anchor_y: "center" + size_hint_y: 1./3 + size_hint_x: 1 + size: root.size + pos: root.pos + + GridLayout: + cols: 2 + spacing: "10dp" + size_hint_x: None + width: self.minimum_width + pos_hint: {'center_x': .5, 'center_y': .5} + + Label: + id: timelabel + text: root.time_text + markup: True + halign: "right" + valign: "middle" + # text_size: self.size + size_hint_x: None #.6 + width: self.texture_size[0] + font_size: self.height * .75 + + Label: + id: ampmlabel + text: root.ampm_text + markup: True + halign: "left" + valign: "middle" + # text_size: self.size + size_hint_x: None #.4 + width: self.texture_size[0] + font_size: self.height * .3 + + FloatLayout: + id: picker_container + #size_hint_y: 2./3 + _bound: {} +""") + + +class Number(Label): + """The class used to show the numbers in the selector. + """ + + size_factor = NumericProperty(.5) + """Font size scale. + + :attr:`size_factor` is a :class:`~kivy.properties.NumericProperty` and + defaults to 0.5. + """ + + +class CircularNumberPicker(CircularLayout): + """A circular number picker based on CircularLayout. A selector will + help you pick a number. You can also set :attr:`multiples_of` to make + it show only some numbers and use the space in between for the other + numbers. + """ + + min = NumericProperty(0) + """The first value of the range. + + :attr:`min` is a :class:`~kivy.properties.NumericProperty` and + defaults to 0. + """ + + max = NumericProperty(0) + """The last value of the range. Note that it behaves like xrange, so + the actual last displayed value will be :attr:`max` - 1. + + :attr:`max` is a :class:`~kivy.properties.NumericProperty` and + defaults to 0. + """ + + range = ReferenceListProperty(min, max) + """Packs :attr:`min` and :attr:`max` into a list for convenience. See + their documentation for further information. + + :attr:`range` is a :class:`~kivy.properties.ReferenceListProperty`. + """ + + multiples_of = NumericProperty(1) + """Only show numbers that are multiples of this number. The other numbers + will be selectable, but won't have their own label. + + :attr:`multiples_of` is a :class:`~kivy.properties.NumericProperty` and + defaults to 1. + """ + + # selector_color = ListProperty([.337, .439, .490]) + selector_color = ListProperty([1, 1, 1]) + """Color of the number selector. RGB. + + :attr:`selector_color` is a :class:`~kivy.properties.ListProperty` and + defaults to [.337, .439, .490] (material green). + """ + + color = ListProperty([0, 0, 0]) + """Color of the number labels and of the center dot. RGB. + + :attr:`color` is a :class:`~kivy.properties.ListProperty` and + defaults to [1, 1, 1] (white). + """ + + selector_alpha = BoundedNumericProperty(.3, min=0, max=1) + """Alpha value for the transparent parts of the selector. + + :attr:`selector_alpha` is a :class:`~kivy.properties.BoundedNumericProperty` and + defaults to 0.3 (min=0, max=1). + """ + + selected = NumericProperty(None) + """Currently selected number. + + :attr:`selected` is a :class:`~kivy.properties.NumericProperty` and + defaults to :attr:`min`. + """ + + number_size_factor = NumericProperty(.5) + """Font size scale factor fot the :class:`Number`s. + + :attr:`number_size_factor` is a :class:`~kivy.properties.NumericProperty` and + defaults to 0.5. + """ + + number_format_string = StringProperty("{}") + """String that will be formatted with the selected number as the first argument. + Can be anything supported by :meth:`str.format` (es. "{:02d}"). + + :attr:`number_format_string` is a :class:`~kivy.properties.StringProperty` and + defaults to "{}". + """ + + scale = NumericProperty(1) + """Canvas scale factor. Used in :class:`CircularTimePicker` transitions. + + :attr:`scale` is a :class:`~kivy.properties.NumericProperty` and + defaults to 1. + """ + + _selection_circle = ObjectProperty(None) + _selection_line = ObjectProperty(None) + _selection_dot = ObjectProperty(None) + _selection_dot_color = ObjectProperty(None) + _selection_color = ObjectProperty(None) + _center_dot = ObjectProperty(None) + _center_color = ObjectProperty(None) + + def _get_items(self): + return self.max - self.min + + items = AliasProperty(_get_items, None) + + def _get_shown_items(self): + sh = 0 + for i in xrange(*self.range): + if i % self.multiples_of == 0: + sh += 1 + return sh + + shown_items = AliasProperty(_get_shown_items, None) + + def __init__(self, **kw): + self._trigger_genitems = Clock.create_trigger(self._genitems, -1) + self.bind(min=self._trigger_genitems, + max=self._trigger_genitems, + multiples_of=self._trigger_genitems) + super(CircularNumberPicker, self).__init__(**kw) + self.selected = self.min + self.bind(selected=self.on_selected, + pos=self.on_selected, + size=self.on_selected) + + cx = self.center_x + self.padding[0] - self.padding[2] + cy = self.center_y + self.padding[3] - self.padding[1] + sx, sy = self.pos_for_number(self.selected) + epos = [i - (self.delta_radii * self.number_size_factor) for i in (sx, sy)] + esize = [self.delta_radii * self.number_size_factor * 2] * 2 + dsize = [i * .3 for i in esize] + dpos = [i + esize[0] / 2. - dsize[0] / 2. for i in epos] + csize = [i * .05 for i in esize] + cpos = [i - csize[0] / 2. for i in (cx, cy)] + dot_alpha = 0 if self.selected % self.multiples_of == 0 else 1 + color = list(self.selector_color) + + with self.canvas: + self._selection_color = Color(*(color + [self.selector_alpha])) + self._selection_circle = Ellipse(pos=epos, size=esize) + self._selection_line = Line(points=[cx, cy, sx, sy], width=dp(1.25)) + self._selection_dot_color = Color(*(color + [dot_alpha])) + self._selection_dot = Ellipse(pos=dpos, size=dsize) + self._center_color = Color(*self.color) + self._center_dot = Ellipse(pos=cpos, size=csize) + + self.bind(selector_color=lambda ign, u: setattr(self._selection_color, "rgba", u + [self.selector_alpha])) + self.bind(selector_color=lambda ign, u: setattr(self._selection_dot_color, "rgb", u)) + self.bind(selector_color=lambda ign, u: self.dot_is_none()) + self.bind(color=lambda ign, u: setattr(self._center_color, "rgb", u)) + Clock.schedule_once(self._genitems) + Clock.schedule_once(self.on_selected) # Just to make sure pos/size are set + + def dot_is_none(self, *args): + dot_alpha = 0 if self.selected % self.multiples_of == 0 else 1 + if self._selection_dot_color: + self._selection_dot_color.a = dot_alpha + + def _genitems(self, *a): + self.clear_widgets() + for i in xrange(*self.range): + if i % self.multiples_of != 0: + continue + n = Number(text=self.number_format_string.format(i), size_factor=self.number_size_factor, color=self.color) + self.bind(color=n.setter("color")) + self.add_widget(n) + + def on_touch_down(self, touch): + if not self.collide_point(*touch.pos): + return + touch.grab(self) + self.selected = self.number_at_pos(*touch.pos) + if self.selected == 60: + self.selected = 0 + + def on_touch_move(self, touch): + if touch.grab_current is not self: + return super(CircularNumberPicker, self).on_touch_move(touch) + self.selected = self.number_at_pos(*touch.pos) + if self.selected == 60: + self.selected = 0 + + def on_touch_up(self, touch): + if touch.grab_current is not self: + return super(CircularNumberPicker, self).on_touch_up(touch) + touch.ungrab(self) + + def on_selected(self, *a): + cx = self.center_x + self.padding[0] - self.padding[2] + cy = self.center_y + self.padding[3] - self.padding[1] + sx, sy = self.pos_for_number(self.selected) + epos = [i - (self.delta_radii * self.number_size_factor) for i in (sx, sy)] + esize = [self.delta_radii * self.number_size_factor * 2] * 2 + dsize = [i * .3 for i in esize] + dpos = [i + esize[0] / 2. - dsize[0] / 2. for i in epos] + csize = [i * .05 for i in esize] + cpos = [i - csize[0] / 2. for i in (cx, cy)] + dot_alpha = 0 if self.selected % self.multiples_of == 0 else 1 + + if self._selection_circle: + self._selection_circle.pos = epos + self._selection_circle.size = esize + if self._selection_line: + self._selection_line.points = [cx, cy, sx, sy] + if self._selection_dot: + self._selection_dot.pos = dpos + self._selection_dot.size = dsize + if self._selection_dot_color: + self._selection_dot_color.a = dot_alpha + if self._center_dot: + self._center_dot.pos = cpos + self._center_dot.size = csize + + def pos_for_number(self, n): + """Returns the center x, y coordinates for a given number. + """ + + if self.items == 0: + return 0, 0 + radius = min(self.width - self.padding[0] - self.padding[2], + self.height - self.padding[1] - self.padding[3]) / 2. + middle_r = radius * sum(self.radius_hint) / 2. + cx = self.center_x + self.padding[0] - self.padding[2] + cy = self.center_y + self.padding[3] - self.padding[1] + sign = +1. + angle_offset = radians(self.start_angle) + if self.direction == 'cw': + angle_offset = 2 * pi - angle_offset + sign = -1. + quota = 2 * pi / self.items + mult_quota = 2 * pi / self.shown_items + angle = angle_offset + n * sign * quota + + if self.items == self.shown_items: + angle += quota / 2 + else: + angle -= mult_quota / 2 + + # kived: looking it up, yes. x = cos(angle) * radius + centerx; y = sin(angle) * radius + centery + x = cos(angle) * middle_r + cx + y = sin(angle) * middle_r + cy + + return x, y + + def number_at_pos(self, x, y): + """Returns the number at a given x, y position. The number is found + using the widget's center as a starting point for angle calculations. + + Not thoroughly tested, may yield wrong results. + """ + if self.items == 0: + return self.min + cx = self.center_x + self.padding[0] - self.padding[2] + cy = self.center_y + self.padding[3] - self.padding[1] + lx = x - cx + ly = y - cy + quota = 2 * pi / self.items + mult_quota = 2 * pi / self.shown_items + if lx == 0 and ly > 0: + angle = pi / 2 + elif lx == 0 and ly < 0: + angle = 3 * pi / 2 + else: + angle = atan(ly / lx) + if lx < 0 < ly: + angle += pi + if lx > 0 > ly: + angle += 2 * pi + if lx < 0 and ly < 0: + angle += pi + angle += radians(self.start_angle) + if self.direction == "cw": + angle = 2 * pi - angle + if mult_quota != quota: + angle -= mult_quota / 2 + if angle < 0: + angle += 2 * pi + elif angle > 2 * pi: + angle -= 2 * pi + + return int(angle / quota) + self.min + + +class CircularMinutePicker(CircularNumberPicker): + """:class:`CircularNumberPicker` implementation for minutes. + """ + + def __init__(self, **kw): + super(CircularMinutePicker, self).__init__(**kw) + self.min = 0 + self.max = 60 + self.multiples_of = 5 + self.number_format_string = "{:02d}" + self.direction = "cw" + self.bind(shown_items=self._update_start_angle) + Clock.schedule_once(self._update_start_angle) + Clock.schedule_once(self.on_selected) + + def _update_start_angle(self, *a): + self.start_angle = -(360. / self.shown_items / 2) - 90 + + +class CircularHourPicker(CircularNumberPicker): + """:class:`CircularNumberPicker` implementation for hours. + """ + + # military = BooleanProperty(False) + + def __init__(self, **kw): + super(CircularHourPicker, self).__init__(**kw) + self.min = 1 + self.max = 13 + # 25 if self.military else 13 + # self.inner_radius_hint = .8 if self.military else .6 + self.multiples_of = 1 + self.number_format_string = "{}" + self.direction = "cw" + self.bind(shown_items=self._update_start_angle) + # self.bind(military=lambda v: setattr(self, "max", 25 if v else 13)) + # self.bind(military=lambda v: setattr(self, "inner_radius_hint", .8 if self.military else .6)) + # Clock.schedule_once(self._genitems) + Clock.schedule_once(self._update_start_angle) + Clock.schedule_once(self.on_selected) + + def _update_start_angle(self, *a): + self.start_angle = (360. / self.shown_items / 2) - 90 + + +class CircularTimePicker(BoxLayout, ThemableBehavior): + """Widget that makes use of :class:`CircularHourPicker` and + :class:`CircularMinutePicker` to create a user-friendly, animated + time picker like the one seen on Android. + + See module documentation for more details. + """ + + primary_dark = ListProperty([1, 1, 1]) + + hours = NumericProperty(0) + """The hours, in military format (0-23). + + :attr:`hours` is a :class:`~kivy.properties.NumericProperty` and + defaults to 0 (12am). + """ + + minutes = NumericProperty(0) + """The minutes. + + :attr:`minutes` is a :class:`~kivy.properties.NumericProperty` and + defaults to 0. + """ + + time_list = ReferenceListProperty(hours, minutes) + """Packs :attr:`hours` and :attr:`minutes` in a list for convenience. + + :attr:`time_list` is a :class:`~kivy.properties.ReferenceListProperty`. + """ + + # military = BooleanProperty(False) + time_format = StringProperty( + "[color={hours_color}][ref=hours]{hours}[/ref][/color][color={primary_dark}][ref=colon]:[/ref][/color]\ +[color={minutes_color}][ref=minutes]{minutes:02d}[/ref][/color]") + """String that will be formatted with the time and shown in the time label. + Can be anything supported by :meth:`str.format`. Make sure you don't + remove the refs. See the default for the arguments passed to format. + :attr:`time_format` is a :class:`~kivy.properties.StringProperty` and + defaults to "[color={hours_color}][ref=hours]{hours}[/ref][/color]:[color={minutes_color}][ref=minutes]\ + {minutes:02d}[/ref][/color]". + """ + + ampm_format = StringProperty( + "[color={am_color}][ref=am]AM[/ref][/color]\n[color={pm_color}][ref=pm]PM[/ref][/color]") + """String that will be formatted and shown in the AM/PM label. + Can be anything supported by :meth:`str.format`. Make sure you don't + remove the refs. See the default for the arguments passed to format. + + :attr:`ampm_format` is a :class:`~kivy.properties.StringProperty` and + defaults to "[color={am_color}][ref=am]AM[/ref][/color]\n[color={pm_color}][ref=pm]PM[/ref][/color]". + """ + + picker = OptionProperty("hours", options=("minutes", "hours")) + """Currently shown time picker. Can be one of "minutes", "hours". + + :attr:`picker` is a :class:`~kivy.properties.OptionProperty` and + defaults to "hours". + """ + + # selector_color = ListProperty([.337, .439, .490]) + selector_color = ListProperty([0, 0, 0]) + """Color of the number selector and of the highlighted text. RGB. + + :attr:`selector_color` is a :class:`~kivy.properties.ListProperty` and + defaults to [.337, .439, .490] (material green). + """ + + color = ListProperty([1, 1, 1]) + """Color of the number labels and of the center dot. RGB. + + :attr:`color` is a :class:`~kivy.properties.ListProperty` and + defaults to [1, 1, 1] (white). + """ + + selector_alpha = BoundedNumericProperty(.3, min=0, max=1) + """Alpha value for the transparent parts of the selector. + + :attr:`selector_alpha` is a :class:`~kivy.properties.BoundedNumericProperty` and + defaults to 0.3 (min=0, max=1). + """ + + _am = BooleanProperty(True) + _h_picker = ObjectProperty(None) + _m_picker = ObjectProperty(None) + _bound = DictProperty({}) + + def _get_time(self): + try: + return datetime.time(*self.time_list) + except ValueError: + self.time_list = [self.hours, 0] + return datetime.time(*self.time_list) + + def set_time(self, dt): + if dt.hour >= 12: + dt.strftime("%I:%M") + self._am = False + self.time_list = [dt.hour, dt.minute] + + time = AliasProperty(_get_time, set_time, bind=("time_list",)) + """Selected time as a datetime.time object. + + :attr:`time` is an :class:`~kivy.properties.AliasProperty`. + """ + + def _get_picker(self): + if self.picker == "hours": + return self._h_picker + return self._m_picker + + _picker = AliasProperty(_get_picker, None) + + def _get_time_text(self): + hc = rgb_to_hex(0, 0, 0) if self.picker == "hours" else rgb_to_hex(*self.primary_dark) + mc = rgb_to_hex(0, 0, 0) if self.picker == "minutes" else rgb_to_hex(*self.primary_dark) + h = self.hours == 0 and 12 or self.hours <= 12 and self.hours or self.hours - 12 + m = self.minutes + primary_dark = rgb_to_hex(*self.primary_dark) + return self.time_format.format(hours_color=hc, + minutes_color=mc, + hours=h, + minutes=m, + primary_dark=primary_dark) + time_text = AliasProperty(_get_time_text, None, bind=("hours", "minutes", "time_format", "picker")) + + def _get_ampm_text(self, *args): + amc = rgb_to_hex(0, 0, 0) if self._am else rgb_to_hex(*self.primary_dark) + pmc = rgb_to_hex(0, 0, 0) if not self._am else rgb_to_hex(*self.primary_dark) + return self.ampm_format.format(am_color=amc, + pm_color=pmc) + + ampm_text = AliasProperty(_get_ampm_text, None, bind=("hours", "ampm_format", "_am")) + + def __init__(self, **kw): + super(CircularTimePicker, self).__init__(**kw) + self.selector_color = self.theme_cls.primary_color[0], self.theme_cls.primary_color[1], \ + self.theme_cls.primary_color[2] + self.color = self.theme_cls.text_color + self.primary_dark = self.theme_cls.primary_dark[0] / 2, self.theme_cls.primary_dark[1] / 2, \ + self.theme_cls.primary_dark[2] / 2 + self.on_ampm() + if self.hours >= 12: + self._am = False + self.bind(time_list=self.on_time_list, + picker=self._switch_picker, + _am=self.on_ampm, + primary_dark=self._get_ampm_text) + self._h_picker = CircularHourPicker() + self.h_picker_touch = False + self._m_picker = CircularMinutePicker() + self.animating = False + Clock.schedule_once(self.on_selected) + Clock.schedule_once(self.on_time_list) + Clock.schedule_once(self._init_later) + Clock.schedule_once(lambda *a: self._switch_picker(noanim=True)) + + def _init_later(self, *args): + self.ids.timelabel.bind(on_ref_press=self.on_ref_press) + self.ids.ampmlabel.bind(on_ref_press=self.on_ref_press) + + def on_ref_press(self, ign, ref): + if not self.animating: + if ref == "hours": + self.picker = "hours" + elif ref == "minutes": + self.picker = "minutes" + if ref == "am": + self._am = True + elif ref == "pm": + self._am = False + + def on_selected(self, *a): + if not self._picker: + return + if self.picker == "hours": + hours = self._picker.selected if self._am else self._picker.selected + 12 + if hours == 24 and not self._am: + hours = 12 + elif hours == 12 and self._am: + hours = 0 + self.hours = hours + elif self.picker == "minutes": + self.minutes = self._picker.selected + + def on_time_list(self, *a): + if not self._picker: + return + self._h_picker.selected = self.hours == 0 and 12 or self._am and self.hours or self.hours - 12 + self._m_picker.selected = self.minutes + self.on_selected() + + def on_ampm(self, *a): + if self._am: + self.hours = self.hours if self.hours < 12 else self.hours - 12 + else: + self.hours = self.hours if self.hours >= 12 else self.hours + 12 + + def is_animating(self, *args): + self.animating = True + + def is_not_animating(self, *args): + self.animating = False + + def on_touch_down(self, touch): + if not self._h_picker.collide_point(*touch.pos): + self.h_picker_touch = False + else: + self.h_picker_touch = True + super(CircularTimePicker, self).on_touch_down(touch) + + def on_touch_up(self, touch): + try: + if not self.h_picker_touch: + return + if not self.animating: + if touch.grab_current is not self: + if self.picker == "hours": + self.picker = "minutes" + except AttributeError: + pass + super(CircularTimePicker, self).on_touch_up(touch) + + def _switch_picker(self, *a, **kw): + noanim = "noanim" in kw + if noanim: + noanim = kw["noanim"] + + try: + container = self.ids.picker_container + except (AttributeError, NameError): + Clock.schedule_once(lambda *a: self._switch_picker(noanim=noanim)) + + if self.picker == "hours": + picker = self._h_picker + prevpicker = self._m_picker + elif self.picker == "minutes": + picker = self._m_picker + prevpicker = self._h_picker + + if len(self._bound) > 0: + prevpicker.unbind(selected=self.on_selected) + self.unbind(**self._bound) + picker.bind(selected=self.on_selected) + self._bound = {"selector_color": picker.setter("selector_color"), + "color": picker.setter("color"), + "selector_alpha": picker.setter("selector_alpha")} + self.bind(**self._bound) + + if len(container._bound) > 0: + container.unbind(**container._bound) + container._bound = {"size": picker.setter("size"), + "pos": picker.setter("pos")} + container.bind(**container._bound) + + picker.pos = container.pos + picker.size = container.size + picker.selector_color = self.selector_color + picker.color = self.color + picker.selector_alpha = self.selector_alpha + if noanim: + if prevpicker in container.children: + container.remove_widget(prevpicker) + if picker.parent: + picker.parent.remove_widget(picker) + container.add_widget(picker) + else: + self.is_animating() + if prevpicker in container.children: + anim = Animation(scale=1.5, d=.5, t="in_back") & Animation(opacity=0, d=.5, t="in_cubic") + anim.start(prevpicker) + Clock.schedule_once(lambda *y: container.remove_widget(prevpicker), .5) # .31) + picker.scale = 1.5 + picker.opacity = 0 + if picker.parent: + picker.parent.remove_widget(picker) + container.add_widget(picker) + anim = Animation(scale=1, d=.5, t="out_back") & Animation(opacity=1, d=.5, t="out_cubic") + anim.bind(on_complete=self.is_not_animating) + Clock.schedule_once(lambda *y: anim.start(picker), .3) + + +if __name__ == "__main__": + from kivy.base import runTouchApp + + c = CircularTimePicker() + runTouchApp(c) diff --git a/src/network/knownnodes.py b/src/knownnodes.py similarity index 79% rename from src/network/knownnodes.py rename to src/knownnodes.py index c53be2cd..bb588fcb 100644 --- a/src/network/knownnodes.py +++ b/src/knownnodes.py @@ -1,27 +1,19 @@ """ Manipulations with knownNodes dictionary. """ -# TODO: knownnodes object maybe? -# pylint: disable=global-statement import json import logging import os -import pickle # nosec B403 +import pickle import threading import time -try: - from collections.abc import Iterable -except ImportError: - from collections import Iterable import state -from bmconfigparser import config +from bmconfigparser import BMConfigParser from network.node import Peer -state.Peer = Peer - -knownNodesLock = threading.RLock() +knownNodesLock = threading.Lock() """Thread lock for knownnodes modification""" knownNodes = {stream: {} for stream in range(1, 4)} """The dict of known nodes for each stream""" @@ -67,12 +59,13 @@ def json_deserialize_knownnodes(source): """ Read JSON from source and make knownnodes dict """ - global knownNodesActual + global knownNodesActual # pylint: disable=global-statement 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 @@ -84,8 +77,8 @@ def pickle_deserialize_old_knownnodes(source): the old format was {Peer:lastseen, ...} the new format is {Peer:{"lastseen":i, "rating":f}} """ - global knownNodes - knownNodes = pickle.load(source) # nosec B301 + global knownNodes # pylint: disable=global-statement + knownNodes = pickle.load(source) for stream in knownNodes.keys(): for node, params in knownNodes[stream].iteritems(): if isinstance(params, (float, int)): @@ -102,44 +95,12 @@ def saveKnownNodes(dirName=None): 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 - + """Add a new node to the dict""" knownNodes[stream][peer] = { - 'lastseen': lastseen, - 'rating': rating or 1 if is_self else 0, - 'self': is_self, + "lastseen": lastseen or time.time(), + "rating": 1 if is_self else 0, + "self": is_self, } - return True def createDefaultKnownNodes(): @@ -165,6 +126,8 @@ def readKnownNodes(): 'Failed to read nodes from knownnodes.dat', exc_info=True) createDefaultKnownNodes() + config = BMConfigParser() + # your own onion address, if setup onionhostname = config.safeGet('bitmessagesettings', 'onionhostname') if onionhostname and ".onion" in onionhostname: @@ -208,7 +171,7 @@ def decreaseRating(peer): def trimKnownNodes(recAddrStream=1): """Triming Knownnodes""" if len(knownNodes[recAddrStream]) < \ - config.safeGetInt("knownnodes", "maxnodes"): + BMConfigParser().safeGetInt("knownnodes", "maxnodes"): return with knownNodesLock: oldestList = sorted( @@ -226,23 +189,20 @@ def dns(): 1, Peer('bootstrap%s.bitmessage.org' % port, port)) -def cleanupKnownNodes(pool): +def cleanupKnownNodes(): """ 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: + if stream not in state.streamsInWhichIAmParticipating: 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"] diff --git a/src/l10n.py b/src/l10n.py index fe02d3f4..7a78525b 100644 --- a/src/l10n.py +++ b/src/l10n.py @@ -1,33 +1,21 @@ -"""Localization helpers""" - +""" +Localization +""" import logging import os -import re -import sys import time -from six.moves import range - -from bmconfigparser import config +from bmconfigparser import BMConfigParser logger = logging.getLogger('default') + DEFAULT_ENCODING = 'ISO8859-1' DEFAULT_LANGUAGE = 'en_US' DEFAULT_TIME_FORMAT = '%Y-%m-%d %H:%M:%S' -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 - +encoding = DEFAULT_ENCODING +language = DEFAULT_LANGUAGE windowsLanguageMap = { "ar": "arabic", @@ -52,57 +40,61 @@ windowsLanguageMap = { "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 not re.search(r'\d', time.strftime(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: time_format = DEFAULT_TIME_FORMAT -# It seems some systems lie about the encoding they use -# so we perform comprehensive decoding tests -elif sys.version_info[0] == 2: +# It seems some systems lie about the encoding they use so we perform +# comprehensive decoding tests +if time_format != DEFAULT_TIME_FORMAT: try: # Check day names - for i in range(7): - time.strftime( - time_format, (0, 0, 0, 0, 0, 0, i, 0, 0)).decode(encoding) + 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 range(1, 13): - time.strftime( - time_format, (0, i, 0, 0, 0, 0, 0, 0, 0)).decode(encoding) + for i in xrange(1, 13): + unicode(time.strftime(time_format, (0, i, 0, 0, 0, 0, 0, 0, 0)), 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) + 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 - time.strftime( - time_format, (0, 0, 0, 0, 0, 0, 0, 0, 1)).decode(encoding) - except Exception: # TODO: write tests and determine exception types + unicode(time.strftime(time_format, (0, 0, 0, 0, 0, 0, 0, 0, 1)), encoding) + except: logger.exception('Could not decode locale formatted timestamp') - # time_format = DEFAULT_TIME_FORMAT + time_format = DEFAULT_TIME_FORMAT encoding = DEFAULT_ENCODING -def setlocale(newlocale): +def setlocale(category, newlocale): """Set the locale""" - try: - locale.setlocale(locale.LC_ALL, newlocale) - except AttributeError: # locale is None - pass + locale.setlocale(category, newlocale) # 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): +def formatTimestamp(timestamp=None, as_unicode=True): """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 (ValueError, TypeError): + except: timestamp = None # timestamp can't be less than 0. @@ -118,14 +110,14 @@ def formatTimestamp(timestamp=None): except ValueError: timestring = time.strftime(time_format) - if sys.version_info[0] == 2: - return timestring.decode(encoding) + if as_unicode: + return unicode(timestring, encoding) return timestring def getTranslationLanguage(): """Return the user's language choice""" - userlocale = config.safeGet( + userlocale = BMConfigParser().safeGet( 'bitmessagesettings', 'userlocale', 'system') return userlocale if userlocale and userlocale != 'system' else language diff --git a/src/main-android-live.py b/src/main-android-live.py deleted file mode 100644 index e1644436..00000000 --- a/src/main-android-live.py +++ /dev/null @@ -1,13 +0,0 @@ -"""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 index ce042b84..71a4cb50 100644 --- a/src/main.py +++ b/src/main.py @@ -1,31 +1,8 @@ -# 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 +"""This module is for thread start.""" import state +from bitmessagemain import main -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" +if __name__ == '__main__': + state.kivy = True + print("Kivy Loading......") main() diff --git a/src/messagetypes/__init__.py b/src/messagetypes/__init__.py index b9ddd1e9..4cae718c 100644 --- a/src/messagetypes/__init__.py +++ b/src/messagetypes/__init__.py @@ -1,54 +1,55 @@ import logging - from importlib import import_module +from os import listdir, path +from string import lower + +import messagetypes +import paths logger = logging.getLogger('default') +class MsgBase(object): # pylint: disable=too-few-public-methods + """Base class for message types""" + def __init__(self): + self.data = {"": lower(type(self).__name__)} + + def constructObject(data): """Constructing an object""" whitelist = ["message"] if data[""] not in whitelist: return None try: - classBase = getattr(import_module(".{}".format(data[""]), __name__), data[""].title()) - except (NameError, AttributeError, ValueError, ImportError): + classBase = getattr(getattr(messagetypes, data[""]), data[""].title()) + except (NameError, AttributeError): 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: # noqa:E722 + except: logger.error("classBase fail", exc_info=True) return None else: return returnObj -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 +if paths.frozen is not None: + import messagetypes.message + import messagetypes.vote else: - import os - for mod in os.listdir(os.path.dirname(__file__)): + for mod in listdir(path.dirname(__file__)): if mod == "__init__.py": continue - splitted = os.path.splitext(mod) + splitted = path.splitext(mod) if splitted[1] != ".py": continue try: - import_module(".{}".format(splitted[0]), __name__) + import_module(".{}".format(splitted[0]), "messagetypes") 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 245c753f..573732d4 100644 --- a/src/messagetypes/message.py +++ b/src/messagetypes/message.py @@ -1,14 +1,10 @@ import logging +from messagetypes import MsgBase + logger = logging.getLogger('default') -class MsgBase(object): # pylint: disable=too-few-public-methods - """Base class for message types""" - def __init__(self): - self.data = {"": type(self).__name__.lower()} - - class Message(MsgBase): """Encapsulate a message""" # pylint: disable=attribute-defined-outside-init @@ -16,27 +12,23 @@ class Message(MsgBase): def decode(self, data): """Decode a message""" # UTF-8 and variable type validator - subject = data.get("subject", "") - body = data.get("body", "") - try: - data["subject"] = subject.decode('utf-8', 'replace') - except: - data["subject"] = '' - - try: - data["body"] = body.decode('utf-8', 'replace') - except: - data["body"] = '' - - self.subject = data["subject"] - self.body = data["body"] + if isinstance(data["subject"], str): + self.subject = unicode(data["subject"], 'utf-8', 'replace') + else: + self.subject = unicode(str(data["subject"]), 'utf-8', 'replace') + if isinstance(data["body"], str): + self.body = unicode(data["body"], 'utf-8', 'replace') + else: + self.body = unicode(str(data["body"]), 'utf-8', 'replace') def encode(self, data): """Encode a message""" super(Message, self).__init__() - self.data["subject"] = data.get("subject", "") - self.data["body"] = data.get("body", "") - + try: + self.data["subject"] = data["subject"] + self.data["body"] = data["body"] + except KeyError as e: + logger.error("Missing key %s", e) return self.data def process(self): diff --git a/src/messagetypes/vote.py b/src/messagetypes/vote.py index b3e96513..b559c256 100644 --- a/src/messagetypes/vote.py +++ b/src/messagetypes/vote.py @@ -1,6 +1,6 @@ import logging -from .message import MsgBase +from messagetypes import MsgBase logger = logging.getLogger('default') diff --git a/src/mockbm/__init__.py b/src/mockbm/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/mockbm/class_addressGenerator.py b/src/mockbm/class_addressGenerator.py deleted file mode 100644 index c84b92d5..00000000 --- a/src/mockbm/class_addressGenerator.py +++ /dev/null @@ -1,98 +0,0 @@ -""" -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 deleted file mode 100644 index 838302ae..00000000 --- a/src/mockbm/helper_startup.py +++ /dev/null @@ -1,13 +0,0 @@ -import os -from pybitmessage.bmconfigparser import config - - -def loadConfig(): - """Loading mock test data""" - config.read(os.path.join(os.environ['BITMESSAGE_HOME'], 'keys.dat')) - - -def total_encrypted_messages_per_month(): - """Loading mock total encrypted message """ - encrypted_messages_per_month = 0 - return encrypted_messages_per_month diff --git a/src/mockbm/kivy_main.py b/src/mockbm/kivy_main.py deleted file mode 100644 index 4cf4da98..00000000 --- a/src/mockbm/kivy_main.py +++ /dev/null @@ -1,40 +0,0 @@ -# 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 deleted file mode 100644 index 8ec76920..00000000 --- a/src/mockbm/multiqueue.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -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 deleted file mode 100644 index 3f33c91b..00000000 --- a/src/mockbm/network.py +++ /dev/null @@ -1,25 +0,0 @@ -# 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/network/multiqueue.py b/src/multiqueue.py similarity index 84% rename from src/network/multiqueue.py rename to src/multiqueue.py index 3fad4e34..d7c10847 100644 --- a/src/network/multiqueue.py +++ b/src/multiqueue.py @@ -2,13 +2,14 @@ A queue with multiple internal subqueues. Elements are added into a random subqueue, and retrieval rotates """ -import random + +import Queue from collections import deque -from six.moves import queue +import helper_random -class MultiQueue(queue.Queue): +class MultiQueue(Queue.Queue): """A base queue class""" # pylint: disable=redefined-builtin,attribute-defined-outside-init defaultQueueCount = 10 @@ -18,7 +19,7 @@ class MultiQueue(queue.Queue): 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): @@ -33,8 +34,7 @@ class MultiQueue(queue.Queue): # Put a new item in the queue def _put(self, item): # self.queue.append(item) - self.queues[random.randrange(self.queueCount)].append( # nosec B311 - (item)) + self.queues[helper_random.randomrandrange(self.queueCount)].append((item)) # Get an item from the queue def _get(self): diff --git a/src/namecoin.py b/src/namecoin.py index a16cb3d7..ae2bde79 100644 --- a/src/namecoin.py +++ b/src/namecoin.py @@ -11,10 +11,11 @@ import socket import sys import defaults +import tr # translate from addresses import decodeAddress -from bmconfigparser import config +from bmconfigparser import BMConfigParser from debug import logger -from tr import _translate # translate + configSection = "bitmessagesettings" @@ -29,7 +30,7 @@ class RPCError(Exception): 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): @@ -52,16 +53,12 @@ class namecoinConnection(object): actually changing the values (yet). """ if options is None: - 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") + 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") else: self.nmctype = options["type"] self.host = options["host"] @@ -73,31 +70,31 @@ class namecoinConnection(object): if self.nmctype == "namecoind": self.con = httplib.HTTPConnection(self.host, self.port, timeout=3) - def query(self, identity): + def query(self, string): """ 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("/") + slashPos = string.find("/") if slashPos < 0: - display_name = identity - identity = "id/" + identity + display_name = string + string = "id/" + string else: - display_name = identity.split("/")[1] + display_name = string.split("/")[1] try: if self.nmctype == "namecoind": - res = self.callRPC("name_show", [identity]) + res = self.callRPC("name_show", [string]) res = res["value"] elif self.nmctype == "nmcontrol": - res = self.callRPC("data", ["getValue", identity]) + res = self.callRPC("data", ["getValue", string]) res = res["reply"] if not res: - return (_translate( - "MainWindow", "The name %1 was not found." - ).arg(identity.decode("utf-8", "ignore")), None) + return (tr._translate( + "MainWindow", 'The name %1 was not found.' + ).arg(unicode(string)), None) else: assert False except RPCError as exc: @@ -106,17 +103,17 @@ class namecoinConnection(object): errmsg = exc.error["message"] else: errmsg = exc.error - return (_translate( - "MainWindow", "The namecoin query failed (%1)" - ).arg(errmsg.decode("utf-8", "ignore")), None) + return (tr._translate( + "MainWindow", 'The namecoin query failed (%1)' + ).arg(unicode(errmsg)), None) except AssertionError: - return (_translate( - "MainWindow", "Unknown namecoin interface type: %1" - ).arg(self.nmctype.decode("utf-8", "ignore")), None) + return (tr._translate( + "MainWindow", 'Unknown namecoin interface type: %1' + ).arg(unicode(self.nmctype)), None) except Exception: logger.exception("Namecoin query exception") - return (_translate( - "MainWindow", "The namecoin query failed."), None) + return (tr._translate( + "MainWindow", 'The namecoin query failed.'), None) try: res = json.loads(res) @@ -129,14 +126,14 @@ class namecoinConnection(object): pass res = res.get("bitmessage") - valid = decodeAddress(res)[0] == "success" + valid = decodeAddress(res)[0] == 'success' return ( None, "%s <%s>" % (display_name, res) ) if valid else ( - _translate( + tr._translate( "MainWindow", - "The name %1 has no associated Bitmessage address." - ).arg(identity.decode("utf-8", "ignore")), None) + 'The name %1 has no associated Bitmessage address.' + ).arg(unicode(string)), None) def test(self): """ @@ -161,43 +158,32 @@ class namecoinConnection(object): else: versStr = "0.%d.%d.%d" % (v1, v2, v3) message = ( - "success", - _translate( + 'success', + tr._translate( "MainWindow", - "Success! Namecoind version %1 running.").arg( - versStr.decode("utf-8", "ignore"))) + 'Success! Namecoind version %1 running.').arg( + unicode(versStr))) elif self.nmctype == "nmcontrol": res = self.callRPC("data", ["status"]) prefix = "Plugin data running" if ("reply" in res) and res["reply"][:len(prefix)] == prefix: - return ( - "success", - _translate( - "MainWindow", - "Success! NMControll is up and running." - ) - ) + return ('success', tr._translate("MainWindow", 'Success! NMControll is up and running.')) logger.error("Unexpected nmcontrol reply: %s", res) - message = ( - "failed", - _translate( - "MainWindow", - "Couldn\'t understand NMControl." - ) - ) + message = ('failed', tr._translate("MainWindow", 'Couldn\'t understand NMControl.')) else: - sys.exit("Unsupported Namecoin type") + print "Unsupported Namecoin type" + sys.exit(1) return message except Exception: logger.info("Namecoin connection test failure") return ( - "failed", - _translate( + 'failed', + tr._translate( "MainWindow", "The connection to namecoin failed.") ) @@ -241,30 +227,23 @@ class namecoinConnection(object): 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) - except: # noqa:E722 + 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: 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 def queryServer(self, data): - """Helper routine sending data to the RPC " - "server and returning the result.""" + """Helper routine sending data to the RPC server and returning the result.""" try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -300,12 +279,13 @@ def lookupNamecoinFolder(): if sys.platform == "darwin": if "HOME" in environ: dataFolder = path.join(os.environ["HOME"], - "Library/Application Support/", app) + "/" + "Library/Application Support/", app) + '/' else: - sys.exit( + print( "Could not find home folder, please report this message" " and your OS X version to the BitMessage Github." ) + sys.exit() elif "win32" in sys.platform or "win64" in sys.platform: dataFolder = path.join(environ["APPDATA"], app) + "\\" @@ -321,14 +301,14 @@ def ensureNamecoinOptions(): 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") + 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 = config.has_option(configSection, "namecoinrpcuser") - hasPass = config.has_option(configSection, "namecoinrpcpassword") - hasPort = config.has_option(configSection, "namecoinrpcport") + hasUser = BMConfigParser().has_option(configSection, "namecoinrpcuser") + hasPass = BMConfigParser().has_option(configSection, "namecoinrpcpassword") + hasPort = BMConfigParser().has_option(configSection, "namecoinrpcport") # Try to read user/password from .namecoin configuration file. defaultUser = "" @@ -356,18 +336,17 @@ def ensureNamecoinOptions(): nmc.close() except IOError: - logger.warning( - "%s unreadable or missing, Namecoin support deactivated", - nmcConfig) + 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: - config.set(configSection, "namecoinrpcuser", defaultUser) + BMConfigParser().set(configSection, "namecoinrpcuser", defaultUser) if not hasPass: - config.set(configSection, "namecoinrpcpassword", defaultPass) + BMConfigParser().set(configSection, "namecoinrpcpassword", defaultPass) # Set default port now, possibly to found value. if not hasPort: - config.set(configSection, "namecoinrpcport", defaults.namecoinDefaultRpcPort) + BMConfigParser().set(configSection, "namecoinrpcport", + defaults.namecoinDefaultRpcPort) diff --git a/src/navigationdrawer/__init__.py b/src/navigationdrawer/__init__.py new file mode 100644 index 00000000..a8fa5ce7 --- /dev/null +++ b/src/navigationdrawer/__init__.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +from kivy.animation import Animation +from kivy.lang import Builder +from kivy.properties import StringProperty, ObjectProperty +from kivymd.elevationbehavior import ElevationBehavior +from kivymd.icon_definitions import md_icons +from kivymd.label import MDLabel +from kivymd.list import OneLineIconListItem, ILeftBody, BaseListItem +from kivymd.slidingpanel import SlidingPanel +from kivymd.theming import ThemableBehavior + +Builder.load_string(''' + + canvas: + Color: + rgba: root.parent.parent.theme_cls.divider_color + Line: + points: self.x, self.y, self.x+self.width,self.y + + + widget_list: widget_list + elevation: 0 + canvas: + Color: + rgba: root.theme_cls.bg_light + Rectangle: + size: root.size + pos: root.pos + BoxLayout: + size_hint: (1, .4) + NavDrawerToolbar: + padding: 10, 10 + canvas.after: + Color: + rgba: (1, 1, 1, 1) + RoundedRectangle: + size: (self.size[1]-dp(14), self.size[1]-dp(14)) + pos: (self.pos[0]+(self.size[0]-self.size[1])/2, self.pos[1]+dp(7)) + source: root.image_source + radius: [self.size[1]-(self.size[1]/2)] + + ScrollView: + do_scroll_x: False + MDList: + id: ml + id: widget_list + + + NDIconLabel: + id: _icon + font_style: 'Icon' + theme_text_color: 'Secondary' +''') + + +class NavigationDrawer(SlidingPanel, ThemableBehavior, ElevationBehavior): + image_source = StringProperty() + widget_list = ObjectProperty() + + def add_widget(self, widget, index=0): + if issubclass(widget.__class__, BaseListItem): + self.widget_list.add_widget(widget, index) + widget.bind(on_release=lambda x: self.toggle()) + else: + super(NavigationDrawer, self).add_widget(widget, index) + + def _get_main_animation(self, duration, t, x, is_closing): + a = super(NavigationDrawer, self)._get_main_animation(duration, t, x, + is_closing) + a &= Animation(elevation=0 if is_closing else 5, t=t, duration=duration) + return a + + +class NDIconLabel(ILeftBody, MDLabel): + pass + + +class NavigationDrawerIconButton(OneLineIconListItem): + icon = StringProperty() + + def on_icon(self, instance, value): + self.ids['_icon'].text = u"{}".format(md_icons[value]) diff --git a/src/network/__init__.py b/src/network/__init__.py index c87ad64d..70613539 100644 --- a/src/network/__init__.py +++ b/src/network/__init__.py @@ -1,54 +1,20 @@ """ -Network subsystem package +Network subsystem packages """ -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"] +from addrthread import AddrThread +from announcethread import AnnounceThread +from connectionpool import BMConnectionPool +from dandelion import Dandelion +from downloadthread import DownloadThread +from invthread import InvThread +from networkthread import BMNetworkThread +from receivequeuethread import ReceiveQueueThread +from threads import StoppableThread +from uploadthread import UploadThread -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() +__all__ = [ + "BMConnectionPool", "Dandelion", + "AddrThread", "AnnounceThread", "BMNetworkThread", "DownloadThread", + "InvThread", "ReceiveQueueThread", "UploadThread", "StoppableThread" +] diff --git a/src/network/addrthread.py b/src/network/addrthread.py index 81e44506..3bf448d8 100644 --- a/src/network/addrthread.py +++ b/src/network/addrthread.py @@ -1,14 +1,13 @@ """ Announce addresses as they are received from other hosts """ -import random -from six.moves import queue - -# magic imports! -import connectionpool -from protocol import assembleAddrMessage -from network import addrQueue # FIXME: init with queue +import Queue +import state +from helper_random import randomshuffle +from network.assemble import assemble_addr +from network.connectionpool import BMConnectionPool +from queues import addrQueue from threads import StoppableThread @@ -17,21 +16,21 @@ class AddrThread(StoppableThread): name = "AddrBroadcaster" def run(self): - while not self._stopped: + while not state.shutdown: chunk = [] while True: try: data = addrQueue.get(False) chunk.append(data) - except queue.Empty: + except Queue.Empty: break if chunk: # Choose peers randomly - connections = connectionpool.pool.establishedConnections() - random.shuffle(connections) + connections = BMConnectionPool().establishedConnections() + randomshuffle(connections) for i in connections: - random.shuffle(chunk) + randomshuffle(chunk) filtered = [] for stream, peer, seen, destination in chunk: # peer's own address or address received from peer @@ -41,7 +40,7 @@ class AddrThread(StoppableThread): continue filtered.append((stream, peer, seen)) if filtered: - i.append_write_buf(assembleAddrMessage(filtered)) + i.append_write_buf(assemble_addr(filtered)) addrQueue.iterate() for i in range(len(chunk)): diff --git a/src/network/advanceddispatcher.py b/src/network/advanceddispatcher.py index 49f0d19d..982be819 100644 --- a/src/network/advanceddispatcher.py +++ b/src/network/advanceddispatcher.py @@ -1,6 +1,7 @@ """ Improved version of asyncore dispatcher """ +# pylint: disable=attribute-defined-outside-init import socket import threading import time @@ -30,8 +31,6 @@ class AdvancedDispatcher(asyncore.dispatcher): 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" @@ -42,7 +41,6 @@ class AdvancedDispatcher(asyncore.dispatcher): 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.""" diff --git a/src/network/announcethread.py b/src/network/announcethread.py index 7cb35e77..19038ab6 100644 --- a/src/network/announcethread.py +++ b/src/network/announcethread.py @@ -3,11 +3,11 @@ Announce myself (node address) """ import time -# magic imports! -import connectionpool -from bmconfigparser import config -from protocol import assembleAddrMessage - +import state +from bmconfigparser import BMConfigParser +from network.assemble import assemble_addr +from network.connectionpool import BMConnectionPool +from network.udp import UDPSocket from node import Peer from threads import StoppableThread @@ -15,13 +15,12 @@ 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: + while not self._stopped and state.shutdown == 0: processed = 0 - if lastSelfAnnounced < time.time() - self.announceInterval: + if lastSelfAnnounced < time.time() - UDPSocket.announceInterval: self.announceSelf() lastSelfAnnounced = time.time() if processed == 0: @@ -30,14 +29,15 @@ class AnnounceThread(StoppableThread): @staticmethod def announceSelf(): """Announce our presence""" - for connection in connectionpool.pool.udpSockets.values(): + for connection in BMConnectionPool().udpSockets.values(): if not connection.announcing: continue - for stream in connectionpool.pool.streams: + for stream in state.streamsInWhichIAmParticipating: addr = ( stream, Peer( '127.0.0.1', - config.safeGetInt('bitmessagesettings', 'port')), - int(time.time())) - connection.append_write_buf(assembleAddrMessage([addr])) + BMConfigParser().safeGetInt( + 'bitmessagesettings', 'port')), + time.time()) + connection.append_write_buf(assemble_addr([addr])) diff --git a/src/network/assemble.py b/src/network/assemble.py new file mode 100644 index 00000000..32fad3e4 --- /dev/null +++ b/src/network/assemble.py @@ -0,0 +1,31 @@ +""" +Create bitmessage protocol command packets +""" +import struct + +import addresses +from network.constants import MAX_ADDR_COUNT +from network.node import Peer +from protocol import CreatePacket, encodeHost + + +def assemble_addr(peerList): + """Create address command""" + if isinstance(peerList, Peer): + peerList = [peerList] + if not peerList: + return b'' + retval = b'' + for i in range(0, len(peerList), MAX_ADDR_COUNT): + payload = addresses.encodeVarint(len(peerList[i:i + MAX_ADDR_COUNT])) + for stream, peer, timestamp in peerList[i:i + MAX_ADDR_COUNT]: + # 64-bit time + payload += struct.pack('>Q', timestamp) + payload += struct.pack('>I', stream) + # service bit flags offered by this node + payload += struct.pack('>q', 1) + payload += encodeHost(peer.host) + # remote port + payload += struct.pack('>H', peer.port) + retval += CreatePacket('addr', payload) + return retval diff --git a/src/network/asyncore_pollchoose.py b/src/network/asyncore_pollchoose.py index 9d0ffc1b..41757f37 100644 --- a/src/network/asyncore_pollchoose.py +++ b/src/network/asyncore_pollchoose.py @@ -9,7 +9,6 @@ Basic infrastructure for asynchronous socket service clients and servers. import os import select import socket -import random import sys import time import warnings @@ -20,6 +19,7 @@ from errno import ( ) from threading import current_thread +import helper_random try: from errno import WSAEWOULDBLOCK @@ -233,13 +233,13 @@ def select_poller(timeout=0.0, map=None): if err.args[0] in (WSAENOTSOCK, ): return - for fd in random.sample(r, len(r)): + for fd in helper_random.randomsample(r, len(r)): obj = map.get(fd) if obj is None: continue read(obj) - for fd in random.sample(w, len(w)): + for fd in helper_random.randomsample(w, len(w)): obj = map.get(fd) if obj is None: continue @@ -297,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 random.sample(r, len(r)): + for fd, flags in helper_random.randomsample(r, len(r)): obj = map.get(fd) if obj is None: continue @@ -357,7 +357,7 @@ def epoll_poller(timeout=0.0, map=None): if err.args[0] != EINTR: raise r = [] - for fd, flags in random.sample(r, len(r)): + for fd, flags in helper_random.randomsample(r, len(r)): obj = map.get(fd) if obj is None: continue @@ -420,7 +420,7 @@ def kqueue_poller(timeout=0.0, map=None): events = kqueue_poller.pollster.control(updates, selectables, timeout) if len(events) > 1: - events = random.sample(events, len(events)) + events = helper_random.randomsample(events, len(events)) for event in events: fd = event.ident @@ -749,7 +749,7 @@ class dispatcher(object): 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)) + print '%s: %s' % (log_type, message) def handle_read_event(self): """Handle a read event""" @@ -972,8 +972,8 @@ if os.name == 'posix': def getsockopt(self, level, optname, buflen=None): """Fake getsockopt()""" - if (level == socket.SOL_SOCKET and optname == socket.SO_ERROR - and not buflen): + if (level == socket.SOL_SOCKET and optname == socket.SO_ERROR and + not buflen): return 0 raise NotImplementedError( "Only asyncore specific behaviour implemented.") diff --git a/src/network/bmobject.py b/src/network/bmobject.py index 83311b9b..12b997d7 100644 --- a/src/network/bmobject.py +++ b/src/network/bmobject.py @@ -6,9 +6,9 @@ import time import protocol import state -import connectionpool -from network import dandelion_ins -from highlevelcrypto import calculateInventoryHash +from addresses import calculateInventoryHash +from inventory import Inventory +from network.dandelion import Dandelion logger = logging.getLogger('default') @@ -19,6 +19,12 @@ class BMObjectInsufficientPOWError(Exception): errorCodes = ("Insufficient proof of work") +class BMObjectInvalidDataError(Exception): + """Exception indicating the data being parsed + does not match the specification.""" + errorCodes = ("Data invalid") + + class BMObjectExpiredError(Exception): """Exception indicating the object's lifetime has expired.""" errorCodes = ("Object expired") @@ -95,12 +101,7 @@ class BMObject(object): # pylint: disable=too-many-instance-attributes def checkStream(self): """Check if object's stream matches streams we are interested in""" - if self.streamNumber < 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: + if self.streamNumber not in state.streamsInWhichIAmParticipating: logger.debug( 'The streamNumber %i isn\'t one we are interested in.', self.streamNumber) @@ -113,9 +114,9 @@ class BMObject(object): # pylint: disable=too-many-instance-attributes or advertise it unnecessarily) """ # if it's a stem duplicate, pretend we don't have it - if dandelion_ins.hasHash(self.inventoryHash): + if Dandelion().hasHash(self.inventoryHash): return - if self.inventoryHash in state.Inventory: + if self.inventoryHash in Inventory(): raise BMObjectAlreadyHaveError() def checkObjectByType(self): diff --git a/src/network/bmproto.py b/src/network/bmproto.py index e5f38bf5..64bde74c 100644 --- a/src/network/bmproto.py +++ b/src/network/bmproto.py @@ -1,35 +1,38 @@ """ -Class BMProto defines bitmessage's network protocol workflow. +Bitmessage Protocol """ - +# pylint: disable=attribute-defined-outside-init, too-few-public-methods import base64 import hashlib import logging -import re import socket import struct import time +from binascii import hexlify -# magic imports! import addresses +import connectionpool import knownnodes import protocol import state -import connectionpool -from bmconfigparser import config -from queues import objectProcessorQueue -from randomtrackingdict import RandomTrackingDict +from bmconfigparser import BMConfigParser +from inventory import Inventory from network.advanceddispatcher import AdvancedDispatcher from network.bmobject import ( BMObject, BMObjectAlreadyHaveError, BMObjectExpiredError, - BMObjectInsufficientPOWError, BMObjectInvalidError, - BMObjectUnwantedStreamError + BMObjectInsufficientPOWError, BMObjectInvalidDataError, + BMObjectInvalidError, BMObjectUnwantedStreamError ) +from network.constants import ( + ADDRESS_ALIVE, MAX_MESSAGE_SIZE, MAX_OBJECT_COUNT, + MAX_OBJECT_PAYLOAD_SIZE, MAX_TIME_OFFSET +) +from network.dandelion import Dandelion from network.proxy import ProxyError -from network import dandelion_ins, invQueue, portCheckerQueue from node import Node, Peer from objectracker import ObjectTracker, missingObjects - +from queues import invQueue, objectProcessorQueue, portCheckerQueue +from randomtrackingdict import RandomTrackingDict logger = logging.getLogger('default') @@ -63,8 +66,6 @@ class BMProto(AdvancedDispatcher, ObjectTracker): self.pendingUpload = RandomTrackingDict() # canonical identifier of network group self.network_group = None - # userAgent initialization - self.userAgent = '' def bm_proto_reset(self): """Reset the bitmessage object parser""" @@ -83,7 +84,7 @@ class BMProto(AdvancedDispatcher, ObjectTracker): self.magic, self.command, self.payloadLength, self.checksum = \ protocol.Header.unpack(self.read_buf[:protocol.Header.size]) self.command = self.command.rstrip('\x00') - if self.magic != protocol.magic: + if self.magic != 0xE9BEB4D9: # skip 1 byte in order to sync self.set_state("bm_header", length=1) self.bm_proto_reset() @@ -92,14 +93,14 @@ class BMProto(AdvancedDispatcher, ObjectTracker): self.close_reason = "Bad magic" self.set_state("close") return False - if self.payloadLength > protocol.MAX_MESSAGE_SIZE: + if self.payloadLength > MAX_MESSAGE_SIZE: self.invalid = True self.set_state( "bm_command", length=protocol.Header.size, expectBytes=self.payloadLength) return True - def state_bm_command(self): # pylint: disable=too-many-branches + 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]: @@ -125,6 +126,8 @@ class BMProto(AdvancedDispatcher, ObjectTracker): logger.debug('too much data, skipping') except BMObjectInsufficientPOWError: logger.debug('insufficient PoW, skipping') + except BMObjectInvalidDataError: + logger.debug('object invalid data, skipping') except BMObjectExpiredError: logger.debug('object expired, skipping') except BMObjectUnwantedStreamError: @@ -182,7 +185,7 @@ class BMProto(AdvancedDispatcher, ObjectTracker): return Node(services, host, port) - # pylint: disable=too-many-branches,too-many-statements + # pylint: disable=too-many-branches, too-many-statements def decode_payload_content(self, pattern="v"): """ Decode the payload depending on pattern: @@ -199,6 +202,7 @@ class BMProto(AdvancedDispatcher, ObjectTracker): , = end of array """ + # pylint: disable=inconsistent-return-statements def decode_simple(self, char="v"): """Decode the payload using one char pattern""" if char == "v": @@ -217,7 +221,6 @@ class BMProto(AdvancedDispatcher, ObjectTracker): self.payloadOffset += 8 return struct.unpack(">Q", self.payload[ self.payloadOffset - 8:self.payloadOffset])[0] - return None size = None isArray = False @@ -251,11 +254,10 @@ class BMProto(AdvancedDispatcher, ObjectTracker): ]) parserStack[-2][4] = len(parserStack[-2][3]) else: - j = 0 - for j in range( - parserStack[-1][4], len(parserStack[-1][3])): + for j in range(parserStack[-1][4], len(parserStack[-1][3])): if parserStack[-1][3][j] not in "lL0123456789": break + # pylint: disable=undefined-loop-variable parserStack.append([ size, size, isArray, parserStack[-1][3][parserStack[-1][4]:j + 1], 0, [] @@ -266,8 +268,7 @@ class BMProto(AdvancedDispatcher, ObjectTracker): elif i == "s": # if parserStack[-2][2]: # parserStack[-1][5].append(self.payload[ - # self.payloadOffset:self.payloadOffset - # + parserStack[-1][0]]) + # self.payloadOffset:self.payloadOffset + parserStack[-1][0]]) # else: parserStack[-1][5] = self.payload[ self.payloadOffset:self.payloadOffset + parserStack[-1][0]] @@ -337,27 +338,23 @@ class BMProto(AdvancedDispatcher, ObjectTracker): self.pendingUpload[str(i)] = now return True - def _command_inv(self, extend_dandelion_stem=False): - """ - Common inv announce implementation: - both inv and dinv depending on *extend_dandelion_stem* kwarg - """ + def _command_inv(self, dandelion=False): items = self.decode_payload_content("l32s") - if len(items) > protocol.MAX_OBJECT_COUNT: + if len(items) > MAX_OBJECT_COUNT: logger.error( - 'Too many items in %sinv message!', 'd' if extend_dandelion_stem else '') + 'Too many items in %sinv message!', 'd' if dandelion else '') raise BMProtoExcessiveDataError() # ignore dinv if dandelion turned off - if extend_dandelion_stem and not dandelion_ins.enabled: + if dandelion and not state.dandelion: return True for i in map(str, items): - if i in state.Inventory and not dandelion_ins.hasHash(i): + if i in Inventory() and not Dandelion().hasHash(i): continue - if extend_dandelion_stem and not dandelion_ins.hasHash(i): - dandelion_ins.addHash(i, self) + if dandelion and not Dandelion().hasHash(i): + Dandelion().addHash(i, self) self.handleReceivedInventory(i) return True @@ -379,11 +376,10 @@ class BMProto(AdvancedDispatcher, ObjectTracker): nonce, expiresTime, objectType, version, streamNumber, self.payload, self.payloadOffset) - payload_len = len(self.payload) - self.payloadOffset - if payload_len > protocol.MAX_OBJECT_PAYLOAD_SIZE: + if len(self.payload) - self.payloadOffset > MAX_OBJECT_PAYLOAD_SIZE: logger.info( - 'The payload length of this object is too large' - ' (%d bytes). Ignoring it.', payload_len) + 'The payload length of this object is too large (%d bytes).' + ' Ignoring it.', len(self.payload) - self.payloadOffset) raise BMProtoExcessiveDataError() try: @@ -397,20 +393,17 @@ class BMProto(AdvancedDispatcher, ObjectTracker): try: self.object.checkStream() except BMObjectUnwantedStreamError: - acceptmismatch = config.getboolean( + acceptmismatch = BMConfigParser().get( "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))) # noqa: F821 + self.object.objectType, buffer(self.object.data))) except BMObjectInvalidError: BMProto.stopDownloadingObject(self.object.inventoryHash, True) else: @@ -419,15 +412,15 @@ class BMProto(AdvancedDispatcher, ObjectTracker): except KeyError: pass - if self.object.inventoryHash in state.Inventory and dandelion_ins.hasHash( + if self.object.inventoryHash in Inventory() and Dandelion().hasHash( self.object.inventoryHash): - dandelion_ins.removeHash( + Dandelion().removeHash( self.object.inventoryHash, "cycle detection") - state.Inventory[self.object.inventoryHash] = ( + 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 + buffer(self.payload[objectOffset:]), self.object.expiresTime, + buffer(self.object.tag) ) self.handleReceivedObject( self.object.streamNumber, self.object.inventoryHash) @@ -441,33 +434,41 @@ class BMProto(AdvancedDispatcher, ObjectTracker): def bm_command_addr(self): """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') - ): + # pylint: disable=redefined-outer-name + addresses = self._decode_addr() + for seenTime, stream, _, ip, port in addresses: + decodedIP = protocol.checkIPAddress(str(ip)) + if stream not in state.streamsInWhichIAmParticipating: continue - decodedIP = protocol.checkIPAddress(ip) if ( - decodedIP and time.time() - seenTime > 0 - and seenTime > time.time() - protocol.ADDRESS_ALIVE - and port > 0 + decodedIP and time.time() - seenTime > 0 and + seenTime > time.time() - 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)) + try: + if knownnodes.knownNodes[stream][peer]["lastseen"] > \ + seenTime: + continue + except KeyError: + pass + if len(knownnodes.knownNodes[stream]) < \ + BMConfigParser().safeGetInt("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, + } + # since we don't track peers outside of knownnodes, + # only spread if in knownnodes to prevent flood + # DISABLED TO WORKAROUND FLOOD/LEAK + # addrQueue.put((stream, peer, seenTime, + # self.destination)) return True def bm_command_portcheck(self): @@ -480,8 +481,7 @@ class BMProto(AdvancedDispatcher, ObjectTracker): self.append_write_buf(protocol.CreatePacket('pong')) return True - @staticmethod - def bm_command_pong(): + def bm_command_pong(self): # pylint: disable=no-self-use """ Incoming pong. Ignore it. PyBitmessage pings connections after about 5 minutes @@ -512,11 +512,9 @@ class BMProto(AdvancedDispatcher, ObjectTracker): 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.sockNode, self.peerNode, self.nonce, self.userAgent, + self.streams) = self.decode_payload_content("IQQiiQlsLv") self.nonce = struct.pack('>Q', self.nonce) self.timeOffset = self.timestamp - int(time.time()) logger.debug('remoteProtocolVersion: %i', self.remoteProtocolVersion) @@ -533,20 +531,16 @@ class BMProto(AdvancedDispatcher, ObjectTracker): return True 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, - connectionpool.pool.streams, dandelion_ins.enabled, True, + connectionpool.BMConnectionPool().streams, 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)): + if ((self.services & protocol.NODE_SSL == protocol.NODE_SSL) and + protocol.haveSSL(not self.isOutbound)): self.isSSL = True if not self.verackReceived: return True @@ -566,24 +560,22 @@ class BMProto(AdvancedDispatcher, ObjectTracker): 'Closing connection to old protocol version %s, node: %s', self.remoteProtocolVersion, self.destination) return False - if self.timeOffset > protocol.MAX_TIME_OFFSET: + if self.timeOffset > 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) + " Closing connection to it.", self.destination, self.timeOffset) BMProto.timeOffsetWrongCount += 1 return False - elif self.timeOffset < -protocol.MAX_TIME_OFFSET: + elif self.timeOffset < -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) + "%s's time is too far in the past (timeOffset %s seconds)." + " Closing connection to it.", self.destination, self.timeOffset) BMProto.timeOffsetWrongCount += 1 return False else: @@ -596,8 +588,7 @@ class BMProto(AdvancedDispatcher, ObjectTracker): 'Closed connection to %s because there is no overlapping' ' interest in streams.', self.destination) return False - if connectionpool.pool.inboundConnections.get( - self.destination): + if self.destination in connectionpool.BMConnectionPool().inboundConnections: try: if not protocol.checkSocksIP(self.destination.host): self.append_write_buf(protocol.assembleErrorMessage( @@ -607,18 +598,19 @@ class BMProto(AdvancedDispatcher, ObjectTracker): 'Closed connection to %s because we are already' ' connected to that IP.', self.destination) return False - except Exception: # nosec B110 # pylint:disable=broad-exception-caught + except: pass if not self.isOutbound: # incoming from a peer we're connected to as outbound, # or server full report the same error to counter deanonymisation if ( Peer(self.destination.host, self.peerNode.port) - in connectionpool.pool.inboundConnections - or len(connectionpool.pool) - > config.safeGetInt( + in connectionpool.BMConnectionPool().inboundConnections + or len(connectionpool.BMConnectionPool().inboundConnections) + + len(connectionpool.BMConnectionPool().outboundConnections) + > BMConfigParser().safeGetInt( 'bitmessagesettings', 'maxtotalconnections') - + config.safeGetInt( + + BMConfigParser().safeGetInt( 'bitmessagesettings', 'maxbootstrapconnections') ): self.append_write_buf(protocol.assembleErrorMessage( @@ -627,7 +619,8 @@ class BMProto(AdvancedDispatcher, ObjectTracker): 'Closed connection to %s due to server full' ' or duplicate inbound/outbound.', self.destination) return False - if connectionpool.pool.isAlreadyConnected(self.nonce): + if connectionpool.BMConnectionPool().isAlreadyConnected( + self.nonce): self.append_write_buf(protocol.assembleErrorMessage( errorText="I'm connected to myself. Closing connection.", fatal=2)) @@ -640,8 +633,8 @@ class BMProto(AdvancedDispatcher, ObjectTracker): @staticmethod def stopDownloadingObject(hashId, forwardAnyway=False): - """Stop downloading object *hashId*""" - for connection in connectionpool.pool.connections(): + """Stop downloading an object""" + for connection in connectionpool.BMConnectionPool().connections(): try: del connection.objectsNewToMe[hashId] except KeyError: @@ -670,8 +663,36 @@ class BMProto(AdvancedDispatcher, ObjectTracker): except AttributeError: try: logger.debug( - '%s:%i: closing', - self.destination.host, self.destination.port) + '%(host)s:%(port)i: closing', self.destination._asdict()) except AttributeError: logger.debug('Disconnected socket closing') AdvancedDispatcher.handle_close(self) + + +class BMStringParser(BMProto): + """ + A special case of BMProto used by objectProcessor to send ACK + """ + def __init__(self): + super(BMStringParser, self).__init__() + self.destination = Peer('127.0.0.1', 8444) + self.payload = None + ObjectTracker.__init__(self) + + def send_data(self, data): + """Send object given by the data string""" + # This class is introduced specially for ACK sending, please + # change log strings if you are going to use it for something else + self.bm_proto_reset() + self.payload = data + try: + self.bm_command_object() + except BMObjectAlreadyHaveError: + pass # maybe the same msg received on different nodes + except BMObjectExpiredError: + logger.debug( + 'Sending ACK failure (expired): %s', hexlify(data)) + except Exception as e: + logger.debug( + 'Exception of type %s while sending ACK', + type(e), exc_info=True) diff --git a/src/network/connectionchooser.py b/src/network/connectionchooser.py index e2981d51..badd98b7 100644 --- a/src/network/connectionchooser.py +++ b/src/network/connectionchooser.py @@ -3,16 +3,13 @@ Select which node to connect to """ # pylint: disable=too-many-branches import logging -import random - -from six.moves import queue +import random # nosec import knownnodes import protocol import state - -from bmconfigparser import config -from network import portCheckerQueue +from bmconfigparser import BMConfigParser +from queues import Queue, portCheckerQueue logger = logging.getLogger('default') @@ -20,7 +17,7 @@ logger = logging.getLogger('default') def getDiscoveredPeer(): """Get a peer from the local peer discovery list""" try: - peer = random.choice(state.discoveredPeers.keys()) # nosec B311 + peer = random.choice(state.discoveredPeers.keys()) except (IndexError, KeyError): raise ValueError try: @@ -32,23 +29,22 @@ def getDiscoveredPeer(): def chooseConnection(stream): """Returns an appropriate connection""" - haveOnion = config.safeGet( + haveOnion = BMConfigParser().safeGet( "bitmessagesettings", "socksproxytype")[0:5] == 'SOCKS' - onionOnly = config.safeGetBoolean( + onionOnly = BMConfigParser().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 random.choice((False, True)) and not haveOnion: # nosec B311 + if random.choice((False, True)) and not haveOnion: # discovered peers are already filtered by allowed streams return getDiscoveredPeer() for _ in range(50): - peer = random.choice( # nosec B311 - knownnodes.knownNodes[stream].keys()) + peer = random.choice(knownnodes.knownNodes[stream].keys()) try: peer_info = knownnodes.knownNodes[stream][peer] if peer_info.get('self'): @@ -74,7 +70,7 @@ def chooseConnection(stream): if rating > 1: rating = 1 try: - if 0.05 / (1.0 - rating) > random.random(): # nosec B311 + if 0.05 / (1.0 - rating) > random.random(): return peer except ZeroDivisionError: return peer diff --git a/src/network/connectionpool.py b/src/network/connectionpool.py index 519b7b67..6264191d 100644 --- a/src/network/connectionpool.py +++ b/src/network/connectionpool.py @@ -7,16 +7,17 @@ import re import socket import sys import time -import random import asyncore_pollchoose as asyncore +import helper_random import knownnodes import protocol import state -from bmconfigparser import config +from bmconfigparser import BMConfigParser from connectionchooser import chooseConnection from node import Peer from proxy import Proxy +from singleton import Singleton from tcp import ( bootstrap, Socks4aBMConnection, Socks5BMConnection, TCPConnection, TCPServer) @@ -25,6 +26,7 @@ from udp import UDPSocket logger = logging.getLogger('default') +@Singleton class BMConnectionPool(object): """Pool of all existing connections""" # pylint: disable=too-many-instance-attributes @@ -44,9 +46,9 @@ class BMConnectionPool(object): def __init__(self): asyncore.set_rates( - config.safeGetInt( + BMConfigParser().safeGetInt( "bitmessagesettings", "maxdownloadrate"), - config.safeGetInt( + BMConfigParser().safeGetInt( "bitmessagesettings", "maxuploadrate") ) self.outboundConnections = {} @@ -58,7 +60,7 @@ class BMConnectionPool(object): self._spawnWait = 2 self._bootstrapped = False - trustedPeer = config.safeGet( + trustedPeer = BMConfigParser().safeGet( 'bitmessagesettings', 'trustedpeer') try: if trustedPeer: @@ -70,9 +72,6 @@ class BMConnectionPool(object): ' trustedpeer=:' ) - def __len__(self): - return len(self.outboundConnections) + len(self.inboundConnections) - def connections(self): """ Shortcut for combined list of connections from @@ -160,27 +159,27 @@ class BMConnectionPool(object): @staticmethod def getListeningIP(): """What IP are we supposed to be listening on?""" - if config.safeGet( - "bitmessagesettings", "onionhostname", "").endswith(".onion"): - host = config.safeGet( + if BMConfigParser().safeGet( + "bitmessagesettings", "onionhostname").endswith(".onion"): + host = BMConfigParser().safeGet( "bitmessagesettings", "onionbindip") else: host = '127.0.0.1' if ( - config.safeGetBoolean("bitmessagesettings", "sockslisten") - or config.safeGet("bitmessagesettings", "socksproxytype") + BMConfigParser().safeGetBoolean("bitmessagesettings", "sockslisten") + or BMConfigParser().safeGet("bitmessagesettings", "socksproxytype") == "none" ): # python doesn't like bind + INADDR_ANY? # host = socket.INADDR_ANY - host = config.get("network", "bind") + host = BMConfigParser().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 = config.safeGetInt("bitmessagesettings", "port") + port = BMConfigParser().safeGetInt("bitmessagesettings", "port") # correct port even if it changed ls = TCPServer(host=bind, port=port) self.listeningSockets[ls.destination] = ls @@ -202,7 +201,7 @@ class BMConnectionPool(object): def startBootstrappers(self): """Run the process of resolving bootstrap hostnames""" - proxy_type = config.safeGet( + proxy_type = BMConfigParser().safeGet( 'bitmessagesettings', 'socksproxytype') # A plugins may be added here hostname = None @@ -210,7 +209,7 @@ class BMConnectionPool(object): connection_base = TCPConnection elif proxy_type == 'SOCKS5': connection_base = Socks5BMConnection - hostname = random.choice([ # nosec B311 + hostname = helper_random.randomchoice([ 'quzwelsuziwqgpt2.onion', None ]) elif proxy_type == 'SOCKS4a': @@ -222,7 +221,7 @@ class BMConnectionPool(object): bootstrapper = bootstrap(connection_base) if not hostname: - port = random.choice([8080, 8444]) # nosec B311 + port = helper_random.randomchoice([8080, 8444]) hostname = 'bootstrap%s.bitmessage.org' % port else: port = 8444 @@ -234,21 +233,21 @@ class BMConnectionPool(object): # defaults to empty loop if outbound connections are maxed spawnConnections = False acceptConnections = True - if config.safeGetBoolean( + if BMConfigParser().safeGetBoolean( 'bitmessagesettings', 'dontconnect'): acceptConnections = False - elif config.safeGetBoolean( + elif BMConfigParser().safeGetBoolean( 'bitmessagesettings', 'sendoutgoingconnections'): spawnConnections = True - socksproxytype = config.safeGet( + socksproxytype = BMConfigParser().safeGet( 'bitmessagesettings', 'socksproxytype', '') - onionsocksproxytype = config.safeGet( + onionsocksproxytype = BMConfigParser().safeGet( 'bitmessagesettings', 'onionsocksproxytype', '') if ( socksproxytype[:5] == 'SOCKS' - and not config.safeGetBoolean( + and not BMConfigParser().safeGetBoolean( 'bitmessagesettings', 'sockslisten') - and '.onion' not in config.safeGet( + and '.onion' not in BMConfigParser().safeGet( 'bitmessagesettings', 'onionhostname', '') ): acceptConnections = False @@ -261,9 +260,9 @@ class BMConnectionPool(object): if not self._bootstrapped: self._bootstrapped = True Proxy.proxy = ( - config.safeGet( + BMConfigParser().safeGet( 'bitmessagesettings', 'sockshostname'), - config.safeGetInt( + BMConfigParser().safeGetInt( 'bitmessagesettings', 'socksport') ) # TODO AUTH @@ -272,9 +271,9 @@ class BMConnectionPool(object): if not onionsocksproxytype.startswith("SOCKS"): raise ValueError Proxy.onion_proxy = ( - config.safeGet( + BMConfigParser().safeGet( 'network', 'onionsockshostname', None), - config.safeGet( + BMConfigParser().safeGet( 'network', 'onionsocksport', None) ) except ValueError: @@ -283,13 +282,13 @@ class BMConnectionPool(object): 1 for c in self.outboundConnections.values() if (c.connected and c.fullyEstablished)) pending = len(self.outboundConnections) - established - if established < config.safeGetInt( + if established < BMConfigParser().safeGetInt( 'bitmessagesettings', 'maxoutboundconnections'): for i in range( state.maximumNumberOfHalfOpenConnections - pending): try: chosen = self.trustedPeer or chooseConnection( - random.choice(self.streams)) # nosec B311 + helper_random.randomchoice(self.streams)) except ValueError: continue if chosen in self.outboundConnections: @@ -331,28 +330,28 @@ class BMConnectionPool(object): self._lastSpawned = time.time() else: - for i in self.outboundConnections.values(): + for i in self.connections(): # FIXME: rating will be increased after next connection i.handle_close() if acceptConnections: if not self.listeningSockets: - if config.safeGet('network', 'bind') == '': + if BMConfigParser().safeGet('network', 'bind') == '': self.startListening() else: for bind in re.sub( r'[^\w.]+', ' ', - config.safeGet('network', 'bind') + BMConfigParser().safeGet('network', 'bind') ).split(): self.startListening(bind) logger.info('Listening for incoming connections.') if not self.udpSockets: - if config.safeGet('network', 'bind') == '': + if BMConfigParser().safeGet('network', 'bind') == '': self.startUDPSocket() else: for bind in re.sub( r'[^\w.]+', ' ', - config.safeGet('network', 'bind') + BMConfigParser().safeGet('network', 'bind') ).split(): self.startUDPSocket(bind) self.startUDPSocket(False) @@ -400,6 +399,3 @@ class BMConnectionPool(object): pass for i in reaper: self.removeConnection(i) - - -pool = BMConnectionPool() diff --git a/src/network/constants.py b/src/network/constants.py new file mode 100644 index 00000000..f8f4120f --- /dev/null +++ b/src/network/constants.py @@ -0,0 +1,17 @@ +""" +Network protocol constants +""" + + +#: address is online if online less than this many seconds ago +ADDRESS_ALIVE = 10800 +#: protocol specification says max 1000 addresses in one addr command +MAX_ADDR_COUNT = 1000 +#: ~1.6 MB which is the maximum possible size of an inv message. +MAX_MESSAGE_SIZE = 1600100 +#: 2**18 = 256kB is the maximum size of an object payload +MAX_OBJECT_PAYLOAD_SIZE = 2**18 +#: protocol specification says max 50000 objects in one inv command +MAX_OBJECT_COUNT = 50000 +#: maximum time offset +MAX_TIME_OFFSET = 3600 diff --git a/src/network/dandelion.py b/src/network/dandelion.py index 564a35f9..03f45bd7 100644 --- a/src/network/dandelion.py +++ b/src/network/dandelion.py @@ -7,6 +7,10 @@ from random import choice, expovariate, sample from threading import RLock from time import time +import connectionpool +import state +from queues import invQueue +from singleton import Singleton # randomise routes after 600 seconds REASSIGN_INTERVAL = 600 @@ -22,6 +26,7 @@ Stem = namedtuple('Stem', ['child', 'stream', 'timeout']) logger = logging.getLogger('default') +@Singleton class Dandelion: # pylint: disable=old-style-class """Dandelion class for tracking stem/fluff stages.""" def __init__(self): @@ -34,8 +39,6 @@ class Dandelion: # pylint: disable=old-style-class # when to rerandomise routes self.refresh = time() + REASSIGN_INTERVAL self.lock = RLock() - self.enabled = None - self.pool = None @staticmethod def poissonTimeout(start=None, average=0): @@ -46,23 +49,10 @@ class Dandelion: # pylint: disable=old-style-class average = FLUFF_TRIGGER_MEAN_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): - """Add inventory vector to dandelion stem return status of dandelion enabled""" - assert self.enabled is not None + """Add inventory vector to dandelion stem""" + if not state.dandelion: + return with self.lock: self.hashMap[hashId] = Stem( self.getNodeStem(source), @@ -101,7 +91,7 @@ class Dandelion: # pylint: disable=old-style-class """Child (i.e. next) node for an inventory vector during stem mode""" return self.hashMap[hashId].child - def maybeAddStem(self, connection, invQueue): + def maybeAddStem(self, connection): """ If we had too few outbound connections, add the current one to the current stem list. Dandelion as designed by the authors should @@ -150,7 +140,7 @@ class Dandelion: # pylint: disable=old-style-class """ try: # pick a random from available stems - stem = choice(range(len(self.stem))) # nosec B311 + stem = choice(range(len(self.stem))) if self.stem[stem] == parent: # one stem available and it's the parent if len(self.stem) == 1: @@ -175,7 +165,7 @@ class Dandelion: # pylint: disable=old-style-class self.nodeMap[node] = self.pickStem(node) return self.nodeMap[node] - def expire(self, invQueue): + def expire(self): """Switch expired objects from stem to fluff mode""" with self.lock: deadline = time() @@ -191,18 +181,16 @@ class Dandelion: # pylint: disable=old-style-class 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( - self.pool.outboundConnections.values(), MAX_STEMS) + connectionpool.BMConnectionPool( + ).outboundConnections.values(), MAX_STEMS) # not enough stems available except ValueError: - self.stem = self.pool.outboundConnections.values() + self.stem = connectionpool.BMConnectionPool( + ).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 7c8bccb6..0ae83b5b 100644 --- a/src/network/downloadthread.py +++ b/src/network/downloadthread.py @@ -2,12 +2,13 @@ `DownloadThread` class definition """ import time -import random -import state + import addresses +import helper_random import protocol -import connectionpool -from network import dandelion_ins +from dandelion import Dandelion +from inventory import Inventory +from network.connectionpool import BMConnectionPool from objectracker import missingObjects from threads import StoppableThread @@ -42,8 +43,8 @@ class DownloadThread(StoppableThread): while not self._stopped: requested = 0 # Choose downloading peers randomly - connections = connectionpool.pool.establishedConnections() - random.shuffle(connections) + connections = BMConnectionPool().establishedConnections() + helper_random.randomshuffle(connections) requestChunk = max(int( min(self.maxRequestChunk, len(missingObjects)) / len(connections)), 1) if connections else 1 @@ -60,7 +61,7 @@ class DownloadThread(StoppableThread): payload = bytearray() chunkCount = 0 for chunk in request: - if chunk in state.Inventory and not dandelion_ins.hasHash(chunk): + if chunk in Inventory() and not Dandelion().hasHash(chunk): try: del i.objectsNewToMe[chunk] except KeyError: diff --git a/src/network/http.py b/src/network/http.py index d7a938fa..8bba38ac 100644 --- a/src/network/http.py +++ b/src/network/http.py @@ -18,19 +18,19 @@ class HttpConnection(AdvancedDispatcher): 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) + print "connecting in background to %s:%i" % (self.destination[0], self.destination[1]) 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)) + print "Sending %ib" % (len(self.write_buf)) self.set_state("http_request_sent", 0) return False def state_http_request_sent(self): if self.read_buf: - print("Received %ib" % len(self.read_buf)) + print "Received %ib" % (len(self.read_buf)) self.read_buf = b"" if not self.connected: self.set_state("close", 0) @@ -62,13 +62,13 @@ if __name__ == "__main__": for host in ("bootstrap8080.bitmessage.org", "bootstrap8444.bitmessage.org"): proxy = Socks5Resolver(host=host) while asyncore.socket_map: - print("loop %s, len %i" % (proxy.state, len(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 asyncore.socket_map: - print("loop %s, len %i" % (proxy.state, len(asyncore.socket_map))) + print "loop %s, len %i" % (proxy.state, len(asyncore.socket_map)) asyncore.loop(timeout=1, count=1) proxy.resolved() diff --git a/src/network/http_old.py b/src/network/http_old.py new file mode 100644 index 00000000..64d09983 --- /dev/null +++ b/src/network/http_old.py @@ -0,0 +1,52 @@ +import asyncore +import socket +import time + +requestCount = 0 +parallel = 50 +duration = 60 + + +class HTTPClient(asyncore.dispatcher): + """An 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): + # pylint: disable=global-statement + 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/invthread.py b/src/network/invthread.py index 503eefa1..e68b7692 100644 --- a/src/network/invthread.py +++ b/src/network/invthread.py @@ -8,8 +8,9 @@ from time import time import addresses import protocol import state -import connectionpool -from network import dandelion_ins, invQueue +from network.connectionpool import BMConnectionPool +from network.dandelion import Dandelion +from queues import invQueue from threads import StoppableThread @@ -18,7 +19,7 @@ def handleExpiredDandelion(expired): the object""" if not expired: return - for i in connectionpool.pool.connections(): + for i in BMConnectionPool().connections(): if not i.fullyEstablished: continue for x in expired: @@ -39,10 +40,10 @@ class InvThread(StoppableThread): @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): + Dandelion().addHash(hashId, stream=stream) + for connection in BMConnectionPool().connections(): + if state.dandelion and connection != \ + Dandelion().objectChildStem(hashId): continue connection.objectsNewToThem[hashId] = time() @@ -51,7 +52,7 @@ class InvThread(StoppableThread): chunk = [] while True: # Dandelion fluff trigger by expiration - handleExpiredDandelion(dandelion_ins.expire(invQueue)) + handleExpiredDandelion(Dandelion().expire()) try: data = invQueue.get(False) chunk.append((data[0], data[1])) @@ -62,7 +63,7 @@ class InvThread(StoppableThread): break if chunk: - for connection in connectionpool.pool.connections(): + for connection in BMConnectionPool().connections(): fluffs = [] stems = [] for inv in chunk: @@ -74,10 +75,10 @@ class InvThread(StoppableThread): except KeyError: continue try: - if connection == dandelion_ins.objectChildStem(inv[1]): + if connection == Dandelion().objectChildStem(inv[1]): # Fluff trigger by RNG # auto-ignore if config set to 0, i.e. dandelion is off - if random.randint(1, 100) >= dandelion_ins.enabled: # nosec B311 + if random.randint(1, 100) >= state.dandelion: fluffs.append(inv[1]) # send a dinv only if the stem node supports dandelion elif connection.services & protocol.NODE_DANDELION > 0: @@ -104,6 +105,7 @@ class InvThread(StoppableThread): for _ in range(len(chunk)): invQueue.task_done() - dandelion_ins.reRandomiseStems() + if Dandelion().refresh < time(): + Dandelion().reRandomiseStems() self.stop.wait(1) diff --git a/src/network/networkthread.py b/src/network/networkthread.py index 640d47a1..61ff6c09 100644 --- a/src/network/networkthread.py +++ b/src/network/networkthread.py @@ -2,7 +2,8 @@ A thread to handle network concerns """ import network.asyncore_pollchoose as asyncore -import connectionpool +import state +from network.connectionpool import BMConnectionPool from queues import excQueue from threads import StoppableThread @@ -13,28 +14,28 @@ class BMNetworkThread(StoppableThread): def run(self): try: - while not self._stopped: - connectionpool.pool.loop() + while not self._stopped and state.shutdown == 0: + BMConnectionPool().loop() except Exception as e: excQueue.put((self.name, e)) raise def stopThread(self): super(BMNetworkThread, self).stopThread() - for i in connectionpool.pool.listeningSockets.values(): + for i in BMConnectionPool().listeningSockets.values(): try: i.close() - except: # nosec B110 # pylint:disable=bare-except + except: pass - for i in connectionpool.pool.outboundConnections.values(): + for i in BMConnectionPool().outboundConnections.values(): try: i.close() - except: # nosec B110 # pylint:disable=bare-except + except: pass - for i in connectionpool.pool.inboundConnections.values(): + for i in BMConnectionPool().inboundConnections.values(): try: i.close() - except: # nosec B110 # pylint:disable=bare-except + except: pass # just in case diff --git a/src/network/objectracker.py b/src/network/objectracker.py index 91bb0552..ca29c023 100644 --- a/src/network/objectracker.py +++ b/src/network/objectracker.py @@ -4,8 +4,8 @@ Module for tracking objects import time from threading import RLock -import connectionpool -from network import dandelion_ins +import network.connectionpool +from network.dandelion import Dandelion from randomtrackingdict import RandomTrackingDict haveBloom = False @@ -100,21 +100,21 @@ class ObjectTracker(object): def handleReceivedObject(self, streamNumber, hashid): """Handling received object""" - for i in connectionpool.pool.connections(): + for i in network.connectionpool.BMConnectionPool().connections(): if not i.fullyEstablished: continue try: del i.objectsNewToMe[hashid] except KeyError: if streamNumber in i.streams and ( - not dandelion_ins.hasHash(hashid) - or dandelion_ins.objectChildStem(hashid) == i): + not Dandelion().hasHash(hashid) or + Dandelion().objectChildStem(hashid) == i): with i.objectsNewToThemLock: i.objectsNewToThem[hashid] = time.time() # update stream number, # which we didn't have when we just received the dinv # also resets expiration of the stem mode - dandelion_ins.setHashStream(hashid, streamNumber) + Dandelion().setHashStream(hashid, streamNumber) if i == self: try: diff --git a/src/network/proxy.py b/src/network/proxy.py index ed1af127..38676d66 100644 --- a/src/network/proxy.py +++ b/src/network/proxy.py @@ -8,7 +8,7 @@ import time import asyncore_pollchoose as asyncore from advanceddispatcher import AdvancedDispatcher -from bmconfigparser import config +from bmconfigparser import BMConfigParser from node import Peer logger = logging.getLogger('default') @@ -61,9 +61,9 @@ class Proxy(AdvancedDispatcher): @proxy.setter def proxy(self, address): """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)): + 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 @@ -113,13 +113,14 @@ class Proxy(AdvancedDispatcher): self.destination = address self.isOutbound = True self.fullyEstablished = False + self.connectedAt = 0 self.create_socket(socket.AF_INET, socket.SOCK_STREAM) - if config.safeGetBoolean( + if BMConfigParser().safeGetBoolean( "bitmessagesettings", "socksauthentication"): self.auth = ( - config.safeGet( + BMConfigParser().safeGet( "bitmessagesettings", "socksusername"), - config.safeGet( + BMConfigParser().safeGet( "bitmessagesettings", "sockspassword")) else: self.auth = None @@ -144,5 +145,6 @@ class Proxy(AdvancedDispatcher): def state_proxy_handshake_done(self): """Handshake is complete at this point""" + # pylint: disable=attribute-defined-outside-init self.connectedAt = time.time() return False diff --git a/src/randomtrackingdict.py b/src/network/randomtrackingdict.py similarity index 79% rename from src/randomtrackingdict.py rename to src/network/randomtrackingdict.py index 5bf19181..e87bf156 100644 --- a/src/randomtrackingdict.py +++ b/src/network/randomtrackingdict.py @@ -1,13 +1,11 @@ """ Track randomize ordered dict """ +import random from threading import RLock from time import time -try: - import helper_random -except ImportError: - from . import helper_random +import helper_random class RandomTrackingDict(object): @@ -104,9 +102,9 @@ class RandomTrackingDict(object): def randomKeys(self, count=1): """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()): + 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 @@ -130,3 +128,41 @@ class RandomTrackingDict(object): self.pendingLen += 1 self.lastPoll = time() return retval + + +if __name__ == '__main__': + + # pylint: disable=redefined-outer-name + def randString(): + """helper function for tests, generates a random string""" + retval = b'' + for _ in range(32): + retval += chr(random.randint(0, 255)) + return retval + + a = [] + k = RandomTrackingDict() + d = {} + + print "populating random tracking dict" + a.append(time()) + for i in range(50000): + k[randString()] = True + a.append(time()) + print "done" + + while k: + retval = k.randomKeys(1000) + if not retval: + print "error getting random keys" + try: + k.randomKeys(100) + print "bad" + except KeyError: + pass + for i in retval: + del k[i] + a.append(time()) + + for x in range(len(a) - 1): + print "%i: %.3f" % (x, a[x + 1] - a[x]) diff --git a/src/network/receivequeuethread.py b/src/network/receivequeuethread.py index 88d3b740..bf1d8300 100644 --- a/src/network/receivequeuethread.py +++ b/src/network/receivequeuethread.py @@ -5,9 +5,10 @@ import errno import Queue import socket -import connectionpool +import state from network.advanceddispatcher import UnknownStateError -from network import receiveDataQueue +from network.connectionpool import BMConnectionPool +from queues import receiveDataQueue from threads import StoppableThread @@ -18,13 +19,13 @@ class ReceiveQueueThread(StoppableThread): super(ReceiveQueueThread, self).__init__(name="ReceiveQueue_%i" % num) def run(self): - while not self._stopped: + while not self._stopped and state.shutdown == 0: try: dest = receiveDataQueue.get(block=True, timeout=1) except Queue.Empty: continue - if self._stopped: + if self._stopped or state.shutdown: break # cycle as long as there is data @@ -35,7 +36,7 @@ class ReceiveQueueThread(StoppableThread): # enough data, or the connection is to be aborted try: - connection = connectionpool.pool.getConnectionByAddr(dest) + connection = BMConnectionPool().getConnectionByAddr(dest) # connection object not found except KeyError: receiveDataQueue.task_done() @@ -50,6 +51,6 @@ class ReceiveQueueThread(StoppableThread): connection.set_state("close", 0) else: self.logger.error('Socket error: %s', err) - except: # noqa:E722 + except: self.logger.error('Error processing', exc_info=True) receiveDataQueue.task_done() diff --git a/src/network/socks4a.py b/src/network/socks4a.py index e9786168..0d4310bc 100644 --- a/src/network/socks4a.py +++ b/src/network/socks4a.py @@ -2,14 +2,11 @@ SOCKS4a proxy module """ # pylint: disable=attribute-defined-outside-init -import logging import socket import struct from proxy import GeneralProxyError, Proxy, ProxyError -logger = logging.getLogger('default') - class Socks4aError(ProxyError): """SOCKS4a error base class""" @@ -143,5 +140,4 @@ class Socks4aResolver(Socks4a): 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()) + print "Resolved %s as %s" % (self.host, self.proxy_sock_name()) diff --git a/src/network/socks5.py b/src/network/socks5.py index d1daae42..fc33f4df 100644 --- a/src/network/socks5.py +++ b/src/network/socks5.py @@ -3,15 +3,12 @@ SOCKS5 proxy module """ # pylint: disable=attribute-defined-outside-init -import logging import socket import struct from node import Peer from proxy import GeneralProxyError, Proxy, ProxyError -logger = logging.getLogger('default') - class Socks5AuthError(ProxyError): """Rised when the socks5 protocol encounters an authentication error""" @@ -220,5 +217,4 @@ class Socks5Resolver(Socks5): 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()) + print "Resolved %s as %s" % (self.host, self.proxy_sock_name()) diff --git a/src/network/stats.py b/src/network/stats.py index 0ab1ae0f..82e6c87f 100644 --- a/src/network/stats.py +++ b/src/network/stats.py @@ -4,7 +4,7 @@ Network statistics import time import asyncore_pollchoose as asyncore -import connectionpool +from network.connectionpool import BMConnectionPool from objectracker import missingObjects @@ -18,7 +18,7 @@ currentSentSpeed = 0 def connectedHostsList(): """List of all the connected hosts""" - return connectionpool.pool.establishedConnections() + return BMConnectionPool().establishedConnections() def sentBytes(): @@ -69,8 +69,8 @@ def pendingDownload(): def pendingUpload(): """Getting pending uploads""" # tmp = {} - # for connection in connectionpool.pool.inboundConnections.values() + \ - # connectionpool.pool.outboundConnections.values(): + # 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 diff --git a/src/network/tcp.py b/src/network/tcp.py index db7d6595..d611b1ca 100644 --- a/src/network/tcp.py +++ b/src/network/tcp.py @@ -2,43 +2,38 @@ TCP protocol handler """ # pylint: disable=too-many-ancestors - import logging import math import random import socket import time -# 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 connectionpool +import helper_random import knownnodes +import protocol +import shared +import state +from bmconfigparser import BMConfigParser +from helper_random import randomBytes +from inventory import Inventory from network.advanceddispatcher import AdvancedDispatcher +from network.assemble import assemble_addr from network.bmproto import BMProto +from network.constants import MAX_OBJECT_COUNT +from network.dandelion import Dandelion from network.objectracker import ObjectTracker from network.socks4a import Socks4aConnection from network.socks5 import Socks5Connection from network.tls import TLSDispatcher from node import Peer - +from queues import invQueue, receiveDataQueue, UISignalQueue logger = logging.getLogger('default') -maximumAgeOfNodesThatIAdvertiseToOthers = 10800 #: Equals three hours -maximumTimeOffsetWrongCount = 3 #: Connections with wrong time offset - - class TCPConnection(BMProto, TLSDispatcher): # pylint: disable=too-many-instance-attributes """ @@ -51,6 +46,7 @@ class TCPConnection(BMProto, TLSDispatcher): 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 = Peer(*sock.getpeername()) @@ -82,8 +78,8 @@ class TCPConnection(BMProto, TLSDispatcher): try: self.local = ( protocol.checkIPAddress( - protocol.encodeHost(self.destination.host), True) - and not protocol.checkSocksIP(self.destination.host) + protocol.encodeHost(self.destination.host), True) and + not protocol.checkSocksIP(self.destination.host) ) except socket.error: # it's probably a hostname @@ -126,22 +122,6 @@ class TCPConnection(BMProto, TLSDispatcher): ' 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 @@ -156,19 +136,16 @@ class TCPConnection(BMProto, TLSDispatcher): def set_connection_fully_established(self): """Initiate inventory synchronisation.""" if not self.isOutbound and not self.local: - state.clientHasReceivedIncomingConnections = True + shared.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 - # The connection having host suitable for knownnodes - if self.isOutbound or not self.local and not state.socksIP: + if self.isOutbound: knownnodes.increaseRating(self.destination) - knownnodes.addKnownNode( - self.streams, self.destination, time.time()) - dandelion_ins.maybeAddStem(self, invQueue) + Dandelion().maybeAddStem(self) self.sendAddr() self.sendBigInv() @@ -177,7 +154,7 @@ class TCPConnection(BMProto, TLSDispatcher): # We are going to share a maximum number of 1000 addrs (per overlapping # stream) with our peer. 500 from overlapping streams, 250 from the # left child stream, and 250 from the right child stream. - maxAddrCount = config.safeGetInt( + maxAddrCount = BMConfigParser().safeGetInt( "bitmessagesettings", "maxaddrperstreamsend", 500) templist = [] @@ -192,20 +169,20 @@ class TCPConnection(BMProto, TLSDispatcher): # 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') + if v["lastseen"] > int(time.time()) - + shared.maximumAgeOfNodesThatIAdvertiseToOthers and + v["rating"] >= 0 and len(k.host) <= 22 ] # 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) + addrs[s] = helper_random.randomsample(filtered, elemCount) for substream in addrs: for peer, params in addrs[substream]: templist.append((substream, peer, params["lastseen"])) if templist: - self.append_write_buf(protocol.assembleAddrMessage(templist)) + self.append_write_buf(assemble_addr(templist)) def sendBigInv(self): """ @@ -228,9 +205,9 @@ class TCPConnection(BMProto, TLSDispatcher): # may lock for a long time, but I think it's better than # thousands of small locks with self.objectsNewToThemLock: - for objHash in state.Inventory.unexpired_hashes_by_stream(stream): + for objHash in Inventory().unexpired_hashes_by_stream(stream): # don't advertise stem objects on bigInv - if dandelion_ins.hasHash(objHash): + if Dandelion().hasHash(objHash): continue bigInvList[objHash] = 0 objectCount = 0 @@ -244,7 +221,7 @@ class TCPConnection(BMProto, TLSDispatcher): # Remove -1 below when sufficient time has passed for users to # upgrade to versions of PyBitmessage that accept inv with 50,000 # items - if objectCount >= protocol.MAX_OBJECT_COUNT - 1: + if objectCount >= MAX_OBJECT_COUNT - 1: sendChunk() payload = b'' objectCount = 0 @@ -267,7 +244,7 @@ class TCPConnection(BMProto, TLSDispatcher): self.append_write_buf( protocol.assembleVersionMessage( self.destination.host, self.destination.port, - connectionpool.pool.streams, dandelion_ins.enabled, + connectionpool.BMConnectionPool().streams, False, nodeid=self.nodeid)) self.connectedAt = time.time() receiveDataQueue.put(self.destination) @@ -275,6 +252,14 @@ class TCPConnection(BMProto, TLSDispatcher): 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): @@ -283,20 +268,15 @@ class TCPConnection(BMProto, TLSDispatcher): def handle_close(self): """Callback for connection being closed.""" - host_is_global = self.isOutbound or not self.local and not state.socksIP + if self.isOutbound and not self.fullyEstablished: + knownnodes.decreaseRating(self.destination) if self.fullyEstablished: 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) + if self.isOutbound: + Dandelion().maybeRemoveStem(self) BMProto.handle_close(self) @@ -318,7 +298,7 @@ class Socks5BMConnection(Socks5Connection, TCPConnection): self.append_write_buf( protocol.assembleVersionMessage( self.destination.host, self.destination.port, - connectionpool.pool.streams, dandelion_ins.enabled, + connectionpool.BMConnectionPool().streams, False, nodeid=self.nodeid)) self.set_state("bm_header", expectBytes=protocol.Header.size) return True @@ -342,7 +322,7 @@ class Socks4aBMConnection(Socks4aConnection, TCPConnection): self.append_write_buf( protocol.assembleVersionMessage( self.destination.host, self.destination.port, - connectionpool.pool.streams, dandelion_ins.enabled, + connectionpool.BMConnectionPool().streams, False, nodeid=self.nodeid)) self.set_state("bm_header", expectBytes=protocol.Header.size) return True @@ -365,21 +345,16 @@ def bootstrap(connection_class): """ BMProto.bm_command_addr(self) self._succeed = True + # pylint: disable=attribute-defined-outside-init 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) + self._connection_base.handle_close(self) if not self._succeed: knownnodes.knownNodesActual = False @@ -398,7 +373,7 @@ class TCPServer(AdvancedDispatcher): try: if attempt > 0: logger.warning('Failed to bind on port %s', port) - port = random.randint(32767, 65535) # nosec B311 + port = random.randint(32767, 65535) self.bind((host, port)) except socket.error as e: if e.errno in (asyncore.EADDRINUSE, asyncore.WSAEADDRINUSE): @@ -406,9 +381,9 @@ class TCPServer(AdvancedDispatcher): else: if attempt > 0: logger.warning('Setting port to %s', port) - config.set( + BMConfigParser().set( 'bitmessagesettings', 'port', str(port)) - config.save() + BMConfigParser().save() break self.destination = Peer(host, port) self.bound = True @@ -430,11 +405,12 @@ class TCPServer(AdvancedDispatcher): state.ownAddresses[Peer(*sock.getsockname())] = True if ( - len(connectionpool.pool) - > config.safeGetInt( - 'bitmessagesettings', 'maxtotalconnections') - + config.safeGetInt( - 'bitmessagesettings', 'maxbootstrapconnections') + 10 + len(connectionpool.BMConnectionPool().inboundConnections) + + len(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 @@ -442,7 +418,7 @@ class TCPServer(AdvancedDispatcher): sock.close() return try: - connectionpool.pool.addConnection( + connectionpool.BMConnectionPool().addConnection( TCPConnection(sock=sock)) except socket.error: pass diff --git a/src/network/tls.py b/src/network/tls.py index 2d5d5e1b..1b325696 100644 --- a/src/network/tls.py +++ b/src/network/tls.py @@ -10,12 +10,13 @@ import sys import network.asyncore_pollchoose as asyncore import paths from network.advanceddispatcher import AdvancedDispatcher -from network import receiveDataQueue +from queues import receiveDataQueue logger = logging.getLogger('default') _DISCONNECTED_SSL = frozenset((ssl.SSL_ERROR_EOF,)) +# sslProtocolVersion if sys.version_info >= (2, 7, 13): # this means TLSv1 or higher # in the future change to @@ -26,16 +27,14 @@ elif sys.version_info >= (2, 7, 9): # 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 + # 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") -): +if ssl.OPENSSL_VERSION_NUMBER >= 0x10100000 and not \ + ssl.OPENSSL_VERSION.startswith("LibreSSL"): sslProtocolCiphers = "AECDH-AES256-SHA@SECLEVEL=0" else: sslProtocolCiphers = "AECDH-AES256-SHA" @@ -48,10 +47,16 @@ class TLSDispatcher(AdvancedDispatcher): def __init__(self, _=None, sock=None, certfile=None, keyfile=None, server_side=False, ciphers=sslProtocolCiphers): self.want_read = self.want_write = True - self.certfile = certfile or os.path.join( - paths.codePath(), 'sslkeys', 'cert.pem') - self.keyfile = keyfile or os.path.join( - paths.codePath(), 'sslkeys', 'key.pem') + 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.server_side = server_side self.ciphers = ciphers self.tlsStarted = False @@ -61,6 +66,7 @@ class TLSDispatcher(AdvancedDispatcher): def state_tls_init(self): """Prepare sockets for TLS handshake""" + # pylint: disable=attribute-defined-outside-init self.isSSL = True self.tlsStarted = True # Once the connection has been established, @@ -90,6 +96,8 @@ class TLSDispatcher(AdvancedDispatcher): 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") @staticmethod def state_tls_handshake(): @@ -104,9 +112,9 @@ class TLSDispatcher(AdvancedDispatcher): try: if self.tlsStarted and not self.tlsDone and not self.write_buf: return self.want_write + return AdvancedDispatcher.writable(self) except AttributeError: - pass - return AdvancedDispatcher.writable(self) + return AdvancedDispatcher.writable(self) def readable(self): """Handle readable check for TLS-enabled sockets""" @@ -114,18 +122,18 @@ class TLSDispatcher(AdvancedDispatcher): # 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: - logger.debug('tls readable, %r', self.want_read) + # print "tls readable, %r" % (self.want_read) return self.want_read # prior to TLS handshake, # receiveDataThread should emulate synchronous behaviour - if not self.fullyEstablished and ( + elif not self.fullyEstablished and ( self.expectBytes == 0 or not self.write_buf_empty()): return False + return AdvancedDispatcher.readable(self) except AttributeError: - pass - return AdvancedDispatcher.readable(self) + return AdvancedDispatcher.readable(self) - def handle_read(self): + def handle_read(self): # pylint: disable=inconsistent-return-statements """ Handle reads for sockets during TLS handshake. Requires special treatment as during the handshake, buffers must remain empty @@ -134,20 +142,28 @@ class TLSDispatcher(AdvancedDispatcher): 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: - AdvancedDispatcher.handle_read(self) + # logger.debug( + # "%s:%i Not TLS handshaking (read)", self.destination.host, + # self.destination.port) + return AdvancedDispatcher.handle_read(self) except AttributeError: - AdvancedDispatcher.handle_read(self) + return AdvancedDispatcher.handle_read(self) except ssl.SSLError as err: if err.errno == ssl.SSL_ERROR_WANT_READ: return - if err.errno not in _DISCONNECTED_SSL: - logger.info("SSL Error: %s", err) - self.close_reason = "SSL Error in handle_read" + elif err.errno in _DISCONNECTED_SSL: + self.handle_close() + return + logger.info("SSL Error: %s", str(err)) self.handle_close() + return - def handle_write(self): + def handle_write(self): # pylint: disable=inconsistent-return-statements """ Handle writes for sockets during TLS handshake. Requires special treatment as during the handshake, buffers must remain empty @@ -156,18 +172,26 @@ class TLSDispatcher(AdvancedDispatcher): 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: - AdvancedDispatcher.handle_write(self) + # logger.debug( + # "%s:%i Not TLS handshaking (write)", self.destination.host, + # self.destination.port) + return AdvancedDispatcher.handle_write(self) except AttributeError: - AdvancedDispatcher.handle_write(self) + return AdvancedDispatcher.handle_write(self) except ssl.SSLError as err: if err.errno == ssl.SSL_ERROR_WANT_WRITE: - return - if err.errno not in _DISCONNECTED_SSL: - logger.info("SSL Error: %s", err) - self.close_reason = "SSL Error in handle_write" + return 0 + elif err.errno in _DISCONNECTED_SSL: + self.handle_close() + return 0 + logger.info("SSL Error: %s", str(err)) self.handle_close() + return def tls_handshake(self): """Perform TLS handshake and handle its stages""" @@ -176,24 +200,23 @@ class TLSDispatcher(AdvancedDispatcher): return False # Perform the handshake. try: - logger.debug("handshaking (internal)") + # print "handshaking (internal)" self.sslSocket.do_handshake() except ssl.SSLError as err: - self.close_reason = "SSL Error in tls_handshake" - logger.info("%s:%i: handshake fail", *self.destination) + # print "%s:%i: handshake fail" % ( + # self.destination.host, self.destination.port) self.want_read = self.want_write = False if err.args[0] == ssl.SSL_ERROR_WANT_READ: - logger.debug("want read") + # print "want read" self.want_read = True if err.args[0] == ssl.SSL_ERROR_WANT_WRITE: - logger.debug("want write") + # print "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 diff --git a/src/network/udp.py b/src/network/udp.py index 30643d40..d5f1cccd 100644 --- a/src/network/udp.py +++ b/src/network/udp.py @@ -5,16 +5,12 @@ import logging import socket import time -# magic imports! import protocol import state -import connectionpool - -from network import receiveDataQueue from bmproto import BMProto from node import Peer from objectracker import ObjectTracker - +from queues import receiveDataQueue logger = logging.getLogger('default') @@ -22,6 +18,7 @@ logger = logging.getLogger('default') 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 @@ -31,6 +28,7 @@ class UDPSocket(BMProto): # pylint: disable=too-many-instance-attributes # .. todo:: sort out streams self.streams = [1] self.fullyEstablished = True + self.connectedAt = 0 self.skipUntil = 0 if sock is None: if host is None: @@ -82,10 +80,10 @@ class UDPSocket(BMProto): # pylint: disable=too-many-instance-attributes remoteport = False for seenTime, stream, _, ip, port in addresses: decodedIP = protocol.checkIPAddress(str(ip)) - if stream not in connectionpool.pool.streams: + if stream not in state.streamsInWhichIAmParticipating: continue - if (seenTime < time.time() - protocol.MAX_TIME_OFFSET - or seenTime > time.time() + protocol.MAX_TIME_OFFSET): + if (seenTime < time.time() - self.maxTimeOffset + or seenTime > time.time() + self.maxTimeOffset): continue if decodedIP is False: # if the address isn't local, interpret it as @@ -96,8 +94,9 @@ class UDPSocket(BMProto): # pylint: disable=too-many-instance-attributes 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() + if self.local: + state.discoveredPeers[Peer(self.destination.host, remoteport)] = \ + time.time() return True def bm_command_portcheck(self): @@ -126,9 +125,9 @@ class UDPSocket(BMProto): # pylint: disable=too-many-instance-attributes def handle_read(self): try: - recdata, addr = self.socket.recvfrom(self._buf_len) - except socket.error: - logger.error("socket error on recvfrom:", exc_info=True) + (recdata, addr) = self.socket.recvfrom(self._buf_len) + except socket.error as e: + logger.error("socket error: %s", e) return self.destination = Peer(*addr) @@ -144,7 +143,10 @@ class UDPSocket(BMProto): # pylint: disable=too-many-instance-attributes try: 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) + except socket.error as e: + logger.error("socket error on sendto: %s", e) + if e.errno == 101: + self.announcing = False + self.socket.close() + retval = 0 self.slice_write_buf(retval) diff --git a/src/network/uploadthread.py b/src/network/uploadthread.py index 60209832..7d80d789 100644 --- a/src/network/uploadthread.py +++ b/src/network/uploadthread.py @@ -3,12 +3,12 @@ """ import time -import random +import helper_random import protocol -import state -import connectionpool +from inventory import Inventory +from network.connectionpool import BMConnectionPool +from network.dandelion import Dandelion from randomtrackingdict import RandomTrackingDict -from network import dandelion_ins from threads import StoppableThread @@ -23,8 +23,8 @@ class UploadThread(StoppableThread): while not self._stopped: uploaded = 0 # Choose uploading peers randomly - connections = connectionpool.pool.establishedConnections() - random.shuffle(connections) + connections = BMConnectionPool().establishedConnections() + helper_random.randomshuffle(connections) for i in connections: now = time.time() # avoid unnecessary delay @@ -41,8 +41,8 @@ class UploadThread(StoppableThread): chunk_count = 0 for chunk in request: del i.pendingUpload[chunk] - if dandelion_ins.hasHash(chunk) and \ - i != dandelion_ins.objectChildStem(chunk): + if Dandelion().hasHash(chunk) and \ + i != Dandelion().objectChildStem(chunk): i.antiIntersectionDelay() self.logger.info( '%s asked for a stem object we didn\'t offer to it.', @@ -50,7 +50,7 @@ class UploadThread(StoppableThread): break try: payload.extend(protocol.CreatePacket( - 'object', state.Inventory[chunk].payload)) + 'object', Inventory()[chunk].payload)) chunk_count += 1 except KeyError: i.antiIntersectionDelay() diff --git a/src/openclpow.py b/src/openclpow.py index 5391590c..35bf46d2 100644 --- a/src/openclpow.py +++ b/src/openclpow.py @@ -1,24 +1,17 @@ +#!/usr/bin/env python2.7 """ Module for Proof of Work using OpenCL """ -import logging +import hashlib import os -from struct import pack +from struct import pack, unpack import paths -from bmconfigparser import config +from bmconfigparser import BMConfigParser +from debug import logger from state import shutdown -try: - import numpy - import pyopencl as cl - libAvailable = True -except ImportError: - libAvailable = False - - -logger = logging.getLogger('default') - +libAvailable = True ctx = False queue = False program = False @@ -27,10 +20,17 @@ enabledGpus = [] vendors = [] hash_dt = None +try: + import pyopencl as cl + import numpy +except ImportError: + libAvailable = False + def initCL(): """Initlialise OpenCL engine""" - global ctx, queue, program, hash_dt # pylint: disable=global-statement + # pylint: disable=global-statement + global ctx, queue, program, hash_dt, libAvailable if libAvailable is False: return del enabledGpus[:] @@ -42,12 +42,12 @@ def initCL(): try: for platform in cl.get_platforms(): gpus.extend(platform.get_devices(device_type=cl.device_type.GPU)) - if config.safeGet("bitmessagesettings", "opencl") == platform.vendor: + if BMConfigParser().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: # nosec B110 # noqa:E722 # pylint:disable=bare-except + except: pass if enabledGpus: ctx = cl.Context(devices=enabledGpus) @@ -109,3 +109,14 @@ def do_opencl_pow(hash_, target): raise Exception("Interrupted") # logger.debug("Took %d tries.", progress) return output[0][0] + + +if __name__ == "__main__": + initCL() + target_ = 54227212183 + initialHash = ("3758f55b5a8d902fd3597e4ce6a2d3f23daff735f65d9698c270987f4e67ad590" + "b93f3ffeba0ef2fd08a8dc2f87b68ae5a0dc819ab57f22ad2c4c9c8618a43b3").decode("hex") + nonce = 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_) diff --git a/src/pathmagic.py b/src/pathmagic.py deleted file mode 100644 index 3f32c0c1..00000000 --- a/src/pathmagic.py +++ /dev/null @@ -1,10 +0,0 @@ -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 e0d43334..e2f8c97e 100644 --- a/src/paths.py +++ b/src/paths.py @@ -21,15 +21,14 @@ def lookupExeFolder(): if frozen: exeFolder = ( # targetdir/Bitmessage.app/Contents/MacOS/Bitmessage - 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')) + os.path.dirname(sys.executable).split(os.path.sep)[0] + os.path.sep + if frozen == "macosx_app" else + os.path.dirname(sys.executable) + os.path.sep) elif __file__: - exeFolder = os.path.dirname(__file__) + exeFolder = os.path.dirname(__file__) + os.path.sep else: - return '' - return exeFolder + os.path.sep + exeFolder = '' + return exeFolder def lookupAppdataFolder(): @@ -50,8 +49,11 @@ def lookupAppdataFolder(): 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 + elif 'win32' in sys.platform or 'win64' in sys.platform: + dataFolder = os.path.join( + os.environ['APPDATA'].decode( + sys.getfilesystemencoding(), 'ignore'), APPNAME + ) + os.path.sep else: try: dataFolder = os.path.join(os.environ['XDG_CONFIG_HOME'], APPNAME) diff --git a/src/plugins/desktop_xdg.py b/src/plugins/desktop_xdg.py deleted file mode 100644 index 0b551e1c..00000000 --- a/src/plugins/desktop_xdg.py +++ /dev/null @@ -1,33 +0,0 @@ -# -*- 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 b471d2ef..60bf5e7e 100644 --- a/src/plugins/indicator_libmessaging.py +++ b/src/plugins/indicator_libmessaging.py @@ -18,7 +18,7 @@ class IndicatorLibmessaging(object): self.app = MessagingMenu.App(desktop_id='pybitmessage.desktop') self.app.register() self.app.connect('activate-source', self.activate) - except: # noqa:E722 + except: self.app = None return diff --git a/src/plugins/notification_notify2.py b/src/plugins/notification_notify2.py index f851737d..84ecbdde 100644 --- a/src/plugins/notification_notify2.py +++ b/src/plugins/notification_notify2.py @@ -5,7 +5,7 @@ Notification plugin using notify2 import gi gi.require_version('Notify', '0.7') -from gi.repository import Notify # noqa:E402 +from gi.repository import Notify Notify.init('pybitmessage') diff --git a/src/plugins/proxyconfig_stem.py b/src/plugins/proxyconfig_stem.py index 25f75f69..7e8dc089 100644 --- a/src/plugins/proxyconfig_stem.py +++ b/src/plugins/proxyconfig_stem.py @@ -13,8 +13,7 @@ Configure tor proxy and hidden service with """ import logging import os -import random -import sys +import random # noseq import tempfile import stem @@ -35,18 +34,15 @@ class DebugLogger(object): # pylint: disable=too-few-public-methods def __call__(self, line): try: - level, line = line.split('[', 1)[1].split(']', 1) + level, line = line.split('[', 1)[1].split(']') 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): +def connect_plugin(config): # pylint: disable=too-many-branches """ Run stem proxy configurator @@ -66,30 +62,23 @@ def connect_plugin(config): ' 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) + control_socket = os.path.join(datadir, 'control') + tor_config = { + 'SocksPort': '9050', + # 'DataDirectory': datadir, # had an exception with control socket + 'ControlSocket': control_socket + } + port = config.safeGet('bitmessagesettings', 'socksport', '9050') for attempt in range(50): if attempt > 0: - port = random.randint(32767, 65535) # nosec B311 + port = random.randint(32767, 65535) 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), + tor_config, take_ownership=True, timeout=20, init_msg_handler=logwrite) except OSError: if not attempt: @@ -101,20 +90,14 @@ def connect_plugin(config): 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 = stem.control.Controller.from_socket_file( + control_socket) controller.authenticate() except stem.SocketError: # something goes wrong way diff --git a/src/plugins/sound_playfile.py b/src/plugins/sound_playfile.py index c6b70f66..e36d9922 100644 --- a/src/plugins/sound_playfile.py +++ b/src/plugins/sound_playfile.py @@ -11,14 +11,14 @@ try: winsound.PlaySound(sound_file, winsound.SND_FILENAME) except ImportError: import os - import subprocess # nosec B404 + import subprocess play_cmd = {} def _subprocess(*args): FNULL = open(os.devnull, 'wb') subprocess.call( - args, stdout=FNULL, stderr=subprocess.STDOUT, close_fds=True) # nosec B603 + args, stdout=FNULL, stderr=subprocess.STDOUT, close_fds=True) def connect_plugin(sound_file): """This function implements the entry point.""" diff --git a/src/proofofwork.py b/src/proofofwork.py index 5e157db9..e43e0f02 100644 --- a/src/proofofwork.py +++ b/src/proofofwork.py @@ -4,71 +4,25 @@ Proof of work calculation """ import ctypes +import hashlib import os -import subprocess # nosec B404 import sys -import tempfile import time from struct import pack, unpack +from subprocess import call -import highlevelcrypto import openclpow import paths import queues import state import tr -from bmconfigparser import config +from bmconfigparser import BMConfigParser from debug import logger 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) @@ -82,25 +36,18 @@ def _set_idle(): pid = win32api.GetCurrentProcessId() handle = win32api.OpenProcess(win32con.PROCESS_ALL_ACCESS, True, pid) win32process.SetPriorityClass(handle, win32process.IDLE_PRIORITY_CLASS) - except: # nosec B110 # noqa:E722 # pylint:disable=bare-except + 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 = trial_value(nonce, initialHash) + trialValue, = unpack('>Q', hashlib.sha512(hashlib.sha512( + pack('>Q', nonce) + initialHash).digest()).digest()[0:8]) return [trialValue, nonce] @@ -110,9 +57,10 @@ def _doSafePoW(target, initialHash): trialValue = float('inf') while trialValue > target and state.shutdown == 0: nonce += 1 - trialValue = trial_value(nonce, initialHash) + trialValue, = unpack('>Q', hashlib.sha512(hashlib.sha512( + pack('>Q', nonce) + initialHash).digest()).digest()[0:8]) if state.shutdown != 0: - raise StopIteration("Interrupted") + raise StopIteration("Interrupted") # pylint: misplaced-bare-raise logger.debug("Safe PoW done") return [trialValue, nonce] @@ -122,11 +70,11 @@ def _doFastPoW(target, initialHash): from multiprocessing import Pool, cpu_count try: pool_size = cpu_count() - except: # noqa:E722 + except: pool_size = 4 try: - maxCores = config.getint('bitmessagesettings', 'maxcores') - except: # noqa:E722 + maxCores = BMConfigParser().getint('bitmessagesettings', 'maxcores') + except: maxCores = 99999 if pool_size > maxCores: pool_size = maxCores @@ -141,7 +89,7 @@ def _doFastPoW(target, initialHash): try: pool.terminate() pool.join() - except: # nosec B110 # noqa:E722 # pylint:disable=bare-except + except: pass raise StopIteration("Interrupted") for i in range(pool_size): @@ -161,15 +109,13 @@ def _doFastPoW(target, initialHash): def _doCPoW(target, initialHash): - 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) + 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]) if state.shutdown != 0: raise StopIteration("Interrupted") logger.debug("C PoW done") @@ -179,7 +125,7 @@ def _doCPoW(target, initialHash): def _doGPUPoW(target, initialHash): logger.debug("GPU PoW start") nonce = openclpow.do_opencl_pow(initialHash.encode("hex"), target) - trialValue = trial_value(nonce, initialHash) + trialValue, = unpack('>Q', hashlib.sha512(hashlib.sha512(pack('>Q', nonce) + initialHash).digest()).digest()[0:8]) if trialValue > target: deviceNames = ", ".join(gpu.name for gpu in openclpow.enabledGpus) queues.UISignalQueue.put(( @@ -278,26 +224,16 @@ def buildCPoW(): try: if "bsd" in sys.platform: # BSD make - subprocess.check_call([ # nosec B607, B603 - "make", "-C", os.path.join(paths.codePath(), "bitmsghash"), - '-f', 'Makefile.bsd']) + call(["make", "-C", os.path.join(paths.codePath(), "bitmsghash"), '-f', 'Makefile.bsd']) else: # GNU make - subprocess.check_call([ # nosec B607, B603 - "make", "-C", os.path.join(paths.codePath(), "bitmsghash")]) - if os.path.exists( - os.path.join(paths.codePath(), "bitmsghash", "bitmsghash.so") - ): + call(["make", "-C", os.path.join(paths.codePath(), "bitmsghash")]) + if os.path.exists(os.path.join(paths.codePath(), "bitmsghash", "bitmsghash.so")): init() notifyBuild(True) else: notifyBuild(True) - except (OSError, subprocess.CalledProcessError): - notifyBuild(True) - except: # noqa:E722 - logger.warning( - 'Unexpected exception rised when tried to build bitmsghash lib', - exc_info=True) + except: notifyBuild(True) @@ -312,14 +248,14 @@ def run(target, initialHash): return _doGPUPoW(target, initialHash) except StopIteration: raise - except: # nosec B110 # noqa:E722 # pylint:disable=bare-except + except: pass # fallback if bmpow: try: return _doCPoW(target, initialHash) except StopIteration: raise - except: # nosec B110 # noqa:E722 # pylint:disable=bare-except + except: pass # fallback if paths.frozen == "macosx_app" or not paths.frozen: # on my (Peter Surda) Windows 10, Windows Defender @@ -331,13 +267,13 @@ def run(target, initialHash): except StopIteration: logger.error("Fast PoW got StopIteration") raise - except: # noqa:E722 # pylint:disable=bare-except + except: logger.error("Fast PoW got exception:", exc_info=True) try: return _doSafePoW(target, initialHash) except StopIteration: raise - except: # nosec B110 # noqa:E722 # pylint:disable=bare-except + except: pass # fallback @@ -355,6 +291,7 @@ def init(): global bitmsglib, bmpow openclpow.initCL() + if sys.platform == "win32": if ctypes.sizeof(ctypes.c_voidp) == 4: bitmsglib = 'bitmsghash32.dll' @@ -368,7 +305,8 @@ def init(): bmpow.restype = ctypes.c_ulonglong _doCPoW(2**63, "") logger.info("Successfully tested C PoW DLL (stdcall) %s", bitmsglib) - except ValueError: + except: + logger.error("C PoW test fail.", exc_info=True) try: # MinGW bso = ctypes.CDLL(os.path.join(paths.codePath(), "bitmsghash", bitmsglib)) @@ -377,12 +315,9 @@ def init(): bmpow.restype = ctypes.c_ulonglong _doCPoW(2**63, "") logger.info("Successfully tested C PoW DLL (cdecl) %s", bitmsglib) - except Exception as e: - logger.error("Error: %s", e, exc_info=True) + except: + logger.error("C PoW test fail.", exc_info=True) bso = None - except Exception as e: - logger.error("Error: %s", e, exc_info=True) - bso = None else: try: bso = ctypes.CDLL(os.path.join(paths.codePath(), "bitmsghash", bitmsglib)) @@ -394,7 +329,7 @@ def init(): ))[0]) except (OSError, IndexError): bso = None - except: # noqa:E722 + except: bso = None else: logger.info("Loaded C PoW DLL %s", bitmsglib) @@ -402,7 +337,7 @@ def init(): try: bmpow = bso.BitmessagePOW bmpow.restype = ctypes.c_ulonglong - except: # noqa:E722 + except: bmpow = None else: bmpow = None diff --git a/src/protocol.py b/src/protocol.py index 96c980bb..4f2d0856 100644 --- a/src/protocol.py +++ b/src/protocol.py @@ -18,27 +18,12 @@ import highlevelcrypto import state from addresses import ( encodeVarint, decodeVarint, decodeAddress, varintDecodeError) -from bmconfigparser import config +from bmconfigparser import BMConfigParser from debug import logger +from fallback import RIPEMD160Hash from helper_sql import sqlExecute -from network.node import Peer from version import softwareVersion -# Network constants -magic = 0xE9BEB4D9 -#: protocol specification says max 1000 addresses in one addr command -MAX_ADDR_COUNT = 1000 -#: address is online if online less than this many seconds ago -ADDRESS_ALIVE = 10800 -#: ~1.6 MB which is the maximum possible size of an inv message. -MAX_MESSAGE_SIZE = 1600100 -#: 2**18 = 256kB is the maximum size of an object payload -MAX_OBJECT_PAYLOAD_SIZE = 2**18 -#: protocol specification says max 50000 objects in one inv command -MAX_OBJECT_COUNT = 50000 -#: maximum time offset -MAX_TIME_OFFSET = 3600 - # Service flags #: This is a normal network node NODE_NETWORK = 1 @@ -71,7 +56,7 @@ OBJECT_I2P = 0x493250 OBJECT_ADDR = 0x61646472 eightBytesOfRandomDataUsedToDetectConnectionsToSelf = pack( - '>Q', random.randrange(1, 18446744073709551615)) # nosec B311 + '>Q', random.randrange(1, 18446744073709551615)) # Compiled struct for packing/unpacking headers # New code should use CreatePacket instead of Header.pack @@ -87,7 +72,7 @@ def getBitfield(address): # bitfield of features supported by me (see the wiki). bitfield = 0 # send ack - if not config.safeGetBoolean(address, 'dontsendack'): + if not BMConfigParser().safeGetBoolean(address, 'dontsendack'): bitfield |= BITFIELD_DOESACK return pack('>I', bitfield) @@ -105,29 +90,24 @@ def isBitSetWithinBitfield(fourByteString, n): x, = unpack('>L', fourByteString) return x & 2**n != 0 -# Streams - -MIN_VALID_STREAM = 1 -MAX_VALID_STREAM = 2**63 - 1 - -# IP addresses +# ip addresses def encodeHost(host): """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( + if host.find('.onion') > -1: + return '\xfd\x87\xd8\x7e\xeb\x43' + base64.b32decode( host.split(".")[0], True) elif host.find(':') == -1: - return b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFF' + \ + return '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFF' + \ socket.inet_aton(host) return socket.inet_pton(socket.AF_INET6, host) def networkType(host): """Determine if a host is IPv4, IPv6 or an onion address""" - if host.endswith('.onion'): + if host.find('.onion') > -1: return 'onion' elif host.find(':') == -1: return 'IPv4' @@ -167,10 +147,10 @@ def checkIPAddress(host, private=False): 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': + if host[0:12] == '\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] == b'\xfd\x87\xd8\x7e\xeb\x43': + elif host[0:6] == '\xfd\x87\xd8\x7e\xeb\x43': # Onion, based on BMD/bitcoind hostStandardFormat = base64.b32encode(host[6:]).lower() + ".onion" if private: @@ -181,7 +161,7 @@ def checkIPAddress(host, private=False): hostStandardFormat = socket.inet_ntop(socket.AF_INET6, host) except ValueError: return False - if len(hostStandardFormat) == 0: + if hostStandardFormat == "": # This can happen on Windows systems which are # not 64-bit compatible so let us drop the IPv6 address. return False @@ -193,23 +173,23 @@ def checkIPv4Address(host, hostStandardFormat, private=False): Returns hostStandardFormat if it is an IPv4 address, otherwise returns False """ - if host[0:1] == b'\x7F': # 127/8 + if host[0] == '\x7F': # 127/8 if not private: logger.debug( 'Ignoring IP address in loopback range: %s', hostStandardFormat) return hostStandardFormat if private else False - if host[0:1] == b'\x0A': # 10/8 + if host[0] == '\x0A': # 10/8 if not private: logger.debug( 'Ignoring IP address in private range: %s', hostStandardFormat) return hostStandardFormat if private else False - if host[0:2] == b'\xC0\xA8': # 192.168/16 + if host[0:2] == '\xC0\xA8': # 192.168/16 if not private: logger.debug( 'Ignoring IP address in private range: %s', hostStandardFormat) return hostStandardFormat if private else False - if host[0:2] >= b'\xAC\x10' and host[0:2] < b'\xAC\x20': # 172.16/12 + if host[0:2] >= '\xAC\x10' and host[0:2] < '\xAC\x20': # 172.16/12 if not private: logger.debug( 'Ignoring IP address in private range: %s', hostStandardFormat) @@ -222,19 +202,15 @@ def checkIPv6Address(host, hostStandardFormat, private=False): Returns hostStandardFormat if it is an IPv6 address, otherwise returns False """ - if host == b'\x00' * 15 + b'\x01': + if host == ('\x00' * 15) + '\x01': if not private: logger.debug('Ignoring loopback address: %s', hostStandardFormat) return False - 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 host[0] == '\xFE' and (ord(host[1]) & 0xc0) == 0x80: if not private: logger.debug('Ignoring local address: %s', hostStandardFormat) return hostStandardFormat if private else False - if host[0] & 0xfe == 0xfc: + if (ord(host[0]) & 0xfe) == 0xfc: if not private: logger.debug( 'Ignoring unique local address: %s', hostStandardFormat) @@ -258,7 +234,7 @@ def haveSSL(server=False): def checkSocksIP(host): """Predicate to check if we're using a SOCKS proxy""" - sockshostname = config.safeGet( + sockshostname = BMConfigParser().safeGet( 'bitmessagesettings', 'sockshostname') try: if not state.socksIP: @@ -274,7 +250,7 @@ def isProofOfWorkSufficient( data, nonceTrialsPerByte=0, payloadLengthExtraBytes=0, recvTime=0): """ Validate an object's Proof of Work using method described - :doc:`here ` + `here `_ Arguments: int nonceTrialsPerByte (default: from `.defaults`) @@ -289,68 +265,47 @@ def isProofOfWorkSufficient( if payloadLengthExtraBytes < defaults.networkDefaultPayloadLengthExtraBytes: payloadLengthExtraBytes = defaults.networkDefaultPayloadLengthExtraBytes endOfLifeTime, = unpack('>Q', data[8:16]) - TTL = endOfLifeTime - int(recvTime if recvTime else time.time()) + TTL = endOfLifeTime - (int(recvTime) if recvTime else int(time.time())) if TTL < 300: TTL = 300 - POW, = unpack('>Q', highlevelcrypto.double_sha512( - data[:8] + hashlib.sha512(data[8:]).digest())[0:8]) + 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)))) + len(data) + payloadLengthExtraBytes + + ((TTL * (len(data) + payloadLengthExtraBytes)) / (2 ** 16)))) # Packet creation -def CreatePacket(command, payload=b''): +def CreatePacket(command, payload=''): """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, magic, command, payload_length, checksum) + Header.pack_into(b, 0, 0xE9BEB4D9, command, payload_length, checksum) b[Header.size:] = payload return bytes(b) -def assembleAddrMessage(peerList): - """Create address command""" - if isinstance(peerList, Peer): - peerList = [peerList] - if not peerList: - return b'' - retval = b'' - for i in range(0, len(peerList), MAX_ADDR_COUNT): - payload = encodeVarint(len(peerList[i:i + MAX_ADDR_COUNT])) - for stream, peer, timestamp in peerList[i:i + MAX_ADDR_COUNT]: - # 64-bit time - payload += pack('>Q', timestamp) - payload += pack('>I', stream) - # service bit flags offered by this node - payload += pack('>q', 1) - payload += encodeHost(peer.host) - # remote port - payload += pack('>H', peer.port) - retval += CreatePacket(b'addr', payload) - return retval - - -def assembleVersionMessage( # pylint: disable=too-many-arguments - remoteHost, remotePort, participatingStreams, dandelion_enabled=True, server=False, nodeid=None, +def assembleVersionMessage( + remoteHost, remotePort, participatingStreams, server=False, nodeid=None ): """ Construct the payload of a version message, return the resulting bytes of running `CreatePacket` on it """ - payload = b'' + payload = '' 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 dandelion_enabled else 0) + NODE_NETWORK | + (NODE_SSL if haveSSL(server) else 0) | + (NODE_DANDELION if state.dandelion else 0) ) payload += pack('>q', int(time.time())) @@ -372,35 +327,35 @@ def assembleVersionMessage( # pylint: disable=too-many-arguments # bitflags of the services I offer. payload += pack( '>q', - NODE_NETWORK - | (NODE_SSL if haveSSL(server) else 0) - | (NODE_DANDELION if dandelion_enabled else 0) + NODE_NETWORK | + (NODE_SSL if haveSSL(server) else 0) | + (NODE_DANDELION if state.dandelion 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( + payload += '\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') + extport = BMConfigParser().safeGetInt('bitmessagesettings', 'extport') if ( extport and ((server and not checkSocksIP(remoteHost)) or ( - config.get('bitmessagesettings', 'socksproxytype') + BMConfigParser().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')) + '>H', BMConfigParser().getint('bitmessagesettings', 'onionport')) else: # no extport and not incoming over Tor payload += pack( - '>H', config.getint('bitmessagesettings', 'port')) + '>H', BMConfigParser().getint('bitmessagesettings', 'port')) if nodeid is not None: payload += nodeid[0:8] else: payload += eightBytesOfRandomDataUsedToDetectConnectionsToSelf - userAgent = ('/PyBitmessage:%s/' % softwareVersion).encode('utf-8') + userAgent = '/PyBitmessage:' + softwareVersion + '/' payload += encodeVarint(len(userAgent)) payload += userAgent @@ -414,7 +369,7 @@ def assembleVersionMessage( # pylint: disable=too-many-arguments if count >= 160000: break - return CreatePacket(b'version', payload) + return CreatePacket('version', payload) def assembleErrorMessage(fatal=0, banTime=0, inventoryVector='', errorText=''): @@ -428,23 +383,12 @@ def assembleErrorMessage(fatal=0, banTime=0, inventoryVector='', errorText=''): payload += inventoryVector payload += encodeVarint(len(errorText)) payload += errorText - return CreatePacket(b'error', payload) + return CreatePacket('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 @@ -501,8 +445,7 @@ def decryptAndCheckPubkeyPayload(data, address): return 'failed' try: decryptedData = cryptorObject.decrypt(encryptedData) - except: # noqa:E722 - # FIXME: use a proper exception after `pyelliptic.ecc` is refactored. + 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.') @@ -511,9 +454,9 @@ def decryptAndCheckPubkeyPayload(data, address): readPosition = 0 # bitfieldBehaviors = decryptedData[readPosition:readPosition + 4] readPosition += 4 - pubSigningKey = '\x04' + decryptedData[readPosition:readPosition + 64] + publicSigningKey = '\x04' + decryptedData[readPosition:readPosition + 64] readPosition += 64 - pubEncryptionKey = '\x04' + decryptedData[readPosition:readPosition + 64] + publicEncryptionKey = '\x04' + decryptedData[readPosition:readPosition + 64] readPosition += 64 specifiedNonceTrialsPerByteLength = decodeVarint( decryptedData[readPosition:readPosition + 10])[1] @@ -529,7 +472,7 @@ def decryptAndCheckPubkeyPayload(data, address): signature = decryptedData[readPosition:readPosition + signatureLength] if not highlevelcrypto.verify( - signedData, signature, hexlify(pubSigningKey)): + signedData, signature, hexlify(publicSigningKey)): logger.info( 'ECDSA verify failed (within decryptAndCheckPubkeyPayload)') return 'failed' @@ -537,7 +480,9 @@ def decryptAndCheckPubkeyPayload(data, address): logger.info( 'ECDSA verify passed (within decryptAndCheckPubkeyPayload)') - embeddedRipe = highlevelcrypto.to_ripe(pubSigningKey, pubEncryptionKey) + sha = hashlib.new('sha512') + sha.update(publicSigningKey + publicEncryptionKey) + embeddedRipe = RIPEMD160Hash(sha.digest()).digest() if embeddedRipe != ripe: # Although this pubkey object had the tag were were looking for @@ -555,7 +500,7 @@ def decryptAndCheckPubkeyPayload(data, address): 'addressVersion: %s, streamNumber: %s\nripe %s\n' 'publicSigningKey in hex: %s\npublicEncryptionKey in hex: %s', addressVersion, streamNumber, hexlify(ripe), - hexlify(pubSigningKey), hexlify(pubEncryptionKey) + hexlify(publicSigningKey), hexlify(publicEncryptionKey) ) t = (address, addressVersion, storedData, int(time.time()), 'yes') diff --git a/src/pyelliptic/arithmetic.py b/src/pyelliptic/arithmetic.py index 23c24b5e..83e634ad 100644 --- a/src/pyelliptic/arithmetic.py +++ b/src/pyelliptic/arithmetic.py @@ -16,7 +16,7 @@ def inv(a, n): lm, hm = 1, 0 low, high = a % n, n while low > 1: - r = high // low + r = high / low nm, new = hm - lm * r, high - low * r lm, low, hm, high = nm, new, lm, low return lm % n @@ -25,31 +25,28 @@ def inv(a, n): def get_code_string(base): """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!") + 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): """Returns the encoded string""" code_string = get_code_string(base) - result = b'' + result = "" while val > 0: - val, i = divmod(val, base) - result = code_string[i:i + 1] + result + result = code_string[val % base] + result + val /= base if len(result) < minlen: - result = code_string[0:1] * (minlen - len(result)) + result + result = code_string[0] * (minlen - len(result)) + result return result @@ -104,11 +101,10 @@ def base10_multiply(a, n): 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) + 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) return None @@ -119,7 +115,7 @@ def hex_to_point(h): def point_to_hex(p): """Converting point value to hexadecimal""" - return b'04' + encode(p[0], 16, 64) + encode(p[1], 16, 64) + return '04' + encode(p[0], 16, 64) + encode(p[1], 16, 64) def multiply(privkey, pubkey): diff --git a/src/pyelliptic/cipher.py b/src/pyelliptic/cipher.py index af6c08ca..4057e169 100644 --- a/src/pyelliptic/cipher.py +++ b/src/pyelliptic/cipher.py @@ -1,10 +1,12 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- """ Symmetric Encryption """ # Copyright (C) 2011 Yann GUIBET # See LICENSE for details. -from .openssl import OpenSSL +from openssl import OpenSSL # pylint: disable=redefined-builtin diff --git a/src/pyelliptic/ecc.py b/src/pyelliptic/ecc.py index c670d023..a7f5a6b7 100644 --- a/src/pyelliptic/ecc.py +++ b/src/pyelliptic/ecc.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- """ Asymmetric cryptography using elliptic curves """ @@ -8,9 +10,9 @@ Asymmetric cryptography using elliptic curves from hashlib import sha512 from struct import pack, unpack -from .cipher import Cipher -from .hash import equals, hmac_sha256 -from .openssl import OpenSSL +from cipher import Cipher +from hash import equals, hmac_sha256 +from openssl import OpenSSL class ECC(object): @@ -18,31 +20,28 @@ class ECC(object): Asymmetric encryption with Elliptic Curve Cryptography (ECC) ECDH, ECDSA and ECIES - >>> from binascii import hexlify >>> import pyelliptic >>> alice = pyelliptic.ECC() # default curve: sect283r1 >>> bob = pyelliptic.ECC(curve='sect571r1') >>> ciphertext = alice.encrypt("Hello Bob", bob.get_pubkey()) - >>> print(bob.decrypt(ciphertext)) + >>> print bob.decrypt(ciphertext) >>> signature = bob.sign("Hello Alice") >>> # alice's job : - >>> print(pyelliptic.ECC( - >>> pubkey=bob.get_pubkey()).verify(signature, "Hello Alice")) + >>> print pyelliptic.ECC( + >>> pubkey=bob.get_pubkey()).verify(signature, "Hello Alice") >>> # ERROR !!! >>> try: >>> key = alice.get_ecdh_key(bob.get_pubkey()) >>> except: - >>> print( - "For ECDH key agreement, the keys must be defined" - " on the same curve!") + >>> print("For ECDH key agreement, the keys must be defined on the same curve !") >>> alice = pyelliptic.ECC(curve='sect571r1') - >>> print(hexlify(alice.get_ecdh_key(bob.get_pubkey()))) - >>> print(hexlify(bob.get_ecdh_key(alice.get_pubkey()))) + >>> print alice.get_ecdh_key(bob.get_pubkey()).encode('hex') + >>> print bob.get_ecdh_key(alice.get_pubkey()).encode('hex') """ @@ -56,7 +55,7 @@ class ECC(object): curve='sect283r1', ): # pylint: disable=too-many-arguments """ - For a normal and high level use, specifie pubkey, + For a normal and High level use, specifie pubkey, privkey (if you need) and the curve """ if isinstance(curve, str): @@ -83,19 +82,20 @@ class ECC(object): self.pubkey_y = None self.privkey = None raise Exception("Bad ECC keys ...") - self.pubkey_x = pubkey_x - self.pubkey_y = pubkey_y - self.privkey = privkey + else: + 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""" + """Encryption object from curve name""" return OpenSSL.get_curve_by_id(self.curve) def get_curve_id(self): @@ -107,19 +107,12 @@ class ECC(object): High level function which returns : curve(2) + len_of_pubkeyX(2) + pubkeyX + len_of_pubkeyY + pubkeyY """ - ctx = OpenSSL.BN_CTX_new() - n = OpenSSL.BN_new() - group = OpenSSL.EC_GROUP_new_by_curve_name(self.curve) - OpenSSL.EC_GROUP_get_order(group, n, ctx) - key_len = OpenSSL.BN_num_bytes(n) - pubkey_x = self.pubkey_x.rjust(key_len, b'\x00') - pubkey_y = self.pubkey_y.rjust(key_len, b'\x00') return b''.join(( pack('!H', self.curve), - pack('!H', len(pubkey_x)), - pubkey_x, - pack('!H', len(pubkey_y)), - pubkey_y, + pack('!H', len(self.pubkey_x)), + self.pubkey_x, + pack('!H', len(self.pubkey_y)), + self.pubkey_y, )) def get_privkey(self): @@ -167,9 +160,9 @@ class ECC(object): key = OpenSSL.EC_KEY_new_by_curve_name(self.curve) if key == 0: raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") - if OpenSSL.EC_KEY_generate_key(key) == 0: + if (OpenSSL.EC_KEY_generate_key(key)) == 0: raise Exception("[OpenSSL] EC_KEY_generate_key FAIL ...") - if OpenSSL.EC_KEY_check_key(key) == 0: + if (OpenSSL.EC_KEY_check_key(key)) == 0: raise Exception("[OpenSSL] EC_KEY_check_key FAIL ...") priv_key = OpenSSL.EC_KEY_get0_private_key(key) @@ -202,7 +195,7 @@ class ECC(object): def get_ecdh_key(self, pubkey): """ High level function. Compute public key with the local private key - and returns a 512bits shared key. + and returns a 512bits shared key """ curve, pubkey_x, pubkey_y, _ = ECC._decode_pubkey(pubkey) if curve != self.curve: @@ -218,31 +211,31 @@ class ECC(object): if other_key == 0: raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") - other_pub_key_x = OpenSSL.BN_bin2bn(pubkey_x, len(pubkey_x), None) - other_pub_key_y = OpenSSL.BN_bin2bn(pubkey_y, len(pubkey_y), None) + 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_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), None) + self.privkey, len(self.privkey), 0) - 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: @@ -268,7 +261,7 @@ class ECC(object): def check_key(self, privkey, pubkey): """ Check the public key and the private key. - The private key is optional (replace by None). + The private key is optional (replace by None) """ curve, pubkey_x, pubkey_y, _ = ECC._decode_pubkey(pubkey) if privkey is None: @@ -286,32 +279,34 @@ class ECC(object): curve = self.curve elif isinstance(curve, str): curve = OpenSSL.get_curve(curve) + else: + curve = curve try: key = OpenSSL.EC_KEY_new_by_curve_name(curve) if key == 0: raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") if privkey is not None: - priv_key = OpenSSL.BN_bin2bn(privkey, len(privkey), 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) + 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) 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 @@ -343,27 +338,25 @@ class ECC(object): if key == 0: raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") - priv_key = OpenSSL.BN_bin2bn(self.privkey, len(self.privkey), 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) + 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) - 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: @@ -372,13 +365,12 @@ class ECC(object): OpenSSL.EVP_MD_CTX_init(md_ctx) OpenSSL.EVP_DigestInit_ex(md_ctx, digest_alg(), None) - if OpenSSL.EVP_DigestUpdate(md_ctx, buff, size) == 0: + if (OpenSSL.EVP_DigestUpdate(md_ctx, buff, size)) == 0: raise Exception("[OpenSSL] EVP_DigestUpdate FAIL ...") OpenSSL.EVP_DigestFinal_ex(md_ctx, digest, dgst_len) OpenSSL.ECDSA_sign(0, digest, dgst_len.contents, sig, siglen, key) - if OpenSSL.ECDSA_verify( - 0, digest, dgst_len.contents, sig, siglen.contents, key - ) != 1: + if (OpenSSL.ECDSA_verify(0, digest, dgst_len.contents, sig, + siglen.contents, key)) != 1: raise Exception("[OpenSSL] ECDSA_verify FAIL ...") return sig.raw[:siglen.contents.value] @@ -397,7 +389,7 @@ class ECC(object): def verify(self, sig, inputb, digest_alg=OpenSSL.digest_ecdsa_sha1): """ Verify the signature with the input and the local public key. - Returns a boolean. + Returns a boolean """ try: bsig = OpenSSL.malloc(sig, len(sig)) @@ -413,29 +405,27 @@ class ECC(object): if key == 0: raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") - pub_key_x = OpenSSL.BN_bin2bn(self.pubkey_x, len(self.pubkey_x), - None) - pub_key_y = OpenSSL.BN_bin2bn(self.pubkey_y, len(self.pubkey_y), - None) + 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) 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) @@ -479,7 +469,7 @@ class ECC(object): ephemcurve=None, ciphername='aes-256-cbc', ): # pylint: disable=too-many-arguments - """ECDH encryption, keys supplied in binary data format""" + """ECHD encryption, keys supplied in binary data format""" if ephemcurve is None: ephemcurve = curve @@ -487,9 +477,9 @@ class ECC(object): key = sha512(ephem.raw_get_ecdh_key(pubkey_x, pubkey_y)).digest() key_e, key_m = key[:32], key[32:] pubkey = ephem.get_pubkey() - _iv = Cipher.gen_IV(ciphername) - ctx = Cipher(key_e, _iv, 1, ciphername) - ciphertext = _iv + pubkey + ctx.ciphering(data) + iv = OpenSSL.rand(OpenSSL.get_cipher(ciphername).get_blocksize()) + ctx = Cipher(key_e, iv, 1, ciphername) + ciphertext = iv + pubkey + ctx.ciphering(data) mac = hmac_sha256(key_m, ciphertext) return ciphertext + mac @@ -498,10 +488,10 @@ class ECC(object): Decrypt data with ECIES method using the local private key """ blocksize = OpenSSL.get_cipher(ciphername).get_blocksize() - _iv = data[:blocksize] + iv = data[:blocksize] i = blocksize - _, pubkey_x, pubkey_y, _i2 = ECC._decode_pubkey(data[i:]) - i += _i2 + _, pubkey_x, pubkey_y, i2 = ECC._decode_pubkey(data[i:]) + i += i2 ciphertext = data[i:len(data) - 32] i += len(ciphertext) mac = data[i:] @@ -509,6 +499,5 @@ class ECC(object): key_e, key_m = key[:32], key[32:] if not equals(hmac_sha256(key_m, data[:len(data) - 32]), mac): raise RuntimeError("Fail to verify data") - ctx = Cipher(key_e, _iv, 0, ciphername) - retval = ctx.ciphering(ciphertext) - return retval + ctx = Cipher(key_e, iv, 0, ciphername) + return ctx.ciphering(ciphertext) diff --git a/src/pyelliptic/eccblind.py b/src/pyelliptic/eccblind.py index df987824..a417451e 100644 --- a/src/pyelliptic/eccblind.py +++ b/src/pyelliptic/eccblind.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python """ ECC blind signature functionality based on "An Efficient Blind Signature Scheme @@ -109,7 +110,7 @@ class ECCBlind(object): # pylint: disable=too-many-instance-attributes """ ECC inversion """ - inverse = OpenSSL.BN_mod_inverse(None, a, self.n, self.ctx) + inverse = OpenSSL.BN_mod_inverse(0, a, self.n, self.ctx) return inverse def ec_gen_keypair(self): @@ -119,7 +120,7 @@ class ECCBlind(object): # pylint: disable=too-many-instance-attributes """ d = self.ec_get_random() Q = OpenSSL.EC_POINT_new(self.group) - OpenSSL.EC_POINT_mul(self.group, Q, d, None, None, None) + OpenSSL.EC_POINT_mul(self.group, Q, d, 0, 0, 0) return (d, Q) def ec_Ftor(self, F): @@ -139,7 +140,7 @@ class ECCBlind(object): # pylint: disable=too-many-instance-attributes x = OpenSSL.BN_new() y = OpenSSL.BN_new() OpenSSL.EC_POINT_get_affine_coordinates( - self.group, point, x, y, None) + self.group, point, x, y, 0) y_byte = (OpenSSL.BN_is_odd(y) & Y_BIT) | COMPRESSED_BIT l_ = OpenSSL.BN_num_bytes(self.n) try: @@ -150,7 +151,7 @@ class ECCBlind(object): # pylint: disable=too-many-instance-attributes # padding manually bx = OpenSSL.malloc(0, OpenSSL.BN_num_bytes(x)) OpenSSL.BN_bn2bin(x, bx) - out = bx.raw.rjust(l_, b'\x00') + out = bx.raw.rjust(l_, chr(0)) return pack(EC, y_byte, out) finally: @@ -160,7 +161,7 @@ class ECCBlind(object): # pylint: disable=too-many-instance-attributes def _ec_point_deserialize(self, data): """Make a string into an EC point""" y_bit, x_raw = unpack(EC, data) - x = OpenSSL.BN_bin2bn(x_raw, OpenSSL.BN_num_bytes(self.n), None) + x = OpenSSL.BN_bin2bn(x_raw, OpenSSL.BN_num_bytes(self.n), 0) y_bit &= Y_BIT retval = OpenSSL.EC_POINT_new(self.group) OpenSSL.EC_POINT_set_compressed_coordinates(self.group, @@ -180,11 +181,11 @@ class ECCBlind(object): # pylint: disable=too-many-instance-attributes except AttributeError: o = OpenSSL.malloc(0, OpenSSL.BN_num_bytes(bn)) OpenSSL.BN_bn2bin(bn, o) - return o.raw.rjust(l_, b'\x00') + return o.raw.rjust(l_, chr(0)) def _bn_deserialize(self, data): """Make a BigNum out of string""" - x = OpenSSL.BN_bin2bn(data, OpenSSL.BN_num_bytes(self.n), None) + x = OpenSSL.BN_bin2bn(data, OpenSSL.BN_num_bytes(self.n), 0) return x def _init_privkey(self, privkey): @@ -261,7 +262,7 @@ class ECCBlind(object): # pylint: disable=too-many-instance-attributes # R = kG self.R = OpenSSL.EC_POINT_new(self.group) - OpenSSL.EC_POINT_mul(self.group, self.R, self.k, None, None, None) + OpenSSL.EC_POINT_mul(self.group, self.R, self.k, 0, 0, 0) return self._ec_point_serialize(self.R) @@ -286,18 +287,17 @@ class ECCBlind(object): # pylint: disable=too-many-instance-attributes # F = b^-1 * R... self.binv = self.ec_invert(self.b) - OpenSSL.EC_POINT_mul(self.group, temp, None, self.R, self.binv, - None) + OpenSSL.EC_POINT_mul(self.group, temp, 0, self.R, self.binv, 0) 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) + OpenSSL.EC_POINT_mul(self.group, temp, 0, self.Q, abinv, 0) + OpenSSL.EC_POINT_add(self.group, self.F, self.F, temp, 0) # ... + 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) + OpenSSL.EC_POINT_mul(self.group, temp, 0, self.G, self.c, 0) + OpenSSL.EC_POINT_add(self.group, self.F, self.F, temp, 0) # F = (x0, y0) self.r = self.ec_Ftor(self.F) @@ -356,10 +356,10 @@ class ECCBlind(object): # pylint: disable=too-many-instance-attributes lhs = OpenSSL.EC_POINT_new(self.group) rhs = OpenSSL.EC_POINT_new(self.group) - OpenSSL.EC_POINT_mul(self.group, lhs, s, None, None, None) + OpenSSL.EC_POINT_mul(self.group, lhs, s, 0, 0, 0) - OpenSSL.EC_POINT_mul(self.group, rhs, None, self.Q, self.m, None) - OpenSSL.EC_POINT_mul(self.group, rhs, None, rhs, self.r, None) + OpenSSL.EC_POINT_mul(self.group, rhs, 0, self.Q, self.m, 0) + OpenSSL.EC_POINT_mul(self.group, rhs, 0, rhs, self.r, 0) OpenSSL.EC_POINT_add(self.group, rhs, rhs, self.F, self.ctx) retval = OpenSSL.EC_POINT_cmp(self.group, lhs, rhs, self.ctx) diff --git a/src/pyelliptic/hash.py b/src/pyelliptic/hash.py index 70c9a6ce..f098d631 100644 --- a/src/pyelliptic/hash.py +++ b/src/pyelliptic/hash.py @@ -4,7 +4,7 @@ Wrappers for hash functions from OpenSSL. # Copyright (C) 2011 Yann GUIBET # See LICENSE for details. -from .openssl import OpenSSL +from openssl import OpenSSL # For python3 diff --git a/src/pyelliptic/openssl.py b/src/pyelliptic/openssl.py index 851dfa15..17a8d6d1 100644 --- a/src/pyelliptic/openssl.py +++ b/src/pyelliptic/openssl.py @@ -72,29 +72,6 @@ def get_version(library): return (version, hexversion, cflags) -class BIGNUM(ctypes.Structure): # pylint: disable=too-few-public-methods - """OpenSSL's BIGNUM struct""" - _fields_ = [ - ('d', ctypes.POINTER(ctypes.c_ulong)), - ('top', ctypes.c_int), - ('dmax', ctypes.c_int), - ('neg', ctypes.c_int), - ('flags', ctypes.c_int), - ] - - -class EC_POINT(ctypes.Structure): # pylint: disable=too-few-public-methods - """OpenSSL's EC_POINT struct""" - _fields_ = [ - ('meth', ctypes.c_void_p), - ('curve_name', ctypes.c_int), - ('X', ctypes.POINTER(BIGNUM)), - ('Y', ctypes.POINTER(BIGNUM)), - ('Z', ctypes.POINTER(BIGNUM)), - ('Z_is_one', ctypes.c_int), - ] - - class _OpenSSL(object): """ Wrapper for OpenSSL using ctypes @@ -106,7 +83,7 @@ class _OpenSSL(object): """ self._lib = ctypes.CDLL(library) self._version, self._hexversion, self._cflags = get_version(self._lib) - self._libreSSL = self._version.startswith(b"LibreSSL") + self._libreSSL = self._version.startswith("LibreSSL") self.pointer = ctypes.pointer self.c_int = ctypes.c_int @@ -114,38 +91,38 @@ class _OpenSSL(object): self.create_string_buffer = ctypes.create_string_buffer self.BN_new = self._lib.BN_new - self.BN_new.restype = ctypes.POINTER(BIGNUM) + self.BN_new.restype = ctypes.c_void_p self.BN_new.argtypes = [] self.BN_free = self._lib.BN_free self.BN_free.restype = None - self.BN_free.argtypes = [ctypes.POINTER(BIGNUM)] + self.BN_free.argtypes = [ctypes.c_void_p] 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_clear_free.argtypes = [ctypes.c_void_p] self.BN_num_bits = self._lib.BN_num_bits self.BN_num_bits.restype = ctypes.c_int - self.BN_num_bits.argtypes = [ctypes.POINTER(BIGNUM)] + self.BN_num_bits.argtypes = [ctypes.c_void_p] self.BN_bn2bin = self._lib.BN_bn2bin self.BN_bn2bin.restype = ctypes.c_int - self.BN_bn2bin.argtypes = [ctypes.POINTER(BIGNUM), ctypes.c_void_p] + self.BN_bn2bin.argtypes = [ctypes.c_void_p, 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, + self.BN_bn2binpad.argtypes = [ctypes.c_void_p, 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.POINTER(BIGNUM) + self.BN_bin2bn.restype = ctypes.c_void_p self.BN_bin2bn.argtypes = [ctypes.c_void_p, ctypes.c_int, - ctypes.POINTER(BIGNUM)] + ctypes.c_void_p] self.EC_KEY_free = self._lib.EC_KEY_free self.EC_KEY_free.restype = None @@ -164,11 +141,11 @@ class _OpenSSL(object): self.EC_KEY_check_key.argtypes = [ctypes.c_void_p] self.EC_KEY_get0_private_key = self._lib.EC_KEY_get0_private_key - self.EC_KEY_get0_private_key.restype = ctypes.POINTER(BIGNUM) + self.EC_KEY_get0_private_key.restype = ctypes.c_void_p 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.POINTER(EC_POINT) + self.EC_KEY_get0_public_key.restype = ctypes.c_void_p self.EC_KEY_get0_public_key.argtypes = [ctypes.c_void_p] self.EC_KEY_get0_group = self._lib.EC_KEY_get0_group @@ -179,9 +156,9 @@ class _OpenSSL(object): self._lib.EC_POINT_get_affine_coordinates_GFp self.EC_POINT_get_affine_coordinates_GFp.restype = ctypes.c_int self.EC_POINT_get_affine_coordinates_GFp.argtypes = [ctypes.c_void_p, - ctypes.POINTER(EC_POINT), - ctypes.POINTER(BIGNUM), - ctypes.POINTER(BIGNUM), + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_void_p, ctypes.c_void_p] try: @@ -193,20 +170,20 @@ class _OpenSSL(object): self._lib.EC_POINT_get_affine_coordinates_GF2m self.EC_POINT_get_affine_coordinates.restype = ctypes.c_int self.EC_POINT_get_affine_coordinates.argtypes = [ctypes.c_void_p, - ctypes.POINTER(EC_POINT), - ctypes.POINTER(BIGNUM), - ctypes.POINTER(BIGNUM), + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_void_p, 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.POINTER(BIGNUM)] + ctypes.c_void_p] 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.POINTER(EC_POINT)] + ctypes.c_void_p] self.EC_KEY_set_group = self._lib.EC_KEY_set_group self.EC_KEY_set_group.restype = ctypes.c_int @@ -217,9 +194,9 @@ class _OpenSSL(object): self._lib.EC_POINT_set_affine_coordinates_GFp self.EC_POINT_set_affine_coordinates_GFp.restype = ctypes.c_int self.EC_POINT_set_affine_coordinates_GFp.argtypes = [ctypes.c_void_p, - ctypes.POINTER(EC_POINT), - ctypes.POINTER(BIGNUM), - ctypes.POINTER(BIGNUM), + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_void_p, ctypes.c_void_p] try: @@ -231,9 +208,9 @@ class _OpenSSL(object): self._lib.EC_POINT_set_affine_coordinates_GF2m self.EC_POINT_set_affine_coordinates.restype = ctypes.c_int self.EC_POINT_set_affine_coordinates.argtypes = [ctypes.c_void_p, - ctypes.POINTER(EC_POINT), - ctypes.POINTER(BIGNUM), - ctypes.POINTER(BIGNUM), + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_void_p, ctypes.c_void_p] try: @@ -242,39 +219,38 @@ class _OpenSSL(object): except AttributeError: # OpenSSL docs say only use this for backwards compatibility self.EC_POINT_set_compressed_coordinates = \ - self._lib.EC_POINT_set_compressed_coordinates_GFp + self._lib.EC_POINT_set_compressed_coordinates_GF2m 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_void_p, + ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p] self.EC_POINT_new = self._lib.EC_POINT_new - self.EC_POINT_new.restype = ctypes.POINTER(EC_POINT) + self.EC_POINT_new.restype = ctypes.c_void_p 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.POINTER(EC_POINT)] + self.EC_POINT_free.argtypes = [ctypes.c_void_p] 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 = ctypes.c_int + self.EC_POINT_mul.restype = None 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, + ctypes.c_void_p, + ctypes.c_void_p, 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.POINTER(BIGNUM)] + ctypes.c_void_p] if self._hexversion >= 0x10100000 and not self._libreSSL: self.EC_KEY_OpenSSL = self._lib.EC_KEY_OpenSSL @@ -392,7 +368,7 @@ class _OpenSSL(object): self.EVP_DigestUpdate = self._lib.EVP_DigestUpdate self.EVP_DigestUpdate.restype = ctypes.c_int self.EVP_DigestUpdate.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, ctypes.c_size_t] + ctypes.c_void_p, ctypes.c_int] self.EVP_DigestFinal = self._lib.EVP_DigestFinal self.EVP_DigestFinal.restype = ctypes.c_int @@ -472,12 +448,12 @@ class _OpenSSL(object): self.HMAC = self._lib.HMAC self.HMAC.restype = ctypes.c_void_p self.HMAC.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int, - ctypes.c_void_p, ctypes.c_size_t, + ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p] try: self.PKCS5_PBKDF2_HMAC = self._lib.PKCS5_PBKDF2_HMAC - except Exception: + except: # The above is not compatible with all versions of OSX. self.PKCS5_PBKDF2_HMAC = self._lib.PKCS5_PBKDF2_HMAC_SHA1 @@ -493,71 +469,70 @@ class _OpenSSL(object): 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_dup.restype = ctypes.c_void_p + self.BN_dup.argtypes = [ctypes.c_void_p] self.BN_rand = self._lib.BN_rand self.BN_rand.restype = ctypes.c_int - self.BN_rand.argtypes = [ctypes.POINTER(BIGNUM), - ctypes.c_int, + self.BN_rand.argtypes = [ctypes.c_void_p, 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), + self.BN_set_word.argtypes = [ctypes.c_void_p, 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), + self.BN_mul.argtypes = [ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_void_p, 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), + self.BN_mod_add.argtypes = [ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_void_p, 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), + self.BN_mod_inverse.restype = ctypes.c_void_p + self.BN_mod_inverse.argtypes = [ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_void_p, 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), + self.BN_mod_mul.argtypes = [ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_void_p, 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), + self.BN_lshift.argtypes = [ctypes.c_void_p, + ctypes.c_void_p, 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), + self.BN_sub_word.argtypes = [ctypes.c_void_p, 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)] + self.BN_cmp.argtypes = [ctypes.c_void_p, + ctypes.c_void_p] 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)] + self.BN_is_odd.argtypes = [ctypes.c_void_p] except AttributeError: # OpenSSL 1.1.0 implements this as a function, but earlier # versions as macro, so we need to workaround @@ -565,7 +540,7 @@ class _OpenSSL(object): self.BN_bn2dec = self._lib.BN_bn2dec self.BN_bn2dec.restype = ctypes.c_char_p - self.BN_bn2dec.argtypes = [ctypes.POINTER(BIGNUM)] + self.BN_bn2dec.argtypes = [ctypes.c_void_p] 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 @@ -574,43 +549,43 @@ class _OpenSSL(object): self.EC_GROUP_get_order = self._lib.EC_GROUP_get_order self.EC_GROUP_get_order.restype = ctypes.c_int self.EC_GROUP_get_order.argtypes = [ctypes.c_void_p, - ctypes.POINTER(BIGNUM), + ctypes.c_void_p, 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, 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.restype = ctypes.c_void_p 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_copy.argtypes = [ctypes.c_void_p, + ctypes.c_void_p] 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, + ctypes.c_void_p, + ctypes.c_void_p, 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, + ctypes.c_void_p, 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)] + ctypes.c_void_p] self._set_ciphers() self._set_curves() @@ -805,10 +780,6 @@ def loadOpenSSL(): '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') @@ -823,7 +794,7 @@ def loadOpenSSL(): try: OpenSSL = _OpenSSL(library) return - except Exception: # nosec B110 + except: pass raise Exception( "Couldn't find and load the OpenSSL library. You must install it.") diff --git a/src/pyelliptic/tests/__init__.py b/src/pyelliptic/tests/__init__.py deleted file mode 100644 index b53ef881..00000000 --- a/src/pyelliptic/tests/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index 0348d3f0..00000000 --- a/src/pyelliptic/tests/samples.py +++ /dev/null @@ -1,111 +0,0 @@ -"""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 deleted file mode 100644 index 1d1aecaf..00000000 --- a/src/pyelliptic/tests/test_arithmetic.py +++ /dev/null @@ -1,100 +0,0 @@ -""" -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_ecc.py b/src/pyelliptic/tests/test_ecc.py deleted file mode 100644 index e87d1c21..00000000 --- a/src/pyelliptic/tests/test_ecc.py +++ /dev/null @@ -1,102 +0,0 @@ -"""Tests for ECC object""" - -import os -import unittest -from hashlib import sha512 - -try: - import pyelliptic -except ImportError: - from pybitmessage import pyelliptic - -from .samples import ( - sample_pubkey, sample_iv, sample_ephem_privkey, sample_ephem_pubkey, - sample_enkey, sample_mackey, sample_data, sample_ciphertext, sample_mac) - - -sample_pubkey_x = sample_ephem_pubkey[1:-32] -sample_pubkey_y = sample_ephem_pubkey[-32:] -sample_pubkey_bin = ( - b'\x02\xca\x00\x20' + sample_pubkey_x + b'\x00\x20' + sample_pubkey_y) -sample_privkey_bin = b'\x02\xca\x00\x20' + sample_ephem_privkey - - -class TestECC(unittest.TestCase): - """The test case for ECC""" - - def test_random_keys(self): - """A dummy test for random keys in ECC object""" - eccobj = pyelliptic.ECC(curve='secp256k1') - self.assertTrue(len(eccobj.privkey) <= 32) - pubkey = eccobj.get_pubkey() - self.assertEqual(pubkey[:4], b'\x02\xca\x00\x20') - - def test_short_keys(self): - """Check formatting of the keys with leading zeroes""" - # pylint: disable=protected-access - def sample_key(_): - """Fake ECC keypair""" - return os.urandom(32), os.urandom(31), os.urandom(30) - - try: - gen_orig = pyelliptic.ECC._generate - pyelliptic.ECC._generate = sample_key - eccobj = pyelliptic.ECC(curve='secp256k1') - pubkey = eccobj.get_pubkey() - self.assertEqual(pubkey[:4], b'\x02\xca\x00\x20') - self.assertEqual(pubkey[36:38], b'\x00\x20') - self.assertEqual(len(pubkey[38:]), 32) - finally: - pyelliptic.ECC._generate = gen_orig - - def test_decode_keys(self): - """Check keys decoding""" - # pylint: disable=protected-access - curve_secp256k1 = pyelliptic.OpenSSL.get_curve('secp256k1') - curve, raw_privkey, _ = pyelliptic.ECC._decode_privkey( - sample_privkey_bin) - self.assertEqual(curve, curve_secp256k1) - self.assertEqual( - pyelliptic.OpenSSL.get_curve_by_id(curve), 'secp256k1') - self.assertEqual(sample_ephem_privkey, raw_privkey) - - curve, pubkey_x, pubkey_y, _ = pyelliptic.ECC._decode_pubkey( - sample_pubkey_bin) - self.assertEqual(curve, curve_secp256k1) - self.assertEqual(sample_pubkey_x, pubkey_x) - self.assertEqual(sample_pubkey_y, pubkey_y) - - def test_encode_keys(self): - """Check keys encoding""" - cryptor = pyelliptic.ECC( - pubkey_x=sample_pubkey_x, - pubkey_y=sample_pubkey_y, - raw_privkey=sample_ephem_privkey, curve='secp256k1') - self.assertEqual(cryptor.get_privkey(), sample_privkey_bin) - self.assertEqual(cryptor.get_pubkey(), sample_pubkey_bin) - - def test_encryption_parts(self): - """Check results of the encryption steps against samples in the Spec""" - ephem = pyelliptic.ECC( - pubkey_x=sample_pubkey_x, - pubkey_y=sample_pubkey_y, - raw_privkey=sample_ephem_privkey, curve='secp256k1') - key = sha512(ephem.raw_get_ecdh_key( - sample_pubkey[1:-32], sample_pubkey[-32:])).digest() - self.assertEqual(sample_enkey, key[:32]) - self.assertEqual(sample_mackey, key[32:]) - - ctx = pyelliptic.Cipher(sample_enkey, sample_iv, 1) - self.assertEqual(ctx.ciphering(sample_data), sample_ciphertext) - self.assertEqual( - sample_mac, - pyelliptic.hash.hmac_sha256( - sample_mackey, - sample_iv + sample_pubkey_bin + sample_ciphertext)) - - def test_decryption(self): - """Check decription of a message by random cryptor""" - random_recipient = pyelliptic.ECC(curve='secp256k1') - payload = pyelliptic.ECC.encrypt( - sample_data, random_recipient.get_pubkey()) - self.assertEqual(random_recipient.decrypt(payload), sample_data) diff --git a/src/qidenticon.py b/src/qidenticon.py index 13be3578..6eab09cd 100644 --- a/src/qidenticon.py +++ b/src/qidenticon.py @@ -1,51 +1,18 @@ -### -# qidenticon.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: -# -# 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. -### - # pylint: disable=too-many-locals,too-many-arguments,too-many-function-args """ Usage ----- ->>> import qidenticon ->>> qidenticon.render_identicon(code, size) +>>> import qtidenticon +>>> qtidenticon.render_identicon(code, size) -Returns an instance of :class:`QPixmap` which have generated identicon image. +Return a PIL Image class instance which have generated identicon image. ``size`` specifies `patch size`. Generated image size is 3 * ``size``. """ -from six.moves import range - -try: - from PyQt5 import QtCore, QtGui -except (ImportError, RuntimeError): - from PyQt4 import QtCore, QtGui +from PyQt4 import QtGui +from PyQt4.QtCore import QPointF, QSize, Qt +from PyQt4.QtGui import QPainter, QPixmap, QPolygonF class IdenticonRendererBase(object): @@ -63,19 +30,17 @@ class IdenticonRendererBase(object): def render(self, size, twoColor, opacity, penwidth): """ - render identicon to QPixmap + render identicon to QPicture :param size: identicon patchsize. (image size is 3 * [size]) - :returns: :class:`QPixmap` + :returns: :class:`QPicture` """ # 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 = QtGui.QPixmap( - QtCore.QSize(size * 3 + penwidth, size * 3 + penwidth)) + image = QPixmap(QSize(size * 3 + penwidth, size * 3 + penwidth)) # fill background backColor = QtGui.QColor(255, 255, 255, opacity) @@ -89,28 +54,26 @@ class IdenticonRendererBase(object): '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['patch_type'] = side[0] - for i in range(4): + for i in xrange(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['patch_type'] = corner[0] - for i in range(4): + for i in xrange(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, patch_type, image, size, foreColor, - backColor, penwidth): # pylint: disable=unused-argument + def drawPatchQt(self, pos, turn, invert, patch_type, image, size, foreColor, + backColor, penwidth): # pylint: disable=unused-argument """ :param size: patch size """ @@ -120,43 +83,39 @@ class IdenticonRendererBase(object): invert = not invert path = [(0., 0.), (1., 0.), (1., 1.), (0., 1.), (0., 0.)] - polygon = QtGui.QPolygonF([ - QtCore.QPointF(x * size, y * size) for x, y in path]) + polygon = QPolygonF([QPointF(x * size, y * size) for x, y in path]) rot = turn % 4 - rect = [ - QtCore.QPointF(0., 0.), QtCore.QPointF(size, 0.), - QtCore.QPointF(size, size), QtCore.QPointF(0., size)] + rect = [QPointF(0., 0.), QPointF(size, 0.), QPointF(size, size), QPointF(0., size)] rotation = [0, 90, 180, 270] - nopen = QtGui.QPen(foreColor, QtCore.Qt.NoPen) - foreBrush = QtGui.QBrush(foreColor, QtCore.Qt.SolidPattern) + nopen = QtGui.QPen(foreColor, Qt.NoPen) + foreBrush = QtGui.QBrush(foreColor, Qt.SolidPattern) if penwidth > 0: pen_color = QtGui.QColor(255, 255, 255) - pen = QtGui.QPen(pen_color, QtCore.Qt.SolidPattern) + pen = QtGui.QPen(pen_color, Qt.SolidPattern) pen.setWidth(penwidth) - painter = QtGui.QPainter() + painter = 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 = QtGui.QPolygonF(rect) + poly_rect = QPolygonF(rect) polygon = poly_rect.subtracted(polygon) painter.setBrush(foreBrush) if penwidth > 0: # draw the borders painter.setPen(pen) - painter.drawPolygon(polygon, QtCore.Qt.WindingFill) + painter.drawPolygon(polygon, Qt.WindingFill) # draw the fill painter.setPen(nopen) - painter.drawPolygon(polygon, QtCore.Qt.WindingFill) + painter.drawPolygon(polygon, Qt.WindingFill) painter.end() @@ -164,13 +123,14 @@ class IdenticonRendererBase(object): def decode(self, code, twoColor): """virtual functions""" + raise NotImplementedError class DonRenderer(IdenticonRendererBase): """ - Don Park's implementation of identicon, see: - https://blog.docuverse.com/2007/01/18/identicon-updated-and-source-released + Don Park's implementation of identicon + see: http://www.docuverse.com/blog/donpark/2007/01/19/identicon-updated-and-source-released """ PATH_SET = [ @@ -206,14 +166,13 @@ class DonRenderer(IdenticonRendererBase): [(0, 0), (2, 0), (0, 2)], # [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, path in enumerate(PATH_SET): - if path: - p = [(vec[0] / 4.0, vec[1] / 4.0) for vec in path] + for idx in xrange(len(PATH_SET)): + if PATH_SET[idx]: + p = [(vec[0] / 4.0, vec[1] / 4.0) for vec in PATH_SET[idx]] PATH_SET[idx] = p + p[:1] def decode(self, code, twoColor): @@ -256,8 +215,7 @@ class DonRenderer(IdenticonRendererBase): 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 @@ -268,9 +226,9 @@ class DonRenderer(IdenticonRendererBase): 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) diff --git a/src/queues.py b/src/queues.py index cee5ce8b..7d9e284a 100644 --- a/src/queues.py +++ b/src/queues.py @@ -1,18 +1,19 @@ """Most of the queues used by bitmessage threads are defined here.""" +import Queue import threading import time -from six.moves import queue +from multiqueue import MultiQueue -class ObjectProcessorQueue(queue.Queue): +class ObjectProcessorQueue(Queue.Queue): """Special queue class using lock for `.threads.objectProcessor`""" maxSize = 32000000 def __init__(self): - queue.Queue.__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 @@ -24,23 +25,27 @@ class ObjectProcessorQueue(queue.Queue): time.sleep(1) with self.sizeLock: self.curSize += len(item[1]) - queue.Queue.put(self, item, block, timeout) + Queue.Queue.put(self, item, block, timeout) def get(self, block=True, timeout=None): - item = queue.Queue.get(self, block, timeout) + 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() +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() #: The address generator thread uses this queue to get information back #: to the API thread. -apiAddressGeneratorReturnQueue = queue.Queue() +apiAddressGeneratorReturnQueue = Queue.Queue() #: for exceptions -excQueue = queue.Queue() +excQueue = Queue.Queue() diff --git a/src/shared.py b/src/shared.py index b85ddb20..beed52ed 100644 --- a/src/shared.py +++ b/src/shared.py @@ -1,4 +1,4 @@ -""" +""" Some shared functions .. deprecated:: 0.6.3 @@ -11,18 +11,34 @@ from __future__ import division import hashlib import os import stat -import subprocess # nosec B404 +import subprocess import sys +import threading from binascii import hexlify # Project imports. import highlevelcrypto import state from addresses import decodeAddress, encodeVarint -from bmconfigparser import config +from bmconfigparser import BMConfigParser from debug import logger from helper_sql import sqlQuery +from pyelliptic import arithmetic + + +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 + myECCryptorObjects = {} MyECSubscriptionCryptorObjects = {} @@ -32,6 +48,19 @@ myAddressesByHash = {} # The key in this dictionary is the tag generated from the address. myAddressesByTag = {} broadcastSendersForWhichImWatching = {} +printLock = threading.Lock() +statusIconColor = 'red' + +thisapp = None # singleton lock instance + +ackdataForWhichImWatching = {} +# used by API command clientStatus +clientHasReceivedIncomingConnections = False +numberOfMessagesProcessed = 0 +numberOfBroadcastsProcessed = 0 +numberOfPubkeysProcessed = 0 + +maximumLengthOfTimeToBotherResendingMessages = 0 def isAddressInMyAddressBook(address): @@ -74,6 +103,35 @@ def isAddressInMyAddressBookSubscriptionsListOrWhitelist(address): return False +def decodeWalletImportFormat(WIFstring): + # pylint: disable=inconsistent-return-statements + """ + Convert private key from base58 that's used in the config file to + 8-bit binary string + """ + fullString = arithmetic.changebase(WIFstring, 58, 256) + privkey = fullString[:-4] + if fullString[-4:] != \ + hashlib.sha256(hashlib.sha256(privkey).digest()).digest()[:4]: + logger.critical( + 'Major problem! When trying to decode one of your' + ' private keys, the checksum failed. Here are the first' + ' 6 characters of the PRIVATE key: %s', + str(WIFstring)[:6] + ) + os._exit(0) # pylint: disable=protected-access + # return "" + elif privkey[0] == '\x80': # checksum passed + return privkey[1:] + + logger.critical( + 'Major problem! When trying to decode one of your private keys,' + ' the checksum passed but the key doesn\'t begin with hex 80.' + ' Here is the PRIVATE key: %s', WIFstring + ) + os._exit(0) # pylint: disable=protected-access + + def reloadMyAddressHashes(): """Reload keys for user's addresses from the config file""" logger.debug('reloading keys from keys.dat file') @@ -85,40 +143,31 @@ def reloadMyAddressHashes(): keyfileSecure = checkSensitiveFilePermissions(os.path.join( state.appdata, 'keys.dat')) hasEnabledKeys = False - for addressInKeysFile in config.addresses(): - if not config.getboolean(addressInKeysFile, 'enabled'): - continue - - hasEnabledKeys = True - - addressVersionNumber, streamNumber, hashobj = decodeAddress( - addressInKeysFile)[1:] - if addressVersionNumber not in (2, 3, 4): - logger.error( - 'Error in reloadMyAddressHashes: Can\'t handle' - ' address versions other than 2, 3, or 4.') - continue - - # Returns a simple 32 bytes of information encoded in 64 Hex characters - try: - privEncryptionKey = hexlify( - highlevelcrypto.decodeWalletImportFormat(config.get( - addressInKeysFile, 'privencryptionkey').encode() - )) - except ValueError: - logger.error( - 'Error in reloadMyAddressHashes: failed to decode' - ' one of the private keys for address %s', addressInKeysFile) - continue - # It is 32 bytes encoded as 64 hex characters - if len(privEncryptionKey) == 64: - myECCryptorObjects[hashobj] = \ - highlevelcrypto.makeCryptor(privEncryptionKey) - myAddressesByHash[hashobj] = addressInKeysFile - tag = highlevelcrypto.double_sha512( - encodeVarint(addressVersionNumber) - + encodeVarint(streamNumber) + hashobj)[32:] - myAddressesByTag[tag] = addressInKeysFile + for addressInKeysFile in BMConfigParser().addresses(): + isEnabled = BMConfigParser().getboolean(addressInKeysFile, 'enabled') + if isEnabled: + hasEnabledKeys = True + # status + addressVersionNumber, streamNumber, hashobj = decodeAddress(addressInKeysFile)[1:] + if addressVersionNumber in (2, 3, 4): + # Returns a simple 32 bytes of information encoded + # in 64 Hex characters, or null if there was an error. + privEncryptionKey = hexlify(decodeWalletImportFormat( + BMConfigParser().get(addressInKeysFile, 'privencryptionkey'))) + # It is 32 bytes encoded as 64 hex characters + if len(privEncryptionKey) == 64: + myECCryptorObjects[hashobj] = \ + highlevelcrypto.makeCryptor(privEncryptionKey) + myAddressesByHash[hashobj] = addressInKeysFile + tag = hashlib.sha512(hashlib.sha512( + encodeVarint(addressVersionNumber) + + encodeVarint(streamNumber) + hashobj).digest()).digest()[32:] + myAddressesByTag[tag] = addressInKeysFile + else: + logger.error( + 'Error in reloadMyAddressHashes: Can\'t handle' + ' address versions other than 2, 3, or 4.\n' + ) if not keyfileSecure: fixSensitiveFilePermissions(os.path.join( @@ -146,16 +195,16 @@ def reloadBroadcastSendersForWhichImWatching(): if addressVersionNumber <= 3: privEncryptionKey = hashlib.sha512( - encodeVarint(addressVersionNumber) - + encodeVarint(streamNumber) + hashobj + encodeVarint(addressVersionNumber) + + encodeVarint(streamNumber) + hashobj ).digest()[:32] MyECSubscriptionCryptorObjects[hashobj] = \ highlevelcrypto.makeCryptor(hexlify(privEncryptionKey)) else: - doubleHashOfAddressData = highlevelcrypto.double_sha512( - encodeVarint(addressVersionNumber) - + encodeVarint(streamNumber) + hashobj - ) + doubleHashOfAddressData = hashlib.sha512(hashlib.sha512( + encodeVarint(addressVersionNumber) + + encodeVarint(streamNumber) + hashobj + ).digest()).digest() tag = doubleHashOfAddressData[32:] privEncryptionKey = doubleHashOfAddressData[:32] MyECSubscriptionCryptorObjects[tag] = \ @@ -165,9 +214,9 @@ def reloadBroadcastSendersForWhichImWatching(): def fixPotentiallyInvalidUTF8Data(text): """Sanitise invalid UTF-8 strings""" try: - text.decode('utf-8') + unicode(text, 'utf-8') return text - except UnicodeDecodeError: + except: return 'Part of the message is corrupt. The message cannot be' \ ' displayed the normal way.\n\n' + repr(text) @@ -190,15 +239,16 @@ def checkSensitiveFilePermissions(filename): # Skip known problems for non-Win32 filesystems # without POSIX permissions. fstype = subprocess.check_output( - ['/usr/bin/stat', '-f', '-c', '%T', filename], + 'stat -f -c "%%T" %s' % (filename), + shell=True, 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 + 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] @@ -230,3 +280,11 @@ def fixSensitiveFilePermissions(filename, hasEnabledKeys): except Exception: logger.exception('Keyfile permissions could not be fixed.') raise + + +def openKeysFile(): + """Open keys file with an external editor""" + 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 441d655e..dbc2af04 100644 --- a/src/shutdown.py +++ b/src/shutdown.py @@ -1,16 +1,16 @@ """shutdown function""" - import os +import Queue import threading import time -from six.moves import queue - +import shared import state from debug import logger from helper_sql import sqlQuery, sqlStoredProcedure +from inventory import Inventory +from knownnodes import saveKnownNodes from network import StoppableThread -from network.knownnodes import saveKnownNodes from queues import ( addressGeneratorQueue, objectProcessorQueue, UISignalQueue, workerQueue) @@ -40,7 +40,7 @@ def doCleanShutdown(): 'updateStatusBar', 'Flushing inventory in memory out to disk.' ' This should normally only take a second...')) - state.Inventory.flush() + Inventory().flush() # Verify that the objectProcessor has finished exiting. It should have # incremented the shutdown variable from 1 to 2. This must finish before @@ -70,19 +70,19 @@ def doCleanShutdown(): sqlStoredProcedure('exit') # flush queues - for q in ( + for queue in ( workerQueue, UISignalQueue, addressGeneratorQueue, objectProcessorQueue): while True: try: - q.get(False) - q.task_done() - except queue.Empty: + queue.get(False) + queue.task_done() + except Queue.Empty: break - if state.thisapp.daemon or not state.enableGUI: + if shared.thisapp.daemon or not state.enableGUI: # ..fixme:: redundant? logger.info('Clean shutdown complete.') - state.thisapp.cleanup() + shared.thisapp.cleanup() os._exit(0) # pylint: disable=protected-access else: logger.info('Core shutdown complete.') diff --git a/src/singleinstance.py b/src/singleinstance.py index cff9d794..d0a0871c 100644 --- a/src/singleinstance.py +++ b/src/singleinstance.py @@ -55,9 +55,12 @@ class singleinstance(object): ) except OSError as e: if e.errno == 13: - sys.exit( - 'Another instance of this application is' - ' already running') + print( + 'Another instance of this application' + ' is already running' + ) + sys.exit(-1) + print(e.errno) raise else: pidLine = "%i\n" % self.lockPid @@ -72,9 +75,8 @@ class singleinstance(object): fcntl.lockf(self.fp, fcntl.LOCK_EX | fcntl.LOCK_NB) self.lockPid = os.getpid() except IOError: - sys.exit( - 'Another instance of this application is' - ' already running') + print 'Another instance of this application is already running' + sys.exit(-1) else: pidLine = "%i\n" % self.lockPid self.fp.truncate(0) @@ -93,11 +95,11 @@ class singleinstance(object): os.close(self.fd) else: fcntl.lockf(self.fp, fcntl.LOCK_UN) - except (IOError, OSError): + except Exception: pass return - + print "Cleaning up lockfile" try: if sys.platform == 'win32': if hasattr(self, 'fd'): @@ -107,5 +109,5 @@ class singleinstance(object): fcntl.lockf(self.fp, fcntl.LOCK_UN) if os.path.isfile(self.lockfile): os.unlink(self.lockfile) - except (IOError, OSError): + except Exception: pass diff --git a/src/sql/config_setting_ver_2.sql b/src/sql/config_setting_ver_2.sql deleted file mode 100644 index 087d297a..00000000 --- a/src/sql/config_setting_ver_2.sql +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index 4bdcccc8..00000000 --- a/src/sql/config_setting_ver_3.sql +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index 8bd8b0b3..00000000 --- a/src/sql/init_version_10.sql +++ /dev/null @@ -1,15 +0,0 @@ --- -- --- -- 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 deleted file mode 100644 index ea42df4c..00000000 --- a/src/sql/init_version_2.sql +++ /dev/null @@ -1,29 +0,0 @@ --- --- 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 deleted file mode 100644 index 9de784a5..00000000 --- a/src/sql/init_version_3.sql +++ /dev/null @@ -1,5 +0,0 @@ --- --- 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 deleted file mode 100644 index d2fd393d..00000000 --- a/src/sql/init_version_4.sql +++ /dev/null @@ -1,17 +0,0 @@ - -- - -- 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 deleted file mode 100644 index a13fa8cf..00000000 --- a/src/sql/init_version_5.sql +++ /dev/null @@ -1,12 +0,0 @@ - -- - -- 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 deleted file mode 100644 index b9a03669..00000000 --- a/src/sql/init_version_6.sql +++ /dev/null @@ -1,25 +0,0 @@ --- --- 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 deleted file mode 100644 index a2f6f6e3..00000000 --- a/src/sql/init_version_7.sql +++ /dev/null @@ -1,11 +0,0 @@ --- --- 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 deleted file mode 100644 index 0c1813d3..00000000 --- a/src/sql/init_version_8.sql +++ /dev/null @@ -1,7 +0,0 @@ --- --- 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 deleted file mode 100644 index bc8296b9..00000000 --- a/src/sql/init_version_9.sql +++ /dev/null @@ -1,74 +0,0 @@ -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 deleted file mode 100644 index 8413aa0a..00000000 --- a/src/sql/initialize_schema.sql +++ /dev/null @@ -1,100 +0,0 @@ -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 deleted file mode 100644 index 18a5ecfc..00000000 --- a/src/sql/upg_sc_if_old_ver_1.sql +++ /dev/null @@ -1,30 +0,0 @@ -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 deleted file mode 100644 index 1fde0098..00000000 --- a/src/sql/upg_sc_if_old_ver_2.sql +++ /dev/null @@ -1,7 +0,0 @@ -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 90c9cf0d..58e1106a 100644 --- a/src/state.py +++ b/src/state.py @@ -3,6 +3,7 @@ Global runtime variables. """ neededPubkeys = {} +streamsInWhichIAmParticipating = [] extPort = None """For UPnP""" @@ -10,7 +11,7 @@ extPort = None socksIP = None """for Tor hidden service""" -appdata = "" +appdata = '' """holds the location of the application data storage directory""" shutdown = 0 @@ -31,66 +32,26 @@ 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 `.threads.sqlThread` when ready for processing""" + maximumNumberOfHalfOpenConnections = 0 -maximumLengthOfTimeToBotherResendingMessages = 0 +invThread = None +addrThread = None +downloadThread = None +uploadThread = None ownAddresses = {} discoveredPeers = {} -kivy = False - -kivyapp = None +dandelion = 0 testmode = False -clientHasReceivedIncomingConnections = False -"""used by API command clientStatus""" +kivy = False -numberOfMessagesProcessed = 0 -numberOfBroadcastsProcessed = 0 -numberOfPubkeysProcessed = 0 - -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") +association = '' diff --git a/src/storage/filesystem.py b/src/storage/filesystem.py index e756a820..b19d9272 100644 --- a/src/storage/filesystem.py +++ b/src/storage/filesystem.py @@ -1,20 +1,19 @@ """ Module for using filesystem (directory with files) for inventory storage """ -import logging -import os +import string import time from binascii import hexlify, unhexlify +from os import listdir, makedirs, path, remove, rmdir from threading import RLock from paths import lookupAppdataFolder -from .storage import InventoryItem, InventoryStorage - -logger = logging.getLogger('default') +from storage import InventoryItem, InventoryStorage class FilesystemInventory(InventoryStorage): """Filesystem for inventory storage""" + # pylint: disable=too-many-ancestors, abstract-method topDir = "inventory" objectDir = "objects" metadataFilename = "metadata" @@ -22,15 +21,15 @@ class FilesystemInventory(InventoryStorage): def __init__(self): super(FilesystemInventory, self).__init__() - self.baseDir = os.path.join( + self.baseDir = 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): + 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) else: - os.makedirs(createDir) + makedirs(createDir) # Guarantees that two receiveDataThreads # don't receive and process the same message # concurrently (probably sent by a malicious individual) @@ -44,9 +43,6 @@ class FilesystemInventory(InventoryStorage): return True return False - def __delitem__(self, hash_): - raise NotImplementedError - def __getitem__(self, hashval): for streamDict in self._inventory.values(): try: @@ -67,18 +63,18 @@ class FilesystemInventory(InventoryStorage): with self.lock: value = InventoryItem(*value) try: - os.makedirs(os.path.join( + makedirs(path.join( self.baseDir, FilesystemInventory.objectDir, - hexlify(hashval).decode())) + hexlify(hashval))) except OSError: pass try: with open( - os.path.join( + path.join( self.baseDir, FilesystemInventory.objectDir, - hexlify(hashval).decode(), + hexlify(hashval), FilesystemInventory.metadataFilename, ), "w", @@ -87,15 +83,15 @@ class FilesystemInventory(InventoryStorage): value.type, value.stream, value.expires, - hexlify(value.tag).decode())) + hexlify(value.tag))) with open( - os.path.join( + path.join( self.baseDir, FilesystemInventory.objectDir, - hexlify(hashval).decode(), + hexlify(hashval), FilesystemInventory.dataFilename, ), - "wb", + "w", ) as f: f.write(value.payload) except IOError: @@ -115,28 +111,28 @@ class FilesystemInventory(InventoryStorage): pass with self.lock: try: - os.remove( - os.path.join( + remove( + path.join( self.baseDir, FilesystemInventory.objectDir, - hexlify(hashval).decode(), + hexlify(hashval), FilesystemInventory.metadataFilename)) except IOError: pass try: - os.remove( - os.path.join( + remove( + path.join( self.baseDir, FilesystemInventory.objectDir, - hexlify(hashval).decode(), + hexlify(hashval), FilesystemInventory.dataFilename)) except IOError: pass try: - os.rmdir(os.path.join( + rmdir(path.join( self.baseDir, FilesystemInventory.objectDir, - hexlify(hashval).decode())) + hexlify(hashval))) except IOError: pass @@ -166,9 +162,10 @@ class FilesystemInventory(InventoryStorage): newInventory[streamNumber][hashId] = InventoryItem( objectType, streamNumber, None, expiresTime, tag) except KeyError: - logger.debug( - 'error loading %s', hexlify(hashId), exc_info=True) + print "error loading %s" % (hexlify(hashId)) 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""" @@ -176,17 +173,17 @@ class FilesystemInventory(InventoryStorage): def object_list(self): """Return inventory vectors (hashes) from a directory""" - return [unhexlify(x) for x in os.listdir(os.path.join( + return [unhexlify(x) for x in listdir(path.join( self.baseDir, FilesystemInventory.objectDir))] def getData(self, hashId): """Get object data""" try: with open( - os.path.join( + path.join( self.baseDir, FilesystemInventory.objectDir, - hexlify(hashId).decode(), + hexlify(hashId), FilesystemInventory.dataFilename, ), "r", @@ -199,16 +196,16 @@ class FilesystemInventory(InventoryStorage): """Get object metadata""" try: with open( - os.path.join( + path.join( self.baseDir, FilesystemInventory.objectDir, - hexlify(hashId).decode(), + hexlify(hashId), FilesystemInventory.metadataFilename, ), "r", ) as f: - objectType, streamNumber, expiresTime, tag = f.read().split( - ",", 4)[:4] + objectType, streamNumber, expiresTime, tag = string.split( + f.read(), ",", 4)[:4] return [ int(objectType), int(streamNumber), @@ -245,10 +242,10 @@ class FilesystemInventory(InventoryStorage): def unexpired_hashes_by_stream(self, stream): """Return unexpired hashes in the inventory for a particular stream""" + t = int(time.time()) try: - return [ - x for x, value in self._inventory[stream].items() - if value.expires > int(time.time())] + return [x for x, value in self._inventory[stream].items() + if value.expires > t] except KeyError: return [] @@ -258,7 +255,7 @@ class FilesystemInventory(InventoryStorage): def clean(self): """Clean out old items from the inventory""" - minTime = int(time.time()) - 60 * 60 * 30 + minTime = int(time.time()) - (60 * 60 * 30) deletes = [] for streamDict in self._inventory.values(): for hashId, item in streamDict.items(): diff --git a/src/storage/sqlite.py b/src/storage/sqlite.py index eb5df098..0992c00e 100644 --- a/src/storage/sqlite.py +++ b/src/storage/sqlite.py @@ -6,10 +6,10 @@ import time from threading import RLock from helper_sql import SqlBulkExecute, sqlExecute, sqlQuery -from .storage import InventoryItem, InventoryStorage +from storage import InventoryItem, InventoryStorage -class SqliteInventory(InventoryStorage): +class SqliteInventory(InventoryStorage): # pylint: disable=too-many-ancestors """Inventory using SQLite""" def __init__(self): super(SqliteInventory, self).__init__() @@ -70,23 +70,15 @@ class SqliteInventory(InventoryStorage): return len(self._inventory) + sqlQuery( 'SELECT count(*) FROM inventory')[0][0] - 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)) + def by_type_and_tag(self, objectType, tag): + """Return objects filtered by object type and tag""" with self.lock: - 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)] + 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))) return values def unexpired_hashes_by_stream(self, stream): diff --git a/src/storage/storage.py b/src/storage/storage.py index 9b33eef7..0391979a 100644 --- a/src/storage/storage.py +++ b/src/storage/storage.py @@ -1,47 +1,73 @@ """ Storing inventory items """ +import collections -from abc import abstractmethod -from collections import namedtuple -try: - from collections import MutableMapping # pylint: disable=deprecated-class -except ImportError: - from collections.abc import MutableMapping +InventoryItem = collections.namedtuple( + 'InventoryItem', 'type stream payload expires tag') -InventoryItem = namedtuple('InventoryItem', 'type stream payload expires tag') +class Storage(object): # pylint: disable=too-few-public-methods + """Base class for storing inventory + (extendable for other items to store)""" + pass -class InventoryStorage(MutableMapping): - """ - Base class for storing inventory - (extendable for other items to store) - """ +class InventoryStorage(Storage, collections.MutableMapping): + """Module used for inventory storage""" - def __init__(self): + def __init__(self): # pylint: disable=super-init-not-called self.numberOfInventoryLookupsPerformed = 0 - @abstractmethod - def __contains__(self, item): - pass + def __contains__(self, _): + raise NotImplementedError + + def __getitem__(self, _): + raise NotImplementedError + + def __setitem__(self, _, value): + raise NotImplementedError + + def __delitem__(self, _): + raise NotImplementedError + + def __iter__(self): + raise NotImplementedError + + def __len__(self): + raise NotImplementedError - @abstractmethod def by_type_and_tag(self, objectType, tag): """Return objects filtered by object type and tag""" - pass + raise NotImplementedError - @abstractmethod def unexpired_hashes_by_stream(self, stream): """Return unexpired inventory vectors filtered by stream""" - pass + raise NotImplementedError - @abstractmethod def flush(self): """Flush cache""" - pass + raise NotImplementedError - @abstractmethod def clean(self): """Free memory / perform garbage collection""" - pass + raise NotImplementedError + + +class MailboxStorage(Storage, collections.MutableMapping): + """Method for storing mails""" + + def __delitem__(self, key): + raise NotImplementedError + + def __getitem__(self, key): + raise NotImplementedError + + def __iter__(self): + raise NotImplementedError + + def __len__(self): + raise NotImplementedError + + def __setitem__(self, key, value): + raise NotImplementedError diff --git a/src/testmode_init.py b/src/testmode_init.py deleted file mode 100644 index a088afc1..00000000 --- a/src/testmode_init.py +++ /dev/null @@ -1,40 +0,0 @@ -import time -import uuid - -import helper_inbox -import helper_sql - -# from .tests.samples import sample_inbox_msg_ids, sample_deterministic_addr4 -sample_deterministic_addr4 = 'BM-2cWzSnwjJ7yRP3nLEWUV5LisTZyREWSzUK' -sample_inbox_msg_ids = ['27e644765a3e4b2e973ee7ccf958ea20', '51fc5531-3989-4d69-bbb5-68d64b756f5b', - '2c975c515f8b414db5eea60ba57ba455', 'bc1f2d8a-681c-4cc0-9a12-6067c7e1ac24'] - - -def populate_api_test_data(): - '''Adding test records in inbox table''' - helper_sql.sql_ready.wait() - - test1 = ( - sample_inbox_msg_ids[0], sample_deterministic_addr4, - sample_deterministic_addr4, 'Test1 subject', int(time.time()), - 'Test1 body', 'inbox', 2, 0, uuid.uuid4().bytes - ) - test2 = ( - sample_inbox_msg_ids[1], sample_deterministic_addr4, - sample_deterministic_addr4, 'Test2 subject', int(time.time()), - 'Test2 body', 'inbox', 2, 0, uuid.uuid4().bytes - ) - test3 = ( - sample_inbox_msg_ids[2], sample_deterministic_addr4, - sample_deterministic_addr4, 'Test3 subject', int(time.time()), - 'Test3 body', 'inbox', 2, 0, uuid.uuid4().bytes - ) - test4 = ( - sample_inbox_msg_ids[3], sample_deterministic_addr4, - sample_deterministic_addr4, 'Test4 subject', int(time.time()), - 'Test4 body', 'inbox', 2, 0, uuid.uuid4().bytes - ) - helper_inbox.insert(test1) - helper_inbox.insert(test2) - helper_inbox.insert(test3) - helper_inbox.insert(test4) diff --git a/src/tests/__init__.py b/src/tests/__init__.py index 1e5fb7b6..e69de29b 100644 --- a/src/tests/__init__.py +++ b/src/tests/__init__.py @@ -1,13 +0,0 @@ -import sys - -if getattr(sys, 'frozen', None): - from test_addresses import TestAddresses - from test_crypto import TestHighlevelcrypto - from test_l10n import TestL10n - from test_packets import TestSerialize - from test_protocol import TestProtocol - - __all__ = [ - "TestAddresses", "TestHighlevelcrypto", "TestL10n", - "TestProtocol", "TestSerialize" - ] diff --git a/src/tests/apinotify_handler.py b/src/tests/apinotify_handler.py index d3993f4d..4574d46a 100755 --- a/src/tests/apinotify_handler.py +++ b/src/tests/apinotify_handler.py @@ -7,7 +7,7 @@ when pybitmessage started in test mode. import sys import tempfile -from common import put_signal_file +from test_process import put_signal_file if __name__ == '__main__': diff --git a/src/tests/common.py b/src/tests/common.py deleted file mode 100644 index 2d60c716..00000000 --- a/src/tests/common.py +++ /dev/null @@ -1,43 +0,0 @@ -import os -import sys -import time -import unittest - - -_files = ( - 'keys.dat', 'debug.log', 'messages.dat', 'knownnodes.dat', - '.api_started', 'unittest.lock' -) - - -def cleanup(home=None, files=_files): - """Cleanup application files""" - if not home: - import state - home = state.appdata - for pfile in files: - try: - os.remove(os.path.join(home, pfile)) - except OSError: - pass - - -def checkup(): - """Checkup files in the src dir""" - src_dir = os.path.abspath( - os.path.join(os.path.dirname(__file__), os.pardir)) - for f in _files: - if os.path.isfile(os.path.join(src_dir, f)): - return 'Found application file %s in src dir' % f - - -def skip_python3(): - """Raise unittest.SkipTest() if detected python3""" - if sys.hexversion >= 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 index fd9b0d08..d56076c3 100644 --- a/src/tests/core.py +++ b/src/tests/core.py @@ -3,44 +3,31 @@ 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 knownnodes import state -import helper_sent -import helper_addressbook - -from bmconfigparser import config +from bmconfigparser import BMConfigParser 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 helper_startup import start_proxyconfig +from network import asyncore_pollchoose as asyncore +from network.connectionpool import BMConnectionPool +from network.node import 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 + import stem.version as stem_version +except ImportError: + stem_version = None + -frozen = getattr(sys, 'frozen', None) knownnodes_file = os.path.join(state.appdata, 'knownnodes.dat') @@ -60,16 +47,13 @@ def pickle_knownnodes(): }, dst) +def cleanup(): + """Cleanup application files""" + os.remove(knownnodes_file) + + 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)""" @@ -111,24 +95,16 @@ class TestCore(unittest.TestCase): @unittest.skip('Bad environment for asyncore.loop') def test_tcpconnection(self): """initial fill script from network.tcp""" - config.set('bitmessagesettings', 'dontconnect', 'true') + BMConfigParser().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 + except: 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: @@ -155,7 +131,7 @@ class TestCore(unittest.TestCase): def test_knownnodes_default(self): """test adding default knownnodes if nothing loaded""" - cleanup(files=('knownnodes.dat',)) + cleanup() self._wipe_knownnodes() knownnodes.readKnownNodes() self.assertGreaterEqual( @@ -165,7 +141,7 @@ class TestCore(unittest.TestCase): """test knownnodes starvation leading to IndexError in Asyncore""" self._outdate_knownnodes() # time.sleep(303) # singleCleaner wakes up every 5 min - knownnodes.cleanupKnownNodes(connectionpool.pool) + knownnodes.cleanupKnownNodes() self.assertTrue(knownnodes.knownNodes[1]) while True: try: @@ -176,21 +152,16 @@ class TestCore(unittest.TestCase): self.fail("IndexError because of empty knownNodes!") def _initiate_bootstrap(self): - config.set('bitmessagesettings', 'dontconnect', 'true') - self._wipe_knownnodes() + BMConfigParser().set('bitmessagesettings', 'dontconnect', 'true') + self._outdate_knownnodes() knownnodes.addKnownNode(1, Peer('127.0.0.1', 8444), is_self=True) - knownnodes.cleanupKnownNodes(connectionpool.pool) - time.sleep(5) + knownnodes.cleanupKnownNodes() + time.sleep(2) - 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. - """ + def _check_bootstrap(self): _started = time.time() - config.remove_option('bitmessagesettings', 'dontconnect') - proxy_type = config.safeGet( + BMConfigParser().remove_option('bitmessagesettings', 'dontconnect') + proxy_type = BMConfigParser().safeGet( 'bitmessagesettings', 'socksproxytype') if proxy_type == 'SOCKS5': connection_base = Socks5BMConnection @@ -198,240 +169,57 @@ class TestCore(unittest.TestCase): connection_base = Socks4aBMConnection else: connection_base = TCPConnection - c = 360 - while c > 0: + for _ in range(180): 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: + for peer, con in BMConnectionPool().outboundConnections.iteritems(): + if not peer.host.startswith('bootstrap'): 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) + 'Failed to connect during %s sec' % (time.time() - _started)) 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') + self._check_bootstrap() - @unittest.skipIf(tor_port_free, 'no running tor detected') + @unittest.skipUnless(stem_version, 'No stem, skipping tor dependent test') def test_bootstrap_tor(self): """test bootstrapping with tor""" - config.set('bitmessagesettings', 'socksproxytype', 'SOCKS5') self._initiate_bootstrap() - self._check_connection() - self._check_knownnodes() + BMConfigParser().set('bitmessagesettings', 'socksproxytype', 'stem') + start_proxyconfig() + self._check_bootstrap() - @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 + @unittest.skipUnless(stem_version, 'No stem, skipping tor dependent test') + def test_onionservicesonly(self): # this should start after bootstrap """ - 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() + set onionservicesonly, wait for 3 connections and check them all + are onions + """ + BMConfigParser().set('bitmessagesettings', 'socksproxytype', 'SOCKS5') + BMConfigParser().set('bitmessagesettings', 'onionservicesonly', 'true') + self._initiate_bootstrap() + BMConfigParser().remove_option('bitmessagesettings', 'dontconnect') 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: + for n, peer in enumerate(BMConnectionPool().outboundConnections): + if n > 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) + if ( + not peer.host.endswith('.onion') + and not peer.host.startswith('bootstrap') + ): + self.fail( + 'Found non onion hostname %s in outbound connections!' + % peer.host) + self.fail('Failed to connect to at least 3 nodes within 360 sec') def run(): - """Starts all tests intended for core run""" - loader = unittest.defaultTestLoader + """Starts all tests defined in this module""" + loader = unittest.TestLoader() 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 deleted file mode 100644 index e69de29b..00000000 diff --git a/src/tests/mockbm/bitmessagemock.py b/src/tests/mockbm/bitmessagemock.py deleted file mode 100644 index d9ee857b..00000000 --- a/src/tests/mockbm/bitmessagemock.py +++ /dev/null @@ -1,32 +0,0 @@ -# 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 deleted file mode 120000 index 847b03ed..00000000 --- a/src/tests/mockbm/images +++ /dev/null @@ -1 +0,0 @@ -../../images/ \ No newline at end of file diff --git a/src/tests/mockbm/kivy_main.py b/src/tests/mockbm/kivy_main.py deleted file mode 100644 index 79bb413e..00000000 --- a/src/tests/mockbm/kivy_main.py +++ /dev/null @@ -1,8 +0,0 @@ -"""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 deleted file mode 100644 index e69de29b..00000000 diff --git a/src/tests/mockbm/pybitmessage/addresses.py b/src/tests/mockbm/pybitmessage/addresses.py deleted file mode 120000 index 88fcee82..00000000 --- a/src/tests/mockbm/pybitmessage/addresses.py +++ /dev/null @@ -1 +0,0 @@ -../../../addresses.py \ No newline at end of file diff --git a/src/tests/mockbm/pybitmessage/bmconfigparser.py b/src/tests/mockbm/pybitmessage/bmconfigparser.py deleted file mode 120000 index da05040e..00000000 --- a/src/tests/mockbm/pybitmessage/bmconfigparser.py +++ /dev/null @@ -1 +0,0 @@ -../../../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 deleted file mode 100644 index 34258bbc..00000000 --- a/src/tests/mockbm/pybitmessage/class_addressGenerator.py +++ /dev/null @@ -1,80 +0,0 @@ -""" -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 deleted file mode 100644 index 6173c3cd..00000000 --- a/src/tests/mockbm/pybitmessage/inventory.py +++ /dev/null @@ -1,15 +0,0 @@ -"""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 deleted file mode 100644 index e69de29b..00000000 diff --git a/src/tests/mockbm/pybitmessage/network/threads.py b/src/tests/mockbm/pybitmessage/network/threads.py deleted file mode 120000 index c95b4c36..00000000 --- a/src/tests/mockbm/pybitmessage/network/threads.py +++ /dev/null @@ -1 +0,0 @@ -../../../../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 deleted file mode 120000 index 8c556015..00000000 --- a/src/tests/mockbm/pybitmessage/queues.py +++ /dev/null @@ -1 +0,0 @@ -../../../queues.py \ No newline at end of file diff --git a/src/tests/mockbm/pybitmessage/shutdown.py b/src/tests/mockbm/pybitmessage/shutdown.py deleted file mode 100644 index 08c885d8..00000000 --- a/src/tests/mockbm/pybitmessage/shutdown.py +++ /dev/null @@ -1,11 +0,0 @@ -# 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 deleted file mode 120000 index 5e112567..00000000 --- a/src/tests/mockbm/pybitmessage/singleton.py +++ /dev/null @@ -1 +0,0 @@ -../../../singleton.py \ No newline at end of file diff --git a/src/tests/mockbm/pybitmessage/state.py b/src/tests/mockbm/pybitmessage/state.py deleted file mode 120000 index 117203f5..00000000 --- a/src/tests/mockbm/pybitmessage/state.py +++ /dev/null @@ -1 +0,0 @@ -../../../state.py \ No newline at end of file diff --git a/src/tests/partial.py b/src/tests/partial.py deleted file mode 100644 index 870f6626..00000000 --- a/src/tests/partial.py +++ /dev/null @@ -1,41 +0,0 @@ -"""A test case for partial run class definition""" - -import os -import sys -import unittest - -from pybitmessage import pathmagic - - -class TestPartialRun(unittest.TestCase): - """ - A base class for test cases running some parts of the app, - e.g. separate threads or packages. - """ - - @classmethod - def setUpClass(cls): - # pylint: disable=import-outside-toplevel,unused-import - cls.dirs = (os.path.abspath(os.curdir), pathmagic.setup()) - - import bmconfigparser - import state - - from debug import logger # noqa:F401 pylint: disable=unused-variable - if sys.hexversion >= 0x3000000: - # pylint: disable=no-name-in-module,relative-import - from mockbm import network as network_mock - import network - network.stats = network_mock.stats - - state.shutdown = 0 - cls.state = state - bmconfigparser.config = cls.config = bmconfigparser.BMConfigParser() - cls.config.read() - - @classmethod - def tearDownClass(cls): - cls.state.shutdown = 1 - # deactivate pathmagic - os.chdir(cls.dirs[0]) - sys.path.remove(cls.dirs[1]) diff --git a/src/tests/samples.py b/src/tests/samples.py deleted file mode 100644 index dd862318..00000000 --- a/src/tests/samples.py +++ /dev/null @@ -1,93 +0,0 @@ -"""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', - 'BM-GtovgYdgs7qXPkoYaRgrLFuFKz1SFpsw'] -sample_subscription_name = 'test sub' - - -sample_object_expires = 1712271487 -# from minode import structure -# obj = structure.Object( -# b'\x00' * 8, sample_object_expires, 42, 1, 2, b'HELLO') -# .. do pow and obj.to_bytes() -sample_object_data = unhexlify( - '00000000001be7fc00000000660f307f0000002a010248454c4c4f') - -sample_msg = unhexlify( - '0592a10584ffabf96539f3d780d776828c67da1ab5b169e9e8aed838aaecc9ed36d49ff' - '1423c55f019e050c66c6324f53588be88894fef4dcffdb74b98e2b200') -sample_sig = unhexlify( - '304402202302475351db6b822de15d922e29397541f10d8a19780ba2ca4a920b1035f075' - '02205e5bba40d5f07a24c23a89ba5f01a3828371dfbb685dd5375fa1c29095fd232b') -sample_sig_sha1 = unhexlify( - '30460221008ad234687d1bdc259932e28ea6ee091b88b0900d8134902aa8c2fd7f016b96e' - 'd022100dafb94e28322c2fa88878f9dcbf0c2d33270466ab3bbffaec3dca0a2d1ef9354') - -# [chan] bitmessage -sample_wif_privsigningkey = unhexlify( - b'a2e8b841a531c1c558ee0680c396789c7a2ea3ac4795ae3f000caf9fe367d144') -sample_wif_privencryptionkey = unhexlify( - b'114ec0e2dca24a826a0eed064b0405b0ac148abc3b1d52729697f4d7b873fdc6') -sample_privsigningkey_wif = \ - b'5K42shDERM5g7Kbi3JT5vsAWpXMqRhWZpX835M2pdSoqQQpJMYm' -sample_privencryptionkey_wif = \ - b'5HwugVWm31gnxtoYcvcK7oywH2ezYTh6Y4tzRxsndAeMi6NHqpA' diff --git a/src/tests/sql/init_version_10.sql b/src/tests/sql/init_version_10.sql deleted file mode 100644 index b1764e76..00000000 --- a/src/tests/sql/init_version_10.sql +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index 133284ec..00000000 --- a/src/tests/sql/init_version_2.sql +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index 875d859d..00000000 --- a/src/tests/sql/init_version_3.sql +++ /dev/null @@ -1 +0,0 @@ -INSERT INTO `settings` VALUES ('version','3'); diff --git a/src/tests/sql/init_version_4.sql b/src/tests/sql/init_version_4.sql deleted file mode 100644 index ea3f1768..00000000 --- a/src/tests/sql/init_version_4.sql +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index b894c038..00000000 --- a/src/tests/sql/init_version_5.sql +++ /dev/null @@ -1 +0,0 @@ -INSERT INTO `objectprocessorqueue` VALUES ('hash', 1); diff --git a/src/tests/sql/init_version_6.sql b/src/tests/sql/init_version_6.sql deleted file mode 100644 index 7cd30571..00000000 --- a/src/tests/sql/init_version_6.sql +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index bd87f8d8..00000000 --- a/src/tests/sql/init_version_7.sql +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index 9d9b6f3a..00000000 --- a/src/tests/sql/init_version_8.sql +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index 764634d2..00000000 --- a/src/tests/sql/init_version_9.sql +++ /dev/null @@ -1,2 +0,0 @@ -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 deleted file mode 100644 index dd989562..00000000 --- a/src/tests/test_addresses.py +++ /dev/null @@ -1,86 +0,0 @@ - -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 deleted file mode 100644 index d7366fe4..00000000 --- a/src/tests/test_addressgenerator.py +++ /dev/null @@ -1,102 +0,0 @@ -"""Tests for AddressGenerator (with thread or not)""" - -from binascii import unhexlify - -import six -from six.moves import queue - -from .partial import TestPartialRun -from .samples import ( - sample_seed, sample_deterministic_addr3, sample_deterministic_addr4, - sample_deterministic_ripe) - -TEST_LABEL = 'test' - - -class TestAddressGenerator(TestPartialRun): - """Test case for AddressGenerator thread""" - - @classmethod - def setUpClass(cls): - super(TestAddressGenerator, cls).setUpClass() - - import defaults - import queues - from class_addressGenerator import addressGenerator - - cls.state.enableGUI = False - - cls.command_queue = queues.addressGeneratorQueue - cls.return_queue = queues.apiAddressGeneratorReturnQueue - cls.worker_queue = queues.workerQueue - - cls.config.set( - 'bitmessagesettings', 'defaultnoncetrialsperbyte', - str(defaults.networkDefaultProofOfWorkNonceTrialsPerByte)) - cls.config.set( - 'bitmessagesettings', 'defaultpayloadlengthextrabytes', - str(defaults.networkDefaultPayloadLengthExtraBytes)) - - thread = addressGenerator() - thread.daemon = True - thread.start() - - def _execute(self, command, *args): - self.command_queue.put((command,) + args) - try: - return self.return_queue.get(timeout=30)[0] - except (IndexError, queue.Empty): - self.fail('Failed to execute command %s' % command) - - def test_deterministic(self): - """Test deterministic commands""" - self.command_queue.put(( - 'getDeterministicAddress', 3, 1, - TEST_LABEL, 1, sample_seed, False)) - self.assertEqual(sample_deterministic_addr3, self.return_queue.get()) - - self.assertEqual( - sample_deterministic_addr3, - self._execute( - 'createDeterministicAddresses', 3, 1, TEST_LABEL, 2, - sample_seed, False, 0, 0)) - - try: - self.assertEqual( - self.worker_queue.get(timeout=30), - ('sendOutOrStoreMyV3Pubkey', - unhexlify(sample_deterministic_ripe))) - - self.worker_queue.get(timeout=30) # get the next addr's task - except queue.Empty: - self.fail('No commands in the worker queue') - - self.assertEqual( - sample_deterministic_addr4, - self._execute('createChan', 4, 1, TEST_LABEL, sample_seed, True)) - try: - self.assertEqual( - self.worker_queue.get(), - ('sendOutOrStoreMyV4Pubkey', sample_deterministic_addr4)) - except queue.Empty: - self.fail('No commands in the worker queue') - self.assertEqual( - self.config.get(sample_deterministic_addr4, 'label'), TEST_LABEL) - self.assertTrue( - self.config.getboolean(sample_deterministic_addr4, 'chan')) - self.assertTrue( - self.config.getboolean(sample_deterministic_addr4, 'enabled')) - - def test_random(self): - """Test random address""" - self.command_queue.put(( - 'createRandomAddress', 4, 1, 'test_random', 1, '', False, 0, 0)) - addr = self.return_queue.get() - 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 index 0df145bc..44505ffe 100644 --- a/src/tests/test_api.py +++ b/src/tests/test_api.py @@ -5,19 +5,9 @@ Tests using API. import base64 import json import time -from binascii import hexlify +import xmlrpclib # nosec -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 +from test_process import TestProcessProto, TestProcessShutdown class TestAPIProto(TestProcessProto): @@ -29,7 +19,7 @@ class TestAPIProto(TestProcessProto): """Setup XMLRPC proxy for pybitmessage API""" super(TestAPIProto, cls).setUpClass() cls.addresses = [] - cls.api = xmlrpc_client.ServerProxy( + cls.api = xmlrpclib.ServerProxy( "http://username:password@127.0.0.1:8442/") for _ in range(5): if cls._get_readline('.api_started'): @@ -37,38 +27,37 @@ class TestAPIProto(TestProcessProto): time.sleep(1) -class TestAPIShutdown(TestAPIProto): +class TestAPIShutdown(TestAPIProto, TestProcessShutdown): """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: + for _ in range(5): + if not self.process.is_running(): + break + time.sleep(2) + else: self.fail( - '%s has not stopped in 20 sec' % ' '.join(self._process_cmd)) - - -# TODO: uncovered API commands -# disseminatePreEncryptedMsg -# disseminatePubkey -# getMessageDataByDestinationHash + '%s has not stopped in 10 sec' % ' '.join(self._process_cmd)) class TestAPI(TestAPIProto): """Main API test case""" - _seed = base64.encodestring(sample_seed) + _seed = base64.encodestring( + 'TIGER, tiger, burning bright. In the forests of the night' + ) def _add_random_address(self, label): - addr = self.api.createRandomAddress(base64.encodestring(label)) - return addr + return self.api.createRandomAddress(base64.encodestring(label)) 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() + api_wrong = xmlrpclib.ServerProxy("http://test:wrong@127.0.0.1:8442/") + self.assertEqual( + api_wrong.clientStatus(), + 'RPC Username or password incorrect or HTTP header lacks' + ' authentication at all.' + ) def test_connection(self): """API command 'helloWorld'""" @@ -88,55 +77,6 @@ class TestAPI(TestAPIProto): '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()) @@ -145,12 +85,6 @@ class TestAPI(TestAPIProto): else: self.assertGreater(status["networkConnections"], 0) - def test_listconnections_consistency(self): - """Checking the return of API command 'listConnections'""" - result = json.loads(self.api.listConnections()) - self.assertGreaterEqual(len(result["inbound"]), 0) - self.assertGreaterEqual(len(result["outbound"]), 0) - def test_list_addresses(self): """Checking the return of API command 'listAddresses'""" self.assertEqual( @@ -161,57 +95,36 @@ class TestAPI(TestAPIProto): def test_decode_address(self): """Checking the return of API command 'decodeAddress'""" result = json.loads( - self.api.decodeAddress(sample_deterministic_addr4)) + self.api.decodeAddress('BM-2cWzSnwjJ7yRP3nLEWUV5LisTZyREWSzUK')) 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""" + """API command 'getDeterministicAddress': with various params""" self.assertEqual( self.api.getDeterministicAddress(self._seed, 4, 1), - sample_deterministic_addr4) + 'BM-2cWzSnwjJ7yRP3nLEWUV5LisTZyREWSzUK' + ) 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:') - + 'BM-2DBPTgeSawWYZceFD69AbDT5q4iUWtj1ZN' + ) + self.assertRegexpMatches( + 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') + self.assertRegexpMatches( + self.api.getDeterministicAddress(self._seed, 3, 2), + r'API Error 0003:' + ) 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]+$') + self.assertRegexpMatches(addr, r'^BM-') + self.assertRegexpMatches(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') @@ -225,217 +138,60 @@ class TestAPI(TestAPIProto): ) # Add known address self.api.addAddressBookEntry( - sample_deterministic_addr4, + 'BM-2cWzSnwjJ7yRP3nLEWUV5LisTZyREWSzUK', base64.encodestring('tiger_4') ) # Check addressbook entry entries = json.loads( self.api.listAddressBookEntries()).get('addresses')[0] self.assertEqual( - entries['address'], sample_deterministic_addr4) + entries['address'], 'BM-2cWzSnwjJ7yRP3nLEWUV5LisTZyREWSzUK') 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) + self.api.deleteAddressBookEntry( + 'BM-2cWzSnwjJ7yRP3nLEWUV5LisTZyREWSzUK') # 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_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""" + """API command 'sendBroadcast': ensure it returns ackData""" addr = self._add_random_address('random_2') - msg = base64.encodestring('test broadcast') - ackdata = self.api.sendBroadcast( - addr, base64.encodestring('test_subject'), msg) - + ack = self.api.sendBroadcast( + addr, base64.encodestring('test_subject'), + base64.encodestring('test message') + ) 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 + int(ack, 16) 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 + # Cheate chan with known address self.assertEqual( - self.api.createChan(self._seed), sample_deterministic_addr4) + self.api.createChan(self._seed), + 'BM-2cWzSnwjJ7yRP3nLEWUV5LisTZyREWSzUK' + ) # cleanup self.assertEqual( - self.api.leaveChan(sample_deterministic_addr4), 'success') + self.api.leaveChan('BM-2cWzSnwjJ7yRP3nLEWUV5LisTZyREWSzUK'), + 'success' + ) # Join chan with addresses of version 3 or 4 - for addr in (sample_deterministic_addr4, sample_deterministic_addr3): + for addr in ( + 'BM-2cWzSnwjJ7yRP3nLEWUV5LisTZyREWSzUK', + 'BM-2DBPTgeSawWYZceFD69AbDT5q4iUWtj1ZN' + ): 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'), + self.assertRegexpMatches( + 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 deleted file mode 100644 index 6e453b19..00000000 --- a/src/tests/test_api_thread.py +++ /dev/null @@ -1,96 +0,0 @@ -"""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/pyelliptic/tests/test_blindsig.py b/src/tests/test_blindsig.py similarity index 94% rename from src/pyelliptic/tests/test_blindsig.py rename to src/tests/test_blindsig.py index 8c4b2b9d..cae16191 100644 --- a/src/pyelliptic/tests/test_blindsig.py +++ b/src/tests/test_blindsig.py @@ -5,10 +5,9 @@ import os import unittest from hashlib import sha256 -try: - from pyelliptic import ECCBlind, ECCBlindChain, OpenSSL -except ImportError: - from pybitmessage.pyelliptic import ECCBlind, ECCBlindChain, OpenSSL +from pybitmessage.pyelliptic.eccblind import ECCBlind +from pybitmessage.pyelliptic.eccblindchain import ECCBlindChain +from pybitmessage.pyelliptic.openssl import OpenSSL # pylint: disable=protected-access @@ -37,12 +36,12 @@ class TestBlindSig(unittest.TestCase): # (3) Signature Generation signature_blinded = signer_obj.blind_sign(msg_blinded) - assert isinstance(signature_blinded, bytes) + assert isinstance(signature_blinded, str) self.assertEqual(len(signature_blinded), 32) # (4) Extraction signature = requester_obj.unblind(signature_blinded) - assert isinstance(signature, bytes) + assert isinstance(signature, str) self.assertEqual(len(signature), 65) self.assertNotEqual(signature, signature_blinded) @@ -58,7 +57,7 @@ class TestBlindSig(unittest.TestCase): x = OpenSSL.BN_new() y = OpenSSL.BN_new() OpenSSL.EC_POINT_get_affine_coordinates( - obj.group, obj.Q, x, y, None) + obj.group, obj.Q, x, y, 0) self.assertEqual(OpenSSL.BN_is_odd(y), OpenSSL.BN_is_odd_compatible(y)) @@ -85,7 +84,7 @@ class TestBlindSig(unittest.TestCase): self.assertEqual(OpenSSL.BN_cmp(y0, y1), 0) self.assertEqual(OpenSSL.BN_cmp(x0, x1), 0) self.assertEqual(OpenSSL.EC_POINT_cmp(obj.group, randompoint, - secondpoint, None), 0) + secondpoint, 0), 0) finally: OpenSSL.BN_free(x0) OpenSSL.BN_free(x1) @@ -164,7 +163,7 @@ class TestBlindSig(unittest.TestCase): output.extend(pubkey) output.extend(signature) signer_obj = child_obj - verifychain = ECCBlindChain(ca=ca.pubkey(), chain=bytes(output)) + verifychain = ECCBlindChain(ca=ca.pubkey(), chain=str(output)) self.assertTrue(verifychain.verify(msg=msg, value=1)) def test_blind_sig_chain_wrong_ca(self): # pylint: disable=too-many-locals @@ -200,7 +199,7 @@ class TestBlindSig(unittest.TestCase): output.extend(pubkey) output.extend(signature) signer_obj = child_obj - verifychain = ECCBlindChain(ca=ca.pubkey(), chain=bytes(output)) + verifychain = ECCBlindChain(ca=ca.pubkey(), chain=str(output)) self.assertFalse(verifychain.verify(msg, 1)) def test_blind_sig_chain_wrong_msg(self): # pylint: disable=too-many-locals @@ -235,7 +234,7 @@ class TestBlindSig(unittest.TestCase): output.extend(pubkey) output.extend(signature) signer_obj = child_obj - verifychain = ECCBlindChain(ca=ca.pubkey(), chain=bytes(output)) + verifychain = ECCBlindChain(ca=ca.pubkey(), chain=str(output)) self.assertFalse(verifychain.verify(fake_msg, 1)) def test_blind_sig_chain_wrong_intermediary(self): # pylint: disable=too-many-locals @@ -273,5 +272,5 @@ class TestBlindSig(unittest.TestCase): output.extend(pubkey) output.extend(signature) signer_obj = child_obj - verifychain = ECCBlindChain(ca=ca.pubkey(), chain=bytes(output)) + verifychain = ECCBlindChain(ca=ca.pubkey(), chain=str(output)) self.assertFalse(verifychain.verify(msg, 1)) diff --git a/src/tests/test_config.py b/src/tests/test_config.py index 44db7c8a..35ddd3fa 100644 --- a/src/tests/test_config.py +++ b/src/tests/test_config.py @@ -1,135 +1,66 @@ """ Various tests for config """ + +import os 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""" +from test_process import TestProcessProto -# 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) + BMConfigParser().safeGet('nonexistent', 'nonexistent'), None) self.assertEqual( - self.config.safeGet('nonexistent', 'nonexistent', 42), 42) + BMConfigParser().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) + BMConfigParser().safeGetBoolean('nonexistent', 'nonexistent'), + False + ) # no arg for default # pylint: disable=too-many-function-args with self.assertRaises(TypeError): - self.config.safeGetBoolean('nonexistent', 'nonexistent', True) + BMConfigParser().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) + BMConfigParser().safeGetInt('nonexistent', 'nonexistent'), 0) self.assertEqual( - self.config.safeGetInt('nonexistent', 'nonexistent', 42), 42) + BMConfigParser().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') +class TestProcessConfig(TestProcessProto): + """A test case for keys.dat""" - 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_config_defaults(self): + """Test settings in the generated config""" + self._stop_process() + config = BMConfigParser() + config.read(os.path.join(self.home, 'keys.dat')) - 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) + 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')) - 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) + # extralowdifficulty is false + self.assertEqual(config.safeGetInt( + 'bitmessagesettings', 'defaultnoncetrialsperbyte'), 1000) + self.assertEqual(config.safeGetInt( + 'bitmessagesettings', 'defaultpayloadlengthextrabytes'), 1000) diff --git a/src/tests/test_config_address.py b/src/tests/test_config_address.py deleted file mode 100644 index b76df7ec..00000000 --- a/src/tests/test_config_address.py +++ /dev/null @@ -1,57 +0,0 @@ -""" -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 deleted file mode 100644 index 9322a2f0..00000000 --- a/src/tests/test_config_process.py +++ /dev/null @@ -1,39 +0,0 @@ -""" -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 index 6dbb2f31..b7eb7177 100644 --- a/src/tests/test_crypto.py +++ b/src/tests/test_crypto.py @@ -3,26 +3,25 @@ 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 - +from binascii import hexlify, unhexlify try: - from Crypto.Hash import RIPEMD160 + from Crypto.Hash import RIPEMD except ImportError: - RIPEMD160 = None + RIPEMD = 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 -) +# These keys are from addresses test script +sample_pubsigningkey = unhexlify( + '044a367f049ec16cb6b6118eb734a9962d10b8db59c890cd08f210c43ff08bdf09d' + '16f502ca26cd0713f38988a1237f1fc8fa07b15653c996dc4013af6d15505ce') +sample_pubencryptionkey = unhexlify( + '044597d59177fc1d89555d38915f581b5ff2286b39d022ca0283d2bdd5c36be5d3c' + 'e7b9b97792327851a562752e4b79475d1f51f5a71352482b241227f45ed36a9') + +sample_ripe = '003cd097eb7f35c87b5dc8b4538c22cb55312a9f' _sha = hashlib.new('sha512') _sha.update(sample_pubsigningkey + sample_pubencryptionkey) @@ -45,8 +44,6 @@ class RIPEMD160TestCase(object): self.assertEqual(hexlify(self._hashdigest(pubkey_sha)), sample_ripe) -@unittest.skipIf( - ssl.OPENSSL_VERSION.startswith('OpenSSL 3'), 'no ripemd160 in openssl 3') class TestHashlib(RIPEMD160TestCase, unittest.TestCase): """RIPEMD160 test case for hashlib""" @staticmethod @@ -56,88 +53,9 @@ class TestHashlib(RIPEMD160TestCase, unittest.TestCase): return hasher.digest() -@unittest.skipUnless(RIPEMD160, 'pycrypto package not found') +@unittest.skipUnless(RIPEMD, '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) - ) + return RIPEMD.RIPEMD160Hash(data).digest() diff --git a/src/tests/test_helper_inbox.py b/src/tests/test_helper_inbox.py deleted file mode 100644 index a0b6de1b..00000000 --- a/src/tests/test_helper_inbox.py +++ /dev/null @@ -1,76 +0,0 @@ -"""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 deleted file mode 100644 index 9227e43a..00000000 --- a/src/tests/test_helper_sent.py +++ /dev/null @@ -1,76 +0,0 @@ -"""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 deleted file mode 100644 index 036bd2c9..00000000 --- a/src/tests/test_helper_sql.py +++ /dev/null @@ -1,131 +0,0 @@ -"""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 deleted file mode 100644 index 4c6be32d..00000000 --- a/src/tests/test_identicon.py +++ /dev/null @@ -1,49 +0,0 @@ -"""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 deleted file mode 100644 index d0b9ff6d..00000000 --- a/src/tests/test_inventory.py +++ /dev/null @@ -1,60 +0,0 @@ -"""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 deleted file mode 100644 index c6988827..00000000 --- a/src/tests/test_l10n.py +++ /dev/null @@ -1,24 +0,0 @@ -"""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 deleted file mode 100644 index 4e74e50d..00000000 --- a/src/tests/test_log.py +++ /dev/null @@ -1,21 +0,0 @@ -"""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 index 7fbb91c8..57448911 100644 --- a/src/tests/test_logger.py +++ b/src/tests/test_logger.py @@ -2,18 +2,15 @@ Testing the logger configuration """ +import logging import os import tempfile - -import six - -from .test_process import TestProcessProto +import unittest -class TestLogger(TestProcessProto): - """A test case for logger configuration""" +class TestLogger(unittest.TestCase): + """A test case for bmconfigparser""" - pattern = r' <===> ' conf_template = ''' [loggers] keys=root @@ -31,28 +28,42 @@ format=%(asctime)s {1} %(message)s class=FileHandler level=NOTSET formatter=default -args=({0!r}, 'w') +args=('{0}', '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""" + """Put logging.dat with special pattern and check it was used""" + tmp = os.environ['BITMESSAGE_HOME'] = tempfile.gettempdir() + log_config = os.path.join(tmp, 'logging.dat') + log_file = os.path.join(tmp, 'debug.log') - self._stop_process() - data = open(self.log_file).read() - six.assertRegex(self, data, self.pattern) - six.assertRegex(self, data, 'Loaded logger configuration') + def gen_log_config(pattern): + """A small closure to generate logging.dat with custom pattern""" + with open(log_config, 'wb') as dst: + dst.write(self.conf_template.format(log_file, pattern)) + + pattern = r' o_0 ' + gen_log_config(pattern) + + try: + from pybitmessage.debug import logger, resetLogging + if not os.path.isfile(log_file): # second pass + pattern = r' <===> ' + gen_log_config(pattern) + resetLogging() + except ImportError: + self.fail('There is no package pybitmessage. Things gone wrong.') + finally: + os.remove(log_config) + + logger_ = logging.getLogger('default') + + self.assertEqual(logger, logger_) + + logger_.info('Testing the logger...') + + self.assertRegexpMatches(open(log_file).read(), pattern) diff --git a/src/tests/test_msg.py b/src/tests/test_msg.py deleted file mode 100644 index cb586fa5..00000000 --- a/src/tests/test_msg.py +++ /dev/null @@ -1,39 +0,0 @@ -"""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 deleted file mode 100644 index 4b041f1c..00000000 --- a/src/tests/test_multiqueue.py +++ /dev/null @@ -1,65 +0,0 @@ -"""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 deleted file mode 100644 index 206117e0..00000000 --- a/src/tests/test_network.py +++ /dev/null @@ -1,96 +0,0 @@ -"""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_networkgroup.py b/src/tests/test_networkgroup.py new file mode 100644 index 00000000..76cfb033 --- /dev/null +++ b/src/tests/test_networkgroup.py @@ -0,0 +1,39 @@ +""" +Test for network group +""" +import unittest + + +class TestNetworkGroup(unittest.TestCase): + """ + Test case for network group + """ + def test_network_group(self): + """Test various types of network groups""" + from pybitmessage.protocol import network_group + + test_ip = '1.2.3.4' + self.assertEqual('\x01\x02', network_group(test_ip)) + + test_ip = '127.0.0.1' + self.assertEqual('IPv4', network_group(test_ip)) + + test_ip = '0102:0304:0506:0708:090A:0B0C:0D0E:0F10' + self.assertEqual( + '\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C', + network_group(test_ip)) + + test_ip = 'bootstrap8444.bitmessage.org' + self.assertEqual( + 'bootstrap8444.bitmessage.org', + network_group(test_ip)) + + test_ip = 'quzwelsuziwqgpt2.onion' + self.assertEqual( + test_ip, + network_group(test_ip)) + + test_ip = None + self.assertEqual( + None, + network_group(test_ip)) diff --git a/src/tests/test_openclpow.py b/src/tests/test_openclpow.py deleted file mode 100644 index 4770072e..00000000 --- a/src/tests/test_openclpow.py +++ /dev/null @@ -1,29 +0,0 @@ -""" -Tests for openclpow module -""" - -import unittest - -from pybitmessage import openclpow, proofofwork - - -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""" - target_ = 54227212183 - initialHash = ( - "3758f55b5a8d902fd3597e4ce6a2d3f23daff735f65d9698c270987f4e67ad590" - "b93f3ffeba0ef2fd08a8dc2f87b68ae5a0dc819ab57f22ad2c4c9c8618a43b3" - ).decode("hex") - nonce = openclpow.do_opencl_pow(initialHash.encode("hex"), target_) - self.assertLess( - nonce - proofofwork.trial_value(nonce, initialHash), target_) diff --git a/src/pyelliptic/tests/test_openssl.py b/src/tests/test_openssl.py similarity index 85% rename from src/pyelliptic/tests/test_openssl.py rename to src/tests/test_openssl.py index cb789277..e947fff3 100644 --- a/src/pyelliptic/tests/test_openssl.py +++ b/src/tests/test_openssl.py @@ -3,10 +3,7 @@ Test if OpenSSL is working correctly """ import unittest -try: - from pyelliptic.openssl import OpenSSL -except ImportError: - from pybitmessage.pyelliptic import OpenSSL +from pybitmessage.pyelliptic.openssl import OpenSSL try: OpenSSL.BN_bn2binpad @@ -36,7 +33,7 @@ class TestOpenSSL(unittest.TestCase): @unittest.skipUnless(have_pad, 'Skipping OpenSSL pad test') def test_padding(self): - """Test an alternative implementation of bn2binpad""" + """Test an alternatie implementation of bn2binpad""" ctx = OpenSSL.BN_CTX_new() a = OpenSSL.BN_new() @@ -52,6 +49,6 @@ class TestOpenSSL(unittest.TestCase): 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'): + if b.raw != c.raw.rjust(OpenSSL.BN_num_bytes(n), chr(0)): bad += 1 self.assertEqual(bad, 0) diff --git a/src/tests/test_packets.py b/src/tests/test_packets.py deleted file mode 100644 index 9dfb1d23..00000000 --- a/src/tests/test_packets.py +++ /dev/null @@ -1,87 +0,0 @@ -"""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 deleted file mode 100644 index a78a4434..00000000 --- a/src/tests/test_pattern/knownnodes.dat +++ /dev/null @@ -1,104 +0,0 @@ -(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 index 37b34541..73a6e493 100644 --- a/src/tests/test_process.py +++ b/src/tests/test_process.py @@ -5,17 +5,17 @@ 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() +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(str(time.time())) class TestProcessProto(unittest.TestCase): @@ -23,72 +23,27 @@ class TestProcessProto(unittest.TestCase): 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' - ] + _threads_count = 15 _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 + cls.home = os.environ['BITMESSAGE_HOME'] = tempfile.gettempdir() 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) + subprocess.call(cls._process_cmd) # nosec + time.sleep(5) + cls.pid = int(cls._get_readline('singleton.lock')) + cls.process = psutil.Process(cls.pid) @classmethod def _get_readline(cls, pfile): pfile = os.path.join(cls.home, pfile) try: - with open(pfile, 'rb') as p: - return p.readline().strip() + return open(pfile, 'rb').readline().strip() except (OSError, IOError): pass @@ -101,85 +56,46 @@ class TestProcessProto(unittest.TestCase): 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) + for pfile in cls._files: + try: + os.remove(os.path.join(cls.home, pfile)) + except OSError: + pass @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 + if not cls._stop_process(): + print(open(os.path.join(cls.home, 'debug.log'), 'rb').read()) + cls.process.kill() 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) + # only count for now + # because of https://github.com/giampaolo/psutil/issues/613 + # PyBitmessage + # - addressGenerator + # - singleWorker + # - SQL + # - objectProcessor + # - singleCleaner + # - singleAPI + # - Asyncore + # - ReceiveQueue_0 + # - ReceiveQueue_1 + # - ReceiveQueue_2 + # - Announcer + # - InvBroadcaster + # - AddrBroadcaster + # - Downloader + self.assertEqual( + len(self.process.threads()), self._threads_count) class TestProcessShutdown(TestProcessProto): @@ -191,29 +107,18 @@ class TestProcessShutdown(TestProcessProto): self._stop_process(20), '%s has not stopped in 20 sec' % ' '.join(self._process_cmd)) + @classmethod + def tearDownClass(cls): + """Special teardown because pybitmessage is already stopped""" + cls._cleanup_files() + 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: diff --git a/src/tests/test_protocol.py b/src/tests/test_protocol.py deleted file mode 100644 index 69e1e82f..00000000 --- a/src/tests/test_protocol.py +++ /dev/null @@ -1,115 +0,0 @@ -""" -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 deleted file mode 100644 index 2db3c423..00000000 --- a/src/tests/test_randomtrackingdict.py +++ /dev/null @@ -1,49 +0,0 @@ -""" -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 deleted file mode 100644 index 39bedf32..00000000 --- a/src/tests/test_shared.py +++ /dev/null @@ -1,152 +0,0 @@ -"""Test cases for shared.py""" - -import unittest -from pybitmessage.shared import ( - isAddressInMyAddressBook, - isAddressInMySubscriptionsList, - checkSensitiveFilePermissions, - reloadBroadcastSendersForWhichImWatching, - fixSensitiveFilePermissions, - MyECSubscriptionCryptorObjects, - stat, - os, -) - -from .samples import sample_address - -try: - # Python 3 - from unittest.mock import patch, PropertyMock -except ImportError: - # Python 2 - from mock import patch, PropertyMock - -# mock os.stat data for file -PERMISSION_MODE1 = stat.S_IRUSR # allow Read permission for the file owner. -PERMISSION_MODE2 = ( - stat.S_IRWXO -) # allow read, write, serach & execute permission for other users -INODE = 753 -DEV = 1795 -NLINK = 1 -UID = 1000 -GID = 0 -SIZE = 1021 -ATIME = 1711587560 -MTIME = 1709449249 -CTIME = 1709449603 - - -class TestShared(unittest.TestCase): - """Test class for shared.py""" - - @patch("pybitmessage.shared.sqlQuery") - def test_isaddress_in_myaddressbook(self, mock_sql_query): - """Test if address is in MyAddressbook""" - address = sample_address - - # if address is in MyAddressbook - mock_sql_query.return_value = [address] - return_val = isAddressInMyAddressBook(address) - mock_sql_query.assert_called_once() - self.assertTrue(return_val) - - # if address is not in MyAddressbook - mock_sql_query.return_value = [] - return_val = isAddressInMyAddressBook(address) - self.assertFalse(return_val) - self.assertEqual(mock_sql_query.call_count, 2) - - @patch("pybitmessage.shared.sqlQuery") - def test_isaddress_in_mysubscriptionslist(self, mock_sql_query): - """Test if address is in MySubscriptionsList""" - - address = sample_address - - # if address is in MySubscriptionsList - mock_sql_query.return_value = [address] - return_val = isAddressInMySubscriptionsList(address) - self.assertTrue(return_val) - - # if address is not in MySubscriptionsList - mock_sql_query.return_value = [] - return_val = isAddressInMySubscriptionsList(address) - self.assertFalse(return_val) - self.assertEqual(mock_sql_query.call_count, 2) - - @patch("pybitmessage.shared.sqlQuery") - def test_reloadBroadcastSendersForWhichImWatching(self, mock_sql_query): - """Test for reload Broadcast Senders For Which Im Watching""" - mock_sql_query.return_value = [ - (sample_address,), - ] - # before reload - self.assertEqual(len(MyECSubscriptionCryptorObjects), 0) - - # reloading with addressVersionNumber 1 - reloadBroadcastSendersForWhichImWatching() - self.assertGreater(len(MyECSubscriptionCryptorObjects), 0) - - @patch("pybitmessage.shared.os.stat") - @patch( - "pybitmessage.shared.sys", - new_callable=PropertyMock, # pylint: disable=used-before-assignment - ) - def test_check_sensitive_file_permissions(self, mock_sys, mock_os_stat): - """Test to check file permissions""" - fake_filename = "path/to/file" - - # test for windows system - mock_sys.platform = "win32" - result = checkSensitiveFilePermissions(fake_filename) - self.assertTrue(result) - - # test for freebsd system - mock_sys.platform = "freebsd7" - # returning file permission mode stat.S_IRUSR - MOCK_OS_STAT_RETURN = os.stat_result( - sequence=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], - dict={ - "st_mode": PERMISSION_MODE1, - "st_ino": INODE, - "st_dev": DEV, - "st_nlink": NLINK, - "st_uid": UID, - "st_gid": GID, - "st_size": SIZE, - "st_atime": ATIME, - "st_mtime": MTIME, - "st_ctime": CTIME, - }, - ) - mock_os_stat.return_value = MOCK_OS_STAT_RETURN - result = checkSensitiveFilePermissions(fake_filename) - self.assertTrue(result) - - @patch("pybitmessage.shared.os.chmod") - @patch("pybitmessage.shared.os.stat") - def test_fix_sensitive_file_permissions( # pylint: disable=no-self-use - self, mock_os_stat, mock_chmod - ): - """Test to fix file permissions""" - fake_filename = "path/to/file" - - # returning file permission mode stat.S_IRWXO - MOCK_OS_STAT_RETURN = os.stat_result( - sequence=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], - dict={ - "st_mode": PERMISSION_MODE2, - "st_ino": INODE, - "st_dev": DEV, - "st_nlink": NLINK, - "st_uid": UID, - "st_gid": GID, - "st_size": SIZE, - "st_atime": ATIME, - "st_mtime": MTIME, - "st_ctime": CTIME, - }, - ) - mock_os_stat.return_value = MOCK_OS_STAT_RETURN - fixSensitiveFilePermissions(fake_filename, False) - mock_chmod.assert_called_once() diff --git a/src/tests/test_sqlthread.py b/src/tests/test_sqlthread.py deleted file mode 100644 index a612df3a..00000000 --- a/src/tests/test_sqlthread.py +++ /dev/null @@ -1,44 +0,0 @@ -"""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 index ac8bf7a6..b7471508 100644 --- a/src/threads.py +++ b/src/threads.py @@ -40,9 +40,7 @@ else: threading.Thread._Thread__bootstrap = _thread_name_hack -printLock = threading.Lock() - __all__ = [ "addressGenerator", "objectProcessor", "singleCleaner", "singleWorker", - "sqlThread", "printLock" + "sqlThread" ] diff --git a/src/tr.py b/src/tr.py index eec82c37..ac76ef4b 100644 --- a/src/tr.py +++ b/src/tr.py @@ -3,10 +3,7 @@ Translating text """ import os -try: - import state -except ImportError: - from . import state +import state class translateClass: @@ -43,12 +40,12 @@ def translateText(context, text, n=None): 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) + 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) diff --git a/src/upnp.py b/src/upnp.py index 42ff0c6d..99000413 100644 --- a/src/upnp.py +++ b/src/upnp.py @@ -5,21 +5,20 @@ Reference: http://mattscodecave.com/posts/using-python-and-upnp-to-forward-a-por """ import httplib -import re import socket import time import urllib2 from random import randint from urlparse import urlparse -from xml.dom.minidom import Document # nosec B408 -from defusedxml.minidom import parseString +from xml.dom.minidom import Document, parseString +import knownnodes import queues import state import tr -from bmconfigparser import config +from bmconfigparser import BMConfigParser from debug import logger -from network import connectionpool, knownnodes, StoppableThread +from network import BMConnectionPool, StoppableThread from network.node import Peer @@ -121,7 +120,7 @@ class Router: # pylint: disable=old-style-class if service.childNodes[0].data.find('WANIPConnection') > 0 or \ service.childNodes[0].data.find('WANPPPConnection') > 0: self.path = service.parentNode.getElementsByTagName('controlURL')[0].childNodes[0].data - self.upnp_schema = re.sub(r'[^A-Za-z0-9:-]', '', service.childNodes[0].data.split(':')[-2]) + self.upnp_schema = service.childNodes[0].data.split(':')[-2] def AddPortMapping( self, @@ -193,7 +192,7 @@ class Router: # pylint: disable=old-style-class if errinfo: logger.error("UPnP error: %s", respData) raise UPnPError(errinfo[0].childNodes[0].data) - except: # noqa:E722 + except: raise UPnPError("Unable to parse SOAP error: %s" % (respData)) return resp @@ -209,7 +208,10 @@ class uPnPThread(StoppableThread): def __init__(self): super(uPnPThread, self).__init__(name="uPnPThread") - self.extPort = config.safeGetInt('bitmessagesettings', 'extport', default=None) + try: + self.extPort = BMConfigParser().getint('bitmessagesettings', 'extport') + except: + self.extPort = None self.localIP = self.getLocalIP() self.routers = [] self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) @@ -228,24 +230,24 @@ class uPnPThread(StoppableThread): # wait until asyncore binds so that we know the listening port bound = False while state.shutdown == 0 and not self._stopped and not bound: - for s in connectionpool.pool.listeningSockets.values(): + for s in BMConnectionPool().listeningSockets.values(): if s.is_bound(): bound = True if not bound: time.sleep(1) # pylint: disable=attribute-defined-outside-init - self.localPort = config.getint('bitmessagesettings', 'port') + self.localPort = BMConfigParser().getint('bitmessagesettings', 'port') - while state.shutdown == 0 and config.safeGetBoolean('bitmessagesettings', 'upnp'): + while state.shutdown == 0 and BMConfigParser().safeGetBoolean('bitmessagesettings', 'upnp'): if time.time() - lastSent > self.sendSleep and not self.routers: try: self.sendSearchRouter() - except: # nosec B110 # noqa:E722 # pylint:disable=bare-except + except: pass lastSent = time.time() try: - while state.shutdown == 0 and config.safeGetBoolean('bitmessagesettings', 'upnp'): + while state.shutdown == 0 and BMConfigParser().safeGetBoolean('bitmessagesettings', 'upnp'): resp, (ip, _) = self.sock.recvfrom(1000) if resp is None: continue @@ -262,7 +264,7 @@ class uPnPThread(StoppableThread): newRouter.GetExternalIPAddress(), self.extPort ) - except: # noqa:E722 + except: logger.debug('Failed to get external IP') else: with knownnodes.knownNodesLock: @@ -274,18 +276,18 @@ class uPnPThread(StoppableThread): break except socket.timeout: pass - except: # noqa:E722 + except: 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 (IOError, OSError): # noqa:E722 + except: pass try: self.sock.close() - except (IOError, OSError): # noqa:E722 + except: pass deleted = False for router in self.routers: @@ -316,7 +318,7 @@ class uPnPThread(StoppableThread): try: logger.debug("Sending UPnP query") self.sock.sendto(ssdpRequest, (uPnPThread.SSDP_ADDR, uPnPThread.SSDP_PORT)) - except: # noqa:E722 + except: logger.exception("UPnP send query failed") def createPortMapping(self, router): @@ -330,7 +332,7 @@ class uPnPThread(StoppableThread): elif i == 1 and self.extPort: extPort = self.extPort # try external port from last time next else: - extPort = randint(32767, 65535) # nosec B311 + extPort = randint(32767, 65535) logger.debug( "Attempt %i, requesting UPnP mapping for %s:%i on external port %i", i, @@ -339,8 +341,8 @@ class uPnPThread(StoppableThread): extPort) router.AddPortMapping(extPort, self.localPort, localIP, 'TCP', 'BitMessage') self.extPort = extPort - config.set('bitmessagesettings', 'extport', str(extPort)) - config.save() + BMConfigParser().set('bitmessagesettings', 'extport', str(extPort)) + BMConfigParser().save() break except UPnPError: logger.debug("UPnP error: ", exc_info=True) diff --git a/start.sh b/start.sh deleted file mode 100755 index 75ed7af3..00000000 --- a/start.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -python2 pybitmessage/bitmessagemain.py "$@" diff --git a/stdeb.cfg b/stdeb.cfg deleted file mode 100644 index 0d4cfbcb..00000000 --- a/stdeb.cfg +++ /dev/null @@ -1,9 +0,0 @@ -[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 deleted file mode 100644 index 9bc08880..00000000 --- a/tests-kivy.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/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 deleted file mode 100644 index 713b25ef..00000000 --- a/tests.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/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 deleted file mode 100644 index 3524bb57..00000000 --- a/tox.ini +++ /dev/null @@ -1,86 +0,0 @@ -[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: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-attributes = 8