diff --git a/.gitignore b/.gitignore index b8c4a56a..2bcb5340 100644 --- a/.gitignore +++ b/.gitignore @@ -17,8 +17,4 @@ dist *.egg-info docs/_*/* docs/autodoc/ -build/sphinx/ pyan/ -.buildozer/ -bin/ -src/images/default_identicon/ \ No newline at end of file diff --git a/.readthedocs.yml b/.readthedocs.yml deleted file mode 100644 index 474ae9ab..00000000 --- a/.readthedocs.yml +++ /dev/null @@ -1,9 +0,0 @@ -version: 2 - -python: - version: 2.7 - install: - - requirements: docs/requirements.txt - - method: setuptools - path: . - system_packages: true diff --git a/.travis.yml b/.travis.yml index d7141188..1edba418 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,6 @@ addons: packages: - build-essential - libcap-dev - - tor install: - pip install -r requirements.txt - ln -s src pybitmessage # tests environment diff --git a/COPYING b/COPYING index 078bf213..2e0ae6c1 100644 --- a/COPYING +++ b/COPYING @@ -1,5 +1,5 @@ Copyright (c) 2012-2016 Jonathan Warren -Copyright (c) 2012-2020 The Bitmessage Developers +Copyright (c) 2012-2019 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/LICENSE b/LICENSE index c2eeff82..3be738c0 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) Copyright (c) 2012-2016 Jonathan Warren -Copyright (c) 2012-2020 The Bitmessage Developers +Copyright (c) 2012-2019 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 @@ -22,7 +22,7 @@ SOFTWARE. ===== qidenticon.py identicon python implementation with QPixmap output by sendiulo -qidenticon.py is Licensed under FreeBSD License. +qidenticon.py is Licesensed under FreeBSD License. (http://www.freebsd.org/copyright/freebsd-license.html) Copyright 2013 "Sendiulo". All rights reserved. @@ -36,7 +36,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ``AS IS'' AND ANY EXPRESS OR I ===== based on identicon.py identicon python implementation. by Shin Adachi -identicon.py is Licensed under FreeBSD License. +identicon.py is Licesensed under FreeBSD License. (http://www.freebsd.org/copyright/freebsd-license.html) Copyright 1994-2009 Shin Adachi. All rights reserved. @@ -47,48 +47,3 @@ Redistribution and use in source and binary forms, with or without modification, 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -===== based on asyncore_pollchoose.py asyncore_pollchoose python implementation. by Sam Rushing - -Copyright 1996 by Sam Rushing. All Rights Reserved - -Permission to use, copy, modify, and distribute this software and -its documentation for any purpose and without fee is hereby -granted, provided that the above copyright notice appear in all -copies and that both that copyright notice and this permission -notice appear in supporting documentation, and that the name of Sam -Rushing not be used in advertising or publicity pertaining to -distribution of the software without specific, written prior -permission. - -SAM RUSHING DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, -INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN -NO EVENT SHALL SAM RUSHING BE LIABLE FOR ANY SPECIAL, INDIRECT OR -CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS -OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, -NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -===== based on namecoin.py namecoin.py python implementation by Daniel Kraft - -Copyright (C) 2013 by Daniel Kraft - -This file is part of the Bitmessage project. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 17049e7a..c3dcb540 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,12 @@ Development ---------- Bitmessage is a collaborative project. You are welcome to submit pull requests although if you plan to put a non-trivial amount of work into coding new -features, it is recommended that you first describe your ideas in the -separate issue. +features, it is recommended that you first solicit feedback on the DevTalk +pseudo-mailing list: +BM-2D9QKN4teYRvoq2fyzpiftPh9WP9qggtzh -Feel welcome to join chan "bitmessage", BM-2cWy7cvHoq3f1rYMerRJp8PT653jjSuEdY +Feel welcome to join chan "bitmessage", BM-2cWy7cvHoq3f1rYMerRJp8PT653jjSuEdY +which is on preview here: https://beamstat.com/chan/bitmessage References ---------- diff --git a/android_instruction.rst b/android_instruction.rst index e6c7797d..fab55b55 100644 --- a/android_instruction.rst +++ b/android_instruction.rst @@ -1,6 +1,6 @@ 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. +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 adresses. Steps for trying out this sample: @@ -13,7 +13,7 @@ This sample uses the kivy as Kivy is an open source, cross-platform Python frame 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. +Kivy in support take Buildozer which is a tool that automates the entire build process. It downloads and sets up all the prequisites 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. diff --git a/build/README.md b/build/README.md new file mode 100644 index 00000000..248d2c41 --- /dev/null +++ b/build/README.md @@ -0,0 +1,2 @@ +This directory contains scripts that are helpful for developers when building +or maintaining PyBitmessage. diff --git a/build/changelang.sh b/build/changelang.sh new file mode 100755 index 00000000..915c5dea --- /dev/null +++ b/build/changelang.sh @@ -0,0 +1,16 @@ +export LANG=de_DE.UTF-8 +export LANGUAGE=de_DE +export LC_CTYPE="de_DE.UTF-8" +export LC_NUMERIC=de_DE.UTF-8 +export LC_TIME=de_DE.UTF-8 +export LC_COLLATE="de_DE.UTF-8" +export LC_MONETARY=de_DE.UTF-8 +export LC_MESSAGES="de_DE.UTF-8" +export LC_PAPER=de_DE.UTF-8 +export LC_NAME=de_DE.UTF-8 +export LC_ADDRESS=de_DE.UTF-8 +export LC_TELEPHONE=de_DE.UTF-8 +export LC_MEASUREMENT=de_DE.UTF-8 +export LC_IDENTIFICATION=de_DE.UTF-8 +export LC_ALL= +python2.7 src/bitmessagemain.py diff --git a/build/compiletest.py b/build/compiletest.py new file mode 100755 index 00000000..fdbf7db1 --- /dev/null +++ b/build/compiletest.py @@ -0,0 +1,23 @@ +#!/usr/bin/python2.7 + +import ctypes +import fnmatch +import os +import sys +import traceback + +matches = [] +for root, dirnames, filenames in os.walk('src'): + for filename in fnmatch.filter(filenames, '*.py'): + matches.append(os.path.join(root, filename)) + +for filename in matches: + source = open(filename, 'r').read() + '\n' + try: + compile(source, filename, 'exec') + except Exception as e: + if 'win' in sys.platform: + ctypes.windll.user32.MessageBoxA(0, traceback.format_exc(), "Exception in " + filename, 1) + else: + print "Exception in %s: %s" % (filename, traceback.format_exc()) + sys.exit(1) diff --git a/build/mergepullrequest.sh b/build/mergepullrequest.sh new file mode 100755 index 00000000..35e87566 --- /dev/null +++ b/build/mergepullrequest.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +if [ -z "$1" ]; then + echo "You must specify pull request number" + exit +fi + +git pull +git checkout v0.6 +git fetch origin pull/"$1"/head:"$1" +git merge --ff-only "$1" diff --git a/build/osx.sh b/build/osx.sh new file mode 100755 index 00000000..e58a49f4 --- /dev/null +++ b/build/osx.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# OS X Build script wrapper around the py2app script. +# This build can only be generated on OS X. +# Requires all build dependencies for Bitmessage +# Especially important is OpenSSL installed through brew + +export ARCHFLAGS="-arch i386 -arch x86_64" + +if [[ -z "$1" ]]; then + echo "Please supply a version number for this release as the first argument." + exit +fi + +echo "Creating OS X packages for Bitmessage." + +export PYBITMESSAGEVERSION=$1 + +cd src && python2.7 build_osx.py py2app + +if [[ $? = "0" ]]; then + hdiutil create -fs HFS+ -volname "Bitmessage" -srcfolder dist/Bitmessage.app dist/bitmessage-v$1.dmg +else + echo "Problem creating Bitmessage.app, stopping." + exit +fi diff --git a/build/updatetranslations.sh b/build/updatetranslations.sh new file mode 100755 index 00000000..ba5a3fdb --- /dev/null +++ b/build/updatetranslations.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +if [ ! -f "$1" ]; then + echo "$1 not found, please specify the file name for source" + exit +fi + +srcdir=`mktemp -d` + +unzip "$1" -d $srcdir + +for i in $srcdir/*ts; do + o=`basename $i|cut -b3-` + o="${o,,}" + o="${o//@/_}" + echo "$i -> $o" + mv "$i" "$HOME/src/PyBitmessage/src/translations/$o" +done + +rm -rf -- $srcdir + +lrelease-qt4 "$HOME/src/PyBitmessage/src/translations/bitmessage.pro" diff --git a/buildscripts/winbuild.sh b/buildscripts/winbuild.sh deleted file mode 100755 index 5b4fc37f..00000000 --- a/buildscripts/winbuild.sh +++ /dev/null @@ -1,165 +0,0 @@ -#!/bin/bash - -# INIT -MACHINE_TYPE=`uname -m` -BASE_DIR=$(pwd) -PYTHON_VERSION=2.7.17 -PYQT_VERSION=4-4.11.4-gpl-Py2.7-Qt4.8.7 -OPENSSL_VERSION=1_0_2t -SRCPATH=~/Downloads - -#Functions -function download_sources_32 { - if [ ! -d ${SRCPATH} ]; then - mkdir -p ${SRCPATH} - fi - wget -P ${SRCPATH} -c -nc --content-disposition \ - https://www.python.org/ftp/python/${PYTHON_VERSION}/python-${PYTHON_VERSION}.msi \ - https://download.microsoft.com/download/1/1/1/1116b75a-9ec3-481a-a3c8-1777b5381140/vcredist_x86.exe \ - https://github.com/Bitmessage/ThirdPartyLibraries/blob/master/PyQt${PYQT_VERSION}-x32.exe?raw=true \ - https://github.com/Bitmessage/ThirdPartyLibraries/blob/master/Win32OpenSSL-${OPENSSL_VERSION}.exe?raw=true \ - https://github.com/Bitmessage/ThirdPartyLibraries/blob/master/pyopencl-2015.1-cp27-none-win32.whl?raw=true -} - -function download_sources_64 { - if [ ! -d ${SRCPATH} ]; then - mkdir -p ${SRCPATH} - fi - wget -P ${SRCPATH} -c -nc --content-disposition \ - http://www.python.org/ftp/python/${PYTHON_VERSION}/python-${PYTHON_VERSION}.amd64.msi \ - https://download.microsoft.com/download/d/2/4/d242c3fb-da5a-4542-ad66-f9661d0a8d19/vcredist_x64.exe \ - https://github.com/Bitmessage/ThirdPartyLibraries/blob/master/PyQt${PYQT_VERSION}-x64.exe?raw=true \ - https://github.com/Bitmessage/ThirdPartyLibraries/blob/master/Win64OpenSSL-${OPENSSL_VERSION}.exe?raw=true \ - https://github.com/Bitmessage/ThirdPartyLibraries/blob/master/pyopencl-2015.1-cp27-none-win_amd64.whl?raw=true -} - -function download_sources { - if [ ${MACHINE_TYPE} == 'x86_64' ]; then - download_sources_64 - else - download_sources_32 - fi -} - -function install_wine { - echo "Setting up wine" - if [ ${MACHINE_TYPE} == 'x86_64' ]; then - export WINEPREFIX=${HOME}/.wine64 WINEARCH=win64 - else - export WINEPREFIX=${HOME}/.wine32 WINEARCH=win32 - fi - rm -rf ${WINEPREFIX} - rm -rf packages/pyinstaller/{build,dist} -} - -function install_python(){ - cd ${SRCPATH} - if [ ${MACHINE_TYPE} == 'x86_64' ]; then - echo "Installing Python ${PYTHON_VERSION} 64b" - wine msiexec -i python-${PYTHON_VERSION}.amd64.msi /q /norestart - echo "Installing vcredist for 64 bit" - wine vcredist_x64.exe /q /norestart - else - echo "Installing Python ${PYTHON_VERSION} 32b" - wine msiexec -i python-${PYTHON_VERSION}.msi /q /norestart - # MSVCR 2008 required for Windows XP - cd ${SRCPATH} - echo "Installing vc_redist (2008) for 32 bit " - wine vcredist_x86.exe /Q - fi - echo "Upgrading pip" - wine python -m pip install --upgrade pip -} - -function install_pyqt(){ - if [ ${MACHINE_TYPE} == 'x86_64' ]; then - echo "Installing PyQt-${PYQT_VERSION} 64b" - wine PyQt${PYQT_VERSION}-x64.exe /S /WX - else - echo "Installing PyQt-${PYQT_VERSION} 32b" - wine PyQt${PYQT_VERSION}-x32.exe /S /WX - fi -} - -function install_openssl(){ - if [ ${MACHINE_TYPE} == 'x86_64' ]; then - echo "Installing OpenSSL ${OPENSSL_VERSION} 64b" - wine Win64OpenSSL-${OPENSSL_VERSION}.exe /q /norestart /silent /verysilent /sp- /suppressmsgboxes - else - echo "Installing OpenSSL ${OPENSSL_VERSION} 32b" - wine Win32OpenSSL-${OPENSSL_VERSION}.exe /q /norestart /silent /verysilent /sp- /suppressmsgboxes - fi -} - -function install_pyinstaller() -{ - cd ${BASE_DIR} - 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_msgpack() -{ - cd ${BASE_DIR} - echo "Installing msgpack" - wine python -m pip install msgpack-python -} - -function install_pyopencl() -{ - cd ${SRCPATH} - echo "Installing PyOpenCL" - if [ ${MACHINE_TYPE} == 'x86_64' ]; then - wine python -m pip install pyopencl-2015.1-cp27-none-win_amd64.whl - else - wine python -m pip install pyopencl-2015.1-cp27-none-win32.whl - fi -} - - -function build_dll(){ - 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=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=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} - cd packages/pyinstaller - wine pyinstaller bitmessagemain.spec -} - -# prepare on ubuntu -# dpkg --add-architecture i386 -# apt update -# apt -y install wget wine-stable wine-development winetricks mingw-w64 wine32 wine64 xvfb - - -download_sources -if [ "$1" == "--download-only" ]; then - exit -fi - -install_wine -install_python -install_pyqt -install_openssl -install_pyopencl -install_msgpack -install_pyinstaller -build_dll -build_exe diff --git a/checkdeps.py b/checkdeps.py index c3dedc1d..03782037 100755 --- a/checkdeps.py +++ b/checkdeps.py @@ -1,6 +1,6 @@ #!/usr/bin/env python2 """ -Check dependencies and give recommendations about how to satisfy them +Check dependendies and give recommendations about how to satisfy them Limitations: @@ -164,7 +164,7 @@ if (not compiler or prereqs) and OPSYS in PACKAGE_MANAGER: if not compiler: compilerToPackages() prereqToPackages() - if prereqs and mandatory: + if mandatory: sys.exit(1) else: print("All the dependencies satisfied, you can install PyBitmessage") diff --git a/docs/_static/custom.css b/docs/_static/custom.css deleted file mode 100644 index 5192985c..00000000 --- a/docs/_static/custom.css +++ /dev/null @@ -1,4 +0,0 @@ -/* Hide "On GitHub" section from versions menu */ -li.wy-breadcrumbs-aside > a.fa { - display: none; -} diff --git a/docs/conf.py b/docs/conf.py index 3464e056..a4dae7c7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -2,24 +2,35 @@ """ Configuration file for the Sphinx documentation builder. -For a full list of options see the documentation: +This file does only contain a selection of the most common options. For a +full list see the documentation: http://www.sphinx-doc.org/en/master/config + +-- Path setup -------------------------------------------------------------- + +If extensions (or modules to document with autodoc) are in another directory, +add these directories to sys.path here. If the directory is relative to the +documentation root, use os.path.abspath to make it absolute, like shown here. """ import os import sys +from sphinx.apidoc import main +from mock import Mock as MagicMock + +sys.path.insert(0, os.path.abspath('.')) +sys.path.insert(0, os.path.abspath('..')) sys.path.insert(0, os.path.abspath('../src')) +sys.path.insert(0, os.path.abspath('../src/pyelliptic')) -from importlib import import_module - -import version # noqa:E402 +import version # -- Project information ----------------------------------------------------- project = u'PyBitmessage' -copyright = u'2019, The Bitmessage Team' # pylint: disable=redefined-builtin +copyright = u'2018, The Bitmessage Team' # pylint: disable=redefined-builtin author = u'The Bitmessage Team' # The short X.Y version @@ -39,18 +50,15 @@ release = version # ones. extensions = [ 'sphinx.ext.autodoc', - 'sphinx.ext.coverage', # FIXME: unused - 'sphinx.ext.imgmath', # legacy unused + # 'sphinx.ext.doctest', # Currently disabled due to bad doctests 'sphinx.ext.intersphinx', - 'sphinx.ext.linkcode', - 'sphinx.ext.napoleon', 'sphinx.ext.todo', - 'sphinxcontrib.apidoc', + 'sphinx.ext.coverage', + 'sphinx.ext.imgmath', + 'sphinx.ext.viewcode', 'm2r', ] -default_role = 'obj' - # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -67,29 +75,23 @@ master_doc = 'index' # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -# language = None +language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path . -exclude_patterns = ['_build'] +exclude_patterns = [u'_build', 'Thumbs.db', '.DS_Store'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' -# Don't prepend every class or function name with full module path -add_module_names = False - -# A list of ignored prefixes for module index sorting. -modindex_common_prefix = ['pybitmessage.'] - # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'sphinx_rtd_theme' +html_theme = 'alabaster' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -102,10 +104,6 @@ html_theme = 'sphinx_rtd_theme' # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] -html_css_files = [ - 'custom.css', -] - # Custom sidebar templates, must be a dictionary that maps document names # to template names. # @@ -116,7 +114,10 @@ html_css_files = [ # # html_sidebars = {} -html_show_sourcelink = False +# Deal with long lines in source view +html_theme_options = { + 'page_width': '1366px', +} # -- Options for HTMLHelp output --------------------------------------------- @@ -198,74 +199,10 @@ epub_exclude_files = ['search.html'] # -- Extension configuration ------------------------------------------------- -autodoc_mock_imports = [ - 'debug', - 'pybitmessage.bitmessagekivy', - 'pybitmessage.bitmessageqt.addressvalidator', - 'pybitmessage.helper_startup', - 'pybitmessage.network.httpd', - 'pybitmessage.network.https', - 'ctypes', - 'dialog', - 'gi', - 'kivy', - 'logging', - 'msgpack', - 'numpy', - 'pkg_resources', - 'pycanberra', - 'pyopencl', - 'PyQt4', - 'pyxdg', - 'qrcode', - 'stem', -] -autodoc_member_order = 'bysource' - -# Apidoc settings -apidoc_module_dir = '../pybitmessage' -apidoc_output_dir = 'autodoc' -apidoc_excluded_paths = [ - 'bitmessagekivy', 'build_osx.py', - 'bitmessageqt/addressvalidator.py', 'bitmessageqt/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 -apidoc_toc_file = False -apidoc_extra_args = ['-a'] - -# Napoleon settings -napoleon_google_docstring = True - - -# linkcode function -def linkcode_resolve(domain, info): - """This generates source URL's for sphinx.ext.linkcode""" - if domain != 'py' or not info['module']: - return - try: - home = os.path.abspath(import_module('pybitmessage').__path__[0]) - mod = import_module(info['module']).__file__ - except ImportError: - return - repo = 'https://github.com/Bitmessage/PyBitmessage/blob/v0.6/src%s' - path = mod.replace(home, '') - if path != mod: - # put the link only for top level definitions - if len(info['fullname'].split('.')) > 1: - return - if path.endswith('.pyc'): - path = path[:-1] - return repo % path - - # -- Options for intersphinx extension --------------------------------------- # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'https://docs.python.org/2.7/': None} +intersphinx_mapping = {'https://docs.python.org/': None} # -- Options for todo extension ---------------------------------------------- diff --git a/docs/contribute.dir/develop.dir/fabric.rst b/docs/contribute.dir/develop.dir/fabric.rst index 8003f33a..434ccf7b 100644 --- a/docs/contribute.dir/develop.dir/fabric.rst +++ b/docs/contribute.dir/develop.dir/fabric.rst @@ -1,2 +1,2 @@ -.. mdinclude:: ../../../fabfile/README.md +.. mdinclude:: fabfile/README.md diff --git a/docs/contribute.dir/develop.dir/opsec.rst b/docs/contribute.dir/develop.dir/opsec.rst index 1af43668..87bf4f49 100644 --- a/docs/contribute.dir/develop.dir/opsec.rst +++ b/docs/contribute.dir/develop.dir/opsec.rst @@ -21,12 +21,12 @@ If we are to make bold claims about protecting your privacy we should demonstrat - looking to audit - warrant canary -Digital footprint +Digital foootprint ------------------ 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 + * Use separate addresses for different puprose * 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. diff --git a/docs/contribute.dir/develop.dir/overview.rst b/docs/contribute.dir/develop.dir/overview.rst index 8bbc8299..e83d884b 100644 --- a/docs/contribute.dir/develop.dir/overview.rst +++ b/docs/contribute.dir/develop.dir/overview.rst @@ -11,17 +11,17 @@ Bitmessage makes use of fabric_ to define tasks such as building documentation o 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. +We aim to be PEP8 compliant but we recognise 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-blindess, 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. +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 off 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. +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 arbirary 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. @@ -49,7 +49,7 @@ British vs American spelling 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. +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 grapghs. To re-build them, run `fab build_docs:dep_graphs=true`. Note that the dot graph takes a lot of time. @@ -62,7 +62,7 @@ To re-build them, run `fab build_docs:dep_graphs=true`. Note that the dot graph .. figure:: ../../../../_static/deps-sfdp.png :alt: SFDP graph of dependencies :width: 100 pc - + :index:`SFDP` graph of dependencies .. figure:: ../../../../_static/deps-dot.png diff --git a/docs/contribute.dir/processes.rst b/docs/contribute.dir/processes.rst index eb913325..8f0385d4 100644 --- a/docs/contribute.dir/processes.rst +++ b/docs/contribute.dir/processes.rst @@ -1,8 +1,8 @@ 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 +In other to keep the Bitmessage project running the team run a number of systems and accounts that form the +development pipeline and continuous delivery process. We are always striving to improve the process. Towards that end it is documented here. @@ -20,7 +20,7 @@ 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 +We eat our own dog food! You can send us bug reports via the Bitmessage chan at xxx .. _website: https://bitmessage.org diff --git a/docs/index.rst b/docs/index.rst index cc8c9523..9dddfa28 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,20 +1,12 @@ .. mdinclude:: ../README.md -Documentation -------------- -.. toctree:: - :maxdepth: 3 - - autodoc/pybitmessage - -Legacy pages ------------- .. toctree:: :maxdepth: 2 + overview usage contribute - + Indices and tables ------------------ diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index 55219ec5..00000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -m2r -sphinxcontrib-apidoc diff --git a/fabfile/README.md b/fabfile/README.md index 5e90147c..643aed8e 100644 --- a/fabfile/README.md +++ b/fabfile/README.md @@ -1,6 +1,6 @@ # Fabric -[Fabric](https://www.fabfile.org) is a Python library for performing devops tasks. You can think of it a bit like a +[Fabric](https://www.fabfile.org) is a Python library for performing devops tasks. You can thing 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. @@ -46,7 +46,7 @@ Furthermore, you can use -- to run arbitrary shell commands rather than tasks: There are a number of advantages that should benefit us: - * Common tasks can be written in Python and executed consistently + * Common tasks can be writen 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 diff --git a/packages/README.md b/packages/README.md index 2905ec20..ed2df3cc 100644 --- a/packages/README.md +++ b/packages/README.md @@ -15,7 +15,7 @@ OSX: https://github.com/Bitmessage/PyBitmessage/releases -Works on OSX 10.7.5 or higher +Wors on OSX 10.7.5 or higher Arch linux: diff --git a/packages/pyinstaller/bitmessagemain.spec b/packages/pyinstaller/bitmessagemain.spec index 92e52f6a..06cf6e76 100644 --- a/packages/pyinstaller/bitmessagemain.spec +++ b/packages/pyinstaller/bitmessagemain.spec @@ -1,89 +1,65 @@ import ctypes import os import time -import sys - -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) -cdrivePath = site_root[0:3] -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") - -importPath = srcPath -sys.path.insert(0,importPath) -os.chdir(sys.path[0]) -from version import softwareVersion +srcPath = "C:\\src\\PyBitmessage\\src\\" +qtPath = "C:\\Qt-4.8.7\\" +openSSLPath = "C:\\OpenSSL-1.0.2j\\bin\\" +outPath = "C:\\src\\PyInstaller-3.2.1\\bitmessagemain" today = time.strftime("%Y%m%d") snapshot = False 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')], +a = Analysis([srcPath + 'bitmessagemain.py'], pathex=[outPath], - hiddenimports=['bitmessageqt.languagebox', 'pyopencl','numpy', 'win32com' , 'setuptools.msvc' ,'_cffi_backend'], + hiddenimports=[], hookspath=None, - runtime_hooks=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": + for file in os.listdir(srcPath + 'translations'): + if file[-3:] != ".qm": continue - extraDatas.append((os.path.join('translations', file_), - os.path.join(srcPath, 'translations', file_), 'DATA')) - for libdir in sys.path: - qtdir = os.path.join(libdir, qtBase, 'translations') - if os.path.isdir(qtdir): - break - if not os.path.isdir(qtdir): - return extraDatas - for file_ in os.listdir(qtdir): - if file_[0:3] != "qt_" or file_[5:8] != ".qm": + extraDatas.append((os.path.join('translations', file), os.path.join(srcPath, 'translations', file), 'DATA')) + for file in os.listdir(qtPath + 'translations'): + if file[0:3] != "qt_" or file[5:8] != ".qm": continue - extraDatas.append((os.path.join('translations', file_), - os.path.join(qtdir, file_), 'DATA')) + extraDatas.append((os.path.join('translations', file), os.path.join(qtPath, 'translations', file), 'DATA')) return extraDatas def addUIs(): import os extraDatas = [] - for file_ in os.listdir(os.path.join(srcPath, 'bitmessageqt')): - if file_[-3:] != ".ui": + for file in os.listdir(srcPath + 'bitmessageqt'): + if file[-3:] != ".ui": continue - extraDatas.append((os.path.join('ui', file_), os.path.join(srcPath, - 'bitmessageqt', file_), 'DATA')) + 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 += addUIs() +if ctypes.sizeof(ctypes.c_voidp) == 4: + arch=32 +else: + arch=64 -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') - ] +a.binaries += [('libeay32.dll', openSSLPath + 'libeay32.dll', 'BINARY'), + (os.path.join('bitmsghash', 'bitmsghash%i.dll' % (arch)), os.path.join(srcPath, 'bitmsghash', 'bitmsghash%i.dll' % (arch)), 'BINARY'), + (os.path.join('bitmsghash', 'bitmsghash.cl'), os.path.join(srcPath, 'bitmsghash', 'bitmsghash.cl'), 'BINARY'), + (os.path.join('sslkeys', 'cert.pem'), os.path.join(srcPath, 'sslkeys', 'cert.pem'), 'BINARY'), + (os.path.join('sslkeys', 'key.pem'), os.path.join(srcPath, 'sslkeys', 'key.pem'), 'BINARY') + ] +with open(os.path.join(srcPath, 'version.py'), 'rt') as f: + softwareVersion = f.readline().split('\'')[1] fname = 'Bitmessage_%s_%s.exe' % ("x86" if arch == 32 else "x64", softwareVersion) if snapshot: @@ -96,18 +72,8 @@ exe = EXE(pyz, 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') - diff --git a/pybitmessage b/pybitmessage deleted file mode 120000 index e8310385..00000000 --- a/pybitmessage +++ /dev/null @@ -1 +0,0 @@ -src \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index a4e0547c..3236eed1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,17 +1,14 @@ # Since there is overlap in the violations that the different tools check for, it makes sense to quiesce some warnings # in some tools if those warnings in other tools are preferred. This avoids the need to add duplicate lint warnings. -# max-line-length should be removed ASAP! - [pycodestyle] max-line-length = 119 [flake8] max-line-length = 119 -ignore = E722,F841,W503 +ignore = E722,F841 # E722: pylint is preferred for bare-except # F841: pylint is preferred for unused-variable -# W503: deprecated: https://bugs.python.org/issue26763 - https://www.python.org/dev/peps/pep-0008/#should-a-line-break-before-or-after-a-binary-operator # pylint honours the [MESSAGES CONTROL] section # as well as [MASTER] section diff --git a/setup.py b/setup.py index 3e585b6b..e3f97bac 100644 --- a/setup.py +++ b/setup.py @@ -16,8 +16,13 @@ EXTRAS_REQUIRE = { 'prctl': ['python_prctl'], # Named threads 'qrcode': ['qrcode'], 'sound;platform_system=="Windows"': ['winsound'], - 'tor': ['stem'], - 'docs': ['sphinx', 'sphinxcontrib-apidoc', 'm2r'] + 'docs': [ + 'sphinx', # fab build_docs + 'graphviz', # fab build_docs + 'curses', # src/depends.py + 'python2-pythondialog', # src/depends.py + 'm2r', # fab build_docs + ] } @@ -64,6 +69,7 @@ if __name__ == "__main__": 'pybitmessage.network', 'pybitmessage.plugins', 'pybitmessage.pyelliptic', + 'pybitmessage.socks', 'pybitmessage.storage' ] @@ -141,17 +147,10 @@ if __name__ == "__main__": 'libmessaging =' 'pybitmessage.plugins.indicator_libmessaging [gir]' ], - 'bitmessage.proxyconfig': [ - 'stem = pybitmessage.plugins.proxyconfig_stem [tor]' - ], # 'console_scripts': [ # 'pybitmessage = pybitmessage.bitmessagemain:main' # ] }, scripts=['src/pybitmessage'], - cmdclass={'install': InstallCmd}, - command_options={ - 'build_sphinx': { - 'source_dir': ('setup.py', 'docs')} - } + cmdclass={'install': InstallCmd} ) diff --git a/src/.buildozer/android/platform/python-for-android-new-toolchain b/src/.buildozer/android/platform/python-for-android-new-toolchain deleted file mode 160000 index 5aa322da..00000000 --- a/src/.buildozer/android/platform/python-for-android-new-toolchain +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 5aa322da9179dae305fde5af1db516c1ad9baea4 diff --git a/src/addresses.py b/src/addresses.py index 0d3d4400..533ec169 100644 --- a/src/addresses.py +++ b/src/addresses.py @@ -1,22 +1,25 @@ """ -Operations with addresses +src/addresses.py +================ + """ # pylint: disable=redefined-outer-name,inconsistent-return-statements + import hashlib from binascii import hexlify, unhexlify from struct import pack, unpack from debug import logger + ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" def encodeBase58(num, alphabet=ALPHABET): """Encode a number in Base X - Args: - num: The number to encode - alphabet: The alphabet to use for encoding + `num`: The number to encode + `alphabet`: The alphabet to use for encoding """ if num == 0: return alphabet[0] @@ -24,6 +27,7 @@ def encodeBase58(num, alphabet=ALPHABET): base = len(alphabet) while num: rem = num % base + # print 'num is:', num num = num // base arr.append(alphabet[rem]) arr.reverse() @@ -33,9 +37,9 @@ def encodeBase58(num, alphabet=ALPHABET): 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 + Arguments: + - `string`: The encoded string + - `alphabet`: The alphabet to use for encoding """ base = len(alphabet) num = 0 @@ -50,20 +54,11 @@ def decodeBase58(string, alphabet=ALPHABET): return num -class varintEncodeError(Exception): - """Exception class for encoding varint""" - pass - - -class varintDecodeError(Exception): - """Exception class for decoding varint data""" - pass - - def encodeVarint(integer): """Convert integer into varint bytes""" if integer < 0: - raise varintEncodeError('varint cannot be < 0') + logger.error('varint cannot be < 0') + raise SystemExit if integer < 253: return pack('>B', integer) if integer >= 253 and integer < 65536: @@ -73,7 +68,13 @@ def encodeVarint(integer): if integer >= 4294967296 and integer < 18446744073709551616: return pack('>B', 255) + pack('>Q', integer) if integer >= 18446744073709551616: - raise varintEncodeError('varint cannot be >= 18446744073709551616') + logger.error('varint cannot be >= 18446744073709551616') + raise SystemExit + + +class varintDecodeError(Exception): + """Exception class for decoding varint data""" + pass def decodeVarint(data): @@ -178,8 +179,7 @@ def decodeAddress(address): returns (status, address version number, stream number, data (almost certainly a ripe hash)) """ - # pylint: disable=too-many-return-statements,too-many-statements - # pylint: disable=too-many-branches + # pylint: disable=too-many-return-statements,too-many-statements,too-many-return-statements,too-many-branches address = str(address).strip() diff --git a/src/alice.png b/src/alice.png deleted file mode 100644 index 3f6c6a92..00000000 Binary files a/src/alice.png and /dev/null differ diff --git a/src/api.py b/src/api.py index 70da0cda..d3e87dfd 100644 --- a/src/api.py +++ b/src/api.py @@ -1,11 +1,19 @@ +# pylint: disable=too-many-locals,too-many-lines,no-self-use,too-many-public-methods,too-many-branches +# pylint: disable=too-many-statements """ +src/api.py +========== + +# Copyright (c) 2012-2016 Jonathan Warren +# Copyright (c) 2012-2019 The Bitmessage developers + 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-2020 The Bitmessage developers -# pylint: disable=too-many-lines,no-self-use,unused-variable,unused-argument + +from __future__ import absolute_import + import base64 import errno import hashlib @@ -13,34 +21,30 @@ import json import random # nosec import socket import subprocess +import threading import time from binascii import hexlify, unhexlify from SimpleXMLRPCServer import SimpleXMLRPCRequestHandler, SimpleXMLRPCServer from struct import pack +from version import softwareVersion + import defaults import helper_inbox import helper_sent +import helper_threading import network.stats import proofofwork import queues import shared import shutdown import state -from addresses import ( - addBMIfNotPresent, - calculateInventoryHash, - decodeAddress, - decodeVarint, - varintDecodeError -) +from addresses import addBMIfNotPresent, calculateInventoryHash, decodeAddress, decodeVarint, varintDecodeError from bmconfigparser import BMConfigParser from debug import logger 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 str_chan = '[chan]' @@ -69,10 +73,11 @@ class StoppableXMLRPCServer(SimpleXMLRPCServer): # This thread, of which there is only one, runs the API. -class singleAPI(StoppableThread): +class singleAPI(threading.Thread, helper_threading.StoppableThread): """API thread""" - - name = "singleAPI" + def __init__(self): + threading.Thread.__init__(self, name="singleAPI") + self.initStop() def stopThread(self): super(singleAPI, self).stopThread() @@ -96,8 +101,6 @@ class singleAPI(StoppableThread): 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) se = StoppableXMLRPCServer( (BMConfigParser().get( @@ -109,9 +112,8 @@ class singleAPI(StoppableThread): continue else: if attempt > 0: - logger.warning('Setting apiport to %s', port) BMConfigParser().set( - 'bitmessagesettings', 'apiport', str(port)) + "bitmessagesettings", "apiport", str(port)) BMConfigParser().save() break se.register_introspection_functions() @@ -137,11 +139,9 @@ class MySimpleXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): """ This is one of several classes that constitute the API - This class was written by Vaibhav Bhatia. - Modified by Jonathan Warren (Atheros). + 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/ """ - # pylint: disable=too-many-public-methods def do_POST(self): """ @@ -178,8 +178,7 @@ class MySimpleXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): # 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( + response = self.server._marshaled_dispatch( # pylint: disable=protected-access data, getattr(self, '_dispatch', None) ) except BaseException: # This should only happen if the module is buggy @@ -217,10 +216,8 @@ class MySimpleXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): _, encstr = self.headers.get('Authorization').split() emailid, password = encstr.decode('base64').split(':') return ( - emailid == BMConfigParser().get( - 'bitmessagesettings', 'apiusername') and - password == BMConfigParser().get( - 'bitmessagesettings', 'apipassword') + emailid == BMConfigParser().get('bitmessagesettings', 'apiusername') and + password == BMConfigParser().get('bitmessagesettings', 'apipassword') ) else: logger.warning( @@ -257,14 +254,10 @@ class MySimpleXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): if status == 'invalidcharacters': raise APIError(9, 'Invalid characters in address: ' + address) if status == 'versiontoohigh': - raise APIError( - 10, - 'Address version number too high (or zero) in address: ' + - address) + raise APIError(10, 'Address version number too high (or zero) in address: ' + address) if status == 'varintmalformed': raise APIError(26, 'Malformed varint in address: ' + address) - raise APIError( - 7, 'Could not decode address: %s : %s' % (address, status)) + raise APIError(7, 'Could not decode address: %s : %s' % (address, status)) if addressVersionNumber < 2 or addressVersionNumber > 4: raise APIError( 11, 'The address version number currently must be 2, 3 or 4.' @@ -282,9 +275,10 @@ class MySimpleXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): def HandleListAddresses(self, method): """Handle a request to list addresses""" + data = '{"addresses":[' for addressInKeysFile in BMConfigParser().addresses(): - status, addressVersionNumber, streamNumber, hash01 = decodeAddress( + status, addressVersionNumber, streamNumber, hash01 = decodeAddress( # pylint: disable=unused-variable addressInKeysFile) if len(data) > 20: data += ',' @@ -388,19 +382,16 @@ class MySimpleXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): elif len(params) == 3: label, eighteenByteRipe, totalDifficulty = params nonceTrialsPerByte = int( - defaults.networkDefaultProofOfWorkNonceTrialsPerByte * - totalDifficulty) + defaults.networkDefaultProofOfWorkNonceTrialsPerByte * totalDifficulty) payloadLengthExtraBytes = BMConfigParser().get( 'bitmessagesettings', 'defaultpayloadlengthextrabytes') elif len(params) == 4: label, eighteenByteRipe, totalDifficulty, \ smallMessageDifficulty = params nonceTrialsPerByte = int( - defaults.networkDefaultProofOfWorkNonceTrialsPerByte * - totalDifficulty) + defaults.networkDefaultProofOfWorkNonceTrialsPerByte * totalDifficulty) payloadLengthExtraBytes = int( - defaults.networkDefaultPayloadLengthExtraBytes * - smallMessageDifficulty) + defaults.networkDefaultPayloadLengthExtraBytes * smallMessageDifficulty) else: raise APIError(0, 'Too many parameters!') label = self._decode(label, "base64") @@ -418,7 +409,6 @@ class MySimpleXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): def HandleCreateDeterministicAddresses(self, params): """Handle a request to create a deterministic address""" - # pylint: disable=too-many-branches, too-many-statements if not params: raise APIError(0, 'I need parameters!') @@ -474,8 +464,7 @@ class MySimpleXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): passphrase, numberOfAddresses, addressVersionNumber, \ streamNumber, eighteenByteRipe, totalDifficulty = params nonceTrialsPerByte = int( - defaults.networkDefaultProofOfWorkNonceTrialsPerByte * - totalDifficulty) + defaults.networkDefaultProofOfWorkNonceTrialsPerByte * totalDifficulty) payloadLengthExtraBytes = BMConfigParser().get( 'bitmessagesettings', 'defaultpayloadlengthextrabytes') @@ -484,11 +473,9 @@ class MySimpleXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): streamNumber, eighteenByteRipe, totalDifficulty, \ smallMessageDifficulty = params nonceTrialsPerByte = int( - defaults.networkDefaultProofOfWorkNonceTrialsPerByte * - totalDifficulty) + defaults.networkDefaultProofOfWorkNonceTrialsPerByte * totalDifficulty) payloadLengthExtraBytes = int( - defaults.networkDefaultPayloadLengthExtraBytes * - smallMessageDifficulty) + defaults.networkDefaultPayloadLengthExtraBytes * smallMessageDifficulty) else: raise APIError(0, 'Too many parameters!') if not passphrase: @@ -622,8 +609,9 @@ class MySimpleXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): label = str_chan + ' ' + passphrase except BaseException: label = str_chan + ' ' + repr(passphrase) - status, addressVersionNumber, streamNumber, toRipe = ( - self._verifyAddress(suppliedAddress)) + + status, addressVersionNumber, streamNumber, toRipe = self._verifyAddress( # pylint: disable=unused-variable + suppliedAddress) suppliedAddress = addBMIfNotPresent(suppliedAddress) queues.apiAddressGeneratorReturnQueue.queue.clear() queues.addressGeneratorQueue.put(( @@ -646,8 +634,8 @@ class MySimpleXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): raise APIError(0, 'I need parameters.') elif len(params) == 1: address, = params - status, addressVersionNumber, streamNumber, toRipe = ( - self._verifyAddress(address)) + # pylint: disable=unused-variable + status, addressVersionNumber, streamNumber, toRipe = self._verifyAddress(address) address = addBMIfNotPresent(address) if not BMConfigParser().has_section(address): raise APIError( @@ -668,8 +656,8 @@ class MySimpleXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): raise APIError(0, 'I need parameters.') elif len(params) == 1: address, = params - status, addressVersionNumber, streamNumber, toRipe = ( - self._verifyAddress(address)) + # pylint: disable=unused-variable + status, addressVersionNumber, streamNumber, toRipe = self._verifyAddress(address) address = addBMIfNotPresent(address) if not BMConfigParser().has_section(address): raise APIError( @@ -681,7 +669,7 @@ class MySimpleXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): shared.reloadMyAddressHashes() return 'success' - def HandleGetAllInboxMessages(self, params): + def HandleGetAllInboxMessages(self, params): # pylint: disable=unused-argument """Handle a request to get all inbox messages""" queryreturn = sqlQuery( @@ -709,7 +697,7 @@ class MySimpleXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): data += ']}' return data - def HandleGetAllInboxMessageIds(self, params): + def HandleGetAllInboxMessageIds(self, params): # pylint: disable=unused-argument """Handle a request to get all inbox message IDs""" queryreturn = sqlQuery( @@ -768,7 +756,7 @@ class MySimpleXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): data += ']}' return data - def HandleGetAllSentMessages(self, params): + def HandleGetAllSentMessages(self, params): # pylint: disable=unused-argument """Handle a request to get all sent messages""" queryreturn = sqlQuery( @@ -797,7 +785,7 @@ class MySimpleXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): data += ']}' return data - def HandleGetAllSentMessageIds(self, params): + def HandleGetAllSentMessageIds(self, params): # pylint: disable=unused-argument """Handle a request to get all sent message IDs""" queryreturn = sqlQuery( @@ -888,7 +876,7 @@ class MySimpleXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): data = '{"sentMessages":[' for row in queryreturn: msgid, toAddress, fromAddress, subject, lastactiontime, message, \ - encodingtype, status, ackdata = row + encodingtype, status, ackdata = row # pylint: disable=unused-variable subject = shared.fixPotentiallyInvalidUTF8Data(subject) message = shared.fixPotentiallyInvalidUTF8Data(message) if len(data) > 25: @@ -967,7 +955,7 @@ class MySimpleXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): sqlExecute('''UPDATE sent SET folder='trash' WHERE msgid=?''', msgid) return 'Trashed sent message (assuming message existed).' - def HandleSendMessage(self, params): # pylint: disable=too-many-locals + def HandleSendMessage(self, params): """Handle a request to send a message""" if not params: @@ -998,6 +986,7 @@ class MySimpleXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): TTL = 28 * 24 * 60 * 60 toAddress = addBMIfNotPresent(toAddress) fromAddress = addBMIfNotPresent(fromAddress) + # pylint: disable=unused-variable status, addressVersionNumber, streamNumber, toRipe = \ self._verifyAddress(toAddress) self._verifyAddress(fromAddress) @@ -1171,9 +1160,10 @@ class MySimpleXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): queues.UISignalQueue.put(('rerenderSubscriptions', '')) return 'Deleted subscription if it existed.' - def ListSubscriptions(self, params): + def ListSubscriptions(self, params): # pylint: disable=unused-argument """Handle a request to list susbcriptions""" + # pylint: disable=unused-variable queryreturn = sqlQuery( "SELECT label, address, enabled FROM subscriptions") data = {'subscriptions': []} @@ -1208,15 +1198,12 @@ class MySimpleXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): ) with shared.printLock: print( - '(For msg message via API) Doing proof of work.' - 'Total required difficulty:', + '(For msg message via API) Doing proof of work. Total required difficulty:', float( requiredAverageProofOfWorkNonceTrialsPerByte ) / defaults.networkDefaultProofOfWorkNonceTrialsPerByte, 'Required small message difficulty:', - float( - requiredPayloadLengthExtraBytes - ) / defaults.networkDefaultPayloadLengthExtraBytes, + float(requiredPayloadLengthExtraBytes) / defaults.networkDefaultPayloadLengthExtraBytes, ) powStartTime = time.time() initialHash = hashlib.sha512(encryptedPayload).digest() @@ -1225,9 +1212,8 @@ class MySimpleXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): 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.', + 'POW took', int(time.time() - powStartTime), 'seconds.', + nonce / (time.time() - powStartTime), 'nonce trials per second.', ) except BaseException: pass @@ -1254,7 +1240,7 @@ class MySimpleXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): sqlExecute("UPDATE sent SET folder='trash' WHERE ackdata=?", ackdata) return 'Trashed sent message (assuming message existed).' - def HandleDissimatePubKey(self, params): + def HandleDissimatePubKey(self, params): # pylint: disable=unused-argument """Handle a request to disseminate a public key""" # The device issuing this command to PyBitmessage supplies a pubkey @@ -1283,6 +1269,7 @@ class MySimpleXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): pubkeyReadPosition += 8 else: pubkeyReadPosition += 4 + # pylint: disable=unused-variable addressVersion, addressVersionLength = decodeVarint( payload[pubkeyReadPosition:pubkeyReadPosition + 10]) pubkeyReadPosition += addressVersionLength @@ -1341,7 +1328,7 @@ class MySimpleXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): data += ']}' return data - def HandleClientStatus(self, params): + def HandleClientStatus(self, params): # pylint: disable=unused-argument """Handle a request to get the status of the client""" connections_num = len(network.stats.connectedHostsList()) diff --git a/src/bitmessagecli.py b/src/bitmessagecli.py index 01dbc9bb..02fed7e9 100644 --- a/src/bitmessagecli.py +++ b/src/bitmessagecli.py @@ -13,15 +13,15 @@ TODO: fix the following (currently ignored) violations: """ +import xmlrpclib import datetime import imghdr -import json import ntpath -import os +import json import socket -import sys import time -import xmlrpclib +import sys +import os from bmconfigparser import BMConfigParser diff --git a/src/bitmessagecurses/__init__.py b/src/bitmessagecurses/__init__.py index d8daeef7..dc6e8a0c 100644 --- a/src/bitmessagecurses/__init__.py +++ b/src/bitmessagecurses/__init__.py @@ -1,39 +1,40 @@ -""" -Bitmessage commandline interface -""" # Copyright (c) 2014 Luke Montalvo # This file adds a alternative commandline interface, feel free to critique and fork -# +# # This has only been tested on Arch Linux and Linux Mint # Dependencies: # * from python2-pip # * python2-pythondialog # * dialog -import ConfigParser -import curses import os import sys +import StringIO +from textwrap import * + import time -from textwrap import fill +from time import strftime, localtime from threading import Timer +import curses +import dialog from dialog import Dialog +from helper_sql import * +from helper_ackPayload import genAckPayload + +from addresses import * +import ConfigParser +from bmconfigparser import BMConfigParser +from inventory import Inventory import l10n -import network.stats +from pyelliptic.openssl import OpenSSL import queues import shared import shutdown - -from addresses import addBMIfNotPresent, decodeAddress -from bmconfigparser import BMConfigParser -from helper_ackPayload import genAckPayload -from helper_sql import sqlExecute, sqlQuery -from inventory import Inventory -# pylint: disable=global-statement +import network.stats -quit_ = False +quit = False menutab = 1 menu = ["Inbox", "Send", "Sent", "Your Identities", "Subscriptions", "Address Book", "Blacklist", "Network Status"] naptime = 100 @@ -59,195 +60,156 @@ bwtype = "black" BROADCAST_STR = "[Broadcast subscribers]" - -class printLog(object): - """Printing logs""" - # pylint: disable=no-self-use - +class printLog: def write(self, output): - """Write logs""" global log log += output - def flush(self): - """Flush logs""" pass - - -class errLog(object): - """Error logs""" - # pylint: disable=no-self-use - +class errLog: def write(self, output): - """Write error logs""" global log - log += "!" + output - + log += "!"+output def flush(self): - """Flush error logs""" pass - - printlog = printLog() errlog = errLog() def cpair(a): - """Color pairs""" r = curses.color_pair(a) - if r not in range(1, curses.COLOR_PAIRS - 1): + if r not in range(1, curses.COLOR_PAIRS-1): r = curses.color_pair(0) return r - - def ascii(s): - """ASCII values""" r = "" for c in s: if ord(c) in range(128): r += c return r - def drawmenu(stdscr): - """Creating menu's""" menustr = " " - for i, _ in enumerate(menu): - if menutab == i + 1: + for i in range(0, len(menu)): + if menutab == i+1: menustr = menustr[:-1] menustr += "[" - menustr += str(i + 1) + menu[i] - if menutab == i + 1: + menustr += str(i+1)+menu[i] + if menutab == i+1: menustr += "] " - elif i != len(menu) - 1: + elif i != len(menu)-1: menustr += " " stdscr.addstr(2, 5, menustr, curses.A_UNDERLINE) - def set_background_title(d, title): - """Setting background title""" try: d.set_background_title(title) except: d.add_persistent_args(("--backtitle", title)) - def scrollbox(d, text, height=None, width=None): - """Setting scroll box""" try: - d.scrollbox(text, height, width, exit_label="Continue") + d.scrollbox(text, height, width, exit_label = "Continue") except: - d.msgbox(text, height or 0, width or 0, ok_label="Continue") - + d.msgbox(text, height or 0, width or 0, ok_label = "Continue") def resetlookups(): - """Reset the Inventory Lookups""" global inventorydata inventorydata = Inventory().numberOfInventoryLookupsPerformed Inventory().numberOfInventoryLookupsPerformed = 0 Timer(1, resetlookups, ()).start() - - def drawtab(stdscr): - """Method for drawing different tabs""" - # pylint: disable=too-many-branches, too-many-statements - if menutab in range(1, len(menu) + 1): - if menutab == 1: # Inbox + if menutab in range(1, len(menu)+1): + if menutab == 1: # Inbox stdscr.addstr(3, 5, "To", curses.A_BOLD) stdscr.addstr(3, 40, "From", curses.A_BOLD) stdscr.addstr(3, 80, "Subject", curses.A_BOLD) stdscr.addstr(3, 120, "Time Received", curses.A_BOLD) stdscr.hline(4, 5, '-', 121) - for i, item in enumerate(inbox[max(min(len(inbox) - curses.LINES + 6, inboxcur - 5), 0):]): - if 6 + i < curses.LINES: + for i, item in enumerate(inbox[max(min(len(inbox)-curses.LINES+6, inboxcur-5), 0):]): + if 6+i < curses.LINES: a = 0 - if i == inboxcur - max(min(len(inbox) - curses.LINES + 6, inboxcur - 5), 0): - # Highlight current address + if i == inboxcur - max(min(len(inbox)-curses.LINES+6, inboxcur-5), 0): # Highlight current address a = a | curses.A_REVERSE - if item[7] is False: # If not read, highlight + if item[7] == False: # If not read, highlight a = a | curses.A_BOLD - stdscr.addstr(5 + i, 5, item[1][:34], a) - stdscr.addstr(5 + i, 40, item[3][:39], a) - stdscr.addstr(5 + i, 80, item[5][:39], a) - stdscr.addstr(5 + i, 120, item[6][:39], a) - elif menutab == 3: # Sent + stdscr.addstr(5+i, 5, item[1][:34], a) + stdscr.addstr(5+i, 40, item[3][:39], a) + stdscr.addstr(5+i, 80, item[5][:39], a) + stdscr.addstr(5+i, 120, item[6][:39], a) + elif menutab == 3: # Sent stdscr.addstr(3, 5, "To", curses.A_BOLD) stdscr.addstr(3, 40, "From", curses.A_BOLD) stdscr.addstr(3, 80, "Subject", curses.A_BOLD) stdscr.addstr(3, 120, "Status", curses.A_BOLD) stdscr.hline(4, 5, '-', 121) - for i, item in enumerate(sentbox[max(min(len(sentbox) - curses.LINES + 6, sentcur - 5), 0):]): - if 6 + i < curses.LINES: + for i, item in enumerate(sentbox[max(min(len(sentbox)-curses.LINES+6, sentcur-5), 0):]): + if 6+i < curses.LINES: a = 0 - if i == sentcur - max(min(len(sentbox) - curses.LINES + 6, sentcur - 5), 0): - # Highlight current address + if i == sentcur - max(min(len(sentbox)-curses.LINES+6, sentcur-5), 0): # Highlight current address a = a | curses.A_REVERSE - stdscr.addstr(5 + i, 5, item[0][:34], a) - stdscr.addstr(5 + i, 40, item[2][:39], a) - stdscr.addstr(5 + i, 80, item[4][:39], a) - stdscr.addstr(5 + i, 120, item[5][:39], a) - elif menutab == 2 or menutab == 4: # Send or Identities + stdscr.addstr(5+i, 5, item[0][:34], a) + stdscr.addstr(5+i, 40, item[2][:39], a) + stdscr.addstr(5+i, 80, item[4][:39], a) + stdscr.addstr(5+i, 120, item[5][:39], a) + elif menutab == 2 or menutab == 4: # Send or Identities stdscr.addstr(3, 5, "Label", curses.A_BOLD) stdscr.addstr(3, 40, "Address", curses.A_BOLD) stdscr.addstr(3, 80, "Stream", curses.A_BOLD) stdscr.hline(4, 5, '-', 81) - for i, item in enumerate(addresses[max(min(len(addresses) - curses.LINES + 6, addrcur - 5), 0):]): - if 6 + i < curses.LINES: + for i, item in enumerate(addresses[max(min(len(addresses)-curses.LINES+6, addrcur-5), 0):]): + if 6+i < curses.LINES: a = 0 - if i == addrcur - max(min(len(addresses) - curses.LINES + 6, addrcur - 5), 0): - # Highlight current address + if i == addrcur - max(min(len(addresses)-curses.LINES+6, addrcur-5), 0): # Highlight current address a = a | curses.A_REVERSE - if item[1] and item[3] not in [8, 9]: # Embolden enabled, non-special addresses + if item[1] == True and item[3] not in [8,9]: # Embolden enabled, non-special addresses a = a | curses.A_BOLD - stdscr.addstr(5 + i, 5, item[0][:34], a) - stdscr.addstr(5 + i, 40, item[2][:39], cpair(item[3]) | a) - stdscr.addstr(5 + i, 80, str(1)[:39], a) - elif menutab == 5: # Subscriptions + stdscr.addstr(5+i, 5, item[0][:34], a) + stdscr.addstr(5+i, 40, item[2][:39], cpair(item[3]) | a) + stdscr.addstr(5+i, 80, str(1)[:39], a) + elif menutab == 5: # Subscriptions stdscr.addstr(3, 5, "Label", curses.A_BOLD) stdscr.addstr(3, 80, "Address", curses.A_BOLD) stdscr.addstr(3, 120, "Enabled", curses.A_BOLD) stdscr.hline(4, 5, '-', 121) - for i, item in enumerate(subscriptions[max(min(len(subscriptions) - curses.LINES + 6, subcur - 5), 0):]): - if 6 + i < curses.LINES: + for i, item in enumerate(subscriptions[max(min(len(subscriptions)-curses.LINES+6, subcur-5), 0):]): + if 6+i < curses.LINES: a = 0 - if i == subcur - max(min(len(subscriptions) - curses.LINES + 6, subcur - 5), 0): - # Highlight current address + if i == subcur - max(min(len(subscriptions)-curses.LINES+6, subcur-5), 0): # Highlight current address a = a | curses.A_REVERSE - if item[2]: # Embolden enabled subscriptions + if item[2] == True: # Embolden enabled subscriptions a = a | curses.A_BOLD - stdscr.addstr(5 + i, 5, item[0][:74], a) - stdscr.addstr(5 + i, 80, item[1][:39], a) - stdscr.addstr(5 + i, 120, str(item[2]), a) - elif menutab == 6: # Address book + stdscr.addstr(5+i, 5, item[0][:74], a) + stdscr.addstr(5+i, 80, item[1][:39], a) + stdscr.addstr(5+i, 120, str(item[2]), a) + elif menutab == 6: # Address book stdscr.addstr(3, 5, "Label", curses.A_BOLD) stdscr.addstr(3, 40, "Address", curses.A_BOLD) stdscr.hline(4, 5, '-', 41) - for i, item in enumerate(addrbook[max(min(len(addrbook) - curses.LINES + 6, abookcur - 5), 0):]): - if 6 + i < curses.LINES: + for i, item in enumerate(addrbook[max(min(len(addrbook)-curses.LINES+6, abookcur-5), 0):]): + if 6+i < curses.LINES: a = 0 - if i == abookcur - max(min(len(addrbook) - curses.LINES + 6, abookcur - 5), 0): - # Highlight current address + if i == abookcur - max(min(len(addrbook)-curses.LINES+6, abookcur-5), 0): # Highlight current address a = a | curses.A_REVERSE - stdscr.addstr(5 + i, 5, item[0][:34], a) - stdscr.addstr(5 + i, 40, item[1][:39], a) - elif menutab == 7: # Blacklist - stdscr.addstr(3, 5, "Type: " + bwtype) + stdscr.addstr(5+i, 5, item[0][:34], a) + stdscr.addstr(5+i, 40, item[1][:39], a) + elif menutab == 7: # Blacklist + stdscr.addstr(3, 5, "Type: "+bwtype) stdscr.addstr(4, 5, "Label", curses.A_BOLD) stdscr.addstr(4, 80, "Address", curses.A_BOLD) stdscr.addstr(4, 120, "Enabled", curses.A_BOLD) stdscr.hline(5, 5, '-', 121) - for i, item in enumerate(blacklist[max(min(len(blacklist) - curses.LINES + 6, blackcur - 5), 0):]): - if 7 + i < curses.LINES: + for i, item in enumerate(blacklist[max(min(len(blacklist)-curses.LINES+6, blackcur-5), 0):]): + if 7+i < curses.LINES: a = 0 - if i == blackcur - max(min(len(blacklist) - curses.LINES + 6, blackcur - 5), 0): - # Highlight current address + if i == blackcur - max(min(len(blacklist)-curses.LINES+6, blackcur-5), 0): # Highlight current address a = a | curses.A_REVERSE - if item[2]: # Embolden enabled subscriptions + if item[2] == True: # Embolden enabled subscriptions a = a | curses.A_BOLD - stdscr.addstr(6 + i, 5, item[0][:74], a) - stdscr.addstr(6 + i, 80, item[1][:39], a) - stdscr.addstr(6 + i, 120, str(item[2]), a) - elif menutab == 8: # Network status + stdscr.addstr(6+i, 5, item[0][:74], a) + stdscr.addstr(6+i, 80, item[1][:39], a) + stdscr.addstr(6+i, 120, str(item[2]), a) + elif menutab == 8: # Network status # Connection data connected_hosts = network.stats.connectedHostsList() stdscr.addstr( @@ -266,96 +228,73 @@ def drawtab(stdscr): for i, item in enumerate(streamcount): if i < 4: if i == 0: - stdscr.addstr(8 + i, 6, "?") + stdscr.addstr(8+i, 6, "?") else: - stdscr.addstr(8 + i, 6, str(i)) - stdscr.addstr(8 + i, 18, str(item).ljust(2)) - + stdscr.addstr(8+i, 6, str(i)) + stdscr.addstr(8+i, 18, str(item).ljust(2)) + # Uptime and processing data - stdscr.addstr(6, 35, "Since startup on " + l10n.formatTimestamp(startuptime, False)) - stdscr.addstr(7, 40, "Processed " + str( - shared.numberOfMessagesProcessed).ljust(4) + " person-to-person messages.") - stdscr.addstr(8, 40, "Processed " + str( - shared.numberOfBroadcastsProcessed).ljust(4) + " broadcast messages.") - stdscr.addstr(9, 40, "Processed " + str( - shared.numberOfPubkeysProcessed).ljust(4) + " public keys.") - + stdscr.addstr(6, 35, "Since startup on "+l10n.formatTimestamp(startuptime, False)) + stdscr.addstr(7, 40, "Processed "+str(shared.numberOfMessagesProcessed).ljust(4)+" person-to-person messages.") + stdscr.addstr(8, 40, "Processed "+str(shared.numberOfBroadcastsProcessed).ljust(4)+" broadcast messages.") + stdscr.addstr(9, 40, "Processed "+str(shared.numberOfPubkeysProcessed).ljust(4)+" public keys.") + # Inventory data - stdscr.addstr(11, 35, "Inventory lookups per second: " + str(inventorydata).ljust(3)) - + stdscr.addstr(11, 35, "Inventory lookups per second: "+str(inventorydata).ljust(3)) + # Log stdscr.addstr(13, 6, "Log", curses.A_BOLD) n = log.count('\n') if n > 0: - lg = log.split('\n') + l = log.split('\n') if n > 512: - del lg[:(n - 256)] + del l[:(n-256)] logpad.erase() - n = len(lg) - for i, item in enumerate(lg): + n = len(l) + for i, item in enumerate(l): a = 0 - if item and item[0] == '!': + if len(item) > 0 and item[0] == '!': a = curses.color_pair(1) item = item[1:] logpad.addstr(i, 0, item, a) - logpad.refresh(n - curses.LINES + 2, 0, 14, 6, curses.LINES - 2, curses.COLS - 7) + logpad.refresh(n-curses.LINES+2, 0, 14, 6, curses.LINES-2, curses.COLS-7) stdscr.refresh() - def redraw(stdscr): - """Redraw menu""" stdscr.erase() stdscr.border() drawmenu(stdscr) stdscr.refresh() - - def dialogreset(stdscr): - """Resetting dialogue""" stdscr.clear() stdscr.keypad(1) curses.curs_set(0) - - -# pylint: disable=too-many-branches, too-many-statements def handlech(c, stdscr): - """Handle character given on the command-line interface""" - # pylint: disable=redefined-outer-name, too-many-nested-blocks, too-many-locals if c != curses.ERR: global inboxcur, addrcur, sentcur, subcur, abookcur, blackcur - if c in range(256): + if c in range(256): if chr(c) in '12345678': global menutab menutab = int(chr(c)) elif chr(c) == 'q': - global quit_ - quit_ = True + global quit + quit = True elif chr(c) == '\n': curses.curs_set(1) d = Dialog(dialog="dialog") if menutab == 1: set_background_title(d, "Inbox Message Dialog Box") - r, t = d.menu( - "Do what with \"" + inbox[inboxcur][5] + "\" from \"" + inbox[inboxcur][3] + "\"?", - choices=[ - ("1", "View message"), + r, t = d.menu("Do what with \""+inbox[inboxcur][5]+"\" from \""+inbox[inboxcur][3]+"\"?", + choices=[("1", "View message"), ("2", "Mark message as unread"), ("3", "Reply"), ("4", "Add sender to Address Book"), ("5", "Save message as text file"), ("6", "Move to trash")]) if r == d.DIALOG_OK: - if t == "1": # View - set_background_title( - d, - "\"" + - inbox[inboxcur][5] + - "\" from \"" + - inbox[inboxcur][3] + - "\" to \"" + - inbox[inboxcur][1] + - "\"") - data = "" # pyint: disable=redefined-outer-name + if t == "1": # View + set_background_title(d, "\""+inbox[inboxcur][5]+"\" from \""+inbox[inboxcur][3]+"\" to \""+inbox[inboxcur][1]+"\"") + data = "" ret = sqlQuery("SELECT message FROM inbox WHERE msgid=?", inbox[inboxcur][0]) if ret != []: for row in ret: @@ -363,16 +302,16 @@ def handlech(c, stdscr): data = shared.fixPotentiallyInvalidUTF8Data(data) msg = "" for i, item in enumerate(data.split("\n")): - msg += fill(item, replace_whitespace=False) + "\n" + msg += fill(item, replace_whitespace=False)+"\n" scrollbox(d, unicode(ascii(msg)), 30, 80) sqlExecute("UPDATE inbox SET read=1 WHERE msgid=?", inbox[inboxcur][0]) inbox[inboxcur][7] = 1 else: scrollbox(d, unicode("Could not fetch message.")) - elif t == "2": # Mark unread + elif t == "2": # Mark unread sqlExecute("UPDATE inbox SET read=0 WHERE msgid=?", inbox[inboxcur][0]) inbox[inboxcur][7] = 0 - elif t == "3": # Reply + elif t == "3": # Reply curses.curs_set(1) m = inbox[inboxcur] fromaddr = m[4] @@ -381,31 +320,29 @@ def handlech(c, stdscr): if fromaddr == item[2] and item[3] != 0: ischan = True break - if not addresses[i][1]: # pylint: disable=undefined-loop-variable - scrollbox(d, unicode( - "Sending address disabled, please either enable it" - "or choose a different address.")) + if not addresses[i][1]: + scrollbox(d, unicode("Sending address disabled, please either enable it or choose a different address.")) return toaddr = m[2] if ischan: toaddr = fromaddr - + subject = m[5] if not m[5][:4] == "Re: ": - subject = "Re: " + m[5] + subject = "Re: "+m[5] body = "" ret = sqlQuery("SELECT message FROM inbox WHERE msgid=?", m[0]) if ret != []: body = "\n\n------------------------------------------------------\n" for row in ret: body, = row - + sendMessage(fromaddr, toaddr, ischan, subject, body, True) dialogreset(stdscr) - elif t == "4": # Add to Address Book + elif t == "4": # Add to Address Book addr = inbox[inboxcur][4] - if addr not in [item[1] for i, item in enumerate(addrbook)]: - r, t = d.inputbox("Label for address \"" + addr + "\"") + if addr not in [item[1] for i,item in enumerate(addrbook)]: + r, t = d.inputbox("Label for address \""+addr+"\"") if r == d.DIALOG_OK: label = t sqlExecute("INSERT INTO addressbook VALUES (?,?)", label, addr) @@ -415,85 +352,61 @@ def handlech(c, stdscr): addrbook.reverse() else: scrollbox(d, unicode("The selected address is already in the Address Book.")) - elif t == "5": # Save message - set_background_title(d, "Save \"" + inbox[inboxcur][5] + "\" as text file") - r, t = d.inputbox("Filename", init=inbox[inboxcur][5] + ".txt") + elif t == "5": # Save message + set_background_title(d, "Save \""+inbox[inboxcur][5]+"\" as text file") + r, t = d.inputbox("Filename", init=inbox[inboxcur][5]+".txt") if r == d.DIALOG_OK: msg = "" ret = sqlQuery("SELECT message FROM inbox WHERE msgid=?", inbox[inboxcur][0]) if ret != []: for row in ret: msg, = row - fh = open(t, "a") # Open in append mode just in case + fh = open(t, "a") # Open in append mode just in case fh.write(msg) fh.close() else: scrollbox(d, unicode("Could not fetch message.")) - elif t == "6": # Move to trash + elif t == "6": # Move to trash sqlExecute("UPDATE inbox SET folder='trash' WHERE msgid=?", inbox[inboxcur][0]) del inbox[inboxcur] - scrollbox(d, unicode( - "Message moved to trash. There is no interface to view your trash," - " \nbut the message is still on disk if you are desperate to recover it.")) + scrollbox(d, unicode("Message moved to trash. There is no interface to view your trash, \nbut the message is still on disk if you are desperate to recover it.")) elif menutab == 2: a = "" - if addresses[addrcur][3] != 0: # if current address is a chan + if addresses[addrcur][3] != 0: # if current address is a chan a = addresses[addrcur][2] sendMessage(addresses[addrcur][2], a) elif menutab == 3: set_background_title(d, "Sent Messages Dialog Box") - r, t = d.menu( - "Do what with \"" + sentbox[sentcur][4] + "\" to \"" + sentbox[sentcur][0] + "\"?", - choices=[ - ("1", "View message"), + r, t = d.menu("Do what with \""+sentbox[sentcur][4]+"\" to \""+sentbox[sentcur][0]+"\"?", + choices=[("1", "View message"), ("2", "Move to trash")]) if r == d.DIALOG_OK: - if t == "1": # View - set_background_title( - d, - "\"" + - sentbox[sentcur][4] + - "\" from \"" + - sentbox[sentcur][3] + - "\" to \"" + - sentbox[sentcur][1] + - "\"") + if t == "1": # View + set_background_title(d, "\""+sentbox[sentcur][4]+"\" from \""+sentbox[sentcur][3]+"\" to \""+sentbox[sentcur][1]+"\"") data = "" - ret = sqlQuery( - "SELECT message FROM sent WHERE subject=? AND ackdata=?", - sentbox[sentcur][4], - sentbox[sentcur][6]) + ret = sqlQuery("SELECT message FROM sent WHERE subject=? AND ackdata=?", sentbox[sentcur][4], sentbox[sentcur][6]) if ret != []: for row in ret: data, = row data = shared.fixPotentiallyInvalidUTF8Data(data) msg = "" for i, item in enumerate(data.split("\n")): - msg += fill(item, replace_whitespace=False) + "\n" + msg += fill(item, replace_whitespace=False)+"\n" scrollbox(d, unicode(ascii(msg)), 30, 80) else: scrollbox(d, unicode("Could not fetch message.")) - elif t == "2": # Move to trash - sqlExecute( - "UPDATE sent SET folder='trash' WHERE subject=? AND ackdata=?", - sentbox[sentcur][4], - sentbox[sentcur][6]) + elif t == "2": # Move to trash + sqlExecute("UPDATE sent SET folder='trash' WHERE subject=? AND ackdata=?", sentbox[sentcur][4], sentbox[sentcur][6]) del sentbox[sentcur] - scrollbox(d, unicode( - "Message moved to trash. There is no interface to view your trash" - " \nbut the message is still on disk if you are desperate to recover it.")) + scrollbox(d, unicode("Message moved to trash. There is no interface to view your trash, \nbut the message is still on disk if you are desperate to recover it.")) elif menutab == 4: set_background_title(d, "Your Identities Dialog Box") if len(addresses) <= addrcur: - r, t = d.menu( - "Do what with addresses?", - choices=[ - ("1", "Create new address")]) + r, t = d.menu("Do what with addresses?", + choices=[("1", "Create new address")]) else: - r, t = d.menu( - "Do what with \"" + addresses[addrcur][0] + "\" : \"" + addresses[addrcur][2] + "\"?", - choices=[ - ("1", "Create new address"), + r, t = d.menu("Do what with \""+addresses[addrcur][0]+"\" : \""+addresses[addrcur][2]+"\"?", + choices=[("1", "Create new address"), ("2", "Send a message from this address"), ("3", "Rename"), ("4", "Enable"), @@ -501,41 +414,31 @@ def handlech(c, stdscr): ("6", "Delete"), ("7", "Special address behavior")]) if r == d.DIALOG_OK: - if t == "1": # Create new address + if t == "1": # Create new address set_background_title(d, "Create new address") - scrollbox( - d, unicode( - "Here you may generate as many addresses as you like.\n" - "Indeed, creating and abandoning addresses is encouraged.\n" - "Deterministic addresses have several pros and cons:\n" - "\nPros:\n" - " * You can recreate your addresses on any computer from memory\n" - " * You need not worry about backing up your keys.dat file as long as you" - " \n can remember your passphrase\n" - "Cons:\n" - " * You must remember (or write down) your passphrase in order to recreate" - " \n your keys if they are lost\n" - " * You must also remember the address version and stream numbers\n" - " * If you choose a weak passphrase someone may be able to brute-force it" - " \n and then send and receive messages as you")) - r, t = d.menu( - "Choose an address generation technique", - choices=[ - ("1", "Use a random number generator"), + scrollbox(d, unicode("Here you may generate as many addresses as you like.\n" + "Indeed, creating and abandoning addresses is encouraged.\n" + "Deterministic addresses have several pros and cons:\n" + "\nPros:\n" + " * You can recreate your addresses on any computer from memory\n" + " * You need not worry about backing up your keys.dat file as long as you \n can remember your passphrase\n" + "Cons:\n" + " * You must remember (or write down) your passphrase in order to recreate \n your keys if they are lost\n" + " * You must also remember the address version and stream numbers\n" + " * If you choose a weak passphrase someone may be able to brute-force it \n and then send and receive messages as you")) + r, t = d.menu("Choose an address generation technique", + choices=[("1", "Use a random number generator"), ("2", "Use a passphrase")]) if r == d.DIALOG_OK: if t == "1": set_background_title(d, "Randomly generate address") r, t = d.inputbox("Label (not shown to anyone except you)") label = "" - if r == d.DIALOG_OK and t: + if r == d.DIALOG_OK and len(t) > 0: label = t - r, t = d.menu( - "Choose a stream", - choices=[("1", "Use the most available stream"), - ("", "(Best if this is the first of many addresses you will create)"), - ("2", "Use the same stream as an existing address"), - ("", "(Saves you some bandwidth and processing power)")]) + r, t = d.menu("Choose a stream", + choices=[("1", "Use the most available stream"),("", "(Best if this is the first of many addresses you will create)"), + ("2", "Use the same stream as an existing address"),("", "(Saves you some bandwidth and processing power)")]) if r == d.DIALOG_OK: if t == "1": stream = 1 @@ -547,69 +450,42 @@ def handlech(c, stdscr): if r == d.DIALOG_OK: stream = decodeAddress(addrs[int(t)][1])[2] shorten = False - r, t = d.checklist( - "Miscellaneous options", - choices=[( - "1", - "Spend time shortening the address", - 1 if shorten else 0)]) + r, t = d.checklist("Miscellaneous options", + choices=[("1", "Spend time shortening the address", 1 if shorten else 0)]) if r == d.DIALOG_OK and "1" in t: shorten = True - queues.addressGeneratorQueue.put(( - "createRandomAddress", - 4, - stream, - label, - 1, - "", - shorten)) + queues.addressGeneratorQueue.put(("createRandomAddress", 4, stream, label, 1, "", shorten)) elif t == "2": set_background_title(d, "Make deterministic addresses") - r, t = d.passwordform( - "Enter passphrase", - [ - ("Passphrase", 1, 1, "", 2, 1, 64, 128), - ("Confirm passphrase", 3, 1, "", 4, 1, 64, 128)], + r, t = d.passwordform("Enter passphrase", + [("Passphrase", 1, 1, "", 2, 1, 64, 128), + ("Confirm passphrase", 3, 1, "", 4, 1, 64, 128)], form_height=4, insecure=True) if r == d.DIALOG_OK: if t[0] == t[1]: passphrase = t[0] - r, t = d.rangebox( - "Number of addresses to generate", - width=48, - min=1, - max=99, - init=8) + r, t = d.rangebox("Number of addresses to generate", + width=48, min=1, max=99, init=8) if r == d.DIALOG_OK: number = t stream = 1 shorten = False - r, t = d.checklist( - "Miscellaneous options", - choices=[( - "1", - "Spend time shortening the address", - 1 if shorten else 0)]) + r, t = d.checklist("Miscellaneous options", + choices=[("1", "Spend time shortening the address", 1 if shorten else 0)]) if r == d.DIALOG_OK and "1" in t: shorten = True - scrollbox( - d, unicode( - "In addition to your passphrase, be sure to remember the" - " following numbers:\n" - "\n * Address version number: " + str(4) + "\n" - " * Stream number: " + str(stream))) - queues.addressGeneratorQueue.put( - ('createDeterministicAddresses', 4, stream, - "unused deterministic address", number, - str(passphrase), shorten)) + scrollbox(d, unicode("In addition to your passphrase, be sure to remember the following numbers:\n" + "\n * Address version number: "+str(4)+"\n" + " * Stream number: "+str(stream))) + queues.addressGeneratorQueue.put(('createDeterministicAddresses', 4, stream, "unused deterministic address", number, str(passphrase), shorten)) else: scrollbox(d, unicode("Passphrases do not match")) - elif t == "2": # Send a message + elif t == "2": # Send a message a = "" - if addresses[addrcur][3] != 0: # if current address is a chan + if addresses[addrcur][3] != 0: # if current address is a chan a = addresses[addrcur][2] sendMessage(addresses[addrcur][2], a) - elif t == "3": # Rename address label + elif t == "3": # Rename address label a = addresses[addrcur][2] label = addresses[addrcur][0] r, t = d.inputbox("New address label", init=label) @@ -619,79 +495,72 @@ def handlech(c, stdscr): # Write config BMConfigParser().save() addresses[addrcur][0] = label - elif t == "4": # Enable address + elif t == "4": # Enable address a = addresses[addrcur][2] - BMConfigParser().set(a, "enabled", "true") # Set config + BMConfigParser().set(a, "enabled", "true") # Set config # Write config BMConfigParser().save() # Change color if BMConfigParser().safeGetBoolean(a, 'chan'): - addresses[addrcur][3] = 9 # orange + addresses[addrcur][3] = 9 # orange elif BMConfigParser().safeGetBoolean(a, 'mailinglist'): - addresses[addrcur][3] = 5 # magenta + addresses[addrcur][3] = 5 # magenta else: - addresses[addrcur][3] = 0 # black + addresses[addrcur][3] = 0 # black addresses[addrcur][1] = True - shared.reloadMyAddressHashes() # Reload address hashes - elif t == "5": # Disable address + shared.reloadMyAddressHashes() # Reload address hashes + elif t == "5": # Disable address a = addresses[addrcur][2] - BMConfigParser().set(a, "enabled", "false") # Set config - addresses[addrcur][3] = 8 # Set color to gray + BMConfigParser().set(a, "enabled", "false") # Set config + addresses[addrcur][3] = 8 # Set color to gray # Write config BMConfigParser().save() addresses[addrcur][1] = False - shared.reloadMyAddressHashes() # Reload address hashes - elif t == "6": # Delete address + shared.reloadMyAddressHashes() # Reload address hashes + elif t == "6": # Delete address r, t = d.inputbox("Type in \"I want to delete this address\"", width=50) if r == d.DIALOG_OK and t == "I want to delete this address": - BMConfigParser().remove_section(addresses[addrcur][2]) - BMConfigParser().save() - del addresses[addrcur] - elif t == "7": # Special address behavior + 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 BMConfigParser().safeGetBoolean(a, "chan"): - scrollbox(d, unicode( - "This is a chan address. You cannot use it as a pseudo-mailing list.")) + scrollbox(d, unicode("This is a chan address. You cannot use it as a pseudo-mailing list.")) else: m = BMConfigParser().safeGetBoolean(a, "mailinglist") - r, t = d.radiolist( - "Select address behavior", - choices=[ - ("1", "Behave as a normal address", not m), + r, t = d.radiolist("Select address behavior", + choices=[("1", "Behave as a normal address", not m), ("2", "Behave as a pseudo-mailing-list address", m)]) if r == d.DIALOG_OK: - if t == "1" and m: + if t == "1" and m == True: BMConfigParser().set(a, "mailinglist", "false") if addresses[addrcur][1]: - addresses[addrcur][3] = 0 # Set color to black + addresses[addrcur][3] = 0 # Set color to black else: - addresses[addrcur][3] = 8 # Set color to gray - elif t == "2" and m is False: + addresses[addrcur][3] = 8 # Set color to gray + elif t == "2" and m == False: try: mn = BMConfigParser().get(a, "mailinglistname") except ConfigParser.NoOptionError: - mn = "" + mn = "" r, t = d.inputbox("Mailing list name", init=mn) if r == d.DIALOG_OK: mn = t BMConfigParser().set(a, "mailinglist", "true") BMConfigParser().set(a, "mailinglistname", mn) - addresses[addrcur][3] = 6 # Set color to magenta + addresses[addrcur][3] = 6 # Set color to magenta # Write config BMConfigParser().save() elif menutab == 5: set_background_title(d, "Subscriptions Dialog Box") if len(subscriptions) <= subcur: - r, t = d.menu( - "Do what with subscription to \"" + subscriptions[subcur][0] + "\"?", - choices=[ - ("1", "Add new subscription")]) + r, t = d.menu("Do what with subscription to \""+subscriptions[subcur][0]+"\"?", + choices=[("1", "Add new subscription")]) else: - r, t = d.menu( - "Do what with subscription to \"" + subscriptions[subcur][0] + "\"?", - choices=[ - ("1", "Add new subscription"), + r, t = d.menu("Do what with subscription to \""+subscriptions[subcur][0]+"\"?", + choices=[("1", "Add new subscription"), ("2", "Delete this subscription"), ("3", "Enable"), ("4", "Disable")]) @@ -712,39 +581,27 @@ def handlech(c, stdscr): sqlExecute("INSERT INTO subscriptions VALUES (?,?,?)", label, addr, True) shared.reloadBroadcastSendersForWhichImWatching() elif t == "2": - r, t = d.inputbox("Type in \"I want to delete this subscription\"") + r, t = d.inpuxbox("Type in \"I want to delete this subscription\"") if r == d.DIALOG_OK and t == "I want to delete this subscription": - sqlExecute( - "DELETE FROM subscriptions WHERE label=? AND address=?", - subscriptions[subcur][0], - subscriptions[subcur][1]) - shared.reloadBroadcastSendersForWhichImWatching() - del subscriptions[subcur] + sqlExecute("DELETE FROM subscriptions WHERE label=? AND address=?", subscriptions[subcur][0], subscriptions[subcur][1]) + shared.reloadBroadcastSendersForWhichImWatching() + del subscriptions[subcur] elif t == "3": - sqlExecute( - "UPDATE subscriptions SET enabled=1 WHERE label=? AND address=?", - subscriptions[subcur][0], - subscriptions[subcur][1]) + sqlExecute("UPDATE subscriptions SET enabled=1 WHERE label=? AND address=?", subscriptions[subcur][0], subscriptions[subcur][1]) shared.reloadBroadcastSendersForWhichImWatching() subscriptions[subcur][2] = True elif t == "4": - sqlExecute( - "UPDATE subscriptions SET enabled=0 WHERE label=? AND address=?", - subscriptions[subcur][0], - subscriptions[subcur][1]) + sqlExecute("UPDATE subscriptions SET enabled=0 WHERE label=? AND address=?", subscriptions[subcur][0], subscriptions[subcur][1]) shared.reloadBroadcastSendersForWhichImWatching() subscriptions[subcur][2] = False elif menutab == 6: set_background_title(d, "Address Book Dialog Box") if len(addrbook) <= abookcur: - r, t = d.menu( - "Do what with addressbook?", + r, t = d.menu("Do what with addressbook?", choices=[("3", "Add new address to Address Book")]) else: - r, t = d.menu( - "Do what with \"" + addrbook[abookcur][0] + "\" : \"" + addrbook[abookcur][1] + "\"", - choices=[ - ("1", "Send a message to this address"), + r, t = d.menu("Do what with \""+addrbook[abookcur][0]+"\" : \""+addrbook[abookcur][1]+"\"", + choices=[("1", "Send a message to this address"), ("2", "Subscribe to this address"), ("3", "Add new address to Address Book"), ("4", "Delete this address")]) @@ -766,8 +623,8 @@ def handlech(c, stdscr): r, t = d.inputbox("Input new address") if r == d.DIALOG_OK: addr = t - if addr not in [item[1] for i, item in enumerate(addrbook)]: - r, t = d.inputbox("Label for address \"" + addr + "\"") + if addr not in [item[1] for i,item in enumerate(addrbook)]: + r, t = d.inputbox("Label for address \""+addr+"\"") if r == d.DIALOG_OK: sqlExecute("INSERT INTO addressbook VALUES (?,?)", t, addr) # Prepend entry @@ -779,39 +636,25 @@ def handlech(c, stdscr): elif t == "4": r, t = d.inputbox("Type in \"I want to delete this Address Book entry\"") if r == d.DIALOG_OK and t == "I want to delete this Address Book entry": - sqlExecute( - "DELETE FROM addressbook WHERE label=? AND address=?", - addrbook[abookcur][0], - addrbook[abookcur][1]) + sqlExecute("DELETE FROM addressbook WHERE label=? AND address=?", addrbook[abookcur][0], addrbook[abookcur][1]) del addrbook[abookcur] elif menutab == 7: set_background_title(d, "Blacklist Dialog Box") - r, t = d.menu( - "Do what with \"" + blacklist[blackcur][0] + "\" : \"" + blacklist[blackcur][1] + "\"?", - choices=[ - ("1", "Delete"), + r, t = d.menu("Do what with \""+blacklist[blackcur][0]+"\" : \""+blacklist[blackcur][1]+"\"?", + choices=[("1", "Delete"), ("2", "Enable"), ("3", "Disable")]) if r == d.DIALOG_OK: if t == "1": r, t = d.inputbox("Type in \"I want to delete this Blacklist entry\"") if r == d.DIALOG_OK and t == "I want to delete this Blacklist entry": - sqlExecute( - "DELETE FROM blacklist WHERE label=? AND address=?", - blacklist[blackcur][0], - blacklist[blackcur][1]) + sqlExecute("DELETE FROM blacklist WHERE label=? AND address=?", blacklist[blackcur][0], blacklist[blackcur][1]) del blacklist[blackcur] elif t == "2": - sqlExecute( - "UPDATE blacklist SET enabled=1 WHERE label=? AND address=?", - blacklist[blackcur][0], - blacklist[blackcur][1]) + sqlExecute("UPDATE blacklist SET enabled=1 WHERE label=? AND address=?", blacklist[blackcur][0], blacklist[blackcur][1]) blacklist[blackcur][2] = True - elif t == "3": - sqlExecute( - "UPDATE blacklist SET enabled=0 WHERE label=? AND address=?", - blacklist[blackcur][0], - blacklist[blackcur][1]) + elif t== "3": + sqlExecute("UPDATE blacklist SET enabled=0 WHERE label=? AND address=?", blacklist[blackcur][0], blacklist[blackcur][1]) blacklist[blackcur][2] = False dialogreset(stdscr) else: @@ -829,17 +672,17 @@ def handlech(c, stdscr): if menutab == 7 and blackcur > 0: blackcur -= 1 elif c == curses.KEY_DOWN: - if menutab == 1 and inboxcur < len(inbox) - 1: + if menutab == 1 and inboxcur < len(inbox)-1: inboxcur += 1 - if (menutab == 2 or menutab == 4) and addrcur < len(addresses) - 1: + if (menutab == 2 or menutab == 4) and addrcur < len(addresses)-1: addrcur += 1 - if menutab == 3 and sentcur < len(sentbox) - 1: + if menutab == 3 and sentcur < len(sentbox)-1: sentcur += 1 - if menutab == 5 and subcur < len(subscriptions) - 1: + if menutab == 5 and subcur < len(subscriptions)-1: subcur += 1 - if menutab == 6 and abookcur < len(addrbook) - 1: + if menutab == 6 and abookcur < len(addrbook)-1: abookcur += 1 - if menutab == 7 and blackcur < len(blacklist) - 1: + if menutab == 7 and blackcur < len(blacklist)-1: blackcur += 1 elif c == curses.KEY_HOME: if menutab == 1: @@ -856,47 +699,38 @@ def handlech(c, stdscr): blackcur = 0 elif c == curses.KEY_END: if menutab == 1: - inboxcur = len(inbox) - 1 + inboxcur = len(inbox)-1 if menutab == 2 or menutab == 4: - addrcur = len(addresses) - 1 + addrcur = len(addresses)-1 if menutab == 3: - sentcur = len(sentbox) - 1 + sentcur = len(sentbox)-1 if menutab == 5: - subcur = len(subscriptions) - 1 + subcur = len(subscriptions)-1 if menutab == 6: - abookcur = len(addrbook) - 1 + abookcur = len(addrbook)-1 if menutab == 7: - blackcur = len(blackcur) - 1 + blackcur = len(blackcur)-1 redraw(stdscr) - - -# pylint: disable=too-many-locals, too-many-arguments def sendMessage(sender="", recv="", broadcast=None, subject="", body="", reply=False): - """Method for message sending""" if sender == "": return d = Dialog(dialog="dialog") set_background_title(d, "Send a message") if recv == "": - r, t = d.inputbox( - "Recipient address (Cancel to load from the Address Book or leave blank to broadcast)", - 10, - 60) + r, t = d.inputbox("Recipient address (Cancel to load from the Address Book or leave blank to broadcast)", 10, 60) if r != d.DIALOG_OK: global menutab menutab = 6 return recv = t - if broadcast is None and sender != recv: - r, t = d.radiolist( - "How to send the message?", - choices=[ - ("1", "Send to one or more specific people", 1), + if broadcast == None and sender != recv: + r, t = d.radiolist("How to send the message?", + choices=[("1", "Send to one or more specific people", 1), ("2", "Broadcast to everyone who is subscribed to your address", 0)]) if r != d.DIALOG_OK: return broadcast = False - if t == "2": # Broadcast + if t == "2": # Broadcast broadcast = True if subject == "" or reply: r, t = d.inputbox("Message subject", width=60, init=subject) @@ -912,12 +746,11 @@ def sendMessage(sender="", recv="", broadcast=None, subject="", body="", reply=F if not broadcast: recvlist = [] - for _, item in enumerate(recv.replace(",", ";").split(";")): + for i, item in enumerate(recv.replace(",", ";").split(";")): recvlist.append(item.strip()) - list(set(recvlist)) # Remove exact duplicates + list(set(recvlist)) # Remove exact duplicates for addr in recvlist: if addr != "": - # pylint: disable=redefined-outer-name status, version, stream, ripe = decodeAddress(addr) if status != "success": set_background_title(d, "Recipient address error") @@ -929,17 +762,13 @@ def sendMessage(sender="", recv="", broadcast=None, subject="", body="", reply=F elif status == "invalidcharacters": err += "The address contains invalid characters." elif status == "versiontoohigh": - err += ("The address version is too high. Either you need to upgrade your Bitmessage software" - " or your acquaintance is doing something clever.") + err += "The address version is too high. Either you need to upgrade your Bitmessage software or your acquaintance is doing something clever." elif status == "ripetooshort": - err += ("Some data encoded in the address is too short. There might be something wrong with" - " the software of your acquaintance.") + err += "Some data encoded in the address is too short. There might be something wrong with the software of your acquaintance." elif status == "ripetoolong": - err += ("Some data encoded in the address is too long. There might be something wrong with" - " the software of your acquaintance.") + err += "Some data encoded in the address is too long. There might be something wrong with the software of your acquaintance." elif status == "varintmalformed": - err += ("Some data encoded in the address is malformed. There might be something wrong with" - " the software of your acquaintance.") + err += "Some data encoded in the address is malformed. There might be something wrong with the software of your acquaintance." else: err += "It is unknown what is wrong with the address." scrollbox(d, unicode(err)) @@ -947,24 +776,17 @@ def sendMessage(sender="", recv="", broadcast=None, subject="", body="", reply=F addr = addBMIfNotPresent(addr) if version > 4 or version <= 1: set_background_title(d, "Recipient address error") - scrollbox(d, unicode( - "Could not understand version number " + - version + - "of address" + - addr + - ".")) + scrollbox(d, unicode("Could not understand version number " + version + "of address" + addr + ".")) continue if stream > 1 or stream == 0: set_background_title(d, "Recipient address error") - scrollbox(d, unicode( - "Bitmessage currently only supports stream numbers of 1," - "unlike as requested for address " + addr + ".")) + scrollbox(d, unicode("Bitmessage currently only supports stream numbers of 1, unlike as requested for address " + addr + ".")) continue if not network.stats.connectedHostsList(): set_background_title(d, "Not connected warning") scrollbox(d, unicode("Because you are not currently connected to the network, ")) stealthLevel = BMConfigParser().safeGetInt('bitmessagesettings', 'ackstealthlevel') - ackdata = genAckPayload(decodeAddress(addr)[2], stealthLevel) + ackdata = genAckPayload(streamNumber, stealthLevel) sqlExecute( "INSERT INTO sent VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", "", @@ -974,22 +796,22 @@ def sendMessage(sender="", recv="", broadcast=None, subject="", body="", reply=F 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. + 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 + 0, # retryNumber "sent", - 2, # encodingType + 2, # encodingType BMConfigParser().getint('bitmessagesettings', 'ttl')) queues.workerQueue.put(("sendmessage", addr)) - else: # Broadcast + else: # Broadcast if recv == "": set_background_title(d, "Empty sender error") scrollbox(d, unicode("You must specify an address to send the message from.")) else: # dummy ackdata, no need for stealth - ackdata = genAckPayload(decodeAddress(addr)[2], 0) + ackdata = genAckPayload(streamNumber, 0) recv = BROADCAST_STR ripe = "" sqlExecute( @@ -1001,24 +823,21 @@ def sendMessage(sender="", recv="", broadcast=None, subject="", body="", reply=F 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. + 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 + 0, # retryNumber + "sent", # folder + 2, # encodingType BMConfigParser().getint('bitmessagesettings', 'ttl')) queues.workerQueue.put(('sendbroadcast', '')) - -# pylint: disable=redefined-outer-name, too-many-locals def loadInbox(): - """Load the list of messages""" sys.stdout = sys.__stdout__ - print "Loading inbox messages..." + print("Loading inbox messages...") sys.stdout = printlog - + where = "toaddress || fromaddress || subject || message" what = "%%" ret = sqlQuery("""SELECT msgid, toaddress, fromaddress, subject, received, read @@ -1028,7 +847,7 @@ def loadInbox(): for row in ret: msgid, toaddr, fromaddr, subject, received, read = row subject = ascii(shared.fixPotentiallyInvalidUTF8Data(subject)) - + # Set label for to address try: if toaddr == BROADCAST_STR: @@ -1040,17 +859,17 @@ def loadInbox(): if tolabel == "": tolabel = toaddr tolabel = shared.fixPotentiallyInvalidUTF8Data(tolabel) - + # Set label for from address fromlabel = "" if BMConfigParser().has_section(fromaddr): fromlabel = BMConfigParser().get(fromaddr, "label") - if fromlabel == "": # Check Address Book + if fromlabel == "": # Check Address Book qr = sqlQuery("SELECT label FROM addressbook WHERE address=?", fromaddr) if qr != []: for r in qr: fromlabel, = r - if fromlabel == "": # Check Subscriptions + if fromlabel == "": # Check Subscriptions qr = sqlQuery("SELECT label FROM subscriptions WHERE address=?", fromaddr) if qr != []: for r in qr: @@ -1058,19 +877,16 @@ def loadInbox(): if fromlabel == "": fromlabel = fromaddr fromlabel = shared.fixPotentiallyInvalidUTF8Data(fromlabel) - + # Load into array - inbox.append([msgid, tolabel, toaddr, fromlabel, fromaddr, subject, l10n.formatTimestamp( - received, False), read]) + inbox.append([msgid, tolabel, toaddr, fromlabel, fromaddr, subject, + l10n.formatTimestamp(received, 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" what = "%%" ret = sqlQuery("""SELECT toaddress, fromaddress, subject, status, ackdata, lastactiontime @@ -1080,7 +896,7 @@ def loadSent(): for row in ret: toaddr, fromaddr, subject, status, ackdata, lastactiontime = row subject = ascii(shared.fixPotentiallyInvalidUTF8Data(subject)) - + # Set label for to address tolabel = "" qr = sqlQuery("SELECT label FROM addressbook WHERE address=?", toaddr) @@ -1097,14 +913,14 @@ def loadSent(): tolabel = BMConfigParser().get(toaddr, "label") if tolabel == "": tolabel = toaddr - + # Set label for from address fromlabel = "" if BMConfigParser().has_section(fromaddr): fromlabel = BMConfigParser().get(fromaddr, "label") if fromlabel == "": fromlabel = fromaddr - + # Set status string if status == "awaitingpubkey": statstr = "Waiting for their public key. Will request it again soon" @@ -1114,20 +930,20 @@ def loadSent(): statstr = "Message queued" elif status == "msgsent": t = l10n.formatTimestamp(lastactiontime, False) - statstr = "Message sent at " + t + ".Waiting for acknowledgement." + statstr = "Message sent at "+t+".Waiting for acknowledgement." elif status == "msgsentnoackexpected": t = l10n.formatTimestamp(lastactiontime, False) - statstr = "Message sent at " + t + "." + statstr = "Message sent at "+t+"." elif status == "doingmsgpow": statstr = "The proof of work required to send the message has been queued." elif status == "ackreceived": t = l10n.formatTimestamp(lastactiontime, False) - statstr = "Acknowledgment of the message received at " + t + "." + statstr = "Acknowledgment of the message received at "+t+"." elif status == "broadcastqueued": statstr = "Broadcast queued." elif status == "broadcastsent": t = l10n.formatTimestamp(lastactiontime, False) - statstr = "Broadcast sent at " + t + "." + statstr = "Broadcast sent at "+t+"." elif status == "forcepow": statstr = "Forced difficulty override. Message will start sending soon." elif status == "badkey": @@ -1136,46 +952,30 @@ def loadSent(): statstr = "Error: The work demanded by the recipient is more difficult than you are willing to do." else: t = l10n.formatTimestamp(lastactiontime, False) - statstr = "Unknown status " + status + " at " + t + "." - + statstr = "Unknown status "+status+" at "+t+"." + # Load into array - sentbox.append([ - tolabel, - toaddr, - fromlabel, - fromaddr, - subject, - statstr, - ackdata, + sentbox.append([tolabel, toaddr, fromlabel, fromaddr, subject, statstr, ackdata, 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") for row in ret: label, addr = row label = shared.fixPotentiallyInvalidUTF8Data(label) addrbook.append([label, addr]) addrbook.reverse() - - def loadSubscriptions(): - """Load subscription functionality""" ret = sqlQuery("SELECT label, address, enabled FROM subscriptions") for row in ret: label, address, enabled = row subscriptions.append([label, address, enabled]) subscriptions.reverse() - - def loadBlackWhiteList(): - """load black/white list""" global bwtype bwtype = BMConfigParser().get("bitmessagesettings", "blackwhitelist") if bwtype == "black": @@ -1187,54 +987,51 @@ def loadBlackWhiteList(): blacklist.append([label, address, enabled]) blacklist.reverse() - def runwrapper(): - """Main method""" sys.stdout = printlog - # sys.stderr = errlog - + #sys.stderr = errlog + + # Load messages from database loadInbox() loadSent() loadAddrBook() loadSubscriptions() loadBlackWhiteList() - + stdscr = curses.initscr() - + global logpad logpad = curses.newpad(1024, curses.COLS) - + stdscr.nodelay(0) curses.curs_set(0) stdscr.timeout(1000) - + curses.wrapper(run) doShutdown() - def run(stdscr): - """Main loop""" # Schedule inventory lookup data resetlookups() - + # Init color pairs if curses.has_colors(): - curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK) # red - curses.init_pair(2, curses.COLOR_GREEN, curses.COLOR_BLACK) # green - curses.init_pair(3, curses.COLOR_YELLOW, curses.COLOR_BLACK) # yellow - curses.init_pair(4, curses.COLOR_BLUE, curses.COLOR_BLACK) # blue - curses.init_pair(5, curses.COLOR_MAGENTA, curses.COLOR_BLACK) # magenta - curses.init_pair(6, curses.COLOR_CYAN, curses.COLOR_BLACK) # cyan - curses.init_pair(7, curses.COLOR_WHITE, curses.COLOR_BLACK) # white + curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK) # red + curses.init_pair(2, curses.COLOR_GREEN, curses.COLOR_BLACK) # green + curses.init_pair(3, curses.COLOR_YELLOW, curses.COLOR_BLACK) # yellow + curses.init_pair(4, curses.COLOR_BLUE, curses.COLOR_BLACK) # blue + curses.init_pair(5, curses.COLOR_MAGENTA, curses.COLOR_BLACK) # magenta + curses.init_pair(6, curses.COLOR_CYAN, curses.COLOR_BLACK) # cyan + curses.init_pair(7, curses.COLOR_WHITE, curses.COLOR_BLACK) # white if curses.can_change_color(): - curses.init_color(8, 500, 500, 500) # gray + curses.init_color(8, 500, 500, 500) # gray curses.init_pair(8, 8, 0) - curses.init_color(9, 844, 465, 0) # orange + curses.init_color(9, 844, 465, 0) # orange curses.init_pair(9, 9, 0) else: - curses.init_pair(8, curses.COLOR_WHITE, curses.COLOR_BLACK) # grayish - curses.init_pair(9, curses.COLOR_YELLOW, curses.COLOR_BLACK) # orangish - + curses.init_pair(8, curses.COLOR_WHITE, curses.COLOR_BLACK) # grayish + curses.init_pair(9, curses.COLOR_YELLOW, curses.COLOR_BLACK) # orangish + # Init list of address in 'Your Identities' tab configSections = BMConfigParser().addresses() for addressInKeysFile in configSections: @@ -1242,28 +1039,27 @@ def run(stdscr): addresses.append([BMConfigParser().get(addressInKeysFile, "label"), isEnabled, addressInKeysFile]) # Set address color if not isEnabled: - addresses[len(addresses) - 1].append(8) # gray + addresses[len(addresses)-1].append(8) # gray elif BMConfigParser().safeGetBoolean(addressInKeysFile, 'chan'): - addresses[len(addresses) - 1].append(9) # orange + addresses[len(addresses)-1].append(9) # orange elif BMConfigParser().safeGetBoolean(addressInKeysFile, 'mailinglist'): - addresses[len(addresses) - 1].append(5) # magenta + addresses[len(addresses)-1].append(5) # magenta else: - addresses[len(addresses) - 1].append(0) # black + addresses[len(addresses)-1].append(0) # black addresses.reverse() - + stdscr.clear() redraw(stdscr) - while quit_ is False: + while quit == False: drawtab(stdscr) handlech(stdscr.getch(), 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__ sys.stderr = sys.__stderr__ - os._exit(0) # pylint: disable=protected-access + + os._exit(0) diff --git a/src/bitmessagekivy/android/python-for-android/recipes/bitmsghash/__init__.py b/src/bitmessagekivy/android/python-for-android/recipes/bitmsghash/__init__.py deleted file mode 100644 index 4566ebfb..00000000 --- a/src/bitmessagekivy/android/python-for-android/recipes/bitmsghash/__init__.py +++ /dev/null @@ -1,50 +0,0 @@ -from pythonforandroid.toolchain import Recipe, shprint, shutil, current_directory -from os.path import exists, join -import os -import sys -from multiprocessing import cpu_count -import sh - - -class BitmsghashRecipe(Recipe): - # This could also inherit from PythonRecipe etc. if you want to - # use their pre-written build processes - - url = 'https://github.com/surbhicis/bitmsghash/archive/master.zip' - # {version} will be replaced with self.version when downloading - - depends = ['openssl'] - - conflicts = [] - - def get_recipe_env(self, arch=None): - env = super(BitmsghashRecipe, self).get_recipe_env(arch) - r = Recipe.get_recipe('openssl', self.ctx) - b = r.get_build_dir(arch.arch) - env['CCFLAGS'] = env['CFLAGS'] = \ - env['CFLAGS'] + ' -I{openssl_build_path}/include ' \ - '-I{openssl_build_path}/include/openssl'.format( - openssl_build_path=b) - env['LDFLAGS'] = \ - env['LDFLAGS'] + ' -L{openssl_build_path} ' \ - '-lcrypto{openssl_version} ' \ - '-lssl{openssl_version}'.format( - openssl_build_path=b, - openssl_version=r.version) - return env - - def should_build(self, arch=None): - super(BitmsghashRecipe, self).should_build(arch) - return not exists( - join(self.ctx.get_libs_dir(arch.arch), 'libbitmsghash.so')) - - def build_arch(self, arch=None): - super(BitmsghashRecipe, self).build_arch(arch) - env = self.get_recipe_env(arch) - with current_directory(join(self.get_build_dir(arch.arch))): - dst_dir = join(self.get_build_dir(arch.arch)) - shprint(sh.make, '-j', str(cpu_count()), _env=env) - self.install_libs(arch, '{}/libbitmsghash.so'.format(dst_dir), - 'libbitmsghash.so') - -recipe = BitmsghashRecipe() diff --git a/src/bitmessagekivy/android/python-for-android/recipes/kivymd/__init__.py b/src/bitmessagekivy/android/python-for-android/recipes/kivymd/__init__.py deleted file mode 100644 index 5a29bba7..00000000 --- a/src/bitmessagekivy/android/python-for-android/recipes/kivymd/__init__.py +++ /dev/null @@ -1,41 +0,0 @@ -""" -src/bitmessagekivy/android/python-for-android/recipes/kivymd/__init__.py -================================= -""" -# pylint: disable=import-error -from os.path import join - -from pythonforandroid.recipe import PythonRecipe -# from pythonforandroid.util import ensure_dir - - -class KivyMDRecipe(PythonRecipe): - """This recipe installs KivyMD into the android dist from source""" - version = 'master' - url = 'https://github.com/surbhicis/kivymd/archive/master.zip' - depends = ['kivy'] - site_packages_name = 'kivymd' - call_hostpython_via_targetpython = False - - def should_build(self, arch): # pylint: disable=no-self-use, unused-argument - """Method helps to build the application""" - return True - - def get_recipe_env(self, arch): - """Method is used for getting all the env paths""" - env = super(KivyMDRecipe, self).get_recipe_env(arch) - env['PYTHON_ROOT'] = self.ctx.get_python_install_dir() - env['CFLAGS'] += ' -I' + env['PYTHON_ROOT'] + '/include/python2.7' - env['LDFLAGS'] += ' -L' + env['PYTHON_ROOT'] + '/lib' + \ - ' -lpython2.7' - if 'sdl2' in self.ctx.recipe_build_order: - env['USE_SDL2'] = '1' - env['KIVY_SDL2_PATH'] = ':'.join([ - join(self.ctx.bootstrap.build_dir, 'jni', 'SDL', 'include'), - join(self.ctx.bootstrap.build_dir, 'jni', 'SDL2_image'), - join(self.ctx.bootstrap.build_dir, 'jni', 'SDL2_mixer'), - join(self.ctx.bootstrap.build_dir, 'jni', 'SDL2_ttf'), ]) - return env - - -recipe = KivyMDRecipe() diff --git a/src/bitmessagekivy/android/python-for-android/recipes/kivymd/kivymd-fix-dev-compatibility.patch b/src/bitmessagekivy/android/python-for-android/recipes/kivymd/kivymd-fix-dev-compatibility.patch deleted file mode 100644 index bc8d5dee..00000000 --- a/src/bitmessagekivy/android/python-for-android/recipes/kivymd/kivymd-fix-dev-compatibility.patch +++ /dev/null @@ -1,36 +0,0 @@ -diff -Naurp KivyMD.orig/kivymd/button.py KivyMD/kivymd/button.py ---- KivyMD.orig/kivymd/button.py 2017-08-25 13:12:34.000000000 +0200 -+++ KivyMD/kivymd/button.py 2018-07-10 10:37:55.719440354 +0200 -@@ -175,7 +175,8 @@ class BaseButton(ThemableBehavior, Butto - self._current_button_color = self.md_bg_color_disabled - else: - self._current_button_color = self.md_bg_color -- super(BaseButton, self).on_disabled(instance, value) -+ # To add compatibility to last kivy (disabled is now an Alias property) -+ # super(BaseButton, self).on_disabled(instance, value) - - - class BasePressedButton(BaseButton): -diff -Naurp KivyMD.orig/kivymd/selectioncontrols.py KivyMD/kivymd/selectioncontrols.py ---- KivyMD.orig/kivymd/selectioncontrols.py 2017-08-25 13:12:34.000000000 +0200 -+++ KivyMD/kivymd/selectioncontrols.py 2018-07-10 10:40:06.971439102 +0200 -@@ -45,6 +45,7 @@ Builder.load_string(''' - pos: self.pos - - : -+ _thumb_pos: (self.right - dp(12), self.center_y - dp(12)) if self.active else (self.x - dp(12), self.center_y - dp(12)) - canvas.before: - Color: - rgba: self._track_color_disabled if self.disabled else \ -diff -Naurp KivyMD.orig/kivymd/tabs.py KivyMD/kivymd/tabs.py ---- KivyMD.orig/kivymd/tabs.py 2017-08-25 13:12:34.000000000 +0200 -+++ KivyMD/kivymd/tabs.py 2018-07-10 10:39:20.603439544 +0200 -@@ -185,7 +185,7 @@ class MDBottomNavigationBar(ThemableBeha - - class MDTabHeader(MDFlatButton): - """ Internal widget for headers based on MDFlatButton""" -- -+ - width = BoundedNumericProperty(dp(0), min=dp(72), max=dp(264), errorhandler=lambda x: dp(72)) - tab = ObjectProperty(None) - panel = ObjectProperty(None) diff --git a/src/bitmessagekivy/identiconGeneration.py b/src/bitmessagekivy/identiconGeneration.py deleted file mode 100644 index ca8bd3bd..00000000 --- a/src/bitmessagekivy/identiconGeneration.py +++ /dev/null @@ -1,93 +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 -# pylint: disable=import-error - - -# constants -RESOLUTION = 128, 128 -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 - # image.show() - - -def generate_hash(string): - """Generating hash""" - try: - # make input case insensitive - string = str.lower(string) - hash_object = hashlib.md5(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 085e2aa2..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 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): - """Method helping for searching mails""" - # pylint: disable=too-many-arguments, too-many-branches +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 == "sent" or folder == "draft": - sqlStatementBase = ( - '''SELECT toaddress, fromaddress, subject, message, status,''' - ''' ackdata, lastactiontime 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,39 +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 == "sent" or folder == "draft": - sqlStatementBase += \ - " ORDER BY lastactiontime 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) - # elif folder == "addressbook": - # sqlStatementBase += " limit {0}, {1}".format(start_indx, end_indx) + if folder == "sent": + sqlStatementBase += " ORDER BY lastactiontime" return sqlQuery(sqlStatementBase, sqlArguments) diff --git a/src/bitmessagekivy/main.kv b/src/bitmessagekivy/main.kv index 37c20f52..ea8936c5 100644 --- a/src/bitmessagekivy/main.kv +++ b/src/bitmessagekivy/main.kv @@ -1,1332 +1,354 @@ +#:import la kivy.adapters.listadapter +#:import factory kivy.factory +#:import mpybit bitmessagekivy.mpybit +#:import C kivy.utils.get_color_from_hex -#:import Toolbar kivymd.toolbar.Toolbar -#:import NavigationLayout kivymd.navigationdrawer.NavigationLayout -#:import NavigationDrawerDivider kivymd.navigationdrawer.NavigationDrawerDivider -#:import NavigationDrawerSubheader kivymd.navigationdrawer.NavigationDrawerSubheader -#:import MDCheckbox kivymd.selectioncontrols.MDCheckbox -#:import MDList kivymd.list.MDList -#:import OneLineListItem kivymd.list.OneLineListItem -#:import MDTextField kivymd.textfields.MDTextField -#:import get_color_from_hex kivy.utils.get_color_from_hex -#:import colors kivymd.color_definitions.colors -#:import MDTabbedPanel kivymd.tabs.MDTabbedPanel -#:import MDTab kivymd.tabs.MDTab -#:import MDFloatingActionButton kivymd.button.MDFloatingActionButton -#:import Factory kivy.factory.Factory -#:import MDScrollViewRefreshLayout kivymd.refreshlayout.MDScrollViewRefreshLayout -#:import MDSpinner kivymd.spinner.MDSpinner -#:import NoTransition kivy.uix.screenmanager.NoTransition -#:import MDSeparator kivymd.card.MDSeparator - -#: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 - -: - icon: 'checkbox-blank-circle' - -: - font_size: '12.5sp' - background_color: color_button if self.state == 'down' else color_button_pressed - background_down: 'atlas://data/images/defaulttheme/button' - color: color_font - -: - drawer_logo: './images/drawer_logo1.png' - NavigationDrawerDivider: - NavigationDrawerSubheader: - text: "Accounts" +: + id: nav_drawer NavigationDrawerIconButton: - CustomSpinner: + Spinner: + pos_hint:{"x":0,"y":.3} id: btn - pos_hint:{"x":0,"y":.0} - option_cls: Factory.get("MySpinnerOption") - font_size: '11.9sp' - text: app.getDefaultAccData() - background_color: color_button if self.state == 'normal' else color_button_pressed - background_down: 'atlas://data/images/defaulttheme/spinner' - color: color_font - values: app.variable_1 + background_color: app.theme_cls.primary_dark + text: app.showmeaddresses(name='text') + values: app.showmeaddresses(name='values') on_text:app.getCurrentAccountData(self.text) - Image: - source: app.get_default_image() - x: self.width/6 - y: self.parent.y + self.parent.height/4 - size: self.parent.height/2, self.parent.height/2 - ArrowImg: + NavigationDrawerIconButton: - id: inbox_cnt icon: 'email-open' - text: "Inbox" + text: "inbox" on_release: app.root.ids.scr_mngr.current = 'inbox' - badge_text: "0" - on_press: app.load_screen(self) NavigationDrawerIconButton: - id: send_cnt - icon: 'send' - text: "Sent" + icon: 'mail-send' + text: "sent" on_release: app.root.ids.scr_mngr.current = 'sent' - badge_text: "0" NavigationDrawerIconButton: - id: draft_cnt - icon: 'message-draw' - text: "Draft" - on_release: app.root.ids.scr_mngr.current = 'draft' - badge_text: "0" - #NavigationDrawerIconButton: - #text: "Starred" - #icon:'star' - #on_release: app.root.ids.scr_mngr.current = 'starred' - #badge_text: "0" - #NavigationDrawerIconButton: - #icon: 'archive' - #text: "Archieve" - #on_release: app.root.ids.scr_mngr.current = 'archieve' - #badge_text: "0" - #NavigationDrawerIconButton: - #icon: 'email-open-outline' - #text: "Spam" - #on_release: app.root.ids.scr_mngr.current = 'spam' - #badge_text: "0" - NavigationDrawerIconButton: - id: trash_cnt - icon: 'delete' - text: "Trash" + icon: 'dropbox' + text: "trash" on_release: app.root.ids.scr_mngr.current = 'trash' - badge_text: "0" NavigationDrawerIconButton: - id: allmail_cnt - text: "All Mails" - icon:'contact-mail' - on_release: app.root.ids.scr_mngr.current = 'allmails' - badge_text: "0" - on_press: app.load_screen(self) - NavigationDrawerDivider: - NavigationDrawerSubheader: - text: "All labels" + 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: "Address Book" - icon:'book-multiple' - on_release: app.root.ids.scr_mngr.current = 'addressbook' - NavigationDrawerIconButton: - text: "Settings" - icon:'settings' - on_release: app.root.ids.scr_mngr.current = 'set' - NavigationDrawerIconButton: - text: "Subscriptions/Payment" - icon:'bell' - on_release: app.root.ids.scr_mngr.current = 'payment' - NavigationDrawerIconButton: - text: "Credits" - icon:'wallet' - on_release: app.root.ids.scr_mngr.current = 'credits' - NavigationDrawerIconButton: - text: "new address" - icon:'account-plus' - on_release: app.root.ids.scr_mngr.current = 'login' - NavigationDrawerIconButton: - text: "Network Status" - icon:'server-network' - on_release: app.root.ids.scr_mngr.current = 'networkstat' - NavigationDrawerIconButton: - text: "My Addresses" - icon:'account-multiple' - on_release: app.root.ids.scr_mngr.current = 'myaddress' + text: "new identity" + icon:'accounts-add' + on_release: app.root.ids.scr_mngr.current = 'newidentity' + +BoxLayout: + orientation: 'vertical' + Toolbar: + id: toolbar + 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() -NavigationLayout: - id: nav_layout - ContentNavigationDrawer: - id: nav_drawer + ScreenManager: + id: scr_mngr + Inbox: + id:sc1 + Sent: + id:sc2 + Trash: + id:sc3 + Dialog: + id:sc4 + Test: + id:sc5 + Create: + id:sc6 + NewIdentity: + id:sc7 + Page: + id:sc8 + AddressSuccessful: + id:sc9 - FloatLayout: - id: float_box - BoxLayout: - id: box_layout - orientation: 'vertical' - Toolbar: - id: toolbar - title: app.current_address_label() - opacity: 1 if app.addressexist() else 0 - disabled: False if app.addressexist() else True - md_bg_color: app.theme_cls.primary_color - background_palette: 'Primary' - background_hue: '500' - left_action_items: [['menu', lambda x: app.root.toggle_nav_drawer()]] - right_action_items: [['account-plus', lambda x: app.addingtoaddressbook()]] + 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 - ScreenManager: - id: scr_mngr - Inbox: - id:sc1 - Page: - id:sc2 - Create: - id:sc3 - Sent: - id:sc4 - Trash: - id:sc5 - Login: - id:sc6 - Random: - id:sc7 - Spam: - id:sc8 - Setting: - id:sc9 - MyAddress: - id:sc10 - AddressBook: - id:sc11 - Payment: - id:sc12 - NetworkStat: - id:sc13 - MailDetail: - id:sc14 - ShowQRCode: - id:sc15 - Draft: - id:sc16 - Allmails: - id:sc17 - Credits: - id:sc18 - Starred: - id:sc19 - Archieve: - id:sc20 +: + text: '' + size_hint_y: None + height: 48 + ignore_perpendicular_swipes: True + data_index: 0 + min_move: 20 / self.width + + on__offset: app.update_index(root.data_index, self.index) + + canvas.before: + Color: + rgba: C('FFFFFF33') + + Rectangle: + pos: self.pos + size: self.size + + Line: + rectangle: self.pos + self.size + + Button: + text: 'delete ({}:{})'.format(root.text, root.data_index) + on_press: app.delete(root.data_index) + + Button: + text: root.text + on_press: app.getInboxMessageDetail(self.text) + + Button: + text: 'archive' + on_press: app.archive(root.data_index) : name: 'inbox' - transition: NoTransition() - BoxLayout: - orientation: 'vertical' - spacing: dp(5) - SearchBar: - GridLayout: - id: identi_tag - padding: [20, 0, 0, 5] - cols: 1 + RecycleView: + data: root.data + viewclass: 'SwipeButton' + do_scroll_x: False + scroll_timeout: 100 + + RecycleBoxLayout: + id:rc + orientation: 'vertical' size_hint_y: None height: self.minimum_height - MDLabel: - text: '' - font_style: 'Body1' - bold: True - #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: + default_size_hint: 1, None + canvas.before: + Color: + rgba: 0,0,0, 1 + Rectangle: + pos: self.pos + size: self.size : name: 'sent' - BoxLayout: - orientation: 'vertical' - spacing: dp(5) - SearchBar: - GridLayout: - id: identi_tag - padding: [20, 0, 0, 5] - cols: 1 + RecycleView: + data: root.data + viewclass: 'SwipeButton' + do_scroll_x: False + scroll_timeout: 100 + + RecycleBoxLayout: + id:rc + orientation: 'vertical' size_hint_y: None height: self.minimum_height - MDLabel: - text: '' - font_style: 'Body1' - bold: True - BoxLayout: - orientation:'vertical' - ScrollView: - id: scroll_y - do_scroll_x: False - MDList: - id: ml - Loader: - ComposerButton: + default_size_hint: 1, None + canvas.before: + Color: + rgba: 0,0,0, 1 + Rectangle: + pos: self.pos + size: self.size : name: 'trash' - BoxLayout: - orientation: 'vertical' - spacing: dp(5) - GridLayout: - id: identi_tag - padding: [20, 20, 0, 5] - spacing: dp(5) - cols: 1 + RecycleView: + data: root.data + viewclass: 'SwipeButton' + do_scroll_x: False + scroll_timeout: 100 + + RecycleBoxLayout: + id:rc + orientation: 'vertical' size_hint_y: None height: self.minimum_height - MDLabel: - text: '' - font_style: 'Body1' - bold: True - BoxLayout: - orientation:'vertical' - ScrollView: - id: scroll_y - do_scroll_x: False - MDList: - id: ml - Loader: - ComposerButton: - -: - 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: - text: '' - font_style: 'Body1' - bold: True - BoxLayout: - orientation:'vertical' - ScrollView: - id: scroll_y - do_scroll_x: False - MDList: - id: ml - ComposerButton: + default_size_hint: 1, None + canvas.before: + Color: + rgba: 0,0,0, 1 + Rectangle: + pos: self.pos + size: self.size -: - name: 'starred' - ScrollView: - do_scroll_x: False - MDList: - id: ml - ComposerButton: - -: - name: 'archieve' - ScrollView: - do_scroll_x: False - MDList: - id: ml - ComposerButton: - -: - name: 'spam' - ScrollView: - do_scroll_x: False - MDList: - id: ml - ComposerButton: - -: - name: 'allmails' - #FloatLayout: - # MDScrollViewRefreshLayout: - # id: refresh_layout - # refresh_callback: root.refresh_callback - # root_layout: root.set_root_layout() - # MDList: - # id: ml - 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: - text: '' - font_style: 'Body1' - bold: True - BoxLayout: - orientation:'vertical' - ScrollView: - id: scroll_y - do_scroll_x: False - MDList: - id: ml - Loader: - ComposerButton: - +: + 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 +: + 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' + + BoxLayout: + size_hint_y: None + height: '32dp' + Label: + text: 'SUBJECT' + color: 0,0,0,1 + TextInput: + id: subject + hint_text: 'SUBJECT' + + BoxLayout: + size_hint_y: None + height: '32dp' + Label: + text: 'BODY' + color: 0,0,0,1 + TextInput: + id: message + multiline:True + size_hint: 1,2 + + 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() + +: + 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 + + 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 + + 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() + : name: 'page' Label: - text:"I am on page" + text: 'I am on description of my email yooooo' color: 0,0,0,1 -: - name: 'create' - Loader: - -: - name: 'credits' - ScrollView: - do_scroll_x: False - MDList: - id: ml - size_hint_y: None - height: dp(200) - OneLineListItem: - text: "Available Credits" - BoxLayout: - AnchorLayout: - MDRaisedButton: - size_hint: .6, .35 - height: dp(40) - MDLabel: - font_style: 'Title' - text: root.available_credits - font_size: '13sp' - color: (1,1,1,1) - halign: 'center' - -: - ScrollView: - BoxLayout: - orientation: 'vertical' - size_hint_y: None - height: self.minimum_height + 2 * self.parent.height/4 - padding: dp(32) - spacing: 15 - BoxLayout: - orientation: 'vertical' - MDTextField: - id: ti - hint_text: 'type or select sender address' - size_hint_y: None - height: 100 - font_size: '13sp' - multiline: False - required: True - helper_text_mode: "on_error" - - BoxLayout: - size_hint_y: None - height: dp(40) - CustomSpinner: - background_color: app.theme_cls.primary_dark - id: btn - values: app.variable_1 - on_text: root.auto_fill_fromaddr() if self.text != 'Select' else '' - option_cls: Factory.get("MySpinnerOption") - background_color: color_button if self.state == 'normal' else color_button_pressed - background_down: 'atlas://data/images/defaulttheme/spinner' - color: color_font - font_size: '12.5sp' - ArrowImg: - - BoxLayout: - orientation: 'vertical' - txt_input: txt_input - rv: rv - size : (890, 60) - size_hint: 1,1 - MyTextInput: - id: txt_input - size_hint_y: None - font_size: '13sp' - height: self.parent.height/2 - #hint_text: 'type or search recipients address starting with BM-' - hint_text: 'type, select or scan QR code for recipients address' - RV: - id: rv - MDTextField: - id: subject - hint_text: 'subject' - required: True - height: 100 - font_size: '13sp' - size_hint_y: None - multiline: False - helper_text_mode: "on_error" - - MDTextField: - id: body - multiline: True - hint_text: 'body' - size_hint_y: None - font_size: '13sp' - required: True - helper_text_mode: "on_error" - 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 - - -: - name: 'login' - ScrollView: - do_scroll_x: False - BoxLayout: - orientation: 'vertical' - size_hint_y: None - height: dp(750) - padding: dp(10) - BoxLayout: - MDLabel: - font_style: 'Body1' - theme_text_color: 'Primary' - text: "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 \n pros and cons:\n" - halign: 'center' - bold: True - color:app.theme_cls.primary_dark - BoxLayout: - MDLabel: - font_style: 'Caption' - theme_text_color: 'Primary' - text: "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" - halign: 'center' - bold: True - color:app.theme_cls.primary_dark - MDCheckbox: - id: grp_chkbox_1 - group: 'test' - active: True - allow_no_selection: False - MDLabel: - font_style: 'Caption' - theme_text_color: 'Primary' - text: "use a random number generator to make an address" - halign: 'center' - size_hint_y: None - bold: True - height: self.texture_size[1] + dp(4) - color: [0.941, 0, 0,1] - MDCheckbox: - id: grp_chkbox_1 - group: 'test' - allow_no_selection: False - MDLabel: - font_style: 'Caption' - theme_text_color: 'Primary' - text: "use a pseudo number generator to make an address" - halign: 'center' - size_hint_y: None - bold: True - color: [0.941, 0, 0,1] - height: self.texture_size[1] + dp(4) - BoxLayout: - AnchorLayout: - MDRaisedButton: - height: dp(40) - on_press: app.root.ids.scr_mngr.current = 'random' - on_press: app.root.ids.sc7.reset_address_label() - MDLabel: - font_style: 'Title' - text: 'proceed' - font_size: '13sp' - color: (1,1,1,1) - halign: 'center' - -: - name: 'random' - ScrollView: - BoxLayout: - orientation: 'vertical' - size_hint_y: None - height: self.minimum_height - padding: dp(20) - spacing: 100 - MDLabel: - font_style: 'Body1' - theme_text_color: 'Primary' - text: "Random Addresses" - halign: 'center' - bold: True - color:app.theme_cls.primary_dark - - MDLabel: - font_style: 'Body1' - theme_text_color: 'Primary' - text: "Here you may generate as many addresses as you like, Indeed creating and abandoning addresses is encouraged" - halign: 'center' - bold: True - color:app.theme_cls.primary_dark - - MDTextField: - id: label - multiline: True - hint_text: "Label" - required: True - helper_text_mode: "on_error" - on_text: root.add_validation(self) - BoxLayout: - AnchorLayout: - MDRaisedButton: - height: dp(40) - on_release: root.generateaddress(app) - opposite_colors: True - MDLabel: - font_style: 'Title' - text: 'next' - font_size: '13sp' - color: (1,1,1,1) - halign: 'center' - -: - name: 'set' - ScrollView: - do_scroll_x: False - MDList: - id: ml - size_hint_y: None - height: dp(500) - OneLineListItem: - text: "SERVER SETTINGS" - BoxLayout: - AnchorLayout: - MDRaisedButton: - size_hint: .6, .55 - height: dp(40) - MDLabel: - font_style: 'Title' - text: 'Server' - font_size: '13sp' - color: (1,1,1,1) - halign: 'center' - OneLineListItem: - text: "DATA SETTINGS" - BoxLayout: - AnchorLayout: - MDRaisedButton: - size_hint: .6, .55 - height: dp(40) - MDLabel: - font_style: 'Title' - text: 'Import or export data' - font_size: '13sp' - color: (1,1,1,1) - halign: 'center' - OneLineListItem: - text: "OTHER SETTINGS" - BoxLayout: - AnchorLayout: - MDRaisedButton: - size_hint: .6, .55 - height: dp(40) - MDLabel: - font_style: 'Title' - text: 'Restart background service' - font_size: '13sp' - color: (1,1,1,1) - halign: 'center' - BoxLayout: - AnchorLayout: - MDLabel: - font_style: 'Body1' - theme_text_color: 'Primary' - text: "bitmessage is 11 seconds behind the network" - halign: 'center' - bold: True - color: [0.941, 0, 0,1] - - BoxLayout: - MDCheckbox: - id: chkbox - size_hint: None, None - size: dp(48), dp(64) - active: True - MDLabel: - font_style: 'Body1' - theme_text_color: 'Primary' - text: "show settings (for advanced users only)" - halign: 'left' - bold: True - color: app.theme_cls.primary_dark - -: - name: 'myaddress' - BoxLayout: - orientation: 'vertical' - spacing: dp(5) - SearchBar: - GridLayout: - id: identi_tag - padding: [20, 0, 0, 5] - cols: 1 - size_hint_y: None - height: self.minimum_height - MDLabel: - text: 'My Addresses' - font_style: 'Body1' - bold: True - FloatLayout: - MDScrollViewRefreshLayout: - id: refresh_layout - refresh_callback: root.refresh_callback - root_layout: root.set_root_layout() - MDList: - id: ml - Loader: - ComposerButton: - -: - name: 'addressbook' - BoxLayout: - orientation: 'vertical' - spacing: dp(5) - SearchBar: - GridLayout: - id: identi_tag - padding: [20, 0, 0, 5] - cols: 1 - size_hint_y: None - height: self.minimum_height - MDLabel: - text: '' - font_style: 'Body1' - bold: True - BoxLayout: - orientation:'vertical' - ScrollView: - id: scroll_y - do_scroll_x: False - MDList: - id: ml - Loader: - ComposerButton: - -: - name: 'payment' - ScrollView: - do_scroll_x: False - BoxLayout: - orientation: 'vertical' - padding: [dp(app.window_size[0]/16 if app.window_size[0] <= 720 else app.window_size[0]/6 if app.window_size[0] <= 800 else app.window_size[0]/18), dp(10)] - spacing: 12 - size_hint_y: None - height: self.minimum_height + dp(app.window_size[1]) if app.window_size[1] > app.window_size[0] else dp(app.window_size[0]) - BoxLayout: - orientation: 'vertical' - padding: dp(5) - canvas.before: - Color: - rgba: app.theme_cls.primary_dark - Rectangle: - # self here refers to the widget i.e FloatLayout - pos: self.pos - size: self.size - MDLabel: - size_hint_y: None - font_style: 'Headline' - theme_text_color: 'Primary' - text: 'Platinum' - halign: 'center' - color: 1,1,1,1 - MDLabel: - font_style: 'Subhead' - theme_text_color: 'Primary' - text: 'We provide subscriptions for proof of work calculation for first month. ' - halign: 'center' - color: 1,1,1,1 - MDLabel: - id: free_pak - font_style: 'Headline' - theme_text_color: 'Primary' - text: '€ 50.0' - halign: 'center' - color: 1,1,1,1 - MDRaisedButton: - canvas: - Color: - rgb: (0.93, 0.93, 0.93) - Rectangle: - pos: self.pos - size: self.size - size_hint: 1, None - height: dp(40) - on_press: root.get_available_credits(self) - MDLabel: - font_style: 'Title' - text: 'Get Free Credits' - font_size: '13sp' - color: (0,0,0,1) - halign: 'center' - BoxLayout: - orientation: 'vertical' - padding: dp(5) - canvas.before: - Color: - rgba: app.theme_cls.primary_dark - Rectangle: - # self here refers to the widget i.e FloatLayout - pos: self.pos - size: self.size - MDLabel: - size_hint_y: None - font_style: 'Headline' - theme_text_color: 'Primary' - text: 'Silver' - halign: 'center' - color: 1,1,1,1 - MDLabel: - font_style: 'Subhead' - theme_text_color: 'Primary' - text: 'We provide for proof of work calculation for six month. ' - halign: 'center' - color: 1,1,1,1 - MDLabel: - font_style: 'Headline' - theme_text_color: 'Primary' - text: '€ 100.0' - halign: 'center' - color: 1,1,1,1 - MDRaisedButton: - canvas: - Color: - rgb: (0.93, 0.93, 0.93) - Rectangle: - pos: self.pos - size: self.size - size_hint: 1, None - height: dp(40) - MDLabel: - font_style: 'Title' - text: 'Get Monthly Credits' - font_size: '13sp' - color: (0,0,0,1) - halign: 'center' - BoxLayout: - orientation: 'vertical' - padding: dp(5) - canvas.before: - Color: - rgba: app.theme_cls.primary_dark - Rectangle: - # self here refers to the widget i.e FloatLayout - pos: self.pos - size: self.size - MDLabel: - size_hint_y: None - font_style: 'Headline' - theme_text_color: 'Primary' - text: 'Gold' - halign: 'center' - color: 1,1,1,1 - MDLabel: - font_style: 'Subhead' - theme_text_color: 'Primary' - text: 'We provide for proof of work calculation for 1years. ' - halign: 'center' - color: 1,1,1,1 - MDLabel: - font_style: 'Headline' - theme_text_color: 'Primary' - text: '€ 500.0' - halign: 'center' - color: 1,1,1,1 - MDRaisedButton: - canvas: - Color: - rgb: (0.93, 0.93, 0.93) - Rectangle: - pos: self.pos - size: self.size - size_hint: 1, None - height: dp(40) - MDLabel: - font_style: 'Title' - text: 'Get Yearly Credits' - font_size: '13sp' - color: (0,0,0,1) - halign: 'center' - - -: - id: popup - size_hint : (None,None) - height: 2*(label.height + address.height) + 10 - width :app.window_size[0] - (app.window_size[0]/10 if app.app_platform == 'android' else app.window_size[0]/4) - title: 'add contact\'s' - background: './images/popup.jpeg' - title_size: sp(20) - title_color: 0.4, 0.3765, 0.3451, 1 - auto_dismiss: False - separator_color: 0.3529, 0.3922, 0.102, 0.7 - BoxLayout: - size_hint_y: 0.5 - orientation: 'vertical' - spacing:dp(20) - id: popup_box - BoxLayout: - orientation: 'vertical' - MDTextField: - id: label - multiline: False - hint_text: "Label" - required: True - helper_text_mode: "on_error" - on_text: root.checkLabel_valid(self) - MDTextField: - id: address - hint_text: "Address" - required: True - helper_text_mode: "on_error" - on_text: root.checkAddress_valid(self) - BoxLayout: - spacing:5 - orientation: 'horizontal' - MDRaisedButton: - id: save_addr - size_hint: 1.5, None - height: dp(40) - on_release: - root.savecontact() - MDLabel: - font_style: 'Title' - text: 'Save' - 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.close_pop() - MDLabel: - font_style: 'Title' - text: 'Cancel' - font_size: '13sp' - color: (1,1,1,1) - halign: 'center' - MDRaisedButton: - size_hint: 2, None - height: dp(40) - MDLabel: - font_style: 'Title' - text: 'Scan QR code' - font_size: '13sp' - color: (1,1,1,1) - halign: 'center' - - -: - name: 'networkstat' - MDTabbedPanel: - id: tab_panel - tab_display_mode:'text' - - MDTab: - name: 'connections' - text: "Total connections" - ScrollView: - do_scroll_x: False - MDList: - id: ml - size_hint_y: None - height: dp(200) - OneLineListItem: - text: "Total Connections" - BoxLayout: - AnchorLayout: - MDRaisedButton: - size_hint: .6, .35 - height: dp(40) - MDLabel: - font_style: 'Title' - text: root.text_variable_1 - font_size: '13sp' - color: (1,1,1,1) - halign: 'center' - MDTab: - name: 'processes' - text: 'Processes' - ScrollView: - do_scroll_x: False - MDList: - id: ml - size_hint_y: None - height: dp(500) - OneLineListItem: - text: "person-to-person" - BoxLayout: - AnchorLayout: - MDRaisedButton: - size_hint: .7, .6 - height: dp(40) - MDLabel: - font_style: 'Title' - text: root.text_variable_2 - font_size: '13sp' - color: (1,1,1,1) - halign: 'center' - OneLineListItem: - text: "Brodcast" - BoxLayout: - AnchorLayout: - MDRaisedButton: - size_hint: .7, .6 - height: dp(40) - MDLabel: - font_style: 'Title' - text: root.text_variable_3 - font_size: '13sp' - color: (1,1,1,1) - halign: 'center' - OneLineListItem: - text: "publickeys" - BoxLayout: - AnchorLayout: - MDRaisedButton: - size_hint: .7, .6 - height: dp(40) - MDLabel: - font_style: 'Title' - text: root.text_variable_4 - font_size: '13sp' - color: (1,1,1,1) - halign: 'center' - OneLineListItem: - text: "objects" - BoxLayout: - AnchorLayout: - MDRaisedButton: - size_hint: .7, .6 - height: dp(40) - MDLabel: - font_style: 'Title' - text: root.text_variable_5 - font_size: '13sp' - color: (1,1,1,1) - halign: 'center' - -: - name: 'mailDetail' - ScrollView: - do_scroll_x: False - BoxLayout: - orientation: 'vertical' - size_hint_y: None - height: dp(500) + self.minimum_height - padding: dp(32) - MDLabel: - font_style: 'Headline' - theme_text_color: 'Primary' - text: root.subject - halign: 'left' - font_size: '20sp' - CopyTextBtn: - MDLabel: - font_style: 'Subhead' - theme_text_color: 'Primary' - text: "From: " + root.from_addr - halign: 'left' - CopyTextBtn: - MDLabel: - font_style: 'Subhead' - theme_text_color: 'Primary' - text: "To: " + root.to_addr - halign: 'left' - CopyTextBtn: - MDLabel: - font_style: 'Subhead' - theme_text_color: 'Primary' - text: root.status - halign: 'left' - MDLabel: - font_style: 'Subhead' - theme_text_color: 'Primary' - text: root.message - halign: 'left' - bold: True - CopyTextBtn: - BoxLayout: - orientation: 'vertical' - size_hint_y: None - height: dp(100) + self.minimum_height - Loader: - -: - id: cpyButton - color: 0,0,0,1 - background_color: (0,0,0,0) - center_x: self.parent.center_x * 2 - self.parent.parent.padding[0]/2 - center_y: self.parent.center_y - on_press:app.root.ids.sc14.copy_composer_text(self) - Image: - source: './images/copy_text.png' - center_x: self.parent.center_x - center_y: self.parent.center_y - size: 20, 20 - -: - size_hint_y: None - height: dp(56) - spacing: '10dp' - pos_hint: {'center_x':0.45, 'center_y': .1} - - Widget: - - MDFloatingActionButton: - icon: 'plus' - opposite_colors: True - elevation_normal: 8 - md_bg_color: [0.941, 0, 0,1] - on_press: app.root.ids.scr_mngr.current = 'create' - on_press: app.clear_composer() - -: - id: myadd_popup - size_hint : (None,None) - height: 4.5*(myaddr_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: './images/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: myaddr_label - font_style: 'Title' - theme_text_color: 'Primary' - text: "Label" - font_size: '17sp' - halign: 'left' - MDLabel: - font_style: 'Subhead' - theme_text_color: 'Primary' - text: root.address_label - font_size: '15sp' - halign: 'left' - MDLabel: - font_style: 'Title' - theme_text_color: 'Primary' - text: "Address" - font_size: '17sp' - halign: 'left' - MDLabel: - font_style: 'Subhead' - theme_text_color: 'Primary' - text: root.address - font_size: '15sp' - halign: 'left' - BoxLayout: - id: my_add_btn - spacing:5 - orientation: 'horizontal' - MDRaisedButton: - size_hint: 2, None - height: dp(40) - on_press: root.send_message_from() - MDLabel: - font_style: 'Title' - text: 'Send message from' - 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: app.root.ids.scr_mngr.current = 'showqrcode' - on_press: app.root.ids.sc15.qrdisplay() - MDLabel: - font_style: 'Title' - text: '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.dismiss() - on_press: root.close_pop() - MDLabel: - font_style: 'Title' - text: 'Cancel' - font_size: '13sp' - color: (1,1,1,1) - halign: 'center' - -: - id: addbook_popup - size_hint : (None,None) - height: 4*(add_label.height) - width :app.window_size[0] - (app.window_size[0]/10 if app.app_platform == 'android' else app.window_size[0]/4) - background: './images/popup.jpeg' - separator_height: 0 - auto_dismiss: False - BoxLayout: - size_hint_y: None - spacing:dp(70) - id: addbook_popup_box - orientation: 'vertical' - BoxLayout: - size_hint_y: None - orientation: 'vertical' - spacing:dp(20) - MDLabel: - font_style: 'Title' - theme_text_color: 'Primary' - text: "Label" - font_size: '17sp' - halign: 'left' - MDTextField: - id: add_label - font_style: 'Subhead' - font_size: '15sp' - halign: 'left' - text: root.address_label - theme_text_color: 'Primary' - required: True - helper_text_mode: "on_error" - on_text: root.checkLabel_valid(self) - MDLabel: - font_style: 'Title' - theme_text_color: 'Primary' - text: "Address" - font_size: '17sp' - halign: 'left' - MDLabel: - id: address - font_style: 'Subhead' - theme_text_color: 'Primary' - text: root.address - font_size: '15sp' - halign: 'left' - BoxLayout: - id: addbook_btn - spacing:5 - orientation: 'horizontal' - MDRaisedButton: - size_hint: 2, None - height: dp(40) - on_press: root.send_message_to() - MDLabel: - font_style: 'Title' - text: 'Send message to' - font_size: '13sp' - color: (1,1,1,1) - halign: 'center' - MDRaisedButton: - size_hint: 1.5, None - height: dp(40) - font_size: '10sp' - on_press: root.update_addbook_label(root.address) - MDLabel: - font_style: 'Title' - text: 'Save' - 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.close_pop() - MDLabel: - font_style: 'Title' - text: 'Cancel' - font_size: '13sp' - color: (1,1,1,1) - halign: 'center' - -: - name: 'showqrcode' - BoxLayout: - orientation: 'vertical' - id: qr - - -: - source: './images/down-arrow.png' if self.parent.is_open == True else './images/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) - - -: - separator_color: 1, 1, 1, 1 - background: "White.png" - Button: - id: btn - disabled: True - background_disabled_normal: "White.png" - Image: - source: './images/loader.zip' - anim_delay: 0 - #mipmap: True - size: root.size - - -: - id: spinner - size_hint: None, None - size: dp(46), dp(46) - pos_hint: {'center_x': 0.5, 'center_y': 0.5} - active: False \ No newline at end of file +: + 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 78921918..3f9b198b 100644 --- a/src/bitmessagekivy/mpybit.py +++ b/src/bitmessagekivy/mpybit.py @@ -1,2523 +1,393 @@ -""" -Bitmessage android(mobile) interface -""" -# pylint: disable=relative-import, import-error, no-name-in-module -# pylint: disable=too-few-public-methods, too-many-lines, unused-argument -import os -import time -from bmconfigparser import BMConfigParser -from functools import partial -from helper_sql import sqlExecute, sqlQuery -from kivy.app import App -from kivy.clock import Clock -from kivy.core.clipboard import Clipboard -from kivy.core.window import Window -from kivy.lang import Builder -from kivy.properties import ( - BooleanProperty, - ListProperty, - NumericProperty, - ObjectProperty, - StringProperty -) -from kivy.uix.behaviors import FocusBehavior -from kivy.uix.boxlayout import BoxLayout -from kivy.uix.button import Button -from kivy.uix.carousel import Carousel -from kivy.uix.image import Image -from kivy.uix.label import Label -from kivy.uix.popup import Popup -from kivy.uix.recycleboxlayout import RecycleBoxLayout -from kivy.uix.recycleview import RecycleView -from kivy.uix.recycleview.layout import LayoutSelectionBehavior -from kivy.uix.recycleview.views import RecycleDataViewBehavior -from kivy.uix.screenmanager import Screen -from kivy.uix.spinner import Spinner -from kivy.uix.textinput import TextInput -from kivy.utils import platform - import kivy_helper_search -from kivymd.button import MDIconButton -from kivymd.dialog import MDDialog -from kivymd.label import MDLabel -from kivymd.list import ( - ILeftBody, - ILeftBodyTouch, - IRightBodyTouch, - TwoLineAvatarIconListItem, - TwoLineListItem -) -from kivymd.navigationdrawer import ( - MDNavigationDrawer, - NavigationDrawerHeaderBase -) -from kivymd.selectioncontrols import MDCheckbox -from kivymd.theming import ThemeManager - +import os import queues -from semaphores import kivyuisignaler - +import shutdown import state -from uikivysignaler import UIkivySignaler +import time -import identiconGeneration -from addresses import addBMIfNotPresent, decodeAddress -import helper_sent +from kivy.app import App +from kivy.lang import Builder +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 + +statusIconColor = 'red' -def toast(text): - """Function displays toast message""" - # pylint: disable=redefined-outer-name - from kivymd.toast.kivytoast import toast - toast(text) - return +class NavigateApp(App, TextInput): + """Application uses kivy in which base Class of Navigate App inherits from the App class.""" + + theme_cls = ThemeManager() + nav_drawer = ObjectProperty() + + def build(self): + """Return a main_widget as a root widget. + + 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.""" + 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 say_exit(self): + """Exit the application as uses shutdown PyBitmessage.""" + print("**************************EXITING FROM APPLICATION*****************************") + App.get_running_app().stop() + shutdown.doCleanShutdown() + + @staticmethod + 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 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 delete(self, data_index): + """It will make delete using remove function.""" + print("delete {}".format(data_index)) + self._remove(data_index) + + def archive(self, data_index): + """It will make archieve using remove function.""" + print("archive {}".format(data_index)) + self._remove(data_index) + + 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 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)) + + @staticmethod + def getCurrentAccount(): + """It uses to get current account label.""" + return BMConfigParser().get(state.association, 'label') + '({})'.format(state.association) -class CustomAvatarIconListItem(TwoLineAvatarIconListItem): +class Navigator(NavigationDrawer): + """Navigator class uses NavigationDrawer. - def __init__(self, **kwargs): - super(CustomAvatarIconListItem, self).__init__(**kwargs) - self.text_color=[0.12, 0.58, 0.95, 1] + 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 + """ - -class Navigatorss(MDNavigationDrawer): - """Navigator class (image, title and logo)""" image_source = StringProperty('images/qidenticon_two.png') title = StringProperty('Navigation') - drawer_logo = StringProperty() class Inbox(Screen): """Inbox Screen uses screen to show widgets of screens.""" - queryreturn = ListProperty() - has_refreshed = True - account = StringProperty() + + data = ListProperty() def __init__(self, *args, **kwargs): - """Method Parsing the address.""" super(Inbox, self).__init__(*args, **kwargs) - Clock.schedule_once(self.init_ui, 0) - - @staticmethod - def set_defaultAddress(): - """This method set's default address""" if state.association == '': - if BMConfigParser().addresses(): - state.association = BMConfigParser().addresses()[0] - - def init_ui(self, dt=0): - """Clock schdule for method inbox accounts.""" - self.loadMessagelist() - - def loadMessagelist(self, where="", what=""): - """Load Inbox list for Inbox messages.""" - # pylint: disable=too-many-locals - self.set_defaultAddress() - self.account = state.association - if state.searcing_text: - self.children[2].children[0].children[0].scroll_y = 1.0 - where = ['subject', 'message'] - what = state.searcing_text - xAddress = 'toaddress' - data = [] - self.inboxDataQuery(xAddress, where, what) - self.ids.identi_tag.children[0].text = '' - if self.queryreturn: - self.ids.identi_tag.children[0].text = 'Inbox' - state.kivyapp.get_inbox_count() - src_mng_obj = state.kivyapp.root.children[2].children[0].ids - src_mng_obj.inbox_cnt.badge_text = state.inbox_count - for mail in self.queryreturn: - # third_text = mail[3].replace('\n', ' ') - data.append({ - 'text': mail[4].strip(), - 'secondary_text': mail[5][:50] + '........' if len( - mail[5]) >= 50 else (mail[5] + ',' + mail[3].replace( - '\n', ''))[0:50] + '........', - 'msgid': mail[1]}) - self.has_refreshed = True - self.set_mdList(data) - self.children[2].children[0].children[0].bind( - scroll_y=self.check_scroll_y) - else: - content = MDLabel( - font_style='Body1', theme_text_color='Primary', - text="No message found!" if state.searcing_text - else "yet no message for this account!!!!!!!!!!!!!", - halign='center', bold=True, size_hint_y=None, valign='top') - self.ids.ml.add_widget(content) - - # pylint: disable=too-many-arguments - def inboxDataQuery(self, xAddress, where, what, start_indx=0, end_indx=20): - """This method used for retrieving inbox data""" - self.queryreturn = kivy_helper_search.search_sql( - xAddress, self.account, "inbox", where, what, - False, start_indx, end_indx) - - def set_mdList(self, data): - """This method is used to create the mdList""" - total_message = len(self.ids.ml.children) - for item in data: - meny = CustomAvatarIconListItem( - text=item['text'], secondary_text=item['secondary_text'], - theme_text_color='Custom', - ) - meny.add_widget(AvatarSampleWidget( - source='./images/text_images/{}.png'.format( - avatarImageFirstLetter(item['secondary_text'].strip())))) - meny.bind(on_press=partial(self.inbox_detail, item['msgid'])) - carousel = Carousel(direction='right') - carousel.height = meny.height - carousel.size_hint_y = None - carousel.ignore_perpendicular_swipes = True - carousel.data_index = 0 - carousel.min_move = 0.2 - del_btn = Button(text='Delete') - del_btn.background_normal = '' - del_btn.background_color = (1, 0, 0, 1) - del_btn.bind(on_press=partial(self.delete, item['msgid'])) - carousel.add_widget(del_btn) - carousel.add_widget(meny) - ach_btn = Button(text='Achieve') - ach_btn.background_color = (0, 1, 0, 1) - ach_btn.bind(on_press=partial(self.archive, item['msgid'])) - carousel.add_widget(ach_btn) - carousel.index = 1 - self.ids.ml.add_widget(carousel) - update_message = len(self.ids.ml.children) - self.has_refreshed = True if total_message != update_message else False - - def check_scroll_y(self, instance, somethingelse): - """Loads data on scroll""" - if self.children[2].children[0].children[ - 0].scroll_y <= -0.0 and self.has_refreshed: - self.children[2].children[0].children[0].scroll_y = 0.06 - total_message = len(self.ids.ml.children) - self.update_inbox_screen_on_scroll(total_message) - else: - pass - - def update_inbox_screen_on_scroll(self, total_message, where="", what=""): - """This method is used to load more data on scroll down""" - data = [] - if state.searcing_text: - where = ['subject', 'message'] - what = state.searcing_text - self.inboxDataQuery('toaddress', where, what, total_message, 5) - for mail in self.queryreturn: - # third_text = mail[3].replace('\n', ' ') - data.append({ - 'text': mail[4].strip(), - 'secondary_text': mail[5][:50] + '........' if len( - mail[5]) >= 50 else (mail[5] + ',' + mail[3].replace( - '\n', ''))[0:50] + '........', - 'msgid': mail[1]}) - self.set_mdList(data) - - def inbox_detail(self, msg_id, *args): - """Load inbox page details""" - state.detailPageType = 'inbox' - state.mail_id = msg_id - if self.manager: - src_mng_obj = self.manager - else: - src_mng_obj = self.parent.parent - src_mng_obj.screens[13].clear_widgets() - src_mng_obj.screens[13].add_widget(MailDetail()) - src_mng_obj.current = 'mailDetail' - - def delete(self, data_index, instance, *args): - """Delete inbox mail from inbox listing""" - sqlExecute( - "UPDATE inbox SET folder = 'trash' WHERE msgid = ?;", str( - data_index)) - try: - msg_count_objs = ( - self.parent.parent.parent.parent.children[2].children[0].ids) - except Exception: - msg_count_objs = ( - self.parent.parent.parent.parent.parent.children[ - 2].children[0].ids) - if int(state.inbox_count) > 0: - msg_count_objs.inbox_cnt.badge_text = str( - int(state.inbox_count) - 1) - msg_count_objs.trash_cnt.badge_text = str( - int(state.trash_count) + 1) - msg_count_objs.allmail_cnt.badge_text = str( - int(state.all_count) - 1) - state.inbox_count = str( - int(state.inbox_count) - 1) - state.trash_count = str( - int(state.trash_count) + 1) - state.all_count = str( - int(state.all_count) - 1) - if int(state.inbox_count) <= 0: - self.ids.identi_tag.children[0].text = '' - self.ids.ml.remove_widget( - instance.parent.parent) - toast('Deleted') - self.update_trash() - - def archive(self, data_index, instance, *args): - """Archive inbox mail from inbox listing""" - sqlExecute( - "UPDATE inbox SET folder = 'trash' WHERE msgid = ?;", - str(data_index)) - self.ids.ml.remove_widget(instance.parent.parent) - self.update_trash() - - def update_trash(self): - """Update trash screen mails which is deleted from inbox""" - try: - self.parent.screens[4].clear_widgets() - self.parent.screens[4].add_widget(Trash()) - except Exception: - self.parent.parent.screens[4].clear_widgets() - self.parent.parent.screens[4].add_widget(Trash()) - - # pylint: disable=attribute-defined-outside-init - 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 inbox screen data""" - state.searcing_text = '' - self.children[2].children[1].ids.search_field.text = '' - self.ids.ml.clear_widgets() - self.loadMessagelist(state.association) - self.has_refreshed = True - self.ids.refresh_layout.refresh_done() - self.tick = 0 - Clock.schedule_once(refresh_callback, 1) - - # def set_root_layout(self): - # """Setting root layout""" - # return self.parent.parent.parent - - -class MyAddress(Screen): - """MyAddress screen uses screen to show widgets of screens.""" - addresses_list = ListProperty() - has_refreshed = True - is_add_created = False - - def __init__(self, *args, **kwargs): - """Clock schdule for method Myaddress accounts.""" - super(MyAddress, self).__init__(*args, **kwargs) + state.association = Navigator().ids.btn.text Clock.schedule_once(self.init_ui, 0) def init_ui(self, dt=0): - """Clock schdule for method Myaddress accounts""" - self.addresses_list = state.kivyapp.variable_1 - if state.searcing_text: - self.ids.refresh_layout.scroll_y = 1.0 - # filtered_list = filter( - # lambda addr: self.filter_address( - # addr), BMConfigParser().addresses()) - filtered_list = [ - x - for x in BMConfigParser().addresses() - if self.filter_address(x)] - self.addresses_list = filtered_list - self.addresses_list = [obj for obj in reversed(self.addresses_list)] - self.ids.identi_tag.children[0].text = '' - if self.addresses_list: - self.ids.identi_tag.children[0].text = 'My Addresses' - self.has_refreshed = True - self.set_mdList(0, 15) - self.ids.refresh_layout.bind(scroll_y=self.check_scroll_y) + """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: - content = MDLabel( - font_style='Body1', theme_text_color='Primary', - text="No address found!" if state.searcing_text - else "yet no address is created by user!!!!!!!!!!!!!", - halign='center', bold=True, size_hint_y=None, valign='top') - self.ids.ml.add_widget(content) - if not state.searcing_text and not self.is_add_created: - try: - self.manager.current = 'login' - except Exception: - pass - - def set_mdList(self, first_index, last_index): - """Creating the mdlist""" - data = [] - for address in self.addresses_list[first_index:last_index]: - data.append({ - 'text': BMConfigParser().get(address, 'label'), - 'secondary_text': address}) - for item in data: - meny = CustomAvatarIconListItem( - text=item['text'], secondary_text=item['secondary_text'], - theme_text_color='Custom') - meny.add_widget(AvatarSampleWidget( - source='./images/text_images/{}.png'.format( - avatarImageFirstLetter(item['text'].strip())))) - meny.bind(on_press=partial( - self.myadd_detail, item['secondary_text'], item['text'])) - self.ids.ml.add_widget(meny) - - def check_scroll_y(self, instance, somethingelse): - """Load data on scroll down""" - if self.ids.refresh_layout.scroll_y <= -0.0 and self.has_refreshed: - self.ids.refresh_layout.scroll_y = 0.06 - 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 - else: - pass - - def update_addressBook_on_scroll(self, my_addresses): - """Loads more data on scroll down""" - self.set_mdList(my_addresses, my_addresses + 20) - - @staticmethod - def myadd_detail(fromaddress, label, *args): - """Load myaddresses details""" - p = MyaddDetailPopup() - p.open() - p.set_address(fromaddress, label) - - # pylint: disable=attribute-defined-outside-init - 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""" - state.searcing_text = '' - state.kivyapp.root.ids.sc10.children[2].active = False - self.children[2].children[2].ids.search_field.text = '' - self.has_refreshed = True - self.ids.ml.clear_widgets() - self.init_ui() - self.ids.refresh_layout.refresh_done() - self.tick = 0 - Clock.schedule_once(refresh_callback, 1) - - @staticmethod - def filter_address(address): - """Method will filter the my address list data""" - # if filter(lambda x: (state.searcing_text).lower() in x, [ - # BMConfigParser().get( - # address, 'label').lower(), address.lower()]): - if [x for x in [BMConfigParser().get( - address, 'label').lower(), address.lower()] if ( - state.searcing_text).lower() in x]: - return True - return False - - def set_root_layout(self): - """Setting root layout""" - return self.manager.parent.parent - - -class AddressBook(Screen): - """AddressBook Screen uses screen to show widgets of screens""" - queryreturn = ListProperty() - has_refreshed = True - - def __init__(self, *args, **kwargs): - """Getting AddressBook Details""" - super(AddressBook, self).__init__(*args, **kwargs) - Clock.schedule_once(self.init_ui, 0) - - def init_ui(self, dt=0): - """Clock Schdule for method AddressBook""" - self.loadAddresslist(None, 'All', '') - print dt - - def loadAddresslist(self, account, where="", what=""): - """Clock Schdule for method AddressBook""" - if state.searcing_text: - self.ids.scroll_y.scroll_y = 1.0 - where = ['label', 'address'] - what = state.searcing_text - xAddress = '' - self.ids.identi_tag.children[0].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.identi_tag.children[0].text = 'Address Book' - self.has_refreshed = True - self.set_mdList(0, 20) - self.ids.scroll_y.bind(scroll_y=self.check_scroll_y) - else: - content = MDLabel( - font_style='Body1', theme_text_color='Primary', - text="No contact found!" if state.searcing_text - else "No contact found yet...... ", - halign='center', bold=True, size_hint_y=None, valign='top') - self.ids.ml.add_widget(content) - - def set_mdList(self, start_index, end_index): - """Creating the mdList""" - for item in self.queryreturn[start_index:end_index]: - meny = CustomAvatarIconListItem( - text=item[0], secondary_text=item[1], theme_text_color='Custom') - meny.add_widget(AvatarSampleWidget( - source='./images/text_images/{}.png'.format( - avatarImageFirstLetter(item[0].strip())))) - meny.bind(on_press=partial( - self.addBook_detail, item[1], item[0])) - carousel = Carousel(direction='right') - carousel.height = meny.height - carousel.size_hint_y = None - carousel.ignore_perpendicular_swipes = True - carousel.data_index = 0 - carousel.min_move = 0.2 - del_btn = Button(text='Delete') - del_btn.background_normal = '' - del_btn.background_color = (1, 0, 0, 1) - del_btn.bind(on_press=partial(self.delete_address, item[1])) - carousel.add_widget(del_btn) - carousel.add_widget(meny) - carousel.index = 1 - self.ids.ml.add_widget(carousel) - - 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 - else: - pass - - 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""" - # state.navinstance.ids.sc11.ids.ml.clear_widgets() - # state.navinstance.ids.sc11.loadAddresslist(None, 'All', '') - pass - - @staticmethod - def addBook_detail(address, label, *args): - """Addressbook details""" - p = AddbookDetailPopup() - p.open() - p.set_addbook_data(address, label) - - 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: - self.ids.identi_tag.children[0].text = '' - sqlExecute( - "DELETE FROM addressbook WHERE address = '{}';".format(address)) - - -class SelectableRecycleBoxLayout( - FocusBehavior, LayoutSelectionBehavior, RecycleBoxLayout): - """Adds selection and focus behaviour to the view""" - # pylint: disable = too-many-ancestors, duplicate-bases - pass - - -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) - - # pylint: disable=inconsistent-return-statements - def on_touch_down(self, touch): - """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: - print "selection changed to {0}".format(rv.data[index]) - rv.parent.txt_input.text = rv.parent.txt_input.text.replace( - rv.parent.txt_input.text, rv.data[index]['text']) - - -class RV(RecycleView): - """Recycling View""" - - def __init__(self, **kwargs): # pylint: disable=useless-super-delegation - """Recycling Method""" - super(RV, self).__init__(**kwargs) - - -class DropDownWidget(BoxLayout): - """Adding Dropdown Widget""" - # pylint: disable=too-many-statements, too-many-locals - # pylint: disable=inconsistent-return-statements - txt_input = ObjectProperty() - rv = ObjectProperty() - - def send(self, navApp): - """Send message from one address to another""" - fromAddress = str(self.ids.ti.text) - toAddress = str(self.ids.txt_input.text) - subject = self.ids.subject.text.encode('utf-8').strip() - message = self.ids.body.text.encode('utf-8').strip() - encoding = 3 - print "message: ", self.ids.body.text - sendMessageToPeople = True - if sendMessageToPeople: - if toAddress != '' and subject and message: - status, addressVersionNumber, streamNumber, ripe = ( - decodeAddress(toAddress)) - valid_from_add = True if fromAddress in state.kivyapp.variable_1 else False - if status == 'success' and valid_from_add: - navApp.root.ids.sc3.children[0].active = True - if state.detailPageType == 'draft' \ - and state.send_draft_mail: - sqlExecute( - "UPDATE sent SET toaddress = ?, fromaddress = ? ," - " subject = ?, message = ?, folder = 'sent' WHERE" - " ackdata = ?;", toAddress, fromAddress, subject, - message, str(state.send_draft_mail)) - self.parent.parent.screens[15].clear_widgets() - self.parent.parent.screens[15].add_widget(Draft()) - else: - toAddress = addBMIfNotPresent(toAddress) - statusIconColor = 'red' - 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') - from helper_ackPayload import genAckPayload - ackdata = genAckPayload(streamNumber, stealthLevel) - t = ( - '', - toAddress, - ripe, - fromAddress, - subject, - message, - ackdata, - int(time.time()), - int(time.time()), - 0, - 'msgqueued', - 0, - 'sent', - encoding, - BMConfigParser().getint('bitmessagesettings', 'ttl') - ) - helper_sent.insert(t) - state.check_sent_acc = fromAddress - state.msg_counter_objs = self.parent.parent.parent.parent\ - .parent.parent.children[2].children[0].ids - if state.detailPageType == 'draft' and state.send_draft_mail: - state.draft_count = str(int(state.draft_count) - 1) - state.msg_counter_objs.draft_cnt.badge_text = state.draft_count - state.detailPageType = '' - state.send_draft_mail = None - # self.parent.parent.screens[0].ids.ml.clear_widgets() - # self.parent.parent.screens[0].loadMessagelist(state.association) - self.parent.parent.screens[3].update_sent_messagelist() - # self.parent.parent.screens[16].clear_widgets() - # self.parent.parent.screens[16].add_widget(Allmails()) - Clock.schedule_once(self.callback_for_msgsend, 3) - queues.workerQueue.put(('sendmessage', toAddress)) - print "sqlExecute successfully #######################" - state.in_composer = True - return - elif valid_from_add is False: - msg = 'Please enter valid sender address' - else: - msg = 'Enter a valid recipients address' - elif not toAddress: - msg = 'Please fill the form' - else: - msg = 'Please fill the form' - self.address_error_message(msg) - - @staticmethod - def callback_for_msgsend(dt=0): - """Callback method for messagesend""" - state.kivyapp.root.ids.sc3.children[0].active = False - state.in_sent_method = True - state.kivyapp.back_press() - toast('sent') - - # pylint: disable=attribute-defined-outside-init - def address_error_message(self, msg): - """Generates error message""" - width = .8 if platform == 'android' else .55 - msg_dialog = MDDialog( - text=msg, - title='', size_hint=(width, .25), text_button_ok='Ok', - events_callback=self.callback_for_menu_items) - msg_dialog.open() - - @staticmethod - def callback_for_menu_items(text_item): - """Callback of alert box""" - toast(text_item) - - def reset_composer(self): - """Method will reset composer""" - self.ids.ti.text = '' - self.ids.btn.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.btn.text - self.ids.ti.focus = True - - -class MyTextInput(TextInput): - """Takes the text input in the field""" - txt_input = ObjectProperty() - flt_list = ObjectProperty() - word_list = ListProperty() - starting_no = NumericProperty(3) - suggestion_text = '' - - def __init__(self, **kwargs): # pylint: disable=useless-super-delegation - """Getting Text Input""" - super(MyTextInput, self).__init__(**kwargs) - - def on_text(self, instance, value): - """Find all the occurrence of the word""" - self.parent.parent.parent.parent.ids.rv.data = [] - 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.ids.rv.data = display_data - if len(matches) <= 10: - self.parent.height = (250 + (len(matches) * 20)) - else: - self.parent.height = 400 - - def keyboard_on_key_down(self, window, keycode, text, modifiers): - """Keyboard on key Down""" - if self.suggestion_text and keycode[1] == 'tab': - self.insert_text(self.suggestion_text + ' ') - return True - return super(MyTextInput, self).keyboard_on_key_down( - window, keycode, text, modifiers) - - -class Payment(Screen): - """Payment module""" - - def get_available_credits(self, instance): # pylint: disable=no-self-use - """Get the available credits""" - state.availabe_credit = instance.parent.children[1].text - existing_credits = ( - state.kivyapp.root.ids.sc18.ids.ml.children[0].children[ - 0].children[0].children[0].text) - if len(existing_credits.split(' ')) > 1: - toast( - 'We already have added free coins' - ' for the subscription to your account!') - else: - toast('Coins added to your account!') - state.kivyapp.root.ids.sc18.ids.ml.children[0].children[ - 0].children[0].children[0].text = '{0}'.format( - state.availabe_credit) - - -class Credits(Screen): - """Module for screen screen""" - available_credits = StringProperty('{0}'.format('0')) - - -class Login(Screen): - """Login Screeen""" - pass - - -class NetworkStat(Screen): - """Method used to show network stat""" - 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""" - import network.stats - import shared - from network import objectracker - self.text_variable_1 = '{0} :: {1}'.format( - 'Total Connections', str(len(network.stats.connectedHostsList()))) - self.text_variable_2 = 'Processed {0} per-to-per messages'.format( - str(shared.numberOfMessagesProcessed)) - self.text_variable_3 = 'Processed {0} brodcast messages'.format( - str(shared.numberOfBroadcastsProcessed)) - self.text_variable_4 = 'Processed {0} public keys'.format( - str(shared.numberOfPubkeysProcessed)) - self.text_variable_5 = '{0} object to be synced'.format( - len(objectracker.missingObjects)) - - -class ContentNavigationDrawer(Navigatorss): - """Navigate Content Drawer""" - pass - - -class Random(Screen): - """Generates Random Address""" - is_active = BooleanProperty(False) - checked = StringProperty("") - - def generateaddress(self, navApp): - """Method for Address Generator""" - entered_label = str(self.ids.label.text).strip() - streamNumberForAddress = 1 - label = self.ids.label.text - eighteenByteRipe = False - nonceTrialsPerByte = 1000 - payloadLengthExtraBytes = 1000 - lables = [BMConfigParser().get(obj, 'label') - for obj in BMConfigParser().addresses()] - if entered_label and entered_label not in lables: - toast('Address Creating...') - queues.addressGeneratorQueue.put(( - 'createRandomAddress', 4, streamNumberForAddress, label, 1, - "", eighteenByteRipe, nonceTrialsPerByte, - payloadLengthExtraBytes)) - self.ids.label.text = '' - self.parent.parent.children[1].opacity = 1 - self.parent.parent.children[1].disabled = False - state.kivyapp.root.ids.sc10.children[1].active = True - self.manager.current = 'myaddress' - Clock.schedule_once(self.address_created_callback, 6) - - def address_created_callback(self, dt=0): - """New address created""" - state.kivyapp.root.ids.sc10.children[1].active = False - state.kivyapp.root.ids.sc10.ids.ml.clear_widgets() - state.kivyapp.root.ids.sc10.is_add_created = True - state.kivyapp.root.ids.sc10.init_ui() - self.reset_address_spinner() - toast('New address created') - - def reset_address_spinner(self): - """reseting spinner address and UI""" - addresses = BMConfigParser().addresses() - self.manager.parent.parent.parent.parent.ids.nav_drawer.ids.btn.values = [] - self.manager.parent.parent.parent.parent.ids.sc3.children[1].ids.btn.values = [] - self.manager.parent.parent.parent.parent.ids.nav_drawer.ids.btn.values = addresses - self.manager.parent.parent.parent.parent.ids.sc3.children[1].ids.btn.values = addresses - - def add_validation(self, instance): - """Checking validation at address creation time""" - entered_label = str(instance.text.strip()) - lables = [BMConfigParser().get(obj, 'label') - for obj in BMConfigParser().addresses()] - if entered_label in lables: - self.ids.label.error = True - self.ids.label.helper_text = 'Label name is already exist you'\ - ' can try this Ex. ( {0}_1, {0}_2 )'.format( - entered_label) - elif entered_label: - self.ids.label.error = False - else: - self.ids.label.error = False - self.ids.label.helper_text = 'This field is required' - - def reset_address_label(self): - """Resetting address labels""" - self.ids.label.text = '' - - -class Sent(Screen): - """Sent Screen uses screen to show widgets of screens""" - queryreturn = ListProperty() - has_refreshed = True - account = StringProperty() - - def __init__(self, *args, **kwargs): - """Association with the screen.""" - super(Sent, self).__init__(*args, **kwargs) - if state.association == '': - if BMConfigParser().addresses(): - state.association = BMConfigParser().addresses()[0] - Clock.schedule_once(self.init_ui, 0) - - def init_ui(self, dt=0): - """Clock Schdule for method sent accounts""" - self.loadSent() - print dt - - def loadSent(self, where="", what=""): - """Load Sent list for Sent messages.""" - self.account = state.association - if state.searcing_text: - self.ids.scroll_y.scroll_y = 1.0 - where = ['subject', 'message'] - what = state.searcing_text - xAddress = 'fromaddress' - data = [] - self.ids.identi_tag.children[0].text = '' - self.sentDataQuery(xAddress, where, what) - if self.queryreturn: - self.ids.identi_tag.children[0].text = 'Sent' - self.set_sentCount(state.sent_count) - for mail in self.queryreturn: - data.append({ - 'text': mail[1].strip(), - 'secondary_text': mail[2][:50] + '........' if len( - mail[2]) >= 50 else (mail[2] + ',' + mail[3].replace( - '\n', ''))[0:50] + '........', - 'ackdata': mail[5]}) - self.set_mdlist(data, 0) - self.has_refreshed = True - self.ids.scroll_y.bind(scroll_y=self.check_scroll_y) - else: - content = MDLabel( - font_style='Body1', theme_text_color='Primary', - text="No message found!" if state.searcing_text - else "yet no message for this account!!!!!!!!!!!!!", - halign='center', bold=True, size_hint_y=None, valign='top') - self.ids.ml.add_widget(content) - - # pylint: disable=too-many-arguments - def sentDataQuery(self, xAddress, where, what, start_indx=0, end_indx=20): - """This method is used to retrieving data from sent table""" - self.queryreturn = kivy_helper_search.search_sql( - xAddress, self.account, "sent", where, what, - False, start_indx, end_indx) - - def set_mdlist(self, data, set_index=0): - """This method is used to create the mdList""" - total_sent_msg = len(self.ids.ml.children) - for item in data: - meny = CustomAvatarIconListItem( - text=item['text'], secondary_text=item['secondary_text'], - theme_text_color='Custom') - meny.add_widget(AvatarSampleWidget( - source='./images/text_images/{}.png'.format( - avatarImageFirstLetter(item['secondary_text'].strip())))) - meny.bind(on_press=partial(self.sent_detail, item['ackdata'])) - carousel = Carousel(direction='right') - carousel.height = meny.height - carousel.size_hint_y = None - carousel.ignore_perpendicular_swipes = True - carousel.data_index = 0 - carousel.min_move = 0.2 - del_btn = Button(text='Delete') - del_btn.background_normal = '' - del_btn.background_color = (1, 0, 0, 1) - del_btn.bind(on_press=partial(self.delete, item['ackdata'])) - carousel.add_widget(del_btn) - carousel.add_widget(meny) - ach_btn = Button(text='Achieve') - ach_btn.background_color = (0, 1, 0, 1) - ach_btn.bind(on_press=partial(self.archive, item['ackdata'])) - carousel.add_widget(ach_btn) - carousel.index = 1 - self.ids.ml.add_widget(carousel, index=set_index) - updated_msgs = len(self.ids.ml.children) - self.has_refreshed = True if total_sent_msg != updated_msgs else False - - def update_sent_messagelist(self): - """This method is used to update screen when new mail is sent""" - self.account = state.association - if len(self.ids.ml.children) < 3: - self.ids.ml.clear_widgets() - self.loadSent() - total_sent = int(state.sent_count) + 1 - self.set_sentCount(total_sent) - else: - data = [] - self.sentDataQuery('fromaddress', '', '', 0, 1) - total_sent = int(state.sent_count) + 1 - self.set_sentCount(total_sent) - for mail in self.queryreturn: - data.append({ - 'text': mail[1].strip(), - 'secondary_text': mail[2][:50] + '........' if len( - mail[2]) >= 50 else (mail[2] + ',' + mail[3].replace( - '\n', ''))[0:50] + '........', - 'ackdata': mail[5]}) - self.set_mdlist(data, total_sent - 1) - if state.msg_counter_objs and state.association == ( - state.check_sent_acc): - state.all_count = str(int(state.all_count) + 1) - state.msg_counter_objs.allmail_cnt.badge_text = state.all_count - state.check_sent_acc = None - - def check_scroll_y(self, instance, somethingelse): - """Load data on scroll down""" - if self.ids.scroll_y.scroll_y <= -0.0 and self.has_refreshed: - self.ids.scroll_y.scroll_y = 0.06 - total_sent_msg = len(self.ids.ml.children) - self.update_sent_screen_on_scroll(total_sent_msg) - else: - pass - - def update_sent_screen_on_scroll(self, total_sent_msg, where="", what=""): - """This method is used to load more data on scroll down""" - if state.searcing_text: - where = ['subject', 'message'] - what = state.searcing_text - self.sentDataQuery('fromaddress', where, what, total_sent_msg, 5) - data = [] - for mail in self.queryreturn: - data.append({ - 'text': mail[1].strip(), - 'secondary_text': mail[2][:50] + '........' if len( - mail[2]) >= 50 else (mail[2] + ',' + mail[3].replace( - '\n', ''))[0:50] + '........', - 'ackdata': mail[5]}) - self.set_mdlist(data, 0) - - @staticmethod - def set_sentCount(total_sent): - """Set the total no. of sent message count""" - src_mng_obj = state.kivyapp.root.children[2].children[0].ids - src_mng_obj.send_cnt.badge_text = str(total_sent) - state.sent_count = str(total_sent) - - def sent_detail(self, ackdata, *args): - """Load sent mail details""" - state.detailPageType = 'sent' - state.mail_id = ackdata - if self.manager: - src_mng_obj = self.manager - else: - src_mng_obj = self.parent.parent - src_mng_obj.screens[13].clear_widgets() - src_mng_obj.screens[13].add_widget(MailDetail()) - src_mng_obj.current = 'mailDetail' - - def delete(self, data_index, instance, *args): - """Delete sent mail from sent mail listing""" - try: - msg_count_objs = self.parent.parent.parent.parent.children[ - 2].children[0].ids - except Exception: - msg_count_objs = self.parent.parent.parent.parent.parent.children[ - 2].children[0].ids - if int(state.sent_count) > 0: - msg_count_objs.send_cnt.badge_text = str( - int(state.sent_count) - 1) - msg_count_objs.trash_cnt.badge_text = str( - int(state.trash_count) + 1) - msg_count_objs.allmail_cnt.badge_text = str( - int(state.all_count) - 1) - state.sent_count = str(int(state.sent_count) - 1) - state.trash_count = str(int(state.trash_count) + 1) - state.all_count = str(int(state.all_count) - 1) - if int(state.sent_count) <= 0: - self.ids.identi_tag.children[0].text = '' - sqlExecute( - "UPDATE sent SET folder = 'trash'" - " WHERE ackdata = ?;", str(data_index)) - self.ids.ml.remove_widget(instance.parent.parent) - toast('Deleted') - self.update_trash() - - def archive(self, data_index, instance, *args): - """Archive sent mail from sent mail listing""" - sqlExecute( - "UPDATE sent SET folder = 'trash' WHERE ackdata = ?;", - str(data_index)) - self.ids.ml.remove_widget(instance.parent.parent) - self.update_trash() - - def update_trash(self): - """Update trash screen mails which is deleted from inbox""" - try: - self.parent.screens[4].clear_widgets() - self.parent.screens[4].add_widget(Trash()) - self.parent.screens[16].clear_widgets() - self.parent.screens[16].add_widget(Allmails()) - except Exception: - self.parent.parent.screens[4].clear_widgets() - self.parent.parent.screens[4].add_widget(Trash()) - self.parent.parent.screens[16].clear_widgets() - self.parent.parent.screens[16].add_widget(Allmails()) - - -class Trash(Screen): - """Trash Screen uses screen to show widgets of screens""" - trash_messages = ListProperty() - has_refreshed = True - delete_index = StringProperty() - table_name = StringProperty() - - def __init__(self, *args, **kwargs): - """Trash method, delete sent message and add in Trash""" - super(Trash, self).__init__(*args, **kwargs) - Clock.schedule_once(self.init_ui, 0) - - def init_ui(self, dt=0): - """Clock Schdule for method trash screen.""" - if state.association == '': - if BMConfigParser().addresses(): - state.association = BMConfigParser().addresses()[0] - self.ids.identi_tag.children[0].text = '' - self.trashDataQuery(0, 20) - if self.trash_messages: - self.ids.identi_tag.children[0].text = 'Trash' - src_mng_obj = state.kivyapp.root.children[2].children[0].ids - src_mng_obj.trash_cnt.badge_text = state.trash_count - self.set_mdList() - self.ids.scroll_y.bind(scroll_y=self.check_scroll_y) - else: - content = MDLabel( - font_style='Body1', theme_text_color='Primary', - text="yet no trashed message for this account!!!!!!!!!!!!!", - halign='center', bold=True, size_hint_y=None, valign='top') - self.ids.ml.add_widget(content) - - def trashDataQuery(self, start_indx, end_indx): - """Trash message query""" - self.trash_messages = sqlQuery( - "SELECT toaddress, fromaddress, subject, message," - " folder ||',' || 'sent' as folder, ackdata As" - " id, DATE(lastactiontime) As actionTime FROM sent" - " WHERE folder = 'trash' and fromaddress = '{0}' UNION" - " SELECT toaddress, fromaddress, subject, message," - " folder ||',' || 'inbox' as folder, msgid As id," - " DATE(received) As actionTime FROM inbox" - " WHERE folder = 'trash' and toaddress = '{0}'" - " ORDER BY actionTime DESC limit {1}, {2}".format( - state.association, start_indx, end_indx)) - - def set_mdList(self): - """This method is used to create the mdlist""" - total_trash_msg = len(self.ids.ml.children) - for item in self.trash_messages: - meny = CustomAvatarIconListItem( - text=item[1], - secondary_text=item[2][:50] + '........' if len( - item[2]) >= 50 else (item[2] + ',' + item[3].replace( - '\n', ''))[0:50] + '........', - theme_text_color='Custom') - img_latter = './images/text_images/{}.png'.format( - item[2][0].upper() if (item[2][0].upper() >= 'A' and item[ - 2][0].upper() <= 'Z') else '!') - meny.add_widget(AvatarSampleWidget(source=img_latter)) - carousel = Carousel(direction='right') - carousel.height = meny.height - carousel.size_hint_y = None - carousel.ignore_perpendicular_swipes = True - carousel.data_index = 0 - carousel.min_move = 0.2 - del_btn = Button(text='Delete') - del_btn.background_normal = '' - del_btn.background_color = (1, 0, 0, 1) - del_btn.bind(on_press=partial( - self.delete_permanently, item[5], item[4])) - carousel.add_widget(del_btn) - carousel.add_widget(meny) - carousel.index = 1 - self.ids.ml.add_widget(carousel) - self.has_refreshed = True if total_trash_msg != len( - self.ids.ml.children) else False - - 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 - total_trash_msg = len(self.ids.ml.children) - self.update_trash_screen_on_scroll(total_trash_msg) - else: - pass - - def update_trash_screen_on_scroll(self, total_trash_msg): - """Load more data on scroll down""" - self.trashDataQuery(total_trash_msg, 5) - self.set_mdList() - - def delete_permanently(self, data_index, folder, instance, *args): - """Deleting trash mail permanently""" - self.table_name = folder.split(',')[1] - self.delete_index = data_index - self.delete_confirmation() - - def callback_for_screen_load(self, dt=0): - """This methos is for loading screen""" - self.ids.ml.clear_widgets() - self.init_ui(0) - self.children[1].active = False - toast('Message is permanently deleted') - - def delete_confirmation(self): - """Show confirmation delete popup""" - width = .8 if platform == 'android' else .55 - delete_msg_dialog = MDDialog( - text='Are you sure you want to delete this' - ' message permanently from trash?', title='', - size_hint=(width, .25), text_button_ok='Yes', - text_button_cancel='No', - events_callback=self.callback_for_delete_msg) - delete_msg_dialog.open() - - def callback_for_delete_msg(self, text_item): - """Getting the callback of alert box""" - if text_item == 'Yes': - self.delete_message_from_trash() - else: - toast(text_item) - - def delete_message_from_trash(self): - """Deleting message from trash""" - self.children[1].active = True - if self.table_name == 'inbox': - sqlExecute( - "DELETE FROM inbox WHERE msgid = ?;", - str(self.delete_index)) - elif self.table_name == 'sent': - sqlExecute( - "DELETE FROM sent WHERE ackdata = ?;", - str(self.delete_index)) - msg_count_objs = state.kivyapp.root.children[2].children[0].ids - if int(state.trash_count) > 0: - msg_count_objs.trash_cnt.badge_text = str( - int(state.trash_count) - 1) - state.trash_count = str(int(state.trash_count) - 1) - Clock.schedule_once(self.callback_for_screen_load, 1) + self.data = [{ + 'data_index': 1, + 'index': 1, + 'height': 48, + 'text': "yet no message for this account!!!!!!!!!!!!!"} + ] class Page(Screen): - """Page Screen show widgets of page""" + 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): - """Creates the screen widgets""" - - def __init__(self, **kwargs): - """Getting Labels and address from addressbook""" - super(Create, self).__init__(**kwargs) - widget_1 = DropDownWidget() - widget_1.ids.txt_input.word_list = [ - addr[1] for addr in sqlQuery( - "SELECT label, address from addressbook")] - widget_1.ids.txt_input.starting_no = 2 - self.add_widget(widget_1) - - -class Setting(Screen): - """Setting the Screen components""" - pass - - -class NavigateApp(App): # pylint: disable=too-many-public-methods - """Navigation Layout of class""" - theme_cls = ThemeManager() - previous_date = ObjectProperty() - obj_1 = ObjectProperty() - variable_1 = ListProperty(BMConfigParser().addresses()) - nav_drawer = ObjectProperty() - state.screen_density = Window.size - window_size = state.screen_density - app_platform = platform - title = "PyBitmessage" - imgstatus = False - count = 0 - - def build(self): - """Method builds the widget""" - main_widget = Builder.load_file( - os.path.join(os.path.dirname(__file__), 'main.kv')) - self.nav_drawer = Navigatorss() - self.obj_1 = AddressBook() - kivysignalthread = UIkivySignaler() - kivysignalthread.daemon = True - kivysignalthread.start() - Window.bind(on_keyboard=self.on_key) - return main_widget - - def run(self): - """Running the widgets""" - kivyuisignaler.release() - super(NavigateApp, self).run() - - # pylint: disable=inconsistent-return-statements - @staticmethod - def showmeaddresses(name="text"): - """Show the addresses in spinner to make as dropdown""" - if name == "text": - if BMConfigParser().addresses(): - return BMConfigParser().addresses()[0][:16] + '..' - return "textdemo" - elif name == "values": - if BMConfigParser().addresses(): - return [address[:16] + '..' - for address in BMConfigParser().addresses()] - return "valuesdemo" - - def getCurrentAccountData(self, text): - """Get Current Address Account Data""" - self.set_identicon(text) - address_label = self.current_address_label( - BMConfigParser().get(text, 'label'), text) - self.root_window.children[1].ids.toolbar.title = address_label - state.association = text - state.searcing_text = '' - LoadingPopup().open() - self.set_message_count() - Clock.schedule_once(self.setCurrentAccountData, 0.5) - - def setCurrentAccountData(self, dt=0): - """This method set the current accout data on all the screens.""" - self.root.ids.sc1.ids.ml.clear_widgets() - self.root.ids.sc1.loadMessagelist(state.association) - - self.root.ids.sc4.ids.ml.clear_widgets() - self.root.ids.sc4.children[2].children[2].ids.search_field.text = '' - self.root.ids.sc4.loadSent(state.association) - - self.root.ids.sc16.clear_widgets() - self.root.ids.sc16.add_widget(Draft()) - - self.root.ids.sc5.clear_widgets() - self.root.ids.sc5.add_widget(Trash()) - - self.root.ids.sc17.clear_widgets() - self.root.ids.sc17.add_widget(Allmails()) - - self.root.ids.scr_mngr.current = 'inbox' - - @staticmethod - def getCurrentAccount(): - """It uses to get current account label""" - if state.association: - return state.association - return "Bitmessage Login" - - @staticmethod - def addingtoaddressbook(): - """Adding to address Book""" - p = GrashofPopup() - p.open() - - def getDefaultAccData(self): - """Getting Default Account Data""" - if BMConfigParser().addresses(): - img = identiconGeneration.generate(BMConfigParser().addresses()[0]) - self.createFolder('./images/default_identicon/') - if platform == 'android': - # android_path = os.path.expanduser - # ("~/user/0/org.test.bitapp/files/app/") - android_path = os.path.join( - os.environ['ANDROID_PRIVATE'] + '/app/') - img.texture.save('{1}/images/default_identicon/{0}.png'.format( - BMConfigParser().addresses()[0], android_path)) - else: - img.texture.save('./images/default_identicon/{}.png'.format( - BMConfigParser().addresses()[0])) - return BMConfigParser().addresses()[0] - return 'Select Address' - - @staticmethod - def createFolder(directory): - """Create directory when app starts""" - try: - if not os.path.exists(directory): - os.makedirs(directory) - except OSError: - print 'Error: Creating directory. ' + directory - - @staticmethod - def get_default_image(): - """Getting default image on address""" - if BMConfigParser().addresses(): - return './images/default_identicon/{}.png'.format( - BMConfigParser().addresses()[0]) - return './images/no_identicons.png' - - @staticmethod - def addressexist(): - """Checking address existence""" - if BMConfigParser().addresses(): - return True - return False - - def on_key(self, window, key, *args): - """Method is used for going on previous screen""" - if key == 27: - if state.in_search_mode and self.root.ids.scr_mngr.current not in [ - "mailDetail", "create"]: - self.closeSearchScreen() - elif self.root.ids.scr_mngr.current == "mailDetail": - self.root.ids.scr_mngr.current = 'sent'\ - if state.detailPageType == 'sent' else 'inbox' \ - if state.detailPageType == 'inbox' else 'draft' - self.back_press() - if state.in_search_mode and state.searcing_text: - toolbar_obj = self.root.ids.toolbar - toolbar_obj.left_action_items = [ - ['arrow-left', lambda x: self.closeSearchScreen()]] - toolbar_obj.right_action_items = [] - self.root.ids.toolbar.title = '' - elif self.root.ids.scr_mngr.current == "create": - self.save_draft() - self.set_common_header() - state.in_composer = False - self.root.ids.scr_mngr.current = 'inbox' - elif self.root.ids.scr_mngr.current == "showqrcode": - self.root.ids.scr_mngr.current = 'myaddress' - elif self.root.ids.scr_mngr.current == "random": - self.root.ids.scr_mngr.current = 'login' - else: - if state.kivyapp.variable_1: - self.root.ids.scr_mngr.current = 'inbox' - self.root.ids.scr_mngr.transition.direction = 'right' - self.root.ids.scr_mngr.transition.bind(on_complete=self.reset) - return True - elif key == 13 and state.searcing_text and not state.in_composer: - if state.search_screen == 'inbox': - self.root.ids.sc1.children[1].active = True - Clock.schedule_once(self.search_callback, 0.5) - elif state.search_screen == 'addressbook': - self.root.ids.sc11.children[1].active = True - Clock.schedule_once(self.search_callback, 0.5) - elif state.search_screen == 'myaddress': - self.root.ids.sc10.children[1].active = True - Clock.schedule_once(self.search_callback, 0.5) - elif state.search_screen == 'sent': - self.root.ids.sc4.children[1].active = True - Clock.schedule_once(self.search_callback, 0.5) - - def search_callback(self, dt=0): - """Show data after loader is loaded""" - if state.search_screen == 'inbox': - self.root.ids.sc1.ids.ml.clear_widgets() - self.root.ids.sc1.loadMessagelist(state.association) - self.root.ids.sc1.children[1].active = False - elif state.search_screen == 'addressbook': - self.root.ids.sc11.ids.ml.clear_widgets() - self.root.ids.sc11.loadAddresslist(None, 'All', '') - self.root.ids.sc11.children[1].active = False - elif state.search_screen == 'myaddress': - self.root.ids.sc10.ids.ml.clear_widgets() - self.root.ids.sc10.init_ui() - self.root.ids.sc10.children[1].active = False - else: - self.root.ids.sc4.ids.ml.clear_widgets() - self.root.ids.sc4.loadSent(state.association) - self.root.ids.sc4.children[1].active = False - self.root.ids.scr_mngr.current = state.search_screen - - def save_draft(self): - """Saving drafts messages""" - composer_objs = self.root - from_addr = str(self.root.ids.sc3.children[1].ids.ti.text) - to_addr = str(self.root.ids.sc3.children[1].ids.txt_input.text) - if from_addr and to_addr and state.detailPageType != 'draft' \ - and not state.in_sent_method: - Draft().draft_msg(composer_objs) - return - - 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) - - @staticmethod - def status_dispatching(data): - """Dispatching Status acknowledgment""" - ackData, message = data - if state.ackdata == ackData: - state.status.status = message - - def clear_composer(self): - """If slow down, the new composer edit screen""" - self.set_navbar_for_composer() - composer_obj = self.root.ids.sc3.children[1].ids - composer_obj.ti.text = '' - composer_obj.btn.text = 'Select' - composer_obj.txt_input.text = '' - composer_obj.subject.text = '' - composer_obj.body.text = '' - state.in_composer = True - state.in_sent_method = 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.sc3.children[1].reset_composer()], - ['send', - lambda x: self.root.ids.sc3.children[1].send(self)]] - - def set_common_header(self): - """Common header for all window""" - 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.toggle_nav_drawer()]] - return - - def back_press(self): - """Method for, reverting composer to previous page""" - self.save_draft() - if self.root.ids.scr_mngr.current == \ - 'mailDetail' and state.in_search_mode: - toolbar_obj = self.root.ids.toolbar - toolbar_obj.left_action_items = [ - ['arrow-left', lambda x: self.closeSearchScreen()]] - toolbar_obj.right_action_items = [] - self.root.ids.toolbar.title = '' - else: - self.set_common_header() - self.root.ids.scr_mngr.current = 'inbox' \ - if state.in_composer else 'allmails'\ - if state.is_allmail else state.detailPageType\ - if state.detailPageType else 'inbox' - self.root.ids.scr_mngr.transition.direction = 'right' - self.root.ids.scr_mngr.transition.bind(on_complete=self.reset) - if state.is_allmail or state.detailPageType == 'draft': - state.is_allmail = False - state.detailPageType = '' - state.in_composer = False - - @staticmethod - def get_inbox_count(): - """Getting inbox count""" - state.inbox_count = str(sqlQuery( - "SELECT COUNT(*) FROM inbox WHERE toaddress = '{}' and" - " folder = 'inbox' ;".format(state.association))[0][0]) - - @staticmethod - def get_sent_count(): - """Getting sent count""" - state.sent_count = str(sqlQuery( - "SELECT COUNT(*) FROM sent WHERE fromaddress = '{}' and" - " folder = 'sent' ;".format(state.association))[0][0]) - - def set_message_count(self): - """Setting message count""" - try: - msg_counter_objs = ( - self.root_window.children[0].children[2].children[0].ids) - except Exception: - msg_counter_objs = ( - self.root_window.children[2].children[2].children[0].ids) - self.get_inbox_count() - self.get_sent_count() - state.trash_count = str(sqlQuery( - "SELECT (SELECT count(*) FROM sent" - " where fromaddress = '{0}' and folder = 'trash' )" - "+(SELECT count(*) FROM inbox where toaddress = '{0}' and" - " folder = 'trash') AS SumCount".format(state.association))[0][0]) - state.draft_count = str(sqlQuery( - "SELECT COUNT(*) FROM sent WHERE fromaddress = '{}' and" - " folder = 'draft' ;".format(state.association))[0][0]) - state.all_count = str(int(state.sent_count) + int(state.inbox_count)) - if msg_counter_objs: - msg_counter_objs.send_cnt.badge_text = state.sent_count - msg_counter_objs.inbox_cnt.badge_text = state.inbox_count - msg_counter_objs.trash_cnt.badge_text = state.trash_count - msg_counter_objs.draft_cnt.badge_text = state.draft_count - msg_counter_objs.allmail_cnt.badge_text = state.all_count - - def on_start(self): - """Method activates on start""" - self.set_message_count() - - @staticmethod - def on_stop(): - """On stop methos is used for stoping the runing script""" - print "*******************EXITING FROM APPLICATION*******************" - import shutdown - shutdown.doCleanShutdown() - - @staticmethod - def current_address_label(current_add_label=None, current_addr=None): - """Getting current address labels""" - if BMConfigParser().addresses(): - if current_add_label: - first_name = current_add_label - addr = current_addr - else: - addr = BMConfigParser().addresses()[0] - first_name = BMConfigParser().get(addr, 'label') - f_name = first_name.split() - label = f_name[0][:14].capitalize() + '...' if len( - f_name[0]) > 15 else f_name[0].capitalize() - address = ' (' + addr + '...)' - return label + address - return '' - - def searchQuery(self, instance): - """Showing searched mails""" - state.search_screen = self.root.ids.scr_mngr.current - state.searcing_text = str(instance.text).strip() - if instance.focus and state.searcing_text: - toolbar_obj = self.root.ids.toolbar - toolbar_obj.left_action_items = [ - ['arrow-left', lambda x: self.closeSearchScreen()]] - toolbar_obj.right_action_items = [] - self.root.ids.toolbar.title = '' - state.in_search_mode = True - - def closeSearchScreen(self): - """Function for close search screen""" - self.set_common_header() - if state.association: - address_label = self.current_address_label( - BMConfigParser().get( - state.association, 'label'), state.association) - self.root.ids.toolbar.title = address_label - state.searcing_text = '' - self.refreshScreen() - state.in_search_mode = False - - def refreshScreen(self): # pylint: disable=unused-variable - """Method show search button only on inbox or sent screen""" - state.searcing_text = '' - if state.search_screen == 'inbox': - try: - self.root.ids.sc1.children[ - 3].children[2].ids.search_field.text = '' - except Exception: - self.root.ids.sc1.children[ - 2].children[2].ids.search_field.text = '' - self.root.ids.sc1.children[1].active = True - Clock.schedule_once(self.search_callback, 0.5) - elif state.search_screen == 'addressbook': - self.root.ids.sc11.children[ - 2].children[2].ids.search_field.text = '' - self.root.ids.sc11.children[ - 1].active = True - Clock.schedule_once(self.search_callback, 0.5) - elif state.search_screen == 'myaddress': - try: - self.root.ids.sc10.children[ - 3].children[2].ids.search_field.text = '' - except Exception: - self.root.ids.sc10.children[ - 2].children[2].ids.search_field.text = '' - self.root.ids.sc10.children[1].active = True - Clock.schedule_once(self.search_callback, 0.5) - else: - self.root.ids.sc4.children[ - 2].children[2].ids.search_field.text = '' - self.root.ids.sc4.children[1].active = True - Clock.schedule_once(self.search_callback, 0.5) - return - - def set_identicon(self, text): - """Show identicon in address spinner""" - img = identiconGeneration.generate(text) - self.root.children[2].children[0].ids.btn.children[1].texture = ( - img.texture) - - def set_mail_detail_header(self): - """Setting the details of the page""" - if state.association and state.in_search_mode: - address_label = self.current_address_label( - BMConfigParser().get( - state.association, 'label'), state.association) - self.root.ids.toolbar.title = address_label - toolbar_obj = self.root.ids.toolbar - toolbar_obj.left_action_items = [ - ['arrow-left', lambda x: self.back_press()]] - delete_btn = ['delete-forever', - lambda x: self.root.ids.sc14.delete_mail()] - dynamic_list = [] - if state.detailPageType == 'inbox': - dynamic_list = [ - ['reply', lambda x: self.root.ids.sc14.inbox_reply()], - delete_btn] - elif state.detailPageType == 'sent': - dynamic_list = [delete_btn] - elif state.detailPageType == 'draft': - dynamic_list = [ - ['pencil', lambda x: self.root.ids.sc14.write_msg(self)], - delete_btn] - toolbar_obj.right_action_items = dynamic_list - - 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.sc1.children[1].active = True - elif instance.text == 'All Mails': - self.root.ids.scr_mngr.current = 'allmails' - try: - self.root.ids.sc17.children[1].active = True - except Exception: - self.root.ids.sc17.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.sc1.ids.ml.clear_widgets() - self.root.ids.sc1.loadMessagelist(state.association) - self.root.ids.sc1.children[1].active = False - elif instance.text == 'All Mails': - if len(self.root.ids.sc17.ids.ml.children) <= 2: - self.root.ids.sc17.clear_widgets() - self.root.ids.sc17.add_widget(Allmails()) - else: - self.root.ids.sc17.ids.ml.clear_widgets() - self.root.ids.sc17.loadMessagelist() - try: - self.root.ids.sc17.children[1].active = False - except Exception: - self.root.ids.sc17.children[0].children[1].active = False - - -class GrashofPopup(Popup): - """Moule for save contacts and error messages""" - valid = False - - def __init__(self, **kwargs): # pylint: disable=useless-super-delegation - """Grash of pop screen settings""" - super(GrashofPopup, self).__init__(**kwargs) - - def savecontact(self): - """Method is used for saving contacts""" - label = self.ids.label.text.strip() - address = self.ids.address.text.strip() - if label == '' and address == '': - self.ids.label.focus = True - self.ids.address.focus = True - elif address == '': - self.ids.address.focus = True - elif label == '': - self.ids.label.focus = True - - stored_address = [addr[1] for addr in kivy_helper_search.search_sql( - folder="addressbook")] - stored_labels = [labels[0] for labels in kivy_helper_search.search_sql( - folder="addressbook")] - if label and address and address not in stored_address \ - and label not in stored_labels and self.valid: - # state.navinstance = self.parent.children[1] - queues.UISignalQueue.put(('rerenderAddressBook', '')) - self.dismiss() - sqlExecute("INSERT INTO addressbook VALUES(?,?)", label, address) - self.parent.children[1].ids.sc11.ids.ml.clear_widgets() - self.parent.children[1].ids.sc11.loadAddresslist(None, 'All', '') - self.parent.children[1].ids.scr_mngr.current = 'addressbook' - toast('Saved') - - @staticmethod - def close_pop(): - """Pop is Canceled""" - toast('Canceled') - - def checkAddress_valid(self, instance): - """Checking address is valid or not""" - my_addresses = ( - self.parent.children[1].children[2].children[0].ids.btn.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 = False - 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 = False - 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) - elif status == 'missingbm': - text = "The address should start with ''BM-''" - elif status == 'checksumfailed': - text = "The address is not typed or copied correctly(the checksum failed)." - elif status == 'versiontoohigh': - text = "The version number of this address is higher"\ - " than this software can support. Please upgrade Bitmessage." - elif status == 'invalidcharacters': - text = "The address contains invalid characters." - elif status == 'ripetooshort': - text = "Some data encoded in the address is too short." - elif status == 'ripetoolong': - text = "Some data encoded in the address is too long." - elif status == 'varintmalformed': - text = "Some data encoded in the address is malformed." - return text - - -class AvatarSampleWidget(ILeftBody, Image): - """Avatar Sample Widget""" - pass - - -class IconLeftSampleWidget(ILeftBodyTouch, MDIconButton): - """Left icon sample widget""" - pass - - -class IconRightSampleWidget(IRightBodyTouch, MDCheckbox): - """Right icon sample widget""" - pass - - -class NavigationDrawerTwoLineListItem( - TwoLineListItem, NavigationDrawerHeaderBase): - """Navigation Drawer in Listitems""" - address_property = StringProperty() - - def __init__(self, **kwargs): - """Method for Navigation Drawer""" - super(NavigationDrawerTwoLineListItem, self).__init__(**kwargs) - Clock.schedule_once(lambda dt: self.setup()) - - def setup(self): - """Bind Controller.current_account property""" - pass - - def on_current_account(self, account): - """Account detail""" - pass - - def _update_specific_text_color(self, instance, value): - pass - - def _set_active(self, active, list_): - pass - - -class MailDetail(Screen): - """MailDetail Screen uses to show the detail of mails""" - to_addr = StringProperty() - from_addr = StringProperty() - subject = StringProperty() - message = StringProperty() - status = StringProperty() - page_type = StringProperty() + """Create Screen uses screen to show widgets of screens.""" def __init__(self, *args, **kwargs): - """Mail Details method""" - super(MailDetail, self).__init__(*args, **kwargs) - Clock.schedule_once(self.init_ui, 0) + super(Create, self).__init__(*args, **kwargs) - def init_ui(self, dt=0): - """Clock Schdule for method MailDetail mails""" - self.page_type = state.detailPageType if state.detailPageType else '' - if state.detailPageType == 'sent' or state.detailPageType == 'draft': - data = sqlQuery( - "select toaddress, fromaddress, subject, message, status," - " ackdata from sent where ackdata = ?;", state.mail_id) - state.status = self - state.ackdata = data[0][5] - self.assign_mail_details(data) - state.kivyapp.set_mail_detail_header() - elif state.detailPageType == 'inbox': - data = sqlQuery( - "select toaddress, fromaddress, subject, message from inbox" - " where msgid = ?;", str(state.mail_id)) - self.assign_mail_details(data) - state.kivyapp.set_mail_detail_header() - - def assign_mail_details(self, data): - """Assigning mail details""" - self.to_addr = data[0][0] - self.from_addr = data[0][1] - self.subject = data[0][2].upper( - ) if data[0][2].upper() else '(no subject)' - self.message = data[0][3] - if len(data[0]) == 6: - self.status = data[0][4] - - def delete_mail(self): - """Method for mail delete""" - msg_count_objs = state.kivyapp.root.children[2].children[0].ids - state.searcing_text = '' - self.children[0].children[0].active = True - if state.detailPageType == 'sent': - state.kivyapp.root.ids.sc4.children[ - 2].children[2].ids.search_field.text = '' - sqlExecute( - "UPDATE sent SET folder = 'trash' WHERE" - " ackdata = ?;", str(state.mail_id)) - msg_count_objs.send_cnt.badge_text = str(int(state.sent_count) - 1) if int(state.sent_count) else '0' - state.sent_count = str(int(state.sent_count) - 1) if int(state.sent_count) else '0' - self.parent.screens[3].ids.ml.clear_widgets() - self.parent.screens[3].loadSent(state.association) - elif state.detailPageType == 'inbox': - state.kivyapp.root.ids.sc1.children[ - 2].children[2].ids.search_field.text = '' - self.parent.screens[0].children[2].children[ - 2].ids.search_field.text = '' - sqlExecute( - "UPDATE inbox SET folder = 'trash' WHERE" - " msgid = ?;", str(state.mail_id)) - msg_count_objs.inbox_cnt.badge_text = str( - int(state.inbox_count) - 1) if int(state.inbox_count) else '0' - state.inbox_count = str(int(state.inbox_count) - 1) if int(state.inbox_count) else '0' - self.parent.screens[0].ids.ml.clear_widgets() - self.parent.screens[0].loadMessagelist(state.association) - - elif state.detailPageType == 'draft': - sqlExecute( - "DELETE FROM sent WHERE ackdata = ?;", str(state.mail_id)) - msg_count_objs.draft_cnt.badge_text = str( - int(state.draft_count) - 1) - state.draft_count = str(int(state.draft_count) - 1) - self.parent.screens[15].clear_widgets() - self.parent.screens[15].add_widget(Draft()) - - # self.parent.current = 'allmails' \ - # if state.is_allmail else state.detailPageType - if state.detailPageType != 'draft': - msg_count_objs.trash_cnt.badge_text = str( - int(state.trash_count) + 1) - msg_count_objs.allmail_cnt.badge_text = str( - int(state.all_count) - 1) if int(state.all_count) else '0' - state.trash_count = str(int(state.trash_count) + 1) - state.all_count = str(int(state.all_count) - 1) if int(state.all_count) else '0' - self.parent.screens[4].clear_widgets() - self.parent.screens[4].add_widget(Trash()) - self.parent.screens[16].clear_widgets() - self.parent.screens[16].add_widget(Allmails()) - Clock.schedule_once(self.callback_for_delete, 4) - - def callback_for_delete(self, dt=0): - """Delete method from allmails""" - self.children[0].children[0].active = False - state.kivyapp.set_common_header() - self.parent.current = 'allmails' \ - if state.is_allmail else state.detailPageType - state.detailPageType = '' - toast('Deleted') - - def inbox_reply(self): - """Reply inbox messages""" - state.in_composer = True - data = sqlQuery( - "select toaddress, fromaddress, subject, message from inbox where" - " msgid = ?;", str(state.mail_id)) - composer_obj = self.parent.screens[2].children[1].ids - composer_obj.ti.text = data[0][0] - composer_obj.btn.text = data[0][0] - composer_obj.txt_input.text = data[0][1] - composer_obj.subject.text = data[0][2] - composer_obj.body.text = '' - state.kivyapp.root.ids.sc3.children[1].ids.rv.data = '' - self.parent.current = 'create' - state.kivyapp.set_navbar_for_composer() - - def write_msg(self, navApp): - """Write on draft mail""" - state.send_draft_mail = state.mail_id - data = sqlQuery( - "select toaddress, fromaddress, subject, message from sent where" - " ackdata = ?;", str(state.mail_id)) - composer_ids = ( - self.parent.parent.parent.parent.parent.ids.sc3.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] != '(no subject)' else '' - composer_ids.body.text = data[0][3] - self.parent.current = 'create' - navApp.set_navbar_for_composer() - - @staticmethod - def copy_composer_text(instance, *args): - """Copy the data from mail detail page""" - if len(instance.parent.text.split(':')) > 1: - cpy_text = instance.parent.text.split(':')[1].strip() - else: - cpy_text = instance.parent.text - Clipboard.copy(cpy_text) - toast('Copied') - - -class MyaddDetailPopup(Popup): - """MyaddDetailPopup pop is used for showing my address detail""" - address_label = StringProperty() - address = StringProperty() - - def __init__(self, **kwargs): # pylint: disable=useless-super-delegation - """My Address Details screen setting""" - super(MyaddDetailPopup, self).__init__(**kwargs) - - def set_address(self, address, label): - """Getting address for displaying details on popup""" - self.address_label = label - self.address = address - - def send_message_from(self): - """Method used to fill from address of composer autofield""" - state.kivyapp.set_navbar_for_composer() - window_obj = self.parent.children[1].ids - window_obj.sc3.children[1].ids.ti.text = self.address - window_obj.sc3.children[1].ids.btn.text = self.address - window_obj.sc3.children[1].ids.txt_input.text = '' - window_obj.sc3.children[1].ids.subject.text = '' - window_obj.sc3.children[1].ids.body.text = '' - window_obj.scr_mngr.current = 'create' - self.dismiss() - - @staticmethod - def close_pop(): - """Pop is Canceled""" - toast('Canceled') - - -class AddbookDetailPopup(Popup): - """AddbookDetailPopup pop is used for showing my address detail""" - address_label = StringProperty() - address = StringProperty() - - def __init__(self, **kwargs): - """Set screen of address detail page""" - # pylint: disable=useless-super-delegation - super(AddbookDetailPopup, self).__init__(**kwargs) - - def set_addbook_data(self, address, label): - """Getting address book data for detial dipaly""" - self.address_label = label - self.address = address - - def update_addbook_label(self, address): - """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.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 = '{}';".format( - str(self.ids.add_label.text), address)) - self.parent.children[1].ids.sc11.ids.ml.clear_widgets() - self.parent.children[1].ids.sc11.loadAddresslist(None, 'All', '') - self.dismiss() - toast('Saved') - - def send_message_to(self): - """Method used to fill to_address of composer autofield""" - state.kivyapp.set_navbar_for_composer() - window_obj = self.parent.children[1].ids - window_obj.sc3.children[1].ids.txt_input.text = self.address - window_obj.sc3.children[1].ids.ti.text = '' - window_obj.sc3.children[1].ids.btn.text = 'Select' - window_obj.sc3.children[1].ids.subject.text = '' - window_obj.sc3.children[1].ids.body.text = '' - window_obj.scr_mngr.current = 'create' - self.dismiss() - - @staticmethod - def close_pop(): - """Pop is Canceled""" - toast('Canceled') - - 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 = False - self.ids.add_label.helper_text = 'This field is required' - - -class ShowQRCode(Screen): - """ShowQRCode Screen uses to show the detail of mails""" - - def qrdisplay(self): - """Showing QR Code""" - # self.manager.parent.parent.parent.ids.search_bar.clear_widgets() - self.ids.qr.clear_widgets() - from kivy.garden.qrcode import QRCodeWidget - self.ids.qr.add_widget(QRCodeWidget( - data=self.manager.get_parent_window().children[0].address)) - toast('Show QR code') - - -class Draft(Screen): - """Draft screen is used to show the list of draft messages""" - data = ListProperty() - account = StringProperty() - queryreturn = ListProperty() - has_refreshed = True - - def __init__(self, *args, **kwargs): - """Method used for storing draft messages""" - super(Draft, self).__init__(*args, **kwargs) - if state.association == '': - if BMConfigParser().addresses(): - state.association = BMConfigParser().addresses()[0] - Clock.schedule_once(self.init_ui, 0) - - def init_ui(self, dt=0): - """Clock Schdule for method draft accounts""" - self.sentaccounts() - print dt - - def sentaccounts(self): - """Load draft accounts.""" - self.account = state.association - self.loadDraft() - - def loadDraft(self, where="", what=""): - """Load draft list for Draft messages.""" - xAddress = 'fromaddress' - self.ids.identi_tag.children[0].text = '' - self.draftDataQuery(xAddress, where, what) - if state.msg_counter_objs: - state.msg_counter_objs.draft_cnt.badge_text = str( - len(self.queryreturn)) - if self.queryreturn: - self.ids.identi_tag.children[0].text = 'Draft' - src_mng_obj = state.kivyapp.root.children[2].children[0].ids - src_mng_obj.draft_cnt.badge_text = state.draft_count - self.set_mdList() - self.ids.scroll_y.bind(scroll_y=self.check_scroll_y) - else: - content = MDLabel( - font_style='Body1', theme_text_color='Primary', - text="yet no message for this account!!!!!!!!!!!!!", - halign='center', bold=True, size_hint_y=None, valign='top') - self.ids.ml.add_widget(content) - - # pylint: disable=too-many-arguments - def draftDataQuery(self, xAddress, where, what, start_indx=0, end_indx=20): - """This methosd is for retrieving draft messages""" - self.queryreturn = kivy_helper_search.search_sql( - xAddress, self.account, "draft", where, what, - False, start_indx, end_indx) - - def set_mdList(self): - """This method is used to create mdlist""" - data = [] - total_draft_msg = len(self.ids.ml.children) - for mail in self.queryreturn: - third_text = mail[3].replace('\n', ' ') - data.append({ - 'text': mail[1].strip(), - 'secondary_text': mail[2][:10] + '...........' if len( - mail[2]) > 10 else mail[2] + '\n' + " " + ( - third_text[:25] + '...!') if len( - third_text) > 25 else third_text, - 'ackdata': mail[5]}) - for item in data: - meny = CustomAvatarIconListItem( - text='Draft', secondary_text=item['text'], - theme_text_color='Custom') - meny.add_widget(AvatarSampleWidget( - source='./images/avatar.png')) - meny.bind(on_press=partial( - self.draft_detail, item['ackdata'])) - carousel = Carousel(direction='right') - carousel.height = meny.height - carousel.size_hint_y = None - carousel.ignore_perpendicular_swipes = True - carousel.data_index = 0 - carousel.min_move = 0.2 - del_btn = Button(text='Delete') - del_btn.background_normal = '' - del_btn.background_color = (1, 0, 0, 1) - del_btn.bind(on_press=partial(self.delete_draft, item['ackdata'])) - carousel.add_widget(del_btn) - carousel.add_widget(meny) - carousel.index = 1 - self.ids.ml.add_widget(carousel) - updated_msg = len(self.ids.ml.children) - self.has_refreshed = True if total_draft_msg != updated_msg else False - - 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 - total_draft_msg = len(self.ids.ml.children) - self.update_draft_screen_on_scroll(total_draft_msg) - else: - pass - - def update_draft_screen_on_scroll(self, total_draft_msg, where='', what=''): - """Load more data on scroll down""" - self.draftDataQuery('fromaddress', where, what, total_draft_msg, 5) - self.set_mdList() - - def draft_detail(self, ackdata, *args): - """Show draft Details""" - state.detailPageType = 'draft' - state.mail_id = ackdata - if self.manager: - src_mng_obj = self.manager - else: - src_mng_obj = self.parent.parent - src_mng_obj.screens[13].clear_widgets() - src_mng_obj.screens[13].add_widget(MailDetail()) - src_mng_obj.current = 'mailDetail' - - def delete_draft(self, data_index, instance, *args): - """Delete draft message permanently""" - sqlExecute("DELETE FROM sent WHERE ackdata = ?;", str( - data_index)) - try: - msg_count_objs = ( - self.parent.parent.parent.parent.parent.parent.children[ - 2].children[0].ids) - except Exception: - msg_count_objs = self.parent.parent.parent.parent.parent.children[ - 2].children[0].ids - if int(state.draft_count) > 0: - msg_count_objs.draft_cnt.badge_text = str( - int(state.draft_count) - 1) - state.draft_count = str(int(state.draft_count) - 1) - if int(state.draft_count) <= 0: - self.ids.identi_tag.children[0].text = '' - self.ids.ml.remove_widget(instance.parent.parent) - toast('Deleted') - - @staticmethod - def draft_msg(src_object): # pylint: disable=too-many-locals - """Save draft mails""" - composer_object = state.kivyapp.root.ids.sc3.children[1].ids - fromAddress = str(composer_object.ti.text) - toAddress = str(composer_object.txt_input.text) - subject = str(composer_object.subject.text) - message = str(composer_object.body.text) + 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: - streamNumber, ripe = decodeAddress(toAddress)[2:] - toAddress = addBMIfNotPresent(toAddress) - stealthLevel = BMConfigParser().safeGetInt( - 'bitmessagesettings', 'ackstealthlevel') - from helper_ackPayload import genAckPayload - ackdata = genAckPayload(streamNumber, stealthLevel) - t = ( - '', - toAddress, - ripe, - fromAddress, - subject, - message, - ackdata, - int(time.time()), - int(time.time()), - 0, - 'msgqueued', - 0, - 'draft', - encoding, - BMConfigParser().getint('bitmessagesettings', 'ttl') + 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) ) - helper_sent.insert(t) - state.msg_counter_objs = src_object.children[2].children[0].ids - state.draft_count = str(int(state.draft_count) + 1) - src_object.ids.sc16.clear_widgets() - src_object.ids.sc16.add_widget(Draft()) - toast('Save draft') - return + self.manager.current = 'add_sucess' -class CustomSpinner(Spinner): - """This class is used for setting spinner size""" - - def __init__(self, *args, **kwargs): - """Setting size of spinner""" - super(CustomSpinner, self).__init__(*args, **kwargs) - self.dropdown_cls.max_height = Window.size[1] / 3 - - -class Allmails(Screen): - """All mails Screen uses screen to show widgets of screens""" - data = ListProperty() - has_refreshed = True - all_mails = ListProperty() - account = StringProperty() - - def __init__(self, *args, **kwargs): - """Method Parsing the address.""" - super(Allmails, self).__init__(*args, **kwargs) - if state.association == '': - if BMConfigParser().addresses(): - state.association = BMConfigParser().addresses()[0] - Clock.schedule_once(self.init_ui, 0) - - def init_ui(self, dt=0): - """Clock Schdule for method all mails""" - self.loadMessagelist() - print dt - - def loadMessagelist(self): - """Load Inbox, Sent anf Draft list of messages.""" - self.account = state.association - self.ids.identi_tag.children[0].text = '' - self.allMessageQuery(0, 20) - if self.all_mails: - self.ids.identi_tag.children[0].text = 'All Mails' - state.kivyapp.get_inbox_count() - state.kivyapp.get_sent_count() - state.all_count = str( - int(state.sent_count) + int(state.inbox_count)) - state.kivyapp.root.children[2].children[ - 0].ids.allmail_cnt.badge_text = state.all_count - self.set_mdlist() - # self.ids.refresh_layout.bind(scroll_y=self.check_scroll_y) - self.ids.scroll_y.bind(scroll_y=self.check_scroll_y) - else: - content = MDLabel( - font_style='Body1', theme_text_color='Primary', - text="yet no message for this account!!!!!!!!!!!!!", - halign='center', bold=True, size_hint_y=None, valign='top') - self.ids.ml.add_widget(content) - - def allMessageQuery(self, start_indx, end_indx): - """Retrieving data from inbox or sent both tables""" - self.all_mails = sqlQuery( - "SELECT toaddress, fromaddress, subject, message, folder, ackdata" - " As id, DATE(lastactiontime) As actionTime FROM sent WHERE" - " folder = 'sent' and fromaddress = '{0}'" - " UNION SELECT toaddress, fromaddress, subject, message, folder," - " msgid As id, DATE(received) As actionTime FROM inbox" - " WHERE folder = 'inbox' and toaddress = '{0}'" - " ORDER BY actionTime DESC limit {1}, {2}".format( - self.account, start_indx, end_indx)) - - def set_mdlist(self): - """This method is used to create mdList for allmaills""" - data_exist = len(self.ids.ml.children) - for item in self.all_mails: - meny = CustomAvatarIconListItem( - text=item[1], - secondary_text=item[2][:50] + '........' if len( - item[2]) >= 50 else ( - item[2] + ',' + item[3].replace( - '\n', ''))[0:50] + '........', - theme_text_color='Custom', - text_color=NavigateApp().theme_cls.primary_color) - meny.add_widget(AvatarSampleWidget( - source='./images/text_images/{}.png'.format( - avatarImageFirstLetter(item[2].strip())))) - meny.bind(on_press=partial( - self.mail_detail, item[5], item[4])) - carousel = Carousel(direction='right') - carousel.height = meny.height - carousel.size_hint_y = None - carousel.ignore_perpendicular_swipes = True - carousel.data_index = 0 - carousel.min_move = 0.2 - del_btn = Button(text='Delete') - del_btn.background_normal = '' - del_btn.background_color = (1, 0, 0, 1) - del_btn.bind(on_press=partial( - self.swipe_delete, item[5], item[4])) - carousel.add_widget(del_btn) - carousel.add_widget(meny) - carousel.index = 1 - self.ids.ml.add_widget(carousel) - updated_data = len(self.ids.ml.children) - self.has_refreshed = True if data_exist != updated_data else False - - def check_scroll_y(self, instance, somethingelse): - """Scroll fixed length""" - if self.ids.scroll_y.scroll_y <= -0.00 and self.has_refreshed: - self.ids.scroll_y.scroll_y = .06 - load_more = len(self.ids.ml.children) - self.updating_allmail(load_more) - else: - pass - - def updating_allmail(self, load_more): - """This method is used to update the all mail - listing value on the scroll of screen""" - self.allMessageQuery(load_more, 5) - self.set_mdlist() - - def mail_detail(self, unique_id, folder, *args): - """Load sent and inbox mail details""" - state.detailPageType = folder - state.is_allmail = True - state.mail_id = unique_id - if self.manager: - src_mng_obj = self.manager - else: - src_mng_obj = self.parent.parent - src_mng_obj.screens[13].clear_widgets() - src_mng_obj.screens[13].add_widget(MailDetail()) - src_mng_obj.current = 'mailDetail' - - def swipe_delete(self, unique_id, folder, instance, *args): - """Delete inbox mail from all mail listing""" - if folder == 'inbox': - sqlExecute( - "UPDATE inbox SET folder = 'trash' WHERE msgid = ?;", - str(unique_id)) - else: - sqlExecute( - "UPDATE sent SET folder = 'trash' WHERE ackdata = ?;", - str(unique_id)) - self.ids.ml.remove_widget(instance.parent.parent) - try: - msg_count_objs = self.parent.parent.parent.parent.parent.children[ - 2].children[0].ids - nav_lay_obj = self.parent.parent.parent.parent.parent.ids - except Exception: - msg_count_objs = self.parent.parent.parent.parent.parent.parent.children[ - 2].children[0].ids - nav_lay_obj = self.parent.parent.parent.parent.parent.parent.ids - if folder == 'inbox': - msg_count_objs.inbox_cnt.badge_text = str( - int(state.inbox_count) - 1) - state.inbox_count = str(int(state.inbox_count) - 1) - nav_lay_obj.sc1.ids.ml.clear_widgets() - nav_lay_obj.sc1.loadMessagelist(state.association) - else: - msg_count_objs.send_cnt.badge_text = str(int(state.sent_count) - 1) - state.sent_count = str(int(state.sent_count) - 1) - nav_lay_obj.sc4.ids.ml.clear_widgets() - nav_lay_obj.sc4.loadSent(state.association) - msg_count_objs.trash_cnt.badge_text = str(int(state.trash_count) + 1) - msg_count_objs.allmail_cnt.badge_text = str(int(state.all_count) - 1) - state.trash_count = str(int(state.trash_count) + 1) - state.all_count = str(int(state.all_count) - 1) - if int(state.all_count) <= 0: - self.ids.identi_tag.children[0].text = '' - nav_lay_obj.sc5.clear_widgets() - nav_lay_obj.sc5.add_widget(Trash()) - nav_lay_obj.sc17.remove_widget(instance.parent.parent) - - # pylint: disable=attribute-defined-outside-init - def refresh_callback(self, *args): - """Method updates the state of application, - While the spinner remains on the screen""" - def refresh_callback(interval): - """Load the allmails screen data""" - self.ids.ml.clear_widgets() - self.remove_widget(self.children[1]) - try: - screens_obj = self.parent.screens[16] - except Exception: - screens_obj = self.parent.parent.screens[16] - screens_obj.add_widget(Allmails()) - self.ids.refresh_layout.refresh_done() - self.tick = 0 - Clock.schedule_once(refresh_callback, 1) - - def set_root_layout(self): - """Setting root layout""" - try: - return self.manager.parent.parent - except Exception: - return state.kivyapp.root.ids.float_box - - -def avatarImageFirstLetter(letter_string): - """This function is used to the first letter for the avatar image""" - try: - if letter_string[0].upper() >= 'A' and letter_string[0].upper() <= 'Z': - img_latter = letter_string[0].upper() - elif int(letter_string[0]) >= 0 and int(letter_string[0]) <= 9: - img_latter = letter_string[0] - else: - img_latter = '!' - except ValueError as e: - img_latter = '!' - return img_latter if img_latter else '!' - - -class Starred(Screen): - """Starred Screen show widgets of page""" - pass - - -class Archieve(Screen): - """Archieve Screen show widgets of page""" - pass - - -class Spam(Screen): - """Spam Screen show widgets of page""" - pass - - -class LoadingPopup(Popup): - """Class for loading Popup""" - - def __init__(self, **kwargs): - super(LoadingPopup, self).__init__(**kwargs) - # call dismiss_popup in 2 seconds - Clock.schedule_once(self.dismiss_popup, 0.5) - - def dismiss_popup(self, dt): - """Dismiss popups""" - self.dismiss() +if __name__ == '__main__': + NavigateApp().run() diff --git a/src/bitmessagekivy/uikivysignaler.py b/src/bitmessagekivy/uikivysignaler.py deleted file mode 100644 index fe9c9884..00000000 --- a/src/bitmessagekivy/uikivysignaler.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -Ui Singnaler for kivy interface -""" -from threading import Thread - -import queues -import state -from semaphores import kivyuisignaler - - -class UIkivySignaler(Thread): - """Kivy ui signaler""" - - def run(self): - kivyuisignaler.acquire() - while state.shutdown == 0: - try: - command, data = queues.UISignalQueue.get() - if command == 'writeNewAddressToTable': - address = data[1] - state.kivyapp.variable_1.append(address) - # elif command == 'rerenderAddressBook': - # state.kivyapp.obj_1.refreshs() - # Need to discuss this - elif command == 'updateSentItemStatusByAckdata': - state.kivyapp.status_dispatching(data) - except Exception as e: - print e diff --git a/src/bitmessagemain.py b/src/bitmessagemain.py index d1e023c6..4efd0154 100755 --- a/src/bitmessagemain.py +++ b/src/bitmessagemain.py @@ -1,16 +1,25 @@ #!/usr/bin/python2.7 -""" -The PyBitmessage startup script -""" # Copyright (c) 2012-2016 Jonathan Warren -# Copyright (c) 2012-2020 The Bitmessage developers +# Copyright (c) 2012-2019 The Bitmessage developers # Distributed under the MIT/X11 software license. See the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. # Right now, PyBitmessage only support connecting to stream 1. It doesn't # yet contain logic to expand into further streams. + +# The software version variable is now held in shared.py + import os import sys + +app_dir = os.path.dirname(os.path.abspath(__file__)) +os.chdir(app_dir) +sys.path.insert(0, app_dir) + + +import depends +depends.check_dependencies() + import ctypes import getopt import multiprocessing @@ -22,40 +31,43 @@ import time import traceback from struct import pack -import defaults -import depends -import shared -import shutdown -import state -from bmconfigparser import BMConfigParser -from debug import logger # this should go before any threads from helper_startup import ( - 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 + isOurOperatingSystemLimitedToHavingVeryFewHalfOpenConnections ) from singleinstance import singleinstance -# Synchronous threads -from threads import ( - set_thread_name, addressGenerator, objectProcessor, singleCleaner, - singleWorker, sqlThread -) -app_dir = os.path.dirname(os.path.abspath(__file__)) -os.chdir(app_dir) -sys.path.insert(0, app_dir) +import defaults +import shared +import knownnodes +import state +import shutdown +from debug import logger -depends.check_dependencies() +# Classes +from class_sqlThread import sqlThread +from class_singleCleaner import singleCleaner +from class_objectProcessor import objectProcessor +from class_singleWorker import singleWorker +from class_addressGenerator import addressGenerator +from bmconfigparser import BMConfigParser + +from inventory import Inventory + +from network.connectionpool import BMConnectionPool +from network.dandelion import Dandelion +from network.networkthread import BMNetworkThread +from network.receivequeuethread import ReceiveQueueThread +from network.announcethread import AnnounceThread +from network.invthread import InvThread +from network.addrthread import AddrThread +from network.downloadthread import DownloadThread +from network.uploadthread import UploadThread + +# Helper Functions +import helper_threading def connectToStream(streamNumber): - """Connect to a stream""" state.streamsInWhichIAmParticipating.append(streamNumber) if isOurOperatingSystemLimitedToHavingVeryFewHalfOpenConnections(): @@ -72,6 +84,14 @@ def connectToStream(streamNumber): except: pass + with knownnodes.knownNodesLock: + if streamNumber not in knownnodes.knownNodes: + knownnodes.knownNodes[streamNumber] = {} + if streamNumber*2 not in knownnodes.knownNodes: + knownnodes.knownNodes[streamNumber*2] = {} + if streamNumber*2+1 not in knownnodes.knownNodes: + knownnodes.knownNodes[streamNumber*2+1] = {} + BMConnectionPool().connectToStream(streamNumber) @@ -88,8 +108,6 @@ def _fixSocket(): 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") @@ -111,8 +129,6 @@ def _fixSocket(): 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), @@ -153,32 +169,29 @@ def signal_handler(signum, frame): if thread.name not in ("PyBitmessage", "MainThread"): return logger.error("Got signal %i", signum) - # there are possible non-UI variants to run bitmessage - # which should shutdown especially test-mode + # there are possible non-UI variants to run bitmessage which should shutdown + # especially test-mode if shared.thisapp.daemon or not state.enableGUI: shutdown.doCleanShutdown() else: - print '# Thread: %s(%d)' % (thread.name, thread.ident) + print('# Thread: %s(%d)' % (thread.name, thread.ident)) for filename, lineno, name, line in traceback.extract_stack(frame): - print 'File: "%s", line %d, in %s' % (filename, lineno, name) + print('File: "%s", line %d, in %s' % (filename, lineno, name)) if line: - print ' %s' % line.strip() - print 'Unfortunately you cannot use Ctrl+C when running the UI \ - because the UI captures the signal.' + print(' %s' % line.strip()) + print('Unfortunately you cannot use Ctrl+C when running the UI' + ' because the UI captures the signal.') -class Main(object): - """Main PyBitmessage class""" +class Main: def start(self): - """Start main application""" - # pylint: disable=too-many-statements,too-many-branches,too-many-locals _fixSocket() - config = BMConfigParser() - daemon = config.safeGetBoolean('bitmessagesettings', 'daemon') + daemon = BMConfigParser().safeGetBoolean( + 'bitmessagesettings', 'daemon') try: - opts, _ = getopt.getopt( + opts, args = getopt.getopt( sys.argv[1:], "hcdt", ["help", "curses", "daemon", "test"]) @@ -186,7 +199,7 @@ class Main(object): self.usage() sys.exit(2) - for opt, _ in opts: + for opt, arg in opts: if opt in ("-h", "--help"): self.usage() sys.exit() @@ -203,6 +216,7 @@ class Main(object): # Fallback: in case when no api command was issued state.last_api_response = time.time() # Apply special settings + config = BMConfigParser() config.set( 'bitmessagesettings', 'apienabled', 'true') config.set( @@ -217,8 +231,7 @@ class Main(object): if daemon: state.enableGUI = False # run without a UI - # is the application already running? If yes then exit. - if state.enableGUI and not state.curses and not state.kivy and not depends.check_pyqt(): + if state.enableGUI and not state.curses and not depends.check_pyqt(): sys.exit( 'PyBitmessage requires PyQt unless you want' ' to run it as a daemon and interact with it' @@ -232,36 +245,32 @@ class Main(object): ' \'-c\' as a commandline argument.' ) # is the application already running? If yes then exit. - try: - shared.thisapp = singleinstance("", daemon) - except Exception: - pass + shared.thisapp = singleinstance("", daemon) if daemon: with shared.printLock: - print 'Running as a daemon. Send TERM signal to end.' + print('Running as a daemon. Send TERM signal to end.') self.daemonize() self.setSignalHandler() - set_thread_name("PyBitmessage") + helper_threading.set_thread_name("PyBitmessage") - state.dandelion = config.safeGetInt('network', 'dandelion') + state.dandelion = BMConfigParser().safeGetInt('network', 'dandelion') # dandelion requires outbound connections, without them, # stem objects will get stuck forever - if state.dandelion and not config.safeGetBoolean( + if state.dandelion and not BMConfigParser().safeGetBoolean( 'bitmessagesettings', 'sendoutgoingconnections'): state.dandelion = 0 - if state.testmode or config.safeGetBoolean( + if state.testmode or BMConfigParser().safeGetBoolean( 'bitmessagesettings', 'extralowdifficulty'): defaults.networkDefaultProofOfWorkNonceTrialsPerByte = int( defaults.networkDefaultProofOfWorkNonceTrialsPerByte / 100) defaults.networkDefaultPayloadLengthExtraBytes = int( defaults.networkDefaultPayloadLengthExtraBytes / 100) - readKnownNodes() - + knownnodes.readKnownNodes() # Not needed if objproc is disabled if state.enableObjProc: @@ -284,21 +293,24 @@ class Main(object): # 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( - 'bitmessagesettings', 'smtpdeliver', '') != '': + if daemon and BMConfigParser().safeGet( + "bitmessagesettings", "smtpdeliver", '') != '': from class_smtpDeliver import smtpDeliver smtpDeliveryThread = smtpDeliver() smtpDeliveryThread.start() # SMTP daemon thread - if daemon and config.safeGetBoolean( - 'bitmessagesettings', 'smtpd'): + if daemon and BMConfigParser().safeGetBoolean( + "bitmessagesettings", "smtpd"): from class_smtpServer import smtpServer smtpServerThread = smtpServer() smtpServerThread.start() @@ -310,30 +322,33 @@ class Main(object): # 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'): + if BMConfigParser().safeGetBoolean('bitmessagesettings', 'apienabled'): import api # pylint: disable=relative-import singleAPIThread = api.singleAPI() # close the main program even if there are threads left singleAPIThread.daemon = True singleAPIThread.start() + # start network components if networking is enabled if state.enableNetwork: - start_proxyconfig() BMConnectionPool() asyncoreThread = BMNetworkThread() asyncoreThread.daemon = True asyncoreThread.start() - for i in range(config.getint('threads', 'receive')): + for i in range(BMConfigParser().getint("threads", "receive")): receiveQueueThread = ReceiveQueueThread(i) receiveQueueThread.daemon = True receiveQueueThread.start() @@ -354,43 +369,41 @@ class Main(object): state.uploadThread.start() connectToStream(1) - if config.safeGetBoolean('bitmessagesettings', 'upnp'): + + if BMConfigParser().safeGetBoolean( + 'bitmessagesettings', 'upnp'): import upnp upnpThread = upnp.uPnPThread() upnpThread.start() else: # Populate with hardcoded value (same as connectToStream above) state.streamsInWhichIAmParticipating.append(1) + if not daemon and state.enableGUI: if state.curses: if not depends.check_curses(): sys.exit() - print 'Running with curses' + print('Running with curses') import bitmessagecurses bitmessagecurses.runwrapper() - elif state.kivy: - config.remove_option('bitmessagesettings', 'dontconnect') + BMConfigParser().remove_option('bitmessagesettings', 'dontconnect') from bitmessagekivy.mpybit import NavigateApp - state.kivyapp = NavigateApp() - state.kivyapp.run() + NavigateApp().run() else: import bitmessageqt bitmessageqt.run() else: - config.remove_option('bitmessagesettings', 'dontconnect') + BMConfigParser().remove_option('bitmessagesettings', 'dontconnect') if daemon: while state.shutdown == 0: time.sleep(1) - if ( - state.testmode and time.time() - - state.last_api_response >= 30 - ): + if (state.testmode and + time.time() - state.last_api_response >= 30): self.stop() elif not state.enableGUI: - # pylint: disable=relative-import - from tests import core as test_core + from tests import core as test_core # pylint: disable=relative-import test_core_result = test_core.run() state.enableGUI = True self.stop() @@ -401,9 +414,7 @@ class Main(object): else 0 ) - @staticmethod - def daemonize(): - """Running as a daemon. Send signal in end.""" + def daemonize(self): grandfatherPid = os.getpid() parentPid = None try: @@ -413,8 +424,7 @@ class Main(object): # wait until grandchild ready while True: time.sleep(1) - - os._exit(0) # pylint: disable=protected-access + os._exit(0) except AttributeError: # fork not implemented pass @@ -435,7 +445,7 @@ class Main(object): # wait until child ready while True: time.sleep(1) - os._exit(0) # pylint: disable=protected-access + os._exit(0) except AttributeError: # fork not implemented pass @@ -456,18 +466,14 @@ class Main(object): os.kill(parentPid, signal.SIGTERM) os.kill(grandfatherPid, signal.SIGTERM) - @staticmethod - def setSignalHandler(): - """Setting the Signal Handler""" + def setSignalHandler(self): signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) # signal.signal(signal.SIGINT, signal.SIG_DFL) - @staticmethod - def usage(): - """Displaying the usages""" - print('Usage: ' + sys.argv[0] + ' [OPTIONS]') - print(''' + def usage(self): + print 'Usage: ' + sys.argv[0] + ' [OPTIONS]' + print ''' Options: -h, --help show this help message and exit -c, --curses use curses (text mode) interface @@ -475,19 +481,15 @@ Options: -t, --test dryrun, make testing All parameters are optional. -''') +''' - @staticmethod - def stop(): - """Stop main application""" + def stop(self): with shared.printLock: - print 'Stopping Bitmessage Deamon.' + print('Stopping Bitmessage Deamon.') shutdown.doCleanShutdown() - # .. todo:: nice function but no one is using this - @staticmethod - def getApiAddress(): - """This function returns API address and port""" + # TODO: nice function but no one is using this + def getApiAddress(self): if not BMConfigParser().safeGetBoolean( 'bitmessagesettings', 'apienabled'): return None @@ -497,7 +499,6 @@ All parameters are optional. def main(): - """Triggers main module""" mainprogram = Main() mainprogram.start() diff --git a/src/bitmessageqt/__init__.py b/src/bitmessageqt/__init__.py index 440d36b2..507e6ca0 100644 --- a/src/bitmessageqt/__init__.py +++ b/src/bitmessageqt/__init__.py @@ -23,6 +23,7 @@ from addresses import decodeAddress, addBMIfNotPresent import shared from bitmessageui import Ui_MainWindow from bmconfigparser import BMConfigParser +import defaults import namecoin from messageview import MessageView from migrationwizard import Ui_MigrationWizard @@ -30,12 +31,15 @@ from foldertree import ( AccountMixin, Ui_FolderWidget, Ui_AddressWidget, Ui_SubscriptionWidget, MessageList_AddressWidget, MessageList_SubjectWidget, Ui_AddressBookWidgetItemLabel, Ui_AddressBookWidgetItemAddress) +from settings import Ui_settingsDialog import settingsmixin import support +import debug from helper_ackPayload import genAckPayload from helper_sql import sqlQuery, sqlExecute, sqlExecuteChunked, sqlStoredProcedure import helper_search import l10n +import openclpow from utils import str_broadcast_subscribers, avatarize from account import ( getSortedAccounts, getSortedSubscriptions, accountClass, BMAccount, @@ -43,15 +47,16 @@ from account import ( import dialogs from network.stats import pendingDownload, pendingUpload from uisignaler import UISignaler +import knownnodes import paths from proofofwork import getPowType import queues import shutdown import state from statusbar import BMStatusBar +from network.asyncore_pollchoose import set_rates import sound -# This is needed for tray icon -import bitmessage_icons_rc # noqa:F401 pylint: disable=unused-import + try: from plugins.plugin import get_plugin, get_plugins @@ -59,6 +64,49 @@ except ImportError: get_plugins = False +def change_translation(newlocale): + global qmytranslator, qsystranslator + try: + if not qmytranslator.isEmpty(): + QtGui.QApplication.removeTranslator(qmytranslator) + except: + pass + try: + if not qsystranslator.isEmpty(): + QtGui.QApplication.removeTranslator(qsystranslator) + except: + pass + + qmytranslator = QtCore.QTranslator() + translationpath = os.path.join (paths.codePath(), 'translations', 'bitmessage_' + newlocale) + qmytranslator.load(translationpath) + QtGui.QApplication.installTranslator(qmytranslator) + + qsystranslator = QtCore.QTranslator() + if paths.frozen: + translationpath = os.path.join (paths.codePath(), 'translations', 'qt_' + newlocale) + else: + translationpath = os.path.join (str(QtCore.QLibraryInfo.location(QtCore.QLibraryInfo.TranslationsPath)), 'qt_' + newlocale) + qsystranslator.load(translationpath) + QtGui.QApplication.installTranslator(qsystranslator) + + lang = locale.normalize(l10n.getTranslationLanguage()) + langs = [lang.split(".")[0] + "." + l10n.encoding, lang.split(".")[0] + "." + 'UTF-8', lang] + if 'win32' in sys.platform or 'win64' in sys.platform: + langs = [l10n.getWindowsLocale(lang)] + for lang in langs: + try: + l10n.setlocale(locale.LC_ALL, lang) + if 'win32' not in sys.platform and 'win64' not in sys.platform: + l10n.encoding = locale.nl_langinfo(locale.CODESET) + else: + l10n.encoding = locale.getlocale()[1] + logger.info("Successfully set locale to %s", lang) + break + except: + logger.error("Failed to set locale to %s", lang, exc_info=True) + + # TODO: rewrite def powQueueSize(): """Returns the size of queues.workerQueue including current unfinished work""" @@ -74,6 +122,9 @@ def powQueueSize(): class MyForm(settingsmixin.SMainWindow): + # the last time that a message arrival sound was played + lastSoundTime = datetime.now() - timedelta(days=1) + # the maximum frequency of message sounds in seconds maxSoundFrequencySec = 60 @@ -81,58 +132,6 @@ class MyForm(settingsmixin.SMainWindow): REPLY_TYPE_CHAN = 1 REPLY_TYPE_UPD = 2 - def change_translation(self, newlocale=None): - """Change translation language for the application""" - if newlocale is None: - newlocale = l10n.getTranslationLanguage() - try: - if not self.qmytranslator.isEmpty(): - QtGui.QApplication.removeTranslator(self.qmytranslator) - except: - pass - try: - if not self.qsystranslator.isEmpty(): - QtGui.QApplication.removeTranslator(self.qsystranslator) - except: - pass - - self.qmytranslator = QtCore.QTranslator() - translationpath = os.path.join( - paths.codePath(), 'translations', 'bitmessage_' + newlocale) - self.qmytranslator.load(translationpath) - QtGui.QApplication.installTranslator(self.qmytranslator) - - self.qsystranslator = QtCore.QTranslator() - if paths.frozen: - translationpath = os.path.join( - paths.codePath(), 'translations', 'qt_' + newlocale) - else: - translationpath = os.path.join( - str(QtCore.QLibraryInfo.location( - QtCore.QLibraryInfo.TranslationsPath)), 'qt_' + newlocale) - self.qsystranslator.load(translationpath) - QtGui.QApplication.installTranslator(self.qsystranslator) - - lang = locale.normalize(l10n.getTranslationLanguage()) - langs = [ - lang.split(".")[0] + "." + l10n.encoding, - lang.split(".")[0] + "." + 'UTF-8', - lang - ] - if 'win32' in sys.platform or 'win64' in sys.platform: - langs = [l10n.getWindowsLocale(lang)] - for lang in langs: - try: - l10n.setlocale(locale.LC_ALL, lang) - if 'win32' not in sys.platform and 'win64' not in sys.platform: - l10n.encoding = locale.nl_langinfo(locale.CODESET) - else: - l10n.encoding = locale.getlocale()[1] - logger.info("Successfully set locale to %s", lang) - break - except: - logger.error("Failed to set locale to %s", lang, exc_info=True) - def init_file_menu(self): QtCore.QObject.connect(self.ui.actionExit, QtCore.SIGNAL( "triggered()"), self.quit) @@ -606,13 +605,6 @@ class MyForm(settingsmixin.SMainWindow): self.ui = Ui_MainWindow() self.ui.setupUi(self) - self.qmytranslator = self.qsystranslator = None - self.indicatorUpdate = None - self.actionStatus = None - - # the last time that a message arrival sound was played - self.lastSoundTime = datetime.now() - timedelta(days=1) - # Ask the user if we may delete their old version 1 addresses if they # have any. for addressInKeysFile in getSortedAccounts(): @@ -628,13 +620,26 @@ class MyForm(settingsmixin.SMainWindow): BMConfigParser().remove_section(addressInKeysFile) BMConfigParser().save() - self.updateStartOnLogon() - - self.change_translation() + # Configure Bitmessage to start on startup (or remove the + # configuration) based on the setting in the keys.dat file + if 'win32' in sys.platform or 'win64' in sys.platform: + # Auto-startup for Windows + RUN_PATH = "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" + self.settings = QtCore.QSettings(RUN_PATH, QtCore.QSettings.NativeFormat) + self.settings.remove( + "PyBitmessage") # In case the user moves the program and the registry entry is no longer valid, this will delete the old registry entry. + if BMConfigParser().getboolean('bitmessagesettings', 'startonlogon'): + self.settings.setValue("PyBitmessage", sys.argv[0]) + elif 'darwin' in sys.platform: + # startup for mac + pass + elif 'linux' in sys.platform: + # startup for linux + pass # e.g. for editing labels self.recurDepth = 0 - + # switch back to this when replying self.replyFromTab = None @@ -781,9 +786,6 @@ class MyForm(settingsmixin.SMainWindow): self.ui.treeWidgetSubscriptions.keyPressEvent = self.treeWidgetKeyPressEvent self.ui.treeWidgetChans.keyPressEvent = self.treeWidgetKeyPressEvent - # Key press in addressbook - self.ui.tableWidgetAddressBook.keyPressEvent = self.addressbookKeyPressEvent - # Key press in messagelist self.ui.tableWidgetInbox.keyPressEvent = self.tableWidgetKeyPressEvent self.ui.tableWidgetInboxSubscriptions.keyPressEvent = self.tableWidgetKeyPressEvent @@ -826,28 +828,6 @@ class MyForm(settingsmixin.SMainWindow): finally: self._contact_selected = None - def updateStartOnLogon(self): - # Configure Bitmessage to start on startup (or remove the - # configuration) based on the setting in the keys.dat file - if 'win32' in sys.platform or 'win64' in sys.platform: - # Auto-startup for Windows - RUN_PATH = "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" - self.settings = QtCore.QSettings( - RUN_PATH, QtCore.QSettings.NativeFormat) - # In case the user moves the program and the registry entry is - # no longer valid, this will delete the old registry entry. - 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) @@ -1453,15 +1433,6 @@ class MyForm(settingsmixin.SMainWindow): def treeWidgetKeyPressEvent(self, event): return self.handleKeyPress(event, self.getCurrentTreeWidget()) - # addressbook - def addressbookKeyPressEvent(self, event): - """Handle keypress event in addressbook widget""" - if event.key() == QtCore.Qt.Key_Delete: - self.on_action_AddressBookDelete() - else: - return QtGui.QTableWidget.keyPressEvent( - self.ui.tableWidgetAddressBook, event) - # inbox / sent def tableWidgetKeyPressEvent(self, event): return self.handleKeyPress(event, self.getCurrentMessagelist()) @@ -1470,12 +1441,11 @@ class MyForm(settingsmixin.SMainWindow): def textEditKeyPressEvent(self, event): return self.handleKeyPress(event, self.getCurrentMessageTextedit()) - def handleKeyPress(self, event, focus=None): - """This method handles keypress events for all widgets on MyForm""" + def handleKeyPress(self, event, focus = None): messagelist = self.getCurrentMessagelist() folder = self.getCurrentFolder() if event.key() == QtCore.Qt.Key_Delete: - if isinstance(focus, MessageView) or isinstance(focus, QtGui.QTableWidget): + if isinstance (focus, MessageView) or isinstance(focus, QtGui.QTableWidget): if folder == "sent": self.on_action_SentTrash() else: @@ -1511,17 +1481,17 @@ class MyForm(settingsmixin.SMainWindow): self.ui.lineEditTo.setFocus() event.ignore() elif event.key() == QtCore.Qt.Key_F: - searchline = self.getCurrentSearchLine(retObj=True) + searchline = self.getCurrentSearchLine(retObj = True) if searchline: searchline.setFocus() event.ignore() if not event.isAccepted(): return - if isinstance(focus, MessageView): + if isinstance (focus, MessageView): return MessageView.keyPressEvent(focus, event) - elif isinstance(focus, QtGui.QTableWidget): + elif isinstance (focus, QtGui.QTableWidget): return QtGui.QTableWidget.keyPressEvent(focus, event) - elif isinstance(focus, QtGui.QTreeWidget): + elif isinstance (focus, QtGui.QTreeWidget): return QtGui.QTreeWidget.keyPressEvent(focus, event) # menu button 'manage keys' @@ -1652,6 +1622,7 @@ class MyForm(settingsmixin.SMainWindow): # The window state has just been changed to # Normal/Maximised/FullScreen pass + # QtGui.QWidget.changeEvent(self, event) def __icon_activated(self, reason): if reason == QtGui.QSystemTrayIcon.Trigger: @@ -2463,7 +2434,225 @@ class MyForm(settingsmixin.SMainWindow): dialogs.AboutDialog(self).exec_() def click_actionSettings(self): - dialogs.SettingsDialog(self, firstrun=self._firstrun).exec_() + self.settingsDialogInstance = settingsDialog(self) + if self._firstrun: + self.settingsDialogInstance.ui.tabWidgetSettings.setCurrentIndex(1) + if self.settingsDialogInstance.exec_(): + if self._firstrun: + BMConfigParser().remove_option( + 'bitmessagesettings', 'dontconnect') + BMConfigParser().set('bitmessagesettings', 'startonlogon', str( + self.settingsDialogInstance.ui.checkBoxStartOnLogon.isChecked())) + BMConfigParser().set('bitmessagesettings', 'minimizetotray', str( + self.settingsDialogInstance.ui.checkBoxMinimizeToTray.isChecked())) + BMConfigParser().set('bitmessagesettings', 'trayonclose', str( + self.settingsDialogInstance.ui.checkBoxTrayOnClose.isChecked())) + BMConfigParser().set('bitmessagesettings', 'hidetrayconnectionnotifications', str( + self.settingsDialogInstance.ui.checkBoxHideTrayConnectionNotifications.isChecked())) + BMConfigParser().set('bitmessagesettings', 'showtraynotifications', str( + self.settingsDialogInstance.ui.checkBoxShowTrayNotifications.isChecked())) + BMConfigParser().set('bitmessagesettings', 'startintray', str( + self.settingsDialogInstance.ui.checkBoxStartInTray.isChecked())) + BMConfigParser().set('bitmessagesettings', 'willinglysendtomobile', str( + self.settingsDialogInstance.ui.checkBoxWillinglySendToMobile.isChecked())) + BMConfigParser().set('bitmessagesettings', 'useidenticons', str( + self.settingsDialogInstance.ui.checkBoxUseIdenticons.isChecked())) + BMConfigParser().set('bitmessagesettings', 'replybelow', str( + self.settingsDialogInstance.ui.checkBoxReplyBelow.isChecked())) + + lang = str(self.settingsDialogInstance.ui.languageComboBox.itemData(self.settingsDialogInstance.ui.languageComboBox.currentIndex()).toString()) + BMConfigParser().set('bitmessagesettings', 'userlocale', lang) + change_translation(l10n.getTranslationLanguage()) + + if int(BMConfigParser().get('bitmessagesettings', 'port')) != int(self.settingsDialogInstance.ui.lineEditTCPPort.text()): + if not BMConfigParser().safeGetBoolean('bitmessagesettings', 'dontconnect'): + QtGui.QMessageBox.about(self, _translate("MainWindow", "Restart"), _translate( + "MainWindow", "You must restart Bitmessage for the port number change to take effect.")) + BMConfigParser().set('bitmessagesettings', 'port', str( + self.settingsDialogInstance.ui.lineEditTCPPort.text())) + if self.settingsDialogInstance.ui.checkBoxUPnP.isChecked() != BMConfigParser().safeGetBoolean('bitmessagesettings', 'upnp'): + BMConfigParser().set('bitmessagesettings', 'upnp', str(self.settingsDialogInstance.ui.checkBoxUPnP.isChecked())) + if self.settingsDialogInstance.ui.checkBoxUPnP.isChecked(): + import upnp + upnpThread = upnp.uPnPThread() + upnpThread.start() + #print 'self.settingsDialogInstance.ui.comboBoxProxyType.currentText()', self.settingsDialogInstance.ui.comboBoxProxyType.currentText() + #print 'self.settingsDialogInstance.ui.comboBoxProxyType.currentText())[0:5]', self.settingsDialogInstance.ui.comboBoxProxyType.currentText()[0:5] + if BMConfigParser().get('bitmessagesettings', 'socksproxytype') == 'none' and self.settingsDialogInstance.ui.comboBoxProxyType.currentText()[0:5] == 'SOCKS': + if shared.statusIconColor != 'red': + QtGui.QMessageBox.about(self, _translate("MainWindow", "Restart"), _translate( + "MainWindow", "Bitmessage will use your proxy from now on but you may want to manually restart Bitmessage now to close existing connections (if any).")) + if BMConfigParser().get('bitmessagesettings', 'socksproxytype')[0:5] == 'SOCKS' and self.settingsDialogInstance.ui.comboBoxProxyType.currentText()[0:5] != 'SOCKS': + self.statusbar.clearMessage() + state.resetNetworkProtocolAvailability() # just in case we changed something in the network connectivity + if self.settingsDialogInstance.ui.comboBoxProxyType.currentText()[0:5] == 'SOCKS': + BMConfigParser().set('bitmessagesettings', 'socksproxytype', str( + self.settingsDialogInstance.ui.comboBoxProxyType.currentText())) + else: + BMConfigParser().set('bitmessagesettings', 'socksproxytype', 'none') + BMConfigParser().set('bitmessagesettings', 'socksauthentication', str( + self.settingsDialogInstance.ui.checkBoxAuthentication.isChecked())) + BMConfigParser().set('bitmessagesettings', 'sockshostname', str( + self.settingsDialogInstance.ui.lineEditSocksHostname.text())) + BMConfigParser().set('bitmessagesettings', 'socksport', str( + self.settingsDialogInstance.ui.lineEditSocksPort.text())) + BMConfigParser().set('bitmessagesettings', 'socksusername', str( + self.settingsDialogInstance.ui.lineEditSocksUsername.text())) + BMConfigParser().set('bitmessagesettings', 'sockspassword', str( + self.settingsDialogInstance.ui.lineEditSocksPassword.text())) + BMConfigParser().set('bitmessagesettings', 'sockslisten', str( + self.settingsDialogInstance.ui.checkBoxSocksListen.isChecked())) + try: + # Rounding to integers just for aesthetics + BMConfigParser().set('bitmessagesettings', 'maxdownloadrate', str( + int(float(self.settingsDialogInstance.ui.lineEditMaxDownloadRate.text())))) + BMConfigParser().set('bitmessagesettings', 'maxuploadrate', str( + int(float(self.settingsDialogInstance.ui.lineEditMaxUploadRate.text())))) + except ValueError: + QtGui.QMessageBox.about(self, _translate("MainWindow", "Number needed"), _translate( + "MainWindow", "Your maximum download and upload rate must be numbers. Ignoring what you typed.")) + else: + set_rates(BMConfigParser().safeGetInt("bitmessagesettings", "maxdownloadrate"), + BMConfigParser().safeGetInt("bitmessagesettings", "maxuploadrate")) + + BMConfigParser().set('bitmessagesettings', 'maxoutboundconnections', str( + int(float(self.settingsDialogInstance.ui.lineEditMaxOutboundConnections.text())))) + + BMConfigParser().set('bitmessagesettings', 'namecoinrpctype', + self.settingsDialogInstance.getNamecoinType()) + BMConfigParser().set('bitmessagesettings', 'namecoinrpchost', str( + self.settingsDialogInstance.ui.lineEditNamecoinHost.text())) + BMConfigParser().set('bitmessagesettings', 'namecoinrpcport', str( + self.settingsDialogInstance.ui.lineEditNamecoinPort.text())) + BMConfigParser().set('bitmessagesettings', 'namecoinrpcuser', str( + self.settingsDialogInstance.ui.lineEditNamecoinUser.text())) + BMConfigParser().set('bitmessagesettings', 'namecoinrpcpassword', str( + self.settingsDialogInstance.ui.lineEditNamecoinPassword.text())) + self.resetNamecoinConnection() + + # Demanded difficulty tab + if float(self.settingsDialogInstance.ui.lineEditTotalDifficulty.text()) >= 1: + BMConfigParser().set('bitmessagesettings', 'defaultnoncetrialsperbyte', str(int(float( + self.settingsDialogInstance.ui.lineEditTotalDifficulty.text()) * defaults.networkDefaultProofOfWorkNonceTrialsPerByte))) + if float(self.settingsDialogInstance.ui.lineEditSmallMessageDifficulty.text()) >= 1: + BMConfigParser().set('bitmessagesettings', 'defaultpayloadlengthextrabytes', str(int(float( + self.settingsDialogInstance.ui.lineEditSmallMessageDifficulty.text()) * defaults.networkDefaultPayloadLengthExtraBytes))) + + if self.settingsDialogInstance.ui.comboBoxOpenCL.currentText().toUtf8() != BMConfigParser().safeGet("bitmessagesettings", "opencl"): + BMConfigParser().set('bitmessagesettings', 'opencl', str(self.settingsDialogInstance.ui.comboBoxOpenCL.currentText())) + queues.workerQueue.put(('resetPoW', '')) + + acceptableDifficultyChanged = False + + if float(self.settingsDialogInstance.ui.lineEditMaxAcceptableTotalDifficulty.text()) >= 1 or float(self.settingsDialogInstance.ui.lineEditMaxAcceptableTotalDifficulty.text()) == 0: + if BMConfigParser().get('bitmessagesettings','maxacceptablenoncetrialsperbyte') != str(int(float( + self.settingsDialogInstance.ui.lineEditMaxAcceptableTotalDifficulty.text()) * defaults.networkDefaultProofOfWorkNonceTrialsPerByte)): + # the user changed the max acceptable total difficulty + acceptableDifficultyChanged = True + BMConfigParser().set('bitmessagesettings', 'maxacceptablenoncetrialsperbyte', str(int(float( + self.settingsDialogInstance.ui.lineEditMaxAcceptableTotalDifficulty.text()) * defaults.networkDefaultProofOfWorkNonceTrialsPerByte))) + if float(self.settingsDialogInstance.ui.lineEditMaxAcceptableSmallMessageDifficulty.text()) >= 1 or float(self.settingsDialogInstance.ui.lineEditMaxAcceptableSmallMessageDifficulty.text()) == 0: + if BMConfigParser().get('bitmessagesettings','maxacceptablepayloadlengthextrabytes') != str(int(float( + self.settingsDialogInstance.ui.lineEditMaxAcceptableSmallMessageDifficulty.text()) * defaults.networkDefaultPayloadLengthExtraBytes)): + # the user changed the max acceptable small message difficulty + acceptableDifficultyChanged = True + BMConfigParser().set('bitmessagesettings', 'maxacceptablepayloadlengthextrabytes', str(int(float( + self.settingsDialogInstance.ui.lineEditMaxAcceptableSmallMessageDifficulty.text()) * defaults.networkDefaultPayloadLengthExtraBytes))) + if acceptableDifficultyChanged: + # It might now be possible to send msgs which were previously marked as toodifficult. + # Let us change them to 'msgqueued'. The singleWorker will try to send them and will again + # mark them as toodifficult if the receiver's required difficulty is still higher than + # we are willing to do. + sqlExecute('''UPDATE sent SET status='msgqueued' WHERE status='toodifficult' ''') + queues.workerQueue.put(('sendmessage', '')) + + #start:UI setting to stop trying to send messages after X days/months + # I'm open to changing this UI to something else if someone has a better idea. + if ((self.settingsDialogInstance.ui.lineEditDays.text()=='') and (self.settingsDialogInstance.ui.lineEditMonths.text()=='')):#We need to handle this special case. Bitmessage has its default behavior. The input is blank/blank + BMConfigParser().set('bitmessagesettings', 'stopresendingafterxdays', '') + BMConfigParser().set('bitmessagesettings', 'stopresendingafterxmonths', '') + shared.maximumLengthOfTimeToBotherResendingMessages = float('inf') + try: + float(self.settingsDialogInstance.ui.lineEditDays.text()) + lineEditDaysIsValidFloat = True + except: + lineEditDaysIsValidFloat = False + try: + float(self.settingsDialogInstance.ui.lineEditMonths.text()) + lineEditMonthsIsValidFloat = True + except: + lineEditMonthsIsValidFloat = False + if lineEditDaysIsValidFloat and not lineEditMonthsIsValidFloat: + self.settingsDialogInstance.ui.lineEditMonths.setText("0") + if lineEditMonthsIsValidFloat and not lineEditDaysIsValidFloat: + self.settingsDialogInstance.ui.lineEditDays.setText("0") + if lineEditDaysIsValidFloat or lineEditMonthsIsValidFloat: + if (float(self.settingsDialogInstance.ui.lineEditDays.text()) >=0 and float(self.settingsDialogInstance.ui.lineEditMonths.text()) >=0): + shared.maximumLengthOfTimeToBotherResendingMessages = (float(str(self.settingsDialogInstance.ui.lineEditDays.text())) * 24 * 60 * 60) + (float(str(self.settingsDialogInstance.ui.lineEditMonths.text())) * (60 * 60 * 24 *365)/12) + if shared.maximumLengthOfTimeToBotherResendingMessages < 432000: # If the time period is less than 5 hours, we give zero values to all fields. No message will be sent again. + QtGui.QMessageBox.about(self, _translate("MainWindow", "Will not resend ever"), _translate( + "MainWindow", "Note that the time limit you entered is less than the amount of time Bitmessage waits for the first resend attempt therefore your messages will never be resent.")) + BMConfigParser().set('bitmessagesettings', 'stopresendingafterxdays', '0') + BMConfigParser().set('bitmessagesettings', 'stopresendingafterxmonths', '0') + shared.maximumLengthOfTimeToBotherResendingMessages = 0 + else: + BMConfigParser().set('bitmessagesettings', 'stopresendingafterxdays', str(float( + self.settingsDialogInstance.ui.lineEditDays.text()))) + BMConfigParser().set('bitmessagesettings', 'stopresendingafterxmonths', str(float( + self.settingsDialogInstance.ui.lineEditMonths.text()))) + + BMConfigParser().save() + + if 'win32' in sys.platform or 'win64' in sys.platform: + # Auto-startup for Windows + RUN_PATH = "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" + self.settings = QtCore.QSettings(RUN_PATH, QtCore.QSettings.NativeFormat) + if BMConfigParser().getboolean('bitmessagesettings', 'startonlogon'): + self.settings.setValue("PyBitmessage", sys.argv[0]) + else: + self.settings.remove("PyBitmessage") + elif 'darwin' in sys.platform: + # startup for mac + pass + elif 'linux' in sys.platform: + # startup for linux + pass + + if state.appdata != paths.lookupExeFolder() and self.settingsDialogInstance.ui.checkBoxPortableMode.isChecked(): # If we are NOT using portable mode now but the user selected that we should... + # Write the keys.dat file to disk in the new location + sqlStoredProcedure('movemessagstoprog') + with open(paths.lookupExeFolder() + 'keys.dat', 'wb') as configfile: + BMConfigParser().write(configfile) + # Write the knownnodes.dat file to disk in the new location + knownnodes.saveKnownNodes(paths.lookupExeFolder()) + os.remove(state.appdata + 'keys.dat') + os.remove(state.appdata + 'knownnodes.dat') + previousAppdataLocation = state.appdata + state.appdata = paths.lookupExeFolder() + debug.resetLogging() + try: + os.remove(previousAppdataLocation + 'debug.log') + os.remove(previousAppdataLocation + 'debug.log.1') + except: + pass + + if state.appdata == paths.lookupExeFolder() and not self.settingsDialogInstance.ui.checkBoxPortableMode.isChecked(): # If we ARE using portable mode now but the user selected that we shouldn't... + state.appdata = paths.lookupAppdataFolder() + if not os.path.exists(state.appdata): + os.makedirs(state.appdata) + sqlStoredProcedure('movemessagstoappdata') + # Write the keys.dat file to disk in the new location + BMConfigParser().save() + # Write the knownnodes.dat file to disk in the new location + knownnodes.saveKnownNodes(state.appdata) + os.remove(paths.lookupExeFolder() + 'keys.dat') + os.remove(paths.lookupExeFolder() + 'knownnodes.dat') + debug.resetLogging() + try: + os.remove(paths.lookupExeFolder() + 'debug.log') + os.remove(paths.lookupExeFolder() + 'debug.log.1') + except: + pass def on_action_Send(self): """Send message to current selected address""" @@ -3081,8 +3270,8 @@ class MyForm(settingsmixin.SMainWindow): 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'") + + "DELETE FROM inbox" if folder == "trash" or shifted else + "UPDATE inbox SET folder='trash'" " WHERE msgid IN ({0})", idCount, *inventoryHashesToTrash) tableWidget.selectRow(0 if currentRow == 0 else currentRow - 1) tableWidget.setUpdatesEnabled(True) @@ -3204,7 +3393,8 @@ class MyForm(settingsmixin.SMainWindow): 0].row() item = self.ui.tableWidgetAddressBook.item(currentRow, 0) sqlExecute( - 'DELETE FROM addressbook WHERE address=?', item.address) + 'DELETE FROM addressbook WHERE label=? AND address=?', + item.label, item.address) self.ui.tableWidgetAddressBook.removeRow(currentRow) self.rerenderMessagelistFromLabels() self.rerenderMessagelistToLabels() @@ -4063,6 +4253,237 @@ class MyForm(settingsmixin.SMainWindow): obj.loadSettings() +class settingsDialog(QtGui.QDialog): + + def __init__(self, parent): + QtGui.QWidget.__init__(self, parent) + self.ui = Ui_settingsDialog() + self.ui.setupUi(self) + self.parent = parent + self.ui.checkBoxStartOnLogon.setChecked( + BMConfigParser().getboolean('bitmessagesettings', 'startonlogon')) + self.ui.checkBoxMinimizeToTray.setChecked( + BMConfigParser().getboolean('bitmessagesettings', 'minimizetotray')) + self.ui.checkBoxTrayOnClose.setChecked( + BMConfigParser().safeGetBoolean('bitmessagesettings', 'trayonclose')) + self.ui.checkBoxHideTrayConnectionNotifications.setChecked( + BMConfigParser().getboolean("bitmessagesettings", "hidetrayconnectionnotifications")) + self.ui.checkBoxShowTrayNotifications.setChecked( + BMConfigParser().getboolean('bitmessagesettings', 'showtraynotifications')) + self.ui.checkBoxStartInTray.setChecked( + BMConfigParser().getboolean('bitmessagesettings', 'startintray')) + self.ui.checkBoxWillinglySendToMobile.setChecked( + BMConfigParser().safeGetBoolean('bitmessagesettings', 'willinglysendtomobile')) + self.ui.checkBoxUseIdenticons.setChecked( + BMConfigParser().safeGetBoolean('bitmessagesettings', 'useidenticons')) + self.ui.checkBoxReplyBelow.setChecked( + BMConfigParser().safeGetBoolean('bitmessagesettings', 'replybelow')) + + if state.appdata == paths.lookupExeFolder(): + self.ui.checkBoxPortableMode.setChecked(True) + else: + try: + import tempfile + tempfile.NamedTemporaryFile( + dir=paths.lookupExeFolder(), delete=True + ).close() # should autodelete + except: + self.ui.checkBoxPortableMode.setDisabled(True) + + if 'darwin' in sys.platform: + self.ui.checkBoxStartOnLogon.setDisabled(True) + self.ui.checkBoxStartOnLogon.setText(_translate( + "MainWindow", "Start-on-login not yet supported on your OS.")) + self.ui.checkBoxMinimizeToTray.setDisabled(True) + self.ui.checkBoxMinimizeToTray.setText(_translate( + "MainWindow", "Minimize-to-tray not yet supported on your OS.")) + self.ui.checkBoxShowTrayNotifications.setDisabled(True) + self.ui.checkBoxShowTrayNotifications.setText(_translate( + "MainWindow", "Tray notifications not yet supported on your OS.")) + elif 'linux' in sys.platform: + self.ui.checkBoxStartOnLogon.setDisabled(True) + self.ui.checkBoxStartOnLogon.setText(_translate( + "MainWindow", "Start-on-login not yet supported on your OS.")) + # On the Network settings tab: + self.ui.lineEditTCPPort.setText(str( + BMConfigParser().get('bitmessagesettings', 'port'))) + self.ui.checkBoxUPnP.setChecked( + BMConfigParser().safeGetBoolean('bitmessagesettings', 'upnp')) + self.ui.checkBoxAuthentication.setChecked(BMConfigParser().getboolean( + 'bitmessagesettings', 'socksauthentication')) + self.ui.checkBoxSocksListen.setChecked(BMConfigParser().getboolean( + 'bitmessagesettings', 'sockslisten')) + if str(BMConfigParser().get('bitmessagesettings', 'socksproxytype')) == 'none': + self.ui.comboBoxProxyType.setCurrentIndex(0) + self.ui.lineEditSocksHostname.setEnabled(False) + self.ui.lineEditSocksPort.setEnabled(False) + self.ui.lineEditSocksUsername.setEnabled(False) + self.ui.lineEditSocksPassword.setEnabled(False) + self.ui.checkBoxAuthentication.setEnabled(False) + self.ui.checkBoxSocksListen.setEnabled(False) + elif str(BMConfigParser().get('bitmessagesettings', 'socksproxytype')) == 'SOCKS4a': + self.ui.comboBoxProxyType.setCurrentIndex(1) + elif str(BMConfigParser().get('bitmessagesettings', 'socksproxytype')) == 'SOCKS5': + self.ui.comboBoxProxyType.setCurrentIndex(2) + + self.ui.lineEditSocksHostname.setText(str( + BMConfigParser().get('bitmessagesettings', 'sockshostname'))) + self.ui.lineEditSocksPort.setText(str( + BMConfigParser().get('bitmessagesettings', 'socksport'))) + self.ui.lineEditSocksUsername.setText(str( + BMConfigParser().get('bitmessagesettings', 'socksusername'))) + self.ui.lineEditSocksPassword.setText(str( + BMConfigParser().get('bitmessagesettings', 'sockspassword'))) + QtCore.QObject.connect(self.ui.comboBoxProxyType, QtCore.SIGNAL( + "currentIndexChanged(int)"), self.comboBoxProxyTypeChanged) + self.ui.lineEditMaxDownloadRate.setText(str( + BMConfigParser().get('bitmessagesettings', 'maxdownloadrate'))) + self.ui.lineEditMaxUploadRate.setText(str( + BMConfigParser().get('bitmessagesettings', 'maxuploadrate'))) + self.ui.lineEditMaxOutboundConnections.setText(str( + BMConfigParser().get('bitmessagesettings', 'maxoutboundconnections'))) + + # Demanded difficulty tab + self.ui.lineEditTotalDifficulty.setText(str((float(BMConfigParser().getint( + 'bitmessagesettings', 'defaultnoncetrialsperbyte')) / defaults.networkDefaultProofOfWorkNonceTrialsPerByte))) + self.ui.lineEditSmallMessageDifficulty.setText(str((float(BMConfigParser().getint( + 'bitmessagesettings', 'defaultpayloadlengthextrabytes')) / defaults.networkDefaultPayloadLengthExtraBytes))) + + # Max acceptable difficulty tab + self.ui.lineEditMaxAcceptableTotalDifficulty.setText(str((float(BMConfigParser().getint( + 'bitmessagesettings', 'maxacceptablenoncetrialsperbyte')) / defaults.networkDefaultProofOfWorkNonceTrialsPerByte))) + self.ui.lineEditMaxAcceptableSmallMessageDifficulty.setText(str((float(BMConfigParser().getint( + 'bitmessagesettings', 'maxacceptablepayloadlengthextrabytes')) / defaults.networkDefaultPayloadLengthExtraBytes))) + + # OpenCL + if openclpow.openclAvailable(): + self.ui.comboBoxOpenCL.setEnabled(True) + else: + self.ui.comboBoxOpenCL.setEnabled(False) + self.ui.comboBoxOpenCL.clear() + self.ui.comboBoxOpenCL.addItem("None") + self.ui.comboBoxOpenCL.addItems(openclpow.vendors) + self.ui.comboBoxOpenCL.setCurrentIndex(0) + for i in range(self.ui.comboBoxOpenCL.count()): + if self.ui.comboBoxOpenCL.itemText(i) == BMConfigParser().safeGet('bitmessagesettings', 'opencl'): + self.ui.comboBoxOpenCL.setCurrentIndex(i) + break + + # Namecoin integration tab + nmctype = BMConfigParser().get('bitmessagesettings', 'namecoinrpctype') + self.ui.lineEditNamecoinHost.setText(str( + BMConfigParser().get('bitmessagesettings', 'namecoinrpchost'))) + self.ui.lineEditNamecoinPort.setText(str( + BMConfigParser().get('bitmessagesettings', 'namecoinrpcport'))) + self.ui.lineEditNamecoinUser.setText(str( + BMConfigParser().get('bitmessagesettings', 'namecoinrpcuser'))) + self.ui.lineEditNamecoinPassword.setText(str( + BMConfigParser().get('bitmessagesettings', 'namecoinrpcpassword'))) + + if nmctype == "namecoind": + self.ui.radioButtonNamecoinNamecoind.setChecked(True) + elif nmctype == "nmcontrol": + self.ui.radioButtonNamecoinNmcontrol.setChecked(True) + self.ui.lineEditNamecoinUser.setEnabled(False) + self.ui.labelNamecoinUser.setEnabled(False) + self.ui.lineEditNamecoinPassword.setEnabled(False) + self.ui.labelNamecoinPassword.setEnabled(False) + else: + assert False + + QtCore.QObject.connect(self.ui.radioButtonNamecoinNamecoind, QtCore.SIGNAL( + "toggled(bool)"), self.namecoinTypeChanged) + QtCore.QObject.connect(self.ui.radioButtonNamecoinNmcontrol, QtCore.SIGNAL( + "toggled(bool)"), self.namecoinTypeChanged) + QtCore.QObject.connect(self.ui.pushButtonNamecoinTest, QtCore.SIGNAL( + "clicked()"), self.click_pushButtonNamecoinTest) + + #Message Resend tab + self.ui.lineEditDays.setText(str( + BMConfigParser().get('bitmessagesettings', 'stopresendingafterxdays'))) + self.ui.lineEditMonths.setText(str( + BMConfigParser().get('bitmessagesettings', 'stopresendingafterxmonths'))) + + + #'System' tab removed for now. + """try: + maxCores = BMConfigParser().getint('bitmessagesettings', 'maxcores') + except: + maxCores = 99999 + if maxCores <= 1: + self.ui.comboBoxMaxCores.setCurrentIndex(0) + elif maxCores == 2: + self.ui.comboBoxMaxCores.setCurrentIndex(1) + elif maxCores <= 4: + self.ui.comboBoxMaxCores.setCurrentIndex(2) + elif maxCores <= 8: + self.ui.comboBoxMaxCores.setCurrentIndex(3) + elif maxCores <= 16: + self.ui.comboBoxMaxCores.setCurrentIndex(4) + else: + self.ui.comboBoxMaxCores.setCurrentIndex(5)""" + + QtGui.QWidget.resize(self, QtGui.QWidget.sizeHint(self)) + + def comboBoxProxyTypeChanged(self, comboBoxIndex): + if comboBoxIndex == 0: + self.ui.lineEditSocksHostname.setEnabled(False) + self.ui.lineEditSocksPort.setEnabled(False) + self.ui.lineEditSocksUsername.setEnabled(False) + self.ui.lineEditSocksPassword.setEnabled(False) + self.ui.checkBoxAuthentication.setEnabled(False) + self.ui.checkBoxSocksListen.setEnabled(False) + elif comboBoxIndex == 1 or comboBoxIndex == 2: + self.ui.lineEditSocksHostname.setEnabled(True) + self.ui.lineEditSocksPort.setEnabled(True) + self.ui.checkBoxAuthentication.setEnabled(True) + self.ui.checkBoxSocksListen.setEnabled(True) + if self.ui.checkBoxAuthentication.isChecked(): + self.ui.lineEditSocksUsername.setEnabled(True) + self.ui.lineEditSocksPassword.setEnabled(True) + + # Check status of namecoin integration radio buttons and translate + # it to a string as in the options. + def getNamecoinType(self): + if self.ui.radioButtonNamecoinNamecoind.isChecked(): + return "namecoind" + if self.ui.radioButtonNamecoinNmcontrol.isChecked(): + return "nmcontrol" + assert False + + # Namecoin connection type was changed. + def namecoinTypeChanged(self, checked): + nmctype = self.getNamecoinType() + assert nmctype == "namecoind" or nmctype == "nmcontrol" + + isNamecoind = (nmctype == "namecoind") + self.ui.lineEditNamecoinUser.setEnabled(isNamecoind) + self.ui.labelNamecoinUser.setEnabled(isNamecoind) + self.ui.lineEditNamecoinPassword.setEnabled(isNamecoind) + self.ui.labelNamecoinPassword.setEnabled(isNamecoind) + + if isNamecoind: + self.ui.lineEditNamecoinPort.setText(defaults.namecoinDefaultRpcPort) + else: + self.ui.lineEditNamecoinPort.setText("9000") + + def click_pushButtonNamecoinTest(self): + """Test the namecoin settings specified in the settings dialog.""" + self.ui.labelNamecoinTestResult.setText(_translate( + "MainWindow", "Testing...")) + options = {} + options["type"] = self.getNamecoinType() + options["host"] = str(self.ui.lineEditNamecoinHost.text().toUtf8()) + options["port"] = str(self.ui.lineEditNamecoinPort.text().toUtf8()) + options["user"] = str(self.ui.lineEditNamecoinUser.text().toUtf8()) + options["password"] = str(self.ui.lineEditNamecoinPassword.text().toUtf8()) + nc = namecoin.namecoinConnection(options) + status, text = nc.test() + self.ui.labelNamecoinTestResult.setText(text) + if status == 'success': + self.parent.namecoin = nc + + # 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. @@ -4137,6 +4558,7 @@ def init(): def run(): global myapp app = init() + change_translation(l10n.getTranslationLanguage()) app.setStyleSheet("QStatusBar::item { border: 0px solid black }") myapp = MyForm() diff --git a/src/bitmessageqt/about.ui b/src/bitmessageqt/about.ui index 7073bbd1..a912927a 100644 --- a/src/bitmessageqt/about.ui +++ b/src/bitmessageqt/about.ui @@ -46,7 +46,7 @@ - <html><head/><body><p>Copyright © 2012-2016 Jonathan Warren<br/>Copyright © 2012-2020 The Bitmessage Developers</p></body></html> + <html><head/><body><p>Copyright © 2012-2016 Jonathan Warren<br/>Copyright © 2012-2019 The Bitmessage Developers</p></body></html> Qt::AlignLeft diff --git a/src/bitmessageqt/dialogs.py b/src/bitmessageqt/dialogs.py index c667edb1..1acdbc3f 100644 --- a/src/bitmessageqt/dialogs.py +++ b/src/bitmessageqt/dialogs.py @@ -5,25 +5,22 @@ src/bitmessageqt/dialogs.py from PyQt4 import QtGui +from version import softwareVersion + import paths import widgets from address_dialogs import ( - AddAddressDialog, EmailGatewayDialog, NewAddressDialog, - NewSubscriptionDialog, RegenerateAddressesDialog, + AddAddressDialog, EmailGatewayDialog, NewAddressDialog, NewSubscriptionDialog, RegenerateAddressesDialog, SpecialAddressBehaviorDialog ) from newchandialog import NewChanDialog from retranslateui import RetranslateMixin -from settings import SettingsDialog from tr import _translate -from version import softwareVersion - __all__ = [ "NewChanDialog", "AddAddressDialog", "NewAddressDialog", "NewSubscriptionDialog", "RegenerateAddressesDialog", - "SpecialAddressBehaviorDialog", "EmailGatewayDialog", - "SettingsDialog" + "SpecialAddressBehaviorDialog", "EmailGatewayDialog" ] @@ -47,7 +44,7 @@ class AboutDialog(QtGui.QDialog, RetranslateMixin): try: self.label_2.setText( self.label_2.text().replace( - '2020', str(last_commit.get('time').year) + '2019', str(last_commit.get('time').year) )) except AttributeError: pass diff --git a/src/bitmessageqt/networkstatus.py b/src/bitmessageqt/networkstatus.py index 6fbf5df6..5f014563 100644 --- a/src/bitmessageqt/networkstatus.py +++ b/src/bitmessageqt/networkstatus.py @@ -14,7 +14,7 @@ import network.stats import shared import widgets from inventory import Inventory -from network import BMConnectionPool +from network.connectionpool import BMConnectionPool from retranslateui import RetranslateMixin from tr import _translate from uisignaler import UISignaler diff --git a/src/bitmessageqt/safehtmlparser.py b/src/bitmessageqt/safehtmlparser.py index edacd4bb..d1d7910c 100644 --- a/src/bitmessageqt/safehtmlparser.py +++ b/src/bitmessageqt/safehtmlparser.py @@ -1,73 +1,51 @@ -"""Subclass of HTMLParser.HTMLParser for MessageView widget""" - +from HTMLParser import HTMLParser import inspect import re -from HTMLParser import HTMLParser - -from urllib import quote_plus +from urllib import quote, quote_plus from urlparse import urlparse - class SafeHTMLParser(HTMLParser): - """HTML parser with sanitisation""" # from html5lib.sanitiser - acceptable_elements = ( - 'a', 'abbr', 'acronym', 'address', 'area', - 'article', 'aside', 'audio', 'b', 'big', 'blockquote', 'br', 'button', - 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', - 'command', 'datagrid', 'datalist', 'dd', 'del', 'details', 'dfn', - 'dialog', 'dir', 'div', 'dl', 'dt', 'em', 'event-source', 'fieldset', - 'figcaption', 'figure', 'footer', 'font', 'header', 'h1', - 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'ins', - 'keygen', 'kbd', 'label', 'legend', 'li', 'm', 'map', 'menu', 'meter', - 'multicol', 'nav', 'nextid', 'ol', 'output', 'optgroup', 'option', - 'p', 'pre', 'progress', 'q', 's', 'samp', 'section', 'select', - 'small', 'sound', 'source', 'spacer', 'span', 'strike', 'strong', - 'sub', 'sup', 'table', 'tbody', 'td', 'textarea', 'time', 'tfoot', - 'th', 'thead', 'tr', 'tt', 'u', 'ul', 'var', 'video' - ) - replaces_pre = ( - ("&", "&"), ("\"", """), ("<", "<"), (">", ">")) - replaces_post = ( - ("\n", "
"), ("\t", "    "), - (" ", "  "), (" ", "  "), ("
", "
 ")) - src_schemes = ["data"] - # uriregex1 = re.compile( - # r'(?i)\b((?:(https?|ftp|bitcoin):(?:/{1,3}|[a-z0-9%])' - # r'|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)' - # r'(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))' - # r'+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:\'".,<>?]))') - uriregex1 = re.compile( - r'((https?|ftp|bitcoin):(?:/{1,3}|[a-z0-9%])' - r'(?:[a-zA-Z]|[0-9]|[$-_@.&+#]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)' - ) + acceptable_elements = ['a', 'abbr', 'acronym', 'address', 'area', + 'article', 'aside', 'audio', 'b', 'big', 'blockquote', 'br', 'button', + 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', + 'command', 'datagrid', 'datalist', 'dd', 'del', 'details', 'dfn', + 'dialog', 'dir', 'div', 'dl', 'dt', 'em', 'event-source', 'fieldset', + 'figcaption', 'figure', 'footer', 'font', 'header', 'h1', + 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'ins', + 'keygen', 'kbd', 'label', 'legend', 'li', 'm', 'map', 'menu', 'meter', + 'multicol', 'nav', 'nextid', 'ol', 'output', 'optgroup', 'option', + 'p', 'pre', 'progress', 'q', 's', 'samp', 'section', 'select', + 'small', 'sound', 'source', 'spacer', 'span', 'strike', 'strong', + 'sub', 'sup', 'table', 'tbody', 'td', 'textarea', 'time', 'tfoot', + 'th', 'thead', 'tr', 'tt', 'u', 'ul', 'var', 'video'] + replaces_pre = [["&", "&"], ["\"", """], ["<", "<"], [">", ">"]] + replaces_post = [["\n", "
"], ["\t", "    "], [" ", "  "], [" ", "  "], ["
", "
 "]] + src_schemes = [ "data" ] + #uriregex1 = re.compile(r'(?i)\b((?:(https?|ftp|bitcoin):(?:/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:\'".,<>?]))') + uriregex1 = re.compile(r'((https?|ftp|bitcoin):(?:/{1,3}|[a-z0-9%])(?:[a-zA-Z]|[0-9]|[$-_@.&+#]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)') uriregex2 = re.compile(r' 1 and text[0] == " ": text = " " + text[1:] return text def __init__(self, *args, **kwargs): HTMLParser.__init__(self, *args, **kwargs) - self.reset() self.reset_safe() - + def reset_safe(self): - """Reset runtime variables specific to this class""" self.elements = set() self.raw = u"" self.sanitised = u"" @@ -75,9 +53,8 @@ class SafeHTMLParser(HTMLParser): self.allow_picture = False self.allow_external_src = False - def add_if_acceptable(self, tag, attrs=None): - """Add tag if it passes sanitisation""" - if tag not in self.acceptable_elements: + def add_if_acceptable(self, tag, attrs = None): + if tag not in SafeHTMLParser.acceptable_elements: return self.sanitised += "<" if inspect.stack()[1][3] == "handle_endtag": @@ -89,7 +66,7 @@ class SafeHTMLParser(HTMLParser): val = "" elif attr == "src" and not self.allow_external_src: url = urlparse(val) - if url.scheme not in self.src_schemes: + if url.scheme not in SafeHTMLParser.src_schemes: val = "" self.sanitised += " " + quote_plus(attr) if not (val is None): @@ -97,26 +74,26 @@ class SafeHTMLParser(HTMLParser): if inspect.stack()[1][3] == "handle_startendtag": self.sanitised += "/" self.sanitised += ">" - + def handle_starttag(self, tag, attrs): - if tag in self.acceptable_elements: + if tag in SafeHTMLParser.acceptable_elements: self.has_html = True self.add_if_acceptable(tag, attrs) def handle_endtag(self, tag): self.add_if_acceptable(tag) - + def handle_startendtag(self, tag, attrs): - if tag in self.acceptable_elements: + if tag in SafeHTMLParser.acceptable_elements: self.has_html = True self.add_if_acceptable(tag, attrs) - + def handle_data(self, data): self.sanitised += data - + def handle_charref(self, name): self.sanitised += "&#" + name + ";" - + def handle_entityref(self, name): self.sanitised += "&" + name + ";" @@ -127,14 +104,15 @@ class SafeHTMLParser(HTMLParser): data = unicode(data, 'utf-8', errors='replace') HTMLParser.feed(self, data) tmp = SafeHTMLParser.replace_pre(data) - tmp = self.uriregex1.sub(r'\1', tmp) - tmp = self.uriregex2.sub(r'\1', tmp) + tmp = SafeHTMLParser.uriregex1.sub( + r'\1', + tmp) + tmp = SafeHTMLParser.uriregex2.sub(r'\1', tmp) tmp = SafeHTMLParser.replace_post(tmp) self.raw += tmp - def is_html(self, text=None, allow_picture=False): - """Detect if string contains HTML tags""" + def is_html(self, text = None, allow_picture = False): if text: self.reset() self.reset_safe() diff --git a/src/bitmessageqt/settings.py b/src/bitmessageqt/settings.py index 011d38ed..3a3db962 100644 --- a/src/bitmessageqt/settings.py +++ b/src/bitmessageqt/settings.py @@ -1,581 +1,630 @@ -import ConfigParser -import os -import sys +# -*- coding: utf-8 -*- +# pylint: disable=too-many-instance-attributes,too-many-locals,too-many-statements,attribute-defined-outside-init +""" +src/bitmessageqt/settings.py +============================ + +Form implementation generated from reading ui file 'settings.ui' + +Created: Thu Dec 25 23:21:20 2014 + by: PyQt4 UI code generator 4.10.3 + +WARNING! All changes made in this file will be lost! +""" + +from sys import platform 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 BMConfigParser -from helper_sql import sqlExecute, sqlStoredProcedure -from helper_startup import start_proxyconfig -from network.asyncore_pollchoose import set_rates -from tr import _translate +from . import bitmessage_icons_rc # pylint: disable=unused-import +from .languagebox import LanguageBox + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + def _fromUtf8(s): + return s + +try: + _encoding = QtGui.QApplication.UnicodeUTF8 + + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig, _encoding) +except AttributeError: + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig) -def getSOCKSProxyType(config): - """Get user socksproxytype setting from *config*""" - try: - result = ConfigParser.SafeConfigParser.get( - config, 'bitmessagesettings', 'socksproxytype') - except (ConfigParser.NoSectionError, ConfigParser.NoOptionError): - return - else: - if result.lower() in ('', 'none', 'false'): - result = None - return result +class Ui_settingsDialog(object): + """Encapsulate a UI settings dialog object""" + def setupUi(self, settingsDialog): + """Set up the UI""" -class SettingsDialog(QtGui.QDialog): - """The "Settings" dialog""" - def __init__(self, parent=None, firstrun=False): - super(SettingsDialog, self).__init__(parent) - widgets.load('settings.ui', self) - - self.parent = parent - self.firstrun = firstrun - self.config = BMConfigParser() - self.net_restart_needed = False - self.timer = QtCore.QTimer() - - try: - import pkg_resources - except ImportError: - pass - else: - # Append proxy types defined in plugins - for ep in pkg_resources.iter_entry_points( - 'bitmessage.proxyconfig'): - self.comboBoxProxyType.addItem(ep.name) - + settingsDialog.setObjectName(_fromUtf8("settingsDialog")) + settingsDialog.resize(521, 413) + self.gridLayout = QtGui.QGridLayout(settingsDialog) + self.gridLayout.setObjectName(_fromUtf8("gridLayout")) + self.buttonBox = QtGui.QDialogButtonBox(settingsDialog) + self.buttonBox.setOrientation(QtCore.Qt.Horizontal) + self.buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Cancel | QtGui.QDialogButtonBox.Ok) + self.buttonBox.setObjectName(_fromUtf8("buttonBox")) + self.gridLayout.addWidget(self.buttonBox, 1, 0, 1, 1) + self.tabWidgetSettings = QtGui.QTabWidget(settingsDialog) + self.tabWidgetSettings.setObjectName(_fromUtf8("tabWidgetSettings")) + self.tabUserInterface = QtGui.QWidget() + self.tabUserInterface.setEnabled(True) + self.tabUserInterface.setObjectName(_fromUtf8("tabUserInterface")) + self.formLayout = QtGui.QFormLayout(self.tabUserInterface) + self.formLayout.setObjectName(_fromUtf8("formLayout")) + self.checkBoxStartOnLogon = QtGui.QCheckBox(self.tabUserInterface) + self.checkBoxStartOnLogon.setObjectName(_fromUtf8("checkBoxStartOnLogon")) + self.formLayout.setWidget(0, QtGui.QFormLayout.LabelRole, self.checkBoxStartOnLogon) + self.groupBoxTray = QtGui.QGroupBox(self.tabUserInterface) + self.groupBoxTray.setObjectName(_fromUtf8("groupBoxTray")) + self.formLayoutTray = QtGui.QFormLayout(self.groupBoxTray) + self.formLayoutTray.setObjectName(_fromUtf8("formLayoutTray")) + self.checkBoxStartInTray = QtGui.QCheckBox(self.groupBoxTray) + self.checkBoxStartInTray.setObjectName(_fromUtf8("checkBoxStartInTray")) + self.formLayoutTray.setWidget(0, QtGui.QFormLayout.SpanningRole, self.checkBoxStartInTray) + self.checkBoxMinimizeToTray = QtGui.QCheckBox(self.groupBoxTray) + self.checkBoxMinimizeToTray.setChecked(True) + self.checkBoxMinimizeToTray.setObjectName(_fromUtf8("checkBoxMinimizeToTray")) + self.formLayoutTray.setWidget(1, QtGui.QFormLayout.LabelRole, self.checkBoxMinimizeToTray) + self.checkBoxTrayOnClose = QtGui.QCheckBox(self.groupBoxTray) + self.checkBoxTrayOnClose.setChecked(True) + self.checkBoxTrayOnClose.setObjectName(_fromUtf8("checkBoxTrayOnClose")) + self.formLayoutTray.setWidget(2, QtGui.QFormLayout.LabelRole, self.checkBoxTrayOnClose) + self.formLayout.setWidget(1, QtGui.QFormLayout.SpanningRole, self.groupBoxTray) + self.checkBoxHideTrayConnectionNotifications = QtGui.QCheckBox(self.tabUserInterface) + self.checkBoxHideTrayConnectionNotifications.setChecked(False) + self.checkBoxHideTrayConnectionNotifications.setObjectName( + _fromUtf8("checkBoxHideTrayConnectionNotifications")) + self.formLayout.setWidget(2, QtGui.QFormLayout.LabelRole, self.checkBoxHideTrayConnectionNotifications) + self.checkBoxShowTrayNotifications = QtGui.QCheckBox(self.tabUserInterface) + self.checkBoxShowTrayNotifications.setObjectName(_fromUtf8("checkBoxShowTrayNotifications")) + self.formLayout.setWidget(3, QtGui.QFormLayout.LabelRole, self.checkBoxShowTrayNotifications) + self.checkBoxPortableMode = QtGui.QCheckBox(self.tabUserInterface) + self.checkBoxPortableMode.setObjectName(_fromUtf8("checkBoxPortableMode")) + self.formLayout.setWidget(4, QtGui.QFormLayout.LabelRole, self.checkBoxPortableMode) + self.PortableModeDescription = QtGui.QLabel(self.tabUserInterface) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.PortableModeDescription.sizePolicy().hasHeightForWidth()) + self.PortableModeDescription.setSizePolicy(sizePolicy) + self.PortableModeDescription.setWordWrap(True) + self.PortableModeDescription.setObjectName(_fromUtf8("PortableModeDescription")) + self.formLayout.setWidget(5, QtGui.QFormLayout.SpanningRole, self.PortableModeDescription) + self.checkBoxWillinglySendToMobile = QtGui.QCheckBox(self.tabUserInterface) + self.checkBoxWillinglySendToMobile.setObjectName(_fromUtf8("checkBoxWillinglySendToMobile")) + self.formLayout.setWidget(6, QtGui.QFormLayout.SpanningRole, self.checkBoxWillinglySendToMobile) + self.checkBoxUseIdenticons = QtGui.QCheckBox(self.tabUserInterface) + self.checkBoxUseIdenticons.setObjectName(_fromUtf8("checkBoxUseIdenticons")) + self.formLayout.setWidget(7, QtGui.QFormLayout.LabelRole, self.checkBoxUseIdenticons) + self.checkBoxReplyBelow = QtGui.QCheckBox(self.tabUserInterface) + self.checkBoxReplyBelow.setObjectName(_fromUtf8("checkBoxReplyBelow")) + self.formLayout.setWidget(8, QtGui.QFormLayout.LabelRole, self.checkBoxReplyBelow) + self.groupBox = QtGui.QGroupBox(self.tabUserInterface) + self.groupBox.setObjectName(_fromUtf8("groupBox")) + self.formLayout_2 = QtGui.QFormLayout(self.groupBox) + self.formLayout_2.setObjectName(_fromUtf8("formLayout_2")) + self.languageComboBox = LanguageBox(self.groupBox) + self.languageComboBox.setMinimumSize(QtCore.QSize(100, 0)) + self.languageComboBox.setObjectName(_fromUtf8("languageComboBox")) # pylint: disable=not-callable + self.formLayout_2.setWidget(0, QtGui.QFormLayout.LabelRole, self.languageComboBox) + self.formLayout.setWidget(9, QtGui.QFormLayout.FieldRole, self.groupBox) + self.tabWidgetSettings.addTab(self.tabUserInterface, _fromUtf8("")) + self.tabNetworkSettings = QtGui.QWidget() + self.tabNetworkSettings.setObjectName(_fromUtf8("tabNetworkSettings")) + self.gridLayout_4 = QtGui.QGridLayout(self.tabNetworkSettings) + self.gridLayout_4.setObjectName(_fromUtf8("gridLayout_4")) + self.groupBox1 = QtGui.QGroupBox(self.tabNetworkSettings) + self.groupBox1.setObjectName(_fromUtf8("groupBox1")) + self.gridLayout_3 = QtGui.QGridLayout(self.groupBox1) + self.gridLayout_3.setObjectName(_fromUtf8("gridLayout_3")) + self.label = QtGui.QLabel(self.groupBox1) + self.label.setObjectName(_fromUtf8("label")) + self.gridLayout_3.addWidget(self.label, 0, 0, 1, 1, QtCore.Qt.AlignRight) + self.lineEditTCPPort = QtGui.QLineEdit(self.groupBox1) + self.lineEditTCPPort.setMaximumSize(QtCore.QSize(70, 16777215)) + self.lineEditTCPPort.setObjectName(_fromUtf8("lineEditTCPPort")) + self.gridLayout_3.addWidget(self.lineEditTCPPort, 0, 1, 1, 1, QtCore.Qt.AlignLeft) + self.labelUPnP = QtGui.QLabel(self.groupBox1) + self.labelUPnP.setObjectName(_fromUtf8("labelUPnP")) + self.gridLayout_3.addWidget(self.labelUPnP, 0, 2, 1, 1, QtCore.Qt.AlignRight) + self.checkBoxUPnP = QtGui.QCheckBox(self.groupBox1) + self.checkBoxUPnP.setObjectName(_fromUtf8("checkBoxUPnP")) + self.gridLayout_3.addWidget(self.checkBoxUPnP, 0, 3, 1, 1, QtCore.Qt.AlignLeft) + self.gridLayout_4.addWidget(self.groupBox1, 0, 0, 1, 1) + self.groupBox_3 = QtGui.QGroupBox(self.tabNetworkSettings) + self.groupBox_3.setObjectName(_fromUtf8("groupBox_3")) + self.gridLayout_9 = QtGui.QGridLayout(self.groupBox_3) + self.gridLayout_9.setObjectName(_fromUtf8("gridLayout_9")) + spacerItem1 = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) + self.gridLayout_9.addItem(spacerItem1, 0, 0, 2, 1) + self.label_24 = QtGui.QLabel(self.groupBox_3) + self.label_24.setObjectName(_fromUtf8("label_24")) + self.gridLayout_9.addWidget(self.label_24, 0, 1, 1, 1) + self.lineEditMaxDownloadRate = QtGui.QLineEdit(self.groupBox_3) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.lineEditMaxDownloadRate.sizePolicy().hasHeightForWidth()) + self.lineEditMaxDownloadRate.setSizePolicy(sizePolicy) + self.lineEditMaxDownloadRate.setMaximumSize(QtCore.QSize(60, 16777215)) + self.lineEditMaxDownloadRate.setObjectName(_fromUtf8("lineEditMaxDownloadRate")) + self.gridLayout_9.addWidget(self.lineEditMaxDownloadRate, 0, 2, 1, 1) + self.label_25 = QtGui.QLabel(self.groupBox_3) + self.label_25.setObjectName(_fromUtf8("label_25")) + self.gridLayout_9.addWidget(self.label_25, 1, 1, 1, 1) + self.lineEditMaxUploadRate = QtGui.QLineEdit(self.groupBox_3) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.lineEditMaxUploadRate.sizePolicy().hasHeightForWidth()) + self.lineEditMaxUploadRate.setSizePolicy(sizePolicy) + self.lineEditMaxUploadRate.setMaximumSize(QtCore.QSize(60, 16777215)) + self.lineEditMaxUploadRate.setObjectName(_fromUtf8("lineEditMaxUploadRate")) + self.gridLayout_9.addWidget(self.lineEditMaxUploadRate, 1, 2, 1, 1) + self.label_26 = QtGui.QLabel(self.groupBox_3) + self.label_26.setObjectName(_fromUtf8("label_26")) + self.gridLayout_9.addWidget(self.label_26, 2, 1, 1, 1) + self.lineEditMaxOutboundConnections = QtGui.QLineEdit(self.groupBox_3) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.lineEditMaxOutboundConnections.sizePolicy().hasHeightForWidth()) + self.lineEditMaxOutboundConnections.setSizePolicy(sizePolicy) + self.lineEditMaxOutboundConnections.setMaximumSize(QtCore.QSize(60, 16777215)) + self.lineEditMaxOutboundConnections.setObjectName(_fromUtf8("lineEditMaxOutboundConnections")) self.lineEditMaxOutboundConnections.setValidator( QtGui.QIntValidator(0, 8, self.lineEditMaxOutboundConnections)) - - self.adjust_from_config(self.config) - if firstrun: - # switch to "Network Settings" tab if user selected - # "Let me configure special network settings first" on first run - self.tabWidgetSettings.setCurrentIndex( - self.tabWidgetSettings.indexOf(self.tabNetworkSettings) - ) - QtGui.QWidget.resize(self, QtGui.QWidget.sizeHint(self)) - - def adjust_from_config(self, config): - """Adjust all widgets state according to config settings""" - # pylint: disable=too-many-branches,too-many-statements - if not self.parent.tray.isSystemTrayAvailable(): - self.groupBoxTray.setEnabled(False) - self.groupBoxTray.setTitle(_translate( - "MainWindow", "Tray (not available in your system)")) - for setting in ( - 'minimizetotray', 'trayonclose', 'startintray'): - config.set('bitmessagesettings', setting, 'false') + self.gridLayout_9.addWidget(self.lineEditMaxOutboundConnections, 2, 2, 1, 1) + self.gridLayout_4.addWidget(self.groupBox_3, 2, 0, 1, 1) + self.groupBox_2 = QtGui.QGroupBox(self.tabNetworkSettings) + self.groupBox_2.setObjectName(_fromUtf8("groupBox_2")) + self.gridLayout_2 = QtGui.QGridLayout(self.groupBox_2) + self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2")) + self.label_2 = QtGui.QLabel(self.groupBox_2) + self.label_2.setObjectName(_fromUtf8("label_2")) + self.gridLayout_2.addWidget(self.label_2, 0, 0, 1, 1) + self.label_3 = QtGui.QLabel(self.groupBox_2) + self.label_3.setObjectName(_fromUtf8("label_3")) + self.gridLayout_2.addWidget(self.label_3, 1, 1, 1, 1) + self.lineEditSocksHostname = QtGui.QLineEdit(self.groupBox_2) + self.lineEditSocksHostname.setObjectName(_fromUtf8("lineEditSocksHostname")) + self.lineEditSocksHostname.setPlaceholderText(_fromUtf8("127.0.0.1")) + self.gridLayout_2.addWidget(self.lineEditSocksHostname, 1, 2, 1, 2) + self.label_4 = QtGui.QLabel(self.groupBox_2) + self.label_4.setObjectName(_fromUtf8("label_4")) + self.gridLayout_2.addWidget(self.label_4, 1, 4, 1, 1) + self.lineEditSocksPort = QtGui.QLineEdit(self.groupBox_2) + self.lineEditSocksPort.setObjectName(_fromUtf8("lineEditSocksPort")) + if platform in ['darwin', 'win32', 'win64']: + self.lineEditSocksPort.setPlaceholderText(_fromUtf8("9150")) else: - self.checkBoxMinimizeToTray.setChecked( - config.getboolean('bitmessagesettings', 'minimizetotray')) - self.checkBoxTrayOnClose.setChecked( - config.safeGetBoolean('bitmessagesettings', 'trayonclose')) - self.checkBoxStartInTray.setChecked( - config.getboolean('bitmessagesettings', 'startintray')) + self.lineEditSocksPort.setPlaceholderText(_fromUtf8("9050")) + self.gridLayout_2.addWidget(self.lineEditSocksPort, 1, 5, 1, 1) + self.checkBoxAuthentication = QtGui.QCheckBox(self.groupBox_2) + self.checkBoxAuthentication.setObjectName(_fromUtf8("checkBoxAuthentication")) + self.gridLayout_2.addWidget(self.checkBoxAuthentication, 2, 1, 1, 1) + self.label_5 = QtGui.QLabel(self.groupBox_2) + self.label_5.setObjectName(_fromUtf8("label_5")) + self.gridLayout_2.addWidget(self.label_5, 2, 2, 1, 1) + self.lineEditSocksUsername = QtGui.QLineEdit(self.groupBox_2) + self.lineEditSocksUsername.setEnabled(False) + self.lineEditSocksUsername.setObjectName(_fromUtf8("lineEditSocksUsername")) + self.gridLayout_2.addWidget(self.lineEditSocksUsername, 2, 3, 1, 1) + self.label_6 = QtGui.QLabel(self.groupBox_2) + self.label_6.setObjectName(_fromUtf8("label_6")) + self.gridLayout_2.addWidget(self.label_6, 2, 4, 1, 1) + self.lineEditSocksPassword = QtGui.QLineEdit(self.groupBox_2) + self.lineEditSocksPassword.setEnabled(False) + self.lineEditSocksPassword.setInputMethodHints( + QtCore.Qt.ImhHiddenText | QtCore.Qt.ImhNoAutoUppercase | QtCore.Qt.ImhNoPredictiveText) + self.lineEditSocksPassword.setEchoMode(QtGui.QLineEdit.Password) + self.lineEditSocksPassword.setObjectName(_fromUtf8("lineEditSocksPassword")) + self.gridLayout_2.addWidget(self.lineEditSocksPassword, 2, 5, 1, 1) + self.checkBoxSocksListen = QtGui.QCheckBox(self.groupBox_2) + self.checkBoxSocksListen.setObjectName(_fromUtf8("checkBoxSocksListen")) + self.gridLayout_2.addWidget(self.checkBoxSocksListen, 3, 1, 1, 4) + self.comboBoxProxyType = QtGui.QComboBox(self.groupBox_2) + self.comboBoxProxyType.setObjectName(_fromUtf8("comboBoxProxyType")) # pylint: disable=not-callable + self.comboBoxProxyType.addItem(_fromUtf8("")) + self.comboBoxProxyType.addItem(_fromUtf8("")) + self.comboBoxProxyType.addItem(_fromUtf8("")) + self.gridLayout_2.addWidget(self.comboBoxProxyType, 0, 1, 1, 1) + self.gridLayout_4.addWidget(self.groupBox_2, 1, 0, 1, 1) + spacerItem2 = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) + self.gridLayout_4.addItem(spacerItem2, 3, 0, 1, 1) + self.tabWidgetSettings.addTab(self.tabNetworkSettings, _fromUtf8("")) + self.tabDemandedDifficulty = QtGui.QWidget() + self.tabDemandedDifficulty.setObjectName(_fromUtf8("tabDemandedDifficulty")) + self.gridLayout_6 = QtGui.QGridLayout(self.tabDemandedDifficulty) + self.gridLayout_6.setObjectName(_fromUtf8("gridLayout_6")) + self.label_9 = QtGui.QLabel(self.tabDemandedDifficulty) + self.label_9.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignTrailing | QtCore.Qt.AlignVCenter) + self.label_9.setObjectName(_fromUtf8("label_9")) + self.gridLayout_6.addWidget(self.label_9, 1, 1, 1, 1) + self.label_10 = QtGui.QLabel(self.tabDemandedDifficulty) + self.label_10.setWordWrap(True) + self.label_10.setObjectName(_fromUtf8("label_10")) + self.gridLayout_6.addWidget(self.label_10, 2, 0, 1, 3) + self.label_11 = QtGui.QLabel(self.tabDemandedDifficulty) + self.label_11.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignTrailing | QtCore.Qt.AlignVCenter) + self.label_11.setObjectName(_fromUtf8("label_11")) + self.gridLayout_6.addWidget(self.label_11, 3, 1, 1, 1) + self.label_8 = QtGui.QLabel(self.tabDemandedDifficulty) + self.label_8.setWordWrap(True) + self.label_8.setObjectName(_fromUtf8("label_8")) + self.gridLayout_6.addWidget(self.label_8, 0, 0, 1, 3) + spacerItem3 = QtGui.QSpacerItem(203, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) + self.gridLayout_6.addItem(spacerItem3, 1, 0, 1, 1) + self.label_12 = QtGui.QLabel(self.tabDemandedDifficulty) + self.label_12.setWordWrap(True) + self.label_12.setObjectName(_fromUtf8("label_12")) + self.gridLayout_6.addWidget(self.label_12, 4, 0, 1, 3) + self.lineEditSmallMessageDifficulty = QtGui.QLineEdit(self.tabDemandedDifficulty) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.lineEditSmallMessageDifficulty.sizePolicy().hasHeightForWidth()) + self.lineEditSmallMessageDifficulty.setSizePolicy(sizePolicy) + self.lineEditSmallMessageDifficulty.setMaximumSize(QtCore.QSize(70, 16777215)) + self.lineEditSmallMessageDifficulty.setObjectName(_fromUtf8("lineEditSmallMessageDifficulty")) + self.gridLayout_6.addWidget(self.lineEditSmallMessageDifficulty, 3, 2, 1, 1) + self.lineEditTotalDifficulty = QtGui.QLineEdit(self.tabDemandedDifficulty) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.lineEditTotalDifficulty.sizePolicy().hasHeightForWidth()) + self.lineEditTotalDifficulty.setSizePolicy(sizePolicy) + self.lineEditTotalDifficulty.setMaximumSize(QtCore.QSize(70, 16777215)) + self.lineEditTotalDifficulty.setObjectName(_fromUtf8("lineEditTotalDifficulty")) + self.gridLayout_6.addWidget(self.lineEditTotalDifficulty, 1, 2, 1, 1) + spacerItem4 = QtGui.QSpacerItem(203, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) + self.gridLayout_6.addItem(spacerItem4, 3, 0, 1, 1) + spacerItem5 = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) + self.gridLayout_6.addItem(spacerItem5, 5, 0, 1, 1) + self.tabWidgetSettings.addTab(self.tabDemandedDifficulty, _fromUtf8("")) + self.tabMaxAcceptableDifficulty = QtGui.QWidget() + self.tabMaxAcceptableDifficulty.setObjectName(_fromUtf8("tabMaxAcceptableDifficulty")) + self.gridLayout_7 = QtGui.QGridLayout(self.tabMaxAcceptableDifficulty) + self.gridLayout_7.setObjectName(_fromUtf8("gridLayout_7")) + self.label_15 = QtGui.QLabel(self.tabMaxAcceptableDifficulty) + self.label_15.setWordWrap(True) + self.label_15.setObjectName(_fromUtf8("label_15")) + self.gridLayout_7.addWidget(self.label_15, 0, 0, 1, 3) + spacerItem6 = QtGui.QSpacerItem(102, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) + self.gridLayout_7.addItem(spacerItem6, 1, 0, 1, 1) + self.label_13 = QtGui.QLabel(self.tabMaxAcceptableDifficulty) + self.label_13.setLayoutDirection(QtCore.Qt.LeftToRight) + self.label_13.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignTrailing | QtCore.Qt.AlignVCenter) + self.label_13.setObjectName(_fromUtf8("label_13")) + self.gridLayout_7.addWidget(self.label_13, 1, 1, 1, 1) + self.lineEditMaxAcceptableTotalDifficulty = QtGui.QLineEdit(self.tabMaxAcceptableDifficulty) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.lineEditMaxAcceptableTotalDifficulty.sizePolicy().hasHeightForWidth()) + self.lineEditMaxAcceptableTotalDifficulty.setSizePolicy(sizePolicy) + self.lineEditMaxAcceptableTotalDifficulty.setMaximumSize(QtCore.QSize(70, 16777215)) + self.lineEditMaxAcceptableTotalDifficulty.setObjectName(_fromUtf8("lineEditMaxAcceptableTotalDifficulty")) + self.gridLayout_7.addWidget(self.lineEditMaxAcceptableTotalDifficulty, 1, 2, 1, 1) + spacerItem7 = QtGui.QSpacerItem(102, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) + self.gridLayout_7.addItem(spacerItem7, 2, 0, 1, 1) + self.label_14 = QtGui.QLabel(self.tabMaxAcceptableDifficulty) + self.label_14.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignTrailing | QtCore.Qt.AlignVCenter) + self.label_14.setObjectName(_fromUtf8("label_14")) + self.gridLayout_7.addWidget(self.label_14, 2, 1, 1, 1) + self.lineEditMaxAcceptableSmallMessageDifficulty = QtGui.QLineEdit(self.tabMaxAcceptableDifficulty) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.lineEditMaxAcceptableSmallMessageDifficulty.sizePolicy().hasHeightForWidth()) + self.lineEditMaxAcceptableSmallMessageDifficulty.setSizePolicy(sizePolicy) + self.lineEditMaxAcceptableSmallMessageDifficulty.setMaximumSize(QtCore.QSize(70, 16777215)) + self.lineEditMaxAcceptableSmallMessageDifficulty.setObjectName( + _fromUtf8("lineEditMaxAcceptableSmallMessageDifficulty")) + self.gridLayout_7.addWidget(self.lineEditMaxAcceptableSmallMessageDifficulty, 2, 2, 1, 1) + spacerItem8 = QtGui.QSpacerItem(20, 147, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) + self.gridLayout_7.addItem(spacerItem8, 3, 1, 1, 1) + self.labelOpenCL = QtGui.QLabel(self.tabMaxAcceptableDifficulty) + self.labelOpenCL.setObjectName(_fromUtf8("labelOpenCL")) + self.gridLayout_7.addWidget(self.labelOpenCL, 4, 0, 1, 1) + self.comboBoxOpenCL = QtGui.QComboBox(self.tabMaxAcceptableDifficulty) + self.comboBoxOpenCL.setObjectName = (_fromUtf8("comboBoxOpenCL")) + self.gridLayout_7.addWidget(self.comboBoxOpenCL, 4, 1, 1, 1) + self.tabWidgetSettings.addTab(self.tabMaxAcceptableDifficulty, _fromUtf8("")) + self.tabNamecoin = QtGui.QWidget() + self.tabNamecoin.setObjectName(_fromUtf8("tabNamecoin")) + self.gridLayout_8 = QtGui.QGridLayout(self.tabNamecoin) + self.gridLayout_8.setObjectName(_fromUtf8("gridLayout_8")) + spacerItem9 = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) + self.gridLayout_8.addItem(spacerItem9, 2, 0, 1, 1) + self.label_16 = QtGui.QLabel(self.tabNamecoin) + self.label_16.setWordWrap(True) + self.label_16.setObjectName(_fromUtf8("label_16")) + self.gridLayout_8.addWidget(self.label_16, 0, 0, 1, 3) + self.label_17 = QtGui.QLabel(self.tabNamecoin) + self.label_17.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignTrailing | QtCore.Qt.AlignVCenter) + self.label_17.setObjectName(_fromUtf8("label_17")) + self.gridLayout_8.addWidget(self.label_17, 2, 1, 1, 1) + self.lineEditNamecoinHost = QtGui.QLineEdit(self.tabNamecoin) + self.lineEditNamecoinHost.setObjectName(_fromUtf8("lineEditNamecoinHost")) + self.gridLayout_8.addWidget(self.lineEditNamecoinHost, 2, 2, 1, 1) + spacerItem10 = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) + self.gridLayout_8.addItem(spacerItem10, 3, 0, 1, 1) + spacerItem11 = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) + self.gridLayout_8.addItem(spacerItem11, 4, 0, 1, 1) + self.label_18 = QtGui.QLabel(self.tabNamecoin) + self.label_18.setEnabled(True) + self.label_18.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignTrailing | QtCore.Qt.AlignVCenter) + self.label_18.setObjectName(_fromUtf8("label_18")) + self.gridLayout_8.addWidget(self.label_18, 3, 1, 1, 1) + self.lineEditNamecoinPort = QtGui.QLineEdit(self.tabNamecoin) + self.lineEditNamecoinPort.setObjectName(_fromUtf8("lineEditNamecoinPort")) + self.gridLayout_8.addWidget(self.lineEditNamecoinPort, 3, 2, 1, 1) + spacerItem12 = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) + self.gridLayout_8.addItem(spacerItem12, 8, 1, 1, 1) + self.labelNamecoinUser = QtGui.QLabel(self.tabNamecoin) + self.labelNamecoinUser.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignTrailing | QtCore.Qt.AlignVCenter) + self.labelNamecoinUser.setObjectName(_fromUtf8("labelNamecoinUser")) + self.gridLayout_8.addWidget(self.labelNamecoinUser, 4, 1, 1, 1) + self.lineEditNamecoinUser = QtGui.QLineEdit(self.tabNamecoin) + self.lineEditNamecoinUser.setObjectName(_fromUtf8("lineEditNamecoinUser")) + self.gridLayout_8.addWidget(self.lineEditNamecoinUser, 4, 2, 1, 1) + spacerItem13 = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) + self.gridLayout_8.addItem(spacerItem13, 5, 0, 1, 1) + self.labelNamecoinPassword = QtGui.QLabel(self.tabNamecoin) + self.labelNamecoinPassword.setAlignment( + QtCore.Qt.AlignRight | QtCore.Qt.AlignTrailing | QtCore.Qt.AlignVCenter) + self.labelNamecoinPassword.setObjectName(_fromUtf8("labelNamecoinPassword")) + self.gridLayout_8.addWidget(self.labelNamecoinPassword, 5, 1, 1, 1) + self.lineEditNamecoinPassword = QtGui.QLineEdit(self.tabNamecoin) + self.lineEditNamecoinPassword.setInputMethodHints( + QtCore.Qt.ImhHiddenText | QtCore.Qt.ImhNoAutoUppercase | QtCore.Qt.ImhNoPredictiveText) + self.lineEditNamecoinPassword.setEchoMode(QtGui.QLineEdit.Password) + self.lineEditNamecoinPassword.setObjectName(_fromUtf8("lineEditNamecoinPassword")) + self.gridLayout_8.addWidget(self.lineEditNamecoinPassword, 5, 2, 1, 1) + self.labelNamecoinTestResult = QtGui.QLabel(self.tabNamecoin) + self.labelNamecoinTestResult.setText(_fromUtf8("")) + self.labelNamecoinTestResult.setObjectName(_fromUtf8("labelNamecoinTestResult")) + self.gridLayout_8.addWidget(self.labelNamecoinTestResult, 7, 0, 1, 2) + self.pushButtonNamecoinTest = QtGui.QPushButton(self.tabNamecoin) + self.pushButtonNamecoinTest.setObjectName(_fromUtf8("pushButtonNamecoinTest")) + self.gridLayout_8.addWidget(self.pushButtonNamecoinTest, 7, 2, 1, 1) + self.horizontalLayout = QtGui.QHBoxLayout() + self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout")) + self.label_21 = QtGui.QLabel(self.tabNamecoin) + self.label_21.setObjectName(_fromUtf8("label_21")) + self.horizontalLayout.addWidget(self.label_21) + self.radioButtonNamecoinNamecoind = QtGui.QRadioButton(self.tabNamecoin) + self.radioButtonNamecoinNamecoind.setObjectName(_fromUtf8("radioButtonNamecoinNamecoind")) + self.horizontalLayout.addWidget(self.radioButtonNamecoinNamecoind) + self.radioButtonNamecoinNmcontrol = QtGui.QRadioButton(self.tabNamecoin) + self.radioButtonNamecoinNmcontrol.setObjectName(_fromUtf8("radioButtonNamecoinNmcontrol")) + self.horizontalLayout.addWidget(self.radioButtonNamecoinNmcontrol) + self.gridLayout_8.addLayout(self.horizontalLayout, 1, 0, 1, 3) + self.tabWidgetSettings.addTab(self.tabNamecoin, _fromUtf8("")) + self.tabResendsExpire = QtGui.QWidget() + self.tabResendsExpire.setObjectName(_fromUtf8("tabResendsExpire")) + self.gridLayout_5 = QtGui.QGridLayout(self.tabResendsExpire) + self.gridLayout_5.setObjectName(_fromUtf8("gridLayout_5")) + self.label_7 = QtGui.QLabel(self.tabResendsExpire) + self.label_7.setWordWrap(True) + self.label_7.setObjectName(_fromUtf8("label_7")) + self.gridLayout_5.addWidget(self.label_7, 0, 0, 1, 3) + spacerItem14 = QtGui.QSpacerItem(212, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) + self.gridLayout_5.addItem(spacerItem14, 1, 0, 1, 1) + self.widget = QtGui.QWidget(self.tabResendsExpire) + self.widget.setMinimumSize(QtCore.QSize(231, 75)) + self.widget.setObjectName(_fromUtf8("widget")) + self.label_19 = QtGui.QLabel(self.widget) + self.label_19.setGeometry(QtCore.QRect(10, 20, 101, 20)) + self.label_19.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignTrailing | QtCore.Qt.AlignVCenter) + self.label_19.setObjectName(_fromUtf8("label_19")) + self.label_20 = QtGui.QLabel(self.widget) + self.label_20.setGeometry(QtCore.QRect(30, 40, 80, 16)) + self.label_20.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignTrailing | QtCore.Qt.AlignVCenter) + self.label_20.setObjectName(_fromUtf8("label_20")) + self.lineEditDays = QtGui.QLineEdit(self.widget) + self.lineEditDays.setGeometry(QtCore.QRect(113, 20, 51, 20)) + self.lineEditDays.setObjectName(_fromUtf8("lineEditDays")) + self.lineEditMonths = QtGui.QLineEdit(self.widget) + self.lineEditMonths.setGeometry(QtCore.QRect(113, 40, 51, 20)) + self.lineEditMonths.setObjectName(_fromUtf8("lineEditMonths")) + self.label_22 = QtGui.QLabel(self.widget) + self.label_22.setGeometry(QtCore.QRect(169, 23, 61, 16)) + self.label_22.setObjectName(_fromUtf8("label_22")) + self.label_23 = QtGui.QLabel(self.widget) + self.label_23.setGeometry(QtCore.QRect(170, 41, 71, 16)) + self.label_23.setObjectName(_fromUtf8("label_23")) + self.gridLayout_5.addWidget(self.widget, 1, 2, 1, 1) + spacerItem15 = QtGui.QSpacerItem(20, 129, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) + self.gridLayout_5.addItem(spacerItem15, 2, 1, 1, 1) + self.tabWidgetSettings.addTab(self.tabResendsExpire, _fromUtf8("")) + self.gridLayout.addWidget(self.tabWidgetSettings, 0, 0, 1, 1) - self.checkBoxHideTrayConnectionNotifications.setChecked( - config.getboolean( - 'bitmessagesettings', 'hidetrayconnectionnotifications')) - self.checkBoxShowTrayNotifications.setChecked( - config.getboolean('bitmessagesettings', 'showtraynotifications')) + self.retranslateUi(settingsDialog) + self.tabWidgetSettings.setCurrentIndex(0) + QtCore.QObject.connect( # pylint: disable=no-member + self.buttonBox, QtCore.SIGNAL(_fromUtf8("accepted()")), settingsDialog.accept) + QtCore.QObject.connect( # pylint: disable=no-member + self.buttonBox, QtCore.SIGNAL(_fromUtf8("rejected()")), settingsDialog.reject) + QtCore.QObject.connect( # pylint: disable=no-member + self.checkBoxAuthentication, + QtCore.SIGNAL( + _fromUtf8("toggled(bool)")), + self.lineEditSocksUsername.setEnabled) + QtCore.QObject.connect( # pylint: disable=no-member + self.checkBoxAuthentication, + QtCore.SIGNAL( + _fromUtf8("toggled(bool)")), + self.lineEditSocksPassword.setEnabled) + QtCore.QMetaObject.connectSlotsByName(settingsDialog) + settingsDialog.setTabOrder(self.tabWidgetSettings, self.checkBoxStartOnLogon) + settingsDialog.setTabOrder(self.checkBoxStartOnLogon, self.checkBoxStartInTray) + settingsDialog.setTabOrder(self.checkBoxStartInTray, self.checkBoxMinimizeToTray) + settingsDialog.setTabOrder(self.checkBoxMinimizeToTray, self.lineEditTCPPort) + settingsDialog.setTabOrder(self.lineEditTCPPort, self.comboBoxProxyType) + settingsDialog.setTabOrder(self.comboBoxProxyType, self.lineEditSocksHostname) + settingsDialog.setTabOrder(self.lineEditSocksHostname, self.lineEditSocksPort) + settingsDialog.setTabOrder(self.lineEditSocksPort, self.checkBoxAuthentication) + settingsDialog.setTabOrder(self.checkBoxAuthentication, self.lineEditSocksUsername) + settingsDialog.setTabOrder(self.lineEditSocksUsername, self.lineEditSocksPassword) + settingsDialog.setTabOrder(self.lineEditSocksPassword, self.checkBoxSocksListen) + settingsDialog.setTabOrder(self.checkBoxSocksListen, self.buttonBox) - self.checkBoxStartOnLogon.setChecked( - config.getboolean('bitmessagesettings', 'startonlogon')) + def retranslateUi(self, settingsDialog): + """Re-translate the UI into the supported languages""" - self.checkBoxWillinglySendToMobile.setChecked( - config.safeGetBoolean( - 'bitmessagesettings', 'willinglysendtomobile')) - self.checkBoxUseIdenticons.setChecked( - config.safeGetBoolean('bitmessagesettings', 'useidenticons')) - self.checkBoxReplyBelow.setChecked( - config.safeGetBoolean('bitmessagesettings', 'replybelow')) - - if state.appdata == paths.lookupExeFolder(): - self.checkBoxPortableMode.setChecked(True) - else: - try: - tempfile.NamedTemporaryFile( - dir=paths.lookupExeFolder(), delete=True - ).close() # should autodelete - except: - 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", - "Minimize-to-tray not yet supported on your OS.")) - self.checkBoxShowTrayNotifications.setDisabled(True) - self.checkBoxShowTrayNotifications.setText(_translate( - "MainWindow", - "Tray notifications not yet supported on your OS.")) - 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.checkBoxAuthentication.setChecked( - config.getboolean('bitmessagesettings', 'socksauthentication')) - self.checkBoxSocksListen.setChecked( - config.getboolean('bitmessagesettings', 'sockslisten')) - self.checkBoxOnionOnly.setChecked( - config.safeGetBoolean('bitmessagesettings', 'onionservicesonly')) - - self._proxy_type = getSOCKSProxyType(config) - self.comboBoxProxyType.setCurrentIndex( - 0 if not self._proxy_type - else self.comboBoxProxyType.findText(self._proxy_type)) - self.comboBoxProxyTypeChanged(self.comboBoxProxyType.currentIndex()) - - self.lineEditSocksHostname.setText( - config.get('bitmessagesettings', 'sockshostname')) - self.lineEditSocksPort.setText(str( - config.get('bitmessagesettings', 'socksport'))) - self.lineEditSocksUsername.setText( - config.get('bitmessagesettings', 'socksusername')) - self.lineEditSocksPassword.setText( - config.get('bitmessagesettings', 'sockspassword')) - - self.lineEditMaxDownloadRate.setText(str( - config.get('bitmessagesettings', 'maxdownloadrate'))) - self.lineEditMaxUploadRate.setText(str( - config.get('bitmessagesettings', 'maxuploadrate'))) - self.lineEditMaxOutboundConnections.setText(str( - config.get('bitmessagesettings', 'maxoutboundconnections'))) - - # Demanded difficulty tab - self.lineEditTotalDifficulty.setText(str((float( - config.getint( - 'bitmessagesettings', 'defaultnoncetrialsperbyte') - ) / defaults.networkDefaultProofOfWorkNonceTrialsPerByte))) - self.lineEditSmallMessageDifficulty.setText(str((float( - config.getint( - 'bitmessagesettings', 'defaultpayloadlengthextrabytes') - ) / defaults.networkDefaultPayloadLengthExtraBytes))) - - # Max acceptable difficulty tab - self.lineEditMaxAcceptableTotalDifficulty.setText(str((float( - config.getint( - 'bitmessagesettings', 'maxacceptablenoncetrialsperbyte') - ) / defaults.networkDefaultProofOfWorkNonceTrialsPerByte))) - self.lineEditMaxAcceptableSmallMessageDifficulty.setText(str((float( - config.getint( - 'bitmessagesettings', 'maxacceptablepayloadlengthextrabytes') - ) / defaults.networkDefaultPayloadLengthExtraBytes))) - - # OpenCL - self.comboBoxOpenCL.setEnabled(openclpow.openclAvailable()) - self.comboBoxOpenCL.clear() - self.comboBoxOpenCL.addItem("None") - self.comboBoxOpenCL.addItems(openclpow.vendors) - self.comboBoxOpenCL.setCurrentIndex(0) - for i in range(self.comboBoxOpenCL.count()): - if self.comboBoxOpenCL.itemText(i) == config.safeGet( - 'bitmessagesettings', 'opencl'): - self.comboBoxOpenCL.setCurrentIndex(i) - break - - # Namecoin integration tab - nmctype = config.get('bitmessagesettings', 'namecoinrpctype') - self.lineEditNamecoinHost.setText( - config.get('bitmessagesettings', 'namecoinrpchost')) - self.lineEditNamecoinPort.setText(str( - config.get('bitmessagesettings', 'namecoinrpcport'))) - self.lineEditNamecoinUser.setText( - config.get('bitmessagesettings', 'namecoinrpcuser')) - self.lineEditNamecoinPassword.setText( - config.get('bitmessagesettings', 'namecoinrpcpassword')) - - if nmctype == "namecoind": - self.radioButtonNamecoinNamecoind.setChecked(True) - elif nmctype == "nmcontrol": - self.radioButtonNamecoinNmcontrol.setChecked(True) - self.lineEditNamecoinUser.setEnabled(False) - self.labelNamecoinUser.setEnabled(False) - self.lineEditNamecoinPassword.setEnabled(False) - self.labelNamecoinPassword.setEnabled(False) - else: - assert False - - # Message Resend tab - self.lineEditDays.setText(str( - config.get('bitmessagesettings', 'stopresendingafterxdays'))) - self.lineEditMonths.setText(str( - config.get('bitmessagesettings', 'stopresendingafterxmonths'))) - - def comboBoxProxyTypeChanged(self, comboBoxIndex): - """A callback for currentIndexChanged event of comboBoxProxyType""" - if comboBoxIndex == 0: - self.lineEditSocksHostname.setEnabled(False) - self.lineEditSocksPort.setEnabled(False) - self.lineEditSocksUsername.setEnabled(False) - self.lineEditSocksPassword.setEnabled(False) - self.checkBoxAuthentication.setEnabled(False) - self.checkBoxSocksListen.setEnabled(False) - self.checkBoxOnionOnly.setEnabled(False) - else: - self.lineEditSocksHostname.setEnabled(True) - self.lineEditSocksPort.setEnabled(True) - self.checkBoxAuthentication.setEnabled(True) - self.checkBoxSocksListen.setEnabled(True) - self.checkBoxOnionOnly.setEnabled(True) - if self.checkBoxAuthentication.isChecked(): - self.lineEditSocksUsername.setEnabled(True) - self.lineEditSocksPassword.setEnabled(True) - - def getNamecoinType(self): - """ - Check status of namecoin integration radio buttons - and translate it to a string as in the options. - """ - if self.radioButtonNamecoinNamecoind.isChecked(): - return "namecoind" - if self.radioButtonNamecoinNmcontrol.isChecked(): - return "nmcontrol" - assert False - - # Namecoin connection type was changed. - def namecoinTypeChanged(self, checked): # pylint: disable=unused-argument - """A callback for toggled event of radioButtonNamecoinNamecoind""" - nmctype = self.getNamecoinType() - assert nmctype == "namecoind" or nmctype == "nmcontrol" - - isNamecoind = (nmctype == "namecoind") - self.lineEditNamecoinUser.setEnabled(isNamecoind) - self.labelNamecoinUser.setEnabled(isNamecoind) - self.lineEditNamecoinPassword.setEnabled(isNamecoind) - self.labelNamecoinPassword.setEnabled(isNamecoind) - - if isNamecoind: - self.lineEditNamecoinPort.setText(defaults.namecoinDefaultRpcPort) - else: - self.lineEditNamecoinPort.setText("9000") - - def click_pushButtonNamecoinTest(self): - """Test the namecoin settings specified in the settings dialog.""" - self.labelNamecoinTestResult.setText( - _translate("MainWindow", "Testing...")) - nc = namecoin.namecoinConnection({ - 'type': self.getNamecoinType(), - 'host': str(self.lineEditNamecoinHost.text().toUtf8()), - 'port': str(self.lineEditNamecoinPort.text().toUtf8()), - 'user': str(self.lineEditNamecoinUser.text().toUtf8()), - 'password': str(self.lineEditNamecoinPassword.text().toUtf8()) - }) - status, text = nc.test() - self.labelNamecoinTestResult.setText(text) - if status == 'success': - self.parent.namecoin = nc - - def accept(self): - """A callback for accepted event of buttonBox (OK button pressed)""" - # pylint: disable=too-many-branches,too-many-statements - super(SettingsDialog, self).accept() - if self.firstrun: - self.config.remove_option('bitmessagesettings', 'dontconnect') - self.config.set('bitmessagesettings', 'startonlogon', str( - self.checkBoxStartOnLogon.isChecked())) - self.config.set('bitmessagesettings', 'minimizetotray', str( - self.checkBoxMinimizeToTray.isChecked())) - self.config.set('bitmessagesettings', 'trayonclose', str( - self.checkBoxTrayOnClose.isChecked())) - self.config.set( - 'bitmessagesettings', 'hidetrayconnectionnotifications', - str(self.checkBoxHideTrayConnectionNotifications.isChecked())) - self.config.set('bitmessagesettings', 'showtraynotifications', str( - self.checkBoxShowTrayNotifications.isChecked())) - self.config.set('bitmessagesettings', 'startintray', str( - self.checkBoxStartInTray.isChecked())) - self.config.set('bitmessagesettings', 'willinglysendtomobile', str( - self.checkBoxWillinglySendToMobile.isChecked())) - self.config.set('bitmessagesettings', 'useidenticons', str( - self.checkBoxUseIdenticons.isChecked())) - self.config.set('bitmessagesettings', 'replybelow', str( - self.checkBoxReplyBelow.isChecked())) - - lang = str(self.languageComboBox.itemData( - self.languageComboBox.currentIndex()).toString()) - self.config.set('bitmessagesettings', 'userlocale', lang) - self.parent.change_translation() - - if int(self.config.get('bitmessagesettings', 'port')) != int( - self.lineEditTCPPort.text()): - self.config.set( - 'bitmessagesettings', 'port', str(self.lineEditTCPPort.text())) - if not self.config.safeGetBoolean('bitmessagesettings', 'dontconnect'): - self.net_restart_needed = True - - if self.checkBoxUPnP.isChecked() != self.config.safeGetBoolean( - 'bitmessagesettings', 'upnp'): - self.config.set( - 'bitmessagesettings', 'upnp', - str(self.checkBoxUPnP.isChecked())) - if self.checkBoxUPnP.isChecked(): - import upnp - upnpThread = upnp.uPnPThread() - upnpThread.start() - - proxytype_index = self.comboBoxProxyType.currentIndex() - if proxytype_index == 0: - if self._proxy_type and shared.statusIconColor != 'red': - self.net_restart_needed = True - elif self.comboBoxProxyType.currentText() != self._proxy_type: - self.net_restart_needed = True - self.parent.statusbar.clearMessage() - - self.config.set( - 'bitmessagesettings', 'socksproxytype', - 'none' if self.comboBoxProxyType.currentIndex() == 0 - else str(self.comboBoxProxyType.currentText()) - ) - if proxytype_index > 2: # last literal proxytype in ui - start_proxyconfig() - - self.config.set('bitmessagesettings', 'socksauthentication', str( - self.checkBoxAuthentication.isChecked())) - self.config.set('bitmessagesettings', 'sockshostname', str( - self.lineEditSocksHostname.text())) - self.config.set('bitmessagesettings', 'socksport', str( - self.lineEditSocksPort.text())) - self.config.set('bitmessagesettings', 'socksusername', str( - self.lineEditSocksUsername.text())) - self.config.set('bitmessagesettings', 'sockspassword', str( - self.lineEditSocksPassword.text())) - self.config.set('bitmessagesettings', 'sockslisten', str( - self.checkBoxSocksListen.isChecked())) - if self.checkBoxOnionOnly.isChecked() \ - and not self.config.safeGetBoolean('bitmessagesettings', 'onionservicesonly'): - self.net_restart_needed = True - self.config.set('bitmessagesettings', 'onionservicesonly', str( - self.checkBoxOnionOnly.isChecked())) - try: - # Rounding to integers just for aesthetics - self.config.set('bitmessagesettings', 'maxdownloadrate', str( - int(float(self.lineEditMaxDownloadRate.text())))) - self.config.set('bitmessagesettings', 'maxuploadrate', str( - int(float(self.lineEditMaxUploadRate.text())))) - except ValueError: - QtGui.QMessageBox.about( - self, _translate("MainWindow", "Number needed"), - _translate( - "MainWindow", - "Your maximum download and upload rate must be numbers." - " Ignoring what you typed.") - ) - else: - set_rates( - self.config.safeGetInt('bitmessagesettings', 'maxdownloadrate'), - self.config.safeGetInt('bitmessagesettings', 'maxuploadrate')) - - self.config.set('bitmessagesettings', 'maxoutboundconnections', str( - int(float(self.lineEditMaxOutboundConnections.text())))) - - self.config.set( - 'bitmessagesettings', 'namecoinrpctype', self.getNamecoinType()) - self.config.set('bitmessagesettings', 'namecoinrpchost', str( - self.lineEditNamecoinHost.text())) - self.config.set('bitmessagesettings', 'namecoinrpcport', str( - self.lineEditNamecoinPort.text())) - self.config.set('bitmessagesettings', 'namecoinrpcuser', str( - self.lineEditNamecoinUser.text())) - self.config.set('bitmessagesettings', 'namecoinrpcpassword', str( - self.lineEditNamecoinPassword.text())) - self.parent.resetNamecoinConnection() - - # Demanded difficulty tab - if float(self.lineEditTotalDifficulty.text()) >= 1: - self.config.set( - 'bitmessagesettings', 'defaultnoncetrialsperbyte', - str(int( - float(self.lineEditTotalDifficulty.text()) * - defaults.networkDefaultProofOfWorkNonceTrialsPerByte))) - if float(self.lineEditSmallMessageDifficulty.text()) >= 1: - self.config.set( - 'bitmessagesettings', 'defaultpayloadlengthextrabytes', - str(int( - float(self.lineEditSmallMessageDifficulty.text()) * - defaults.networkDefaultPayloadLengthExtraBytes))) - - if self.comboBoxOpenCL.currentText().toUtf8() != self.config.safeGet( - 'bitmessagesettings', 'opencl'): - self.config.set( - 'bitmessagesettings', 'opencl', - str(self.comboBoxOpenCL.currentText())) - queues.workerQueue.put(('resetPoW', '')) - - acceptableDifficultyChanged = False - - if ( - float(self.lineEditMaxAcceptableTotalDifficulty.text()) >= 1 or - float(self.lineEditMaxAcceptableTotalDifficulty.text()) == 0 - ): - if self.config.get( - 'bitmessagesettings', 'maxacceptablenoncetrialsperbyte' - ) != str(int( - float(self.lineEditMaxAcceptableTotalDifficulty.text()) * - defaults.networkDefaultProofOfWorkNonceTrialsPerByte) - ): - # the user changed the max acceptable total difficulty - acceptableDifficultyChanged = True - self.config.set( - 'bitmessagesettings', 'maxacceptablenoncetrialsperbyte', - str(int( - float(self.lineEditMaxAcceptableTotalDifficulty.text()) * - defaults.networkDefaultProofOfWorkNonceTrialsPerByte)) - ) - if ( - float(self.lineEditMaxAcceptableSmallMessageDifficulty.text()) >= 1 or - float(self.lineEditMaxAcceptableSmallMessageDifficulty.text()) == 0 - ): - if self.config.get( - 'bitmessagesettings', 'maxacceptablepayloadlengthextrabytes' - ) != str(int( - float(self.lineEditMaxAcceptableSmallMessageDifficulty.text()) * - defaults.networkDefaultPayloadLengthExtraBytes) - ): - # the user changed the max acceptable small message difficulty - acceptableDifficultyChanged = True - self.config.set( - 'bitmessagesettings', 'maxacceptablepayloadlengthextrabytes', - str(int( - float(self.lineEditMaxAcceptableSmallMessageDifficulty.text()) * - defaults.networkDefaultPayloadLengthExtraBytes)) - ) - if acceptableDifficultyChanged: - # It might now be possible to send msgs which were previously - # marked as toodifficult. Let us change them to 'msgqueued'. - # The singleWorker will try to send them and will again mark - # them as toodifficult if the receiver's required difficulty - # is still higher than we are willing to do. - sqlExecute( - "UPDATE sent SET status='msgqueued'" - " WHERE status='toodifficult'") - queues.workerQueue.put(('sendmessage', '')) - - # UI setting to stop trying to send messages after X days/months - # I'm open to changing this UI to something else if someone has a better idea. - if self.lineEditDays.text() == '' and self.lineEditMonths.text() == '': - # We need to handle this special case. Bitmessage has its - # default behavior. The input is blank/blank - self.config.set('bitmessagesettings', 'stopresendingafterxdays', '') - self.config.set('bitmessagesettings', 'stopresendingafterxmonths', '') - shared.maximumLengthOfTimeToBotherResendingMessages = float('inf') - - try: - days = float(self.lineEditDays.text()) - except ValueError: - self.lineEditDays.setText("0") - days = 0.0 - try: - months = float(self.lineEditMonths.text()) - except ValueError: - self.lineEditMonths.setText("0") - months = 0.0 - - if days >= 0 and months >= 0: - shared.maximumLengthOfTimeToBotherResendingMessages = \ - days * 24 * 60 * 60 + months * 60 * 60 * 24 * 365 / 12 - if shared.maximumLengthOfTimeToBotherResendingMessages < 432000: - # If the time period is less than 5 hours, we give - # zero values to all fields. No message will be sent again. - QtGui.QMessageBox.about( - self, - _translate("MainWindow", "Will not resend ever"), - _translate( - "MainWindow", - "Note that the time limit you entered is less" - " than the amount of time Bitmessage waits for" - " the first resend attempt therefore your" - " messages will never be resent.") - ) - self.config.set( - 'bitmessagesettings', 'stopresendingafterxdays', '0') - self.config.set( - 'bitmessagesettings', 'stopresendingafterxmonths', '0') - shared.maximumLengthOfTimeToBotherResendingMessages = 0.0 - else: - self.config.set( - 'bitmessagesettings', 'stopresendingafterxdays', str(days)) - self.config.set( - 'bitmessagesettings', 'stopresendingafterxmonths', - str(months)) - - self.config.save() - - if self.net_restart_needed: - self.net_restart_needed = False - self.config.setTemp('bitmessagesettings', 'dontconnect', 'true') - self.timer.singleShot( - 5000, lambda: - self.config.setTemp( - 'bitmessagesettings', 'dontconnect', 'false') - ) - - self.parent.updateStartOnLogon() - - if ( - state.appdata != paths.lookupExeFolder() and - self.checkBoxPortableMode.isChecked() - ): - # If we are NOT using portable mode now but the user selected - # that we should... - # Write the keys.dat file to disk in the new location - sqlStoredProcedure('movemessagstoprog') - with open(paths.lookupExeFolder() + 'keys.dat', 'wb') as configfile: - self.config.write(configfile) - # Write the knownnodes.dat file to disk in the new location - knownnodes.saveKnownNodes(paths.lookupExeFolder()) - os.remove(state.appdata + 'keys.dat') - os.remove(state.appdata + 'knownnodes.dat') - previousAppdataLocation = state.appdata - state.appdata = paths.lookupExeFolder() - debug.resetLogging() - try: - os.remove(previousAppdataLocation + 'debug.log') - os.remove(previousAppdataLocation + 'debug.log.1') - except: - pass - - if ( - state.appdata == paths.lookupExeFolder() and - not self.checkBoxPortableMode.isChecked() - ): - # If we ARE using portable mode now but the user selected - # that we shouldn't... - state.appdata = paths.lookupAppdataFolder() - if not os.path.exists(state.appdata): - os.makedirs(state.appdata) - sqlStoredProcedure('movemessagstoappdata') - # Write the keys.dat file to disk in the new location - self.config.save() - # Write the knownnodes.dat file to disk in the new location - knownnodes.saveKnownNodes(state.appdata) - os.remove(paths.lookupExeFolder() + 'keys.dat') - os.remove(paths.lookupExeFolder() + 'knownnodes.dat') - debug.resetLogging() - try: - os.remove(paths.lookupExeFolder() + 'debug.log') - os.remove(paths.lookupExeFolder() + 'debug.log.1') - except: - pass + settingsDialog.setWindowTitle(_translate("settingsDialog", "Settings", None)) + self.checkBoxStartOnLogon.setText(_translate("settingsDialog", "Start Bitmessage on user login", None)) + self.groupBoxTray.setTitle(_translate("settingsDialog", "Tray", None)) + self.checkBoxStartInTray.setText( + _translate( + "settingsDialog", + "Start Bitmessage in the tray (don\'t show main window)", + None)) + self.checkBoxMinimizeToTray.setText(_translate("settingsDialog", "Minimize to tray", None)) + self.checkBoxTrayOnClose.setText(_translate("settingsDialog", "Close to tray", None)) + self.checkBoxHideTrayConnectionNotifications.setText( + _translate("settingsDialog", "Hide connection notifications", None)) + self.checkBoxShowTrayNotifications.setText( + _translate( + "settingsDialog", + "Show notification when message received", + None)) + self.checkBoxPortableMode.setText(_translate("settingsDialog", "Run in Portable Mode", None)) + self.PortableModeDescription.setText( + _translate( + "settingsDialog", + "In Portable Mode, messages and config files are stored in the same directory as the" + " program rather than the normal application-data folder. This makes it convenient to" + " run Bitmessage from a USB thumb drive.", + None)) + self.checkBoxWillinglySendToMobile.setText( + _translate( + "settingsDialog", + "Willingly include unencrypted destination address when sending to a mobile device", + None)) + self.checkBoxUseIdenticons.setText(_translate("settingsDialog", "Use Identicons", None)) + self.checkBoxReplyBelow.setText(_translate("settingsDialog", "Reply below Quote", None)) + self.groupBox.setTitle(_translate("settingsDialog", "Interface Language", None)) + self.languageComboBox.setItemText(0, _translate("settingsDialog", "System Settings", "system")) + self.tabWidgetSettings.setTabText( + self.tabWidgetSettings.indexOf( + self.tabUserInterface), + _translate( + "settingsDialog", "User Interface", None)) + self.groupBox1.setTitle(_translate("settingsDialog", "Listening port", None)) + self.label.setText(_translate("settingsDialog", "Listen for connections on port:", None)) + self.labelUPnP.setText(_translate("settingsDialog", "UPnP:", None)) + self.groupBox_3.setTitle(_translate("settingsDialog", "Bandwidth limit", None)) + self.label_24.setText(_translate("settingsDialog", "Maximum download rate (kB/s): [0: unlimited]", None)) + self.label_25.setText(_translate("settingsDialog", "Maximum upload rate (kB/s): [0: unlimited]", None)) + self.label_26.setText(_translate("settingsDialog", "Maximum outbound connections: [0: none]", None)) + self.groupBox_2.setTitle(_translate("settingsDialog", "Proxy server / Tor", None)) + self.label_2.setText(_translate("settingsDialog", "Type:", None)) + self.label_3.setText(_translate("settingsDialog", "Server hostname:", None)) + self.label_4.setText(_translate("settingsDialog", "Port:", None)) + self.checkBoxAuthentication.setText(_translate("settingsDialog", "Authentication", None)) + self.label_5.setText(_translate("settingsDialog", "Username:", None)) + self.label_6.setText(_translate("settingsDialog", "Pass:", None)) + self.checkBoxSocksListen.setText( + _translate( + "settingsDialog", + "Listen for incoming connections when using proxy", + None)) + self.comboBoxProxyType.setItemText(0, _translate("settingsDialog", "none", None)) + self.comboBoxProxyType.setItemText(1, _translate("settingsDialog", "SOCKS4a", None)) + self.comboBoxProxyType.setItemText(2, _translate("settingsDialog", "SOCKS5", None)) + self.tabWidgetSettings.setTabText( + self.tabWidgetSettings.indexOf( + self.tabNetworkSettings), + _translate( + "settingsDialog", "Network Settings", None)) + self.label_9.setText(_translate("settingsDialog", "Total difficulty:", None)) + self.label_10.setText( + _translate( + "settingsDialog", + "The \'Total difficulty\' affects the absolute amount of work the sender must complete." + " Doubling this value doubles the amount of work.", + None)) + self.label_11.setText(_translate("settingsDialog", "Small message difficulty:", None)) + self.label_8.setText(_translate( + "settingsDialog", + "When someone sends you a message, their computer must first complete some work. The difficulty of this" + " work, by default, is 1. You may raise this default for new addresses you create by changing the values" + " here. Any new addresses you create will require senders to meet the higher difficulty. There is one" + " exception: if you add a friend or acquaintance to your address book, Bitmessage will automatically" + " notify them when you next send a message that they need only complete the minimum amount of" + " work: difficulty 1. ", + None)) + self.label_12.setText( + _translate( + "settingsDialog", + "The \'Small message difficulty\' mostly only affects the difficulty of sending small messages." + " Doubling this value makes it almost twice as difficult to send a small message but doesn\'t really" + " affect large messages.", + None)) + self.tabWidgetSettings.setTabText( + self.tabWidgetSettings.indexOf( + self.tabDemandedDifficulty), + _translate( + "settingsDialog", "Demanded difficulty", None)) + self.label_15.setText( + _translate( + "settingsDialog", + "Here you may set the maximum amount of work you are willing to do to send a message to another" + " person. Setting these values to 0 means that any value is acceptable.", + None)) + self.label_13.setText(_translate("settingsDialog", "Maximum acceptable total difficulty:", None)) + self.label_14.setText(_translate("settingsDialog", "Maximum acceptable small message difficulty:", None)) + self.tabWidgetSettings.setTabText( + self.tabWidgetSettings.indexOf( + self.tabMaxAcceptableDifficulty), + _translate( + "settingsDialog", "Max acceptable difficulty", None)) + self.labelOpenCL.setText(_translate("settingsDialog", "Hardware GPU acceleration (OpenCL):", None)) + self.label_16.setText(_translate( + "settingsDialog", + "

Bitmessage can utilize a different Bitcoin-based program called Namecoin to make" + " addresses human-friendly. For example, instead of having to tell your friend your long Bitmessage" + " address, you can simply tell him to send a message to test." + "

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

" + "

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

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

By default, if you send a message to someone and he is offline for more than two" + " days, Bitmessage will send the message again after an additional two days. This will be continued with" + " exponential backoff forever; messages will be resent after 5, 10, 20 days ect. until the receiver" + " acknowledges them. Here you may change that behavior by having Bitmessage give up after a certain" + " number of days or months.

Leave these input fields blank for the default behavior." + "

", + None)) + self.label_19.setText(_translate("settingsDialog", "Give up after", None)) + self.label_20.setText(_translate("settingsDialog", "and", None)) + self.label_22.setText(_translate("settingsDialog", "days", None)) + self.label_23.setText(_translate("settingsDialog", "months.", None)) + self.tabWidgetSettings.setTabText( + self.tabWidgetSettings.indexOf( + self.tabResendsExpire), + _translate( + "settingsDialog", "Resends Expire", None)) diff --git a/src/bitmessageqt/settings.ui b/src/bitmessageqt/settings.ui index 0ffbf442..4aeba3ce 100644 --- a/src/bitmessageqt/settings.ui +++ b/src/bitmessageqt/settings.ui @@ -37,18 +37,6 @@ User Interface - - 8 - - - 8 - - - 8 - - - 8 - @@ -56,43 +44,20 @@ - - - - Tray + + + + Start Bitmessage in the tray (don't show main window) - - - - - Start Bitmessage in the tray (don't show main window) - - - - - - - Minimize to tray - - - false - - - - - - - Close to tray - - - - - + - Hide connection notifications + Minimize to tray + + + true @@ -152,15 +117,90 @@ Interface Language - - - + + + 100 0 + + + System Settings + + + + + English + + + + + Esperanto + + + + + Français + + + + + Deutsch + + + + + Español + + + + + русский + + + + + Norsk + + + + + العربية + + + + + 简体中文 + + + + + 日本語 + + + + + Nederlands + + + + + Česky + + + + + Pirate English + + + + + Other (set in keys.dat) + + @@ -173,18 +213,6 @@ Network Settings - - 8 - - - 8 - - - 8 - - - 8 - @@ -192,13 +220,26 @@ + + + Qt::Horizontal + + + + 125 + 20 + + + + + Listen for connections on port: - + @@ -208,26 +249,6 @@ - - - - UPnP - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - @@ -403,13 +424,6 @@ - - - - Only connect to onion services (*.onion) - - - @@ -419,12 +433,12 @@ - SOCKS4a + SOCKS4a - SOCKS5 + SOCKS5 @@ -452,18 +466,6 @@ Demanded difficulty - - 8 - - - 8 - - - 8 - - - 8 - @@ -592,18 +594,6 @@ Max acceptable difficulty - - 8 - - - 8 - - - 8 - - - 8 - @@ -708,33 +698,6 @@ - - - - - - Hardware GPU acceleration (OpenCL): - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - @@ -742,18 +705,6 @@ Namecoin integration - - 8 - - - 8 - - - 8 - - - 8 - @@ -937,18 +888,6 @@ Resends Expire - - 8 - - - 8 - - - 8 - - - 8 - @@ -973,69 +912,91 @@ - + 231 75 - - + + + 10 + 20 + 101 + 20 + + Give up after - + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - + - - + + + 30 + 40 + 80 + 16 + + and - + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - 55 - 100 - - + + + + 113 + 20 + 51 + 20 + + - - - - - - 55 - 100 - - + + + + 113 + 40 + 51 + 20 + + - - - + + + + 169 + 23 + 61 + 16 + + days - - - + + + + 170 + 41 + 71 + 16 + + months. - - @@ -1056,14 +1017,7 @@ -
- - - LanguageBox - QComboBox -
bitmessageqt.languagebox
-
-
+ tabWidgetSettings checkBoxStartOnLogon @@ -1147,53 +1101,5 @@ - - comboBoxProxyType - currentIndexChanged(int) - settingsDialog - comboBoxProxyTypeChanged - - - 20 - 20 - - - 20 - 20 - - - - - radioButtonNamecoinNamecoind - toggled(bool) - settingsDialog - namecoinTypeChanged - - - 20 - 20 - - - 20 - 20 - - - - - pushButtonNamecoinTest - clicked() - settingsDialog - click_pushButtonNamecoinTest - - - 20 - 20 - - - 20 - 20 - - - diff --git a/src/bitmessageqt/support.py b/src/bitmessageqt/support.py index d6d4543d..2a1ddb18 100644 --- a/src/bitmessageqt/support.py +++ b/src/bitmessageqt/support.py @@ -15,7 +15,6 @@ from openclpow import openclAvailable, openclEnabled import paths import proofofwork from pyelliptic.openssl import OpenSSL -from settings import getSOCKSProxyType import queues import network.stats import state @@ -119,7 +118,8 @@ def createSupportMessage(myapp): BMConfigParser().safeGet('bitmessagesettings', 'opencl') ) if openclEnabled() else "None" locale = getTranslationLanguage() - socks = getSOCKSProxyType(BMConfigParser()) or "N/A" + socks = BMConfigParser().safeGet( + 'bitmessagesettings', 'socksproxytype', "N/A") upnp = BMConfigParser().safeGet('bitmessagesettings', 'upnp', "N/A") connectedhosts = len(network.stats.connectedHostsList()) diff --git a/src/bitmsghash/bitmsghash.cpp b/src/bitmsghash/bitmsghash.cpp index ce305ebf..24775475 100644 --- a/src/bitmsghash/bitmsghash.cpp +++ b/src/bitmsghash/bitmsghash.cpp @@ -162,4 +162,4 @@ extern "C" EXPORT unsigned long long BitmessagePOW(unsigned char * starthash, un free(threads); free(threaddata); return successval; -} \ No newline at end of file +} diff --git a/src/bmconfigparser.py b/src/bmconfigparser.py index 328cf0c7..1ee64e94 100644 --- a/src/bmconfigparser.py +++ b/src/bmconfigparser.py @@ -3,8 +3,8 @@ BMConfigParser class definition and default configuration settings """ import ConfigParser -import os import shutil +import os from datetime import datetime import state @@ -43,13 +43,8 @@ BMConfigDefaults = { @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 = {} + """Singleton class inherited from ConfigParser.SafeConfigParser + with additional methods specific to bitmessage config.""" def set(self, section, option, value=None): if self._optcre is self.OPTCRE or value: @@ -60,15 +55,10 @@ class BMConfigParser(ConfigParser.SafeConfigParser): return ConfigParser.ConfigParser.set(self, section, option, value) def get(self, section, option, raw=False, variables=None): - # pylint: disable=arguments-differ try: 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: @@ -80,15 +70,7 @@ class BMConfigParser(ConfigParser.SafeConfigParser): except (KeyError, ValueError, AttributeError): raise e - def setTemp(self, section, option, value=None): - """Temporary set option to value, not saving.""" - try: - self._temp[section][option] = value - except KeyError: - self._temp[section] = {option: value} - def safeGetBoolean(self, section, field): - """Return value as boolean, False on exceptions""" try: return self.getboolean(section, field) except (ConfigParser.NoSectionError, ConfigParser.NoOptionError, @@ -96,8 +78,6 @@ class BMConfigParser(ConfigParser.SafeConfigParser): return False def safeGetInt(self, section, field, default=0): - """Return value as integer, default on exceptions, - 0 if default missing""" try: return self.getint(section, field) except (ConfigParser.NoSectionError, ConfigParser.NoOptionError, @@ -105,7 +85,6 @@ class BMConfigParser(ConfigParser.SafeConfigParser): return default def safeGet(self, section, option, default=None): - """Return value as is, default on exceptions, None if default missing""" try: return self.get(section, option) except (ConfigParser.NoSectionError, ConfigParser.NoOptionError, @@ -113,16 +92,11 @@ class BMConfigParser(ConfigParser.SafeConfigParser): return default def items(self, section, raw=False, variables=None): - """Return section variables as parent, - but override the "raw" argument to always True""" - # pylint: disable=arguments-differ return ConfigParser.ConfigParser.items(self, section, True, variables) - @staticmethod - def addresses(): - """Return a list of local bitmessage addresses (from section labels)""" - return [ - x for x in BMConfigParser().sections() if x.startswith('BM-')] + def addresses(self): + return filter( + lambda x: x.startswith('BM-'), BMConfigParser().sections()) def read(self, filenames): ConfigParser.ConfigParser.read(self, filenames) @@ -143,7 +117,6 @@ class BMConfigParser(ConfigParser.SafeConfigParser): continue def save(self): - """Save the runtime config onto the filesystem""" fileName = os.path.join(state.appdata, 'keys.dat') fileNameBak = '.'.join([ fileName, datetime.now().strftime("%Y%j%H%M%S%f"), 'bak']) @@ -165,15 +138,12 @@ class BMConfigParser(ConfigParser.SafeConfigParser): os.remove(fileNameBak) def validate(self, section, option, value): - """Input validator interface (using factory pattern)""" try: return getattr(self, 'validate_%s_%s' % (section, option))(value) except AttributeError: return True - @staticmethod - def validate_bitmessagesettings_maxoutboundconnections(value): - """Reject maxoutboundconnections that are too high or too low""" + def validate_bitmessagesettings_maxoutboundconnections(self, value): try: value = int(value) except ValueError: diff --git a/src/bob.png b/src/bob.png deleted file mode 100644 index 74aeb99e..00000000 Binary files a/src/bob.png and /dev/null differ diff --git a/src/build_osx.py b/src/build_osx.py index 83d2f280..1d8f470e 100644 --- a/src/build_osx.py +++ b/src/build_osx.py @@ -1,6 +1,5 @@ -"""Building osx.""" -import os from glob import glob +import os from PyQt4 import QtCore from setuptools import setup @@ -13,26 +12,20 @@ DATA_FILES = [ ('bitmsghash', ['bitmsghash/bitmsghash.cl', 'bitmsghash/bitmsghash.so']), ('translations', glob('translations/*.qm')), ('ui', glob('bitmessageqt/*.ui')), - ( - 'translations', - glob(os.path.join(str(QtCore.QLibraryInfo.location( - QtCore.QLibraryInfo.TranslationsPath)), 'qt_??.qm'))), - ( - 'translations', - glob(os.path.join(str(QtCore.QLibraryInfo.location( - QtCore.QLibraryInfo.TranslationsPath)), 'qt_??_??.qm'))), + ('translations', glob(str(QtCore.QLibraryInfo.location(QtCore.QLibraryInfo.TranslationsPath)) + '/qt_??.qm')), + ('translations', glob(str(QtCore.QLibraryInfo.location(QtCore.QLibraryInfo.TranslationsPath)) + '/qt_??_??.qm')), ] setup( - name=name, - version=version, - app=mainscript, - data_files=DATA_FILES, - setup_requires=["py2app"], - options=dict( - py2app=dict( - includes=['sip', 'PyQt4._qt'], - iconfile="images/bitmessage.icns" + name = name, + version = version, + app = mainscript, + data_files = DATA_FILES, + setup_requires = ["py2app"], + options = dict( + py2app = dict( + includes = ['sip', 'PyQt4._qt'], + iconfile = "images/bitmessage.icns" ) ) ) diff --git a/src/buildozer.spec b/src/buildozer.spec index fc0bf18b..07f9e6b2 100644 --- a/src/buildozer.spec +++ b/src/buildozer.spec @@ -1,10 +1,10 @@ [app] # (str) Title of your application -title = bitapp +title = PyBitmessage # (str) Package name -package.name = bitapp +package.name = PyBitmessage # (str) Package domain (needed for android/ios packaging) package.domain = org.test @@ -13,7 +13,7 @@ package.domain = org.test source.dir = . # (list) Source files to include (let empty to include all the files) -source.include_exts = py,png,jpg,kv,atlas,gif,zip +source.include_exts = py,png,jpg,kv,atlas # (list) List of inclusions using pattern matching #source.include_patterns = assets/*,images/*.png @@ -35,25 +35,16 @@ version = 0.1 # version.filename = %(source.dir)s/main.py # (list) Application requirements -# comma separated e.g. requirements = sqlite3,kivy -requirements = - openssl, - sqlite3, - python2, - kivy, - bitmsghash, - kivymd, - kivy-garden, - qrcode, - Pillow, - msgpack +# 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 = qrcode +#garden_requirements = # (str) Presplash of the application #presplash.filename = %(source.dir)s/data/presplash.png @@ -75,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 @@ -95,22 +87,22 @@ fullscreen = 0 #android.presplash_color = #FFFFFF # (list) Permissions -android.permissions = INTERNET, WRITE_EXTERNAL_STORAGE, READ_EXTERNAL_STORAGE +android.permissions = INTERNET # (int) Android API to use -android.api = 27 +#android.api = 19 # (int) Minimum API required -android.minapi = 21 +#android.minapi = 9 # (int) Android SDK version to use -android.sdk = 20 +#android.sdk = 20 # (str) Android NDK version to use -android.ndk = 17c +#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 = @@ -132,6 +124,9 @@ android.ndk = 17c # (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 = @@ -155,12 +150,9 @@ android.ndk = 17c # (list) Gradle dependencies to add (currently works only with sdl2_gradle # bootstrap) #android.gradle_dependencies = - -# (list) Java classes to add as activities to the manifest. -#android.add_activites = 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 = release-2019.07.08 +#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 @@ -172,10 +164,7 @@ p4a.branch = release-2019.07.08 # (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_x86 = libs/android-x86/*.so @@ -209,7 +198,7 @@ android.arch = armeabi-v7a #p4a.source_dir = # (str) The directory in which python-for-android should look for your own build recipes (if any) -p4a.local_recipes = /home/cis/navjotrepo/PyBitmessage/src/bitmessagekivy/android/python-for-android/recipes/ +#p4a.local_recipes = # (str) Filename to the hook for p4a #p4a.hook = @@ -217,9 +206,6 @@ p4a.local_recipes = /home/cis/navjotrepo/PyBitmessage/src/bitmessagekivy/android # (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 = - # # iOS specific @@ -286,4 +272,4 @@ warn_on_root = 1 # # Then, invoke the command line with the "demo" profile: # -#buildozer --profile demo android debug \ No newline at end of file +#buildozer --profile demo android debug diff --git a/src/class_addressGenerator.py b/src/class_addressGenerator.py index 520733fb..0893b73a 100644 --- a/src/class_addressGenerator.py +++ b/src/class_addressGenerator.py @@ -1,28 +1,30 @@ -""" -A thread for creating addresses -""" -import hashlib -import time -from binascii import hexlify -import defaults -import highlevelcrypto -import queues -import shared -import state -import tr -from addresses import decodeAddress, encodeAddress, encodeVarint -from bmconfigparser import BMConfigParser -from fallback import RIPEMD160Hash -from network import StoppableThread +import time +import threading +import hashlib +from binascii import hexlify from pyelliptic import arithmetic from pyelliptic.openssl import OpenSSL +import tr +import queues +import state +import shared +import defaults +import highlevelcrypto +from bmconfigparser import BMConfigParser +from debug import logger +from addresses import decodeAddress, encodeAddress, encodeVarint +from fallback import RIPEMD160Hash +from helper_threading import StoppableThread -class addressGenerator(StoppableThread): - """A thread for creating addresses""" - name = "addressGenerator" +class addressGenerator(threading.Thread, StoppableThread): + + def __init__(self): + # QThread.__init__(self, parent) + threading.Thread.__init__(self, name="addressGenerator") + self.initStop() def stopThread(self): try: @@ -32,12 +34,6 @@ class addressGenerator(StoppableThread): super(addressGenerator, self).stopThread() def run(self): - """ - Process the requests for addresses generation - from `.queues.addressGeneratorQueue` - """ - # 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 @@ -93,12 +89,12 @@ class addressGenerator(StoppableThread): elif queueValue[0] == 'stopThread': break else: - self.logger.error( + logger.error( 'Programming error: A structure with the wrong number' ' of values was passed into the addressGeneratorQueue.' ' Here is the queueValue: %r\n', queueValue) if addressVersionNumber < 3 or addressVersionNumber > 4: - self.logger.error( + logger.error( 'Program error: For some reason the address generator' ' queue has been given a request to create at least' ' one version %s address which it cannot do.\n', @@ -119,7 +115,9 @@ class addressGenerator(StoppableThread): defaults.networkDefaultPayloadLengthExtraBytes if command == 'createRandomAddress': queues.UISignalQueue.put(( - 'updateStatusBar', "" + 'updateStatusBar', + tr._translate( + "MainWindow", "Generating one new address") )) # This next section is a little bit strange. We're going # to generate keys over and over until we find one @@ -145,10 +143,10 @@ class addressGenerator(StoppableThread): '\x00' * numberOfNullBytesDemandedOnFrontOfRipeHash ): break - self.logger.info( + logger.info( 'Generated address with ripe digest: %s', hexlify(ripe)) try: - self.logger.info( + logger.info( 'Address generator calculated %s addresses at %s' ' addresses per second before finding one with' ' the correct ripe-prefix.', @@ -176,6 +174,7 @@ class addressGenerator(StoppableThread): 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') @@ -195,7 +194,11 @@ class addressGenerator(StoppableThread): queues.apiAddressGeneratorReturnQueue.put(address) queues.UISignalQueue.put(( - 'updateStatusBar', "" + 'updateStatusBar', + tr._translate( + "MainWindow", + "Done generating address. Doing work necessary" + " to broadcast it...") )) queues.UISignalQueue.put(('writeNewAddressToTable', ( label, address, streamNumber))) @@ -210,8 +213,8 @@ class addressGenerator(StoppableThread): elif command == 'createDeterministicAddresses' \ or command == 'getDeterministicAddress' \ or command == 'createChan' or command == 'joinChan': - if not deterministicPassphrase: - self.logger.warning( + if len(deterministicPassphrase) == 0: + logger.warning( 'You are creating deterministic' ' address(es) using a blank passphrase.' ' Bitmessage will do it but it is rather stupid.') @@ -264,10 +267,10 @@ class addressGenerator(StoppableThread): ): break - self.logger.info( + logger.info( 'Generated address with ripe digest: %s', hexlify(ripe)) try: - self.logger.info( + logger.info( 'Address generator calculated %s addresses' ' at %s addresses per second before finding' ' one with the correct ripe-prefix.', @@ -317,7 +320,7 @@ class addressGenerator(StoppableThread): addressAlreadyExists = True if addressAlreadyExists: - self.logger.info( + logger.info( '%s already exists. Not adding it again.', address ) @@ -330,7 +333,7 @@ class addressGenerator(StoppableThread): ).arg(address) )) else: - self.logger.debug('label: %s', label) + logger.debug('label: %s', label) BMConfigParser().set(address, 'label', label) BMConfigParser().set(address, 'enabled', 'true') BMConfigParser().set(address, 'decoy', 'false') @@ -359,7 +362,7 @@ class addressGenerator(StoppableThread): address) shared.myECCryptorObjects[ripe] = \ highlevelcrypto.makeCryptor( - hexlify(potentialPrivEncryptionKey)) + hexlify(potentialPrivEncryptionKey)) shared.myAddressesByHash[ripe] = address tag = hashlib.sha512(hashlib.sha512( encodeVarint(addressVersionNumber) + diff --git a/src/class_objectProcessor.py b/src/class_objectProcessor.py index 824580c2..720cdf02 100644 --- a/src/class_objectProcessor.py +++ b/src/class_objectProcessor.py @@ -1,42 +1,32 @@ -""" -The objectProcessor thread, of which there is only one, -processes the network objects -""" -# pylint: disable=too-many-locals,too-many-return-statements -# pylint: disable=too-many-branches,too-many-statements import hashlib -import logging import random +import shared import threading import time from binascii import hexlify from subprocess import call # nosec +import highlevelcrypto +import knownnodes +from addresses import ( + calculateInventoryHash, decodeAddress, decodeVarint, encodeAddress, + encodeVarint, varintDecodeError +) +from bmconfigparser import BMConfigParser import helper_bitcoin import helper_inbox import helper_msgcoding import helper_sent -import highlevelcrypto -import knownnodes -import l10n +from helper_sql import SqlBulkExecute, sqlExecute, sqlQuery +from helper_ackPayload import genAckPayload +from network import bmproto import protocol import queues -import shared import state import tr -from addresses import ( - calculateInventoryHash, decodeAddress, decodeVarint, - encodeAddress, encodeVarint, varintDecodeError -) -from bmconfigparser import BMConfigParser +from debug import logger 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 -# pylint: disable=too-many-locals, too-many-return-statements, too-many-branches, too-many-statements - -logger = logging.getLogger('default') +import l10n class objectProcessor(threading.Thread): @@ -45,13 +35,12 @@ class objectProcessor(threading.Thread): objects (msg, broadcast, pubkey, getpubkey) from the receiveDataThreads. """ def __init__(self): - threading.Thread.__init__(self, name="objectProcessor") - random.seed() # 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. + threading.Thread.__init__(self, name="objectProcessor") queryreturn = sqlQuery( '''SELECT objecttype, data FROM objectprocessorqueue''') for row in queryreturn: @@ -65,7 +54,6 @@ class objectProcessor(threading.Thread): self.successfullyDecryptMessageTimings = [] def run(self): - """Process the objects from `.queues.objectProcessorQueue`""" while True: objectType, data = queues.objectProcessorQueue.get() @@ -129,10 +117,7 @@ class objectProcessor(threading.Thread): state.shutdown = 2 break - @staticmethod - def checkackdata(data): - """Checking Acknowledgement of message received or not?""" - # pylint: disable=protected-access + def checkackdata(self, data): # Let's check whether this is a message acknowledgement bound for us. if len(data) < 32: return @@ -149,13 +134,11 @@ class objectProcessor(threading.Thread): 'ackreceived', int(time.time()), data[readPosition:]) queues.UISignalQueue.put(( 'updateSentItemStatusByAckdata', - ( - data[readPosition:], - tr._translate( - "MainWindow", - "Acknowledgement of the message received %1" - ).arg(l10n.formatTimestamp()) - ) + (data[readPosition:], + tr._translate( + "MainWindow", + "Acknowledgement of the message received %1" + ).arg(l10n.formatTimestamp())) )) else: logger.debug('This object is not an acknowledgement bound for me.') @@ -174,7 +157,7 @@ class objectProcessor(threading.Thread): if not host: return - peer = Peer(host, port) + peer = state.Peer(host, port) with knownnodes.knownNodesLock: knownnodes.addKnownNode( stream, peer, is_self=state.ownAddresses.get(peer)) @@ -284,7 +267,6 @@ class objectProcessor(threading.Thread): queues.workerQueue.put(('sendOutOrStoreMyV4Pubkey', myAddress)) def processpubkey(self, data): - """Process a pubkey object""" pubkeyProcessingStartTime = time.time() shared.numberOfPubkeysProcessed += 1 queues.UISignalQueue.put(( @@ -333,14 +315,13 @@ class objectProcessor(threading.Thread): '\x04' + publicSigningKey + '\x04' + publicEncryptionKey) ripe = RIPEMD160Hash(sha.digest()).digest() - if logger.isEnabledFor(logging.DEBUG): - logger.debug( - 'within recpubkey, addressVersion: %s, streamNumber: %s' - '\nripe %s\npublicSigningKey in hex: %s' - '\npublicEncryptionKey in hex: %s', - addressVersion, streamNumber, hexlify(ripe), - hexlify(publicSigningKey), hexlify(publicEncryptionKey) - ) + logger.debug( + 'within recpubkey, addressVersion: %s, streamNumber: %s' + '\nripe %s\npublicSigningKey in hex: %s' + '\npublicEncryptionKey in hex: %s', + addressVersion, streamNumber, hexlify(ripe), + hexlify(publicSigningKey), hexlify(publicEncryptionKey) + ) address = encodeAddress(addressVersion, streamNumber, ripe) @@ -398,14 +379,13 @@ class objectProcessor(threading.Thread): sha.update(publicSigningKey + publicEncryptionKey) ripe = RIPEMD160Hash(sha.digest()).digest() - if logger.isEnabledFor(logging.DEBUG): - logger.debug( - 'within recpubkey, addressVersion: %s, streamNumber: %s' - '\nripe %s\npublicSigningKey in hex: %s' - '\npublicEncryptionKey in hex: %s', - addressVersion, streamNumber, hexlify(ripe), - hexlify(publicSigningKey), hexlify(publicEncryptionKey) - ) + logger.debug( + 'within recpubkey, addressVersion: %s, streamNumber: %s' + '\nripe %s\npublicSigningKey in hex: %s' + '\npublicEncryptionKey in hex: %s', + addressVersion, streamNumber, hexlify(ripe), + hexlify(publicSigningKey), hexlify(publicEncryptionKey) + ) address = encodeAddress(addressVersion, streamNumber, ripe) queryreturn = sqlQuery( @@ -457,7 +437,6 @@ class objectProcessor(threading.Thread): timeRequiredToProcessPubkey) def processmsg(self, data): - """Process a message object""" messageProcessingStartTime = time.time() shared.numberOfMessagesProcessed += 1 queues.UISignalQueue.put(( @@ -599,18 +578,17 @@ class objectProcessor(threading.Thread): logger.debug('ECDSA verify failed') return logger.debug('ECDSA verify passed') - if logger.isEnabledFor(logging.DEBUG): - logger.debug( - 'As a matter of intellectual curiosity, here is the Bitcoin' - ' address associated with the keys owned by the other person:' - ' %s ..and here is the testnet address: %s. The other person' - ' must take their private signing key from Bitmessage and' - ' import it into Bitcoin (or a service like Blockchain.info)' - ' for it to be of any use. Do not use this unless you know' - ' what you are doing.', - helper_bitcoin.calculateBitcoinAddressFromPubkey(pubSigningKey), - helper_bitcoin.calculateTestnetAddressFromPubkey(pubSigningKey) - ) + logger.debug( + 'As a matter of intellectual curiosity, here is the Bitcoin' + ' address associated with the keys owned by the other person:' + ' %s ..and here is the testnet address: %s. The other person' + ' must take their private signing key from Bitmessage and' + ' import it into Bitcoin (or a service like Blockchain.info)' + ' for it to be of any use. Do not use this unless you know' + ' what you are doing.', + helper_bitcoin.calculateBitcoinAddressFromPubkey(pubSigningKey), + helper_bitcoin.calculateTestnetAddressFromPubkey(pubSigningKey) + ) # Used to detect and ignore duplicate messages in our inbox sigHash = hashlib.sha512( hashlib.sha512(signature).digest()).digest()[32:] @@ -647,8 +625,7 @@ class objectProcessor(threading.Thread): if decodeAddress(toAddress)[1] >= 3 \ and not BMConfigParser().safeGetBoolean(toAddress, 'chan'): # If I'm not friendly with this person: - if not shared.isAddressInMyAddressBookSubscriptionsListOrWhitelist( - fromAddress): + if not shared.isAddressInMyAddressBookSubscriptionsListOrWhitelist(fromAddress): requiredNonceTrialsPerByte = BMConfigParser().getint( toAddress, 'noncetrialsperbyte') requiredPayloadLengthExtraBytes = BMConfigParser().getint( @@ -754,7 +731,7 @@ class objectProcessor(threading.Thread): # 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 + TTL = 2*7*24*60*60 # 2 weeks t = ('', toAddress, ripe, @@ -806,7 +783,6 @@ class objectProcessor(threading.Thread): ) def processbroadcast(self, data): - """Process a broadcast object""" messageProcessingStartTime = time.time() shared.numberOfBroadcastsProcessed += 1 queues.UISignalQueue.put(( @@ -991,7 +967,7 @@ class objectProcessor(threading.Thread): fromAddress = encodeAddress( sendersAddressVersion, sendersStream, calculatedRipe) - logger.info('fromAddress: %s', fromAddress) + logger.info('fromAddress: %s' % fromAddress) # Let's store the public key in case we want to reply to this person. sqlExecute('''INSERT INTO pubkeys VALUES (?,?,?,?,?)''', @@ -1008,7 +984,7 @@ class objectProcessor(threading.Thread): fromAddress = encodeAddress( sendersAddressVersion, sendersStream, calculatedRipe) - logger.debug('fromAddress: %s', fromAddress) + logger.debug('fromAddress: ' + fromAddress) try: decodedMessage = helper_msgcoding.MsgDecode( @@ -1069,18 +1045,17 @@ class objectProcessor(threading.Thread): # for it. elif addressVersion >= 4: tag = hashlib.sha512(hashlib.sha512( - encodeVarint(addressVersion) + encodeVarint(streamNumber) - + ripe + encodeVarint(addressVersion) + encodeVarint(streamNumber) + ripe ).digest()).digest()[32:] if tag in state.neededPubkeys: del state.neededPubkeys[tag] self.sendMessages(address) - @staticmethod - def sendMessages(address): + def sendMessages(self, address): """ - This method is called by the `possibleNewPubkey` when it sees - that we now have the necessary pubkey to send one or more messages. + This function is called by the possibleNewPubkey function when + that function sees that we now have the necessary pubkey + to send one or more messages. """ logger.info('We have been awaiting the arrival of this pubkey.') sqlExecute( @@ -1090,9 +1065,7 @@ class objectProcessor(threading.Thread): " AND folder='sent'", address) queues.workerQueue.put(('sendmessage', '')) - @staticmethod - def ackDataHasAValidHeader(ackData): - """Checking ackData with valid Header, not sending ackData when false""" + def ackDataHasAValidHeader(self, ackData): if len(ackData) < protocol.Header.size: logger.info( 'The length of ackData is unreasonably short. Not sending' @@ -1127,12 +1100,11 @@ class objectProcessor(threading.Thread): return False return True - @staticmethod - def addMailingListNameToSubject(subject, mailingListName): - """Adding mailingListName to subject""" + def addMailingListNameToSubject(self, subject, mailingListName): subject = subject.strip() if subject[:3] == 'Re:' or subject[:3] == 'RE:': subject = subject[3:].strip() if '[' + mailingListName + ']' in subject: return subject - return '[' + mailingListName + '] ' + subject + else: + return '[' + mailingListName + '] ' + subject diff --git a/src/class_objectProcessorQueue.py b/src/class_objectProcessorQueue.py new file mode 100644 index 00000000..b6628816 --- /dev/null +++ b/src/class_objectProcessorQueue.py @@ -0,0 +1,24 @@ +import Queue +import threading +import time + +class ObjectProcessorQueue(Queue.Queue): + maxSize = 32000000 + + def __init__(self): + Queue.Queue.__init__(self) + self.sizeLock = threading.Lock() + self.curSize = 0 # in Bytes. We maintain this to prevent nodes from flooing us with objects which take up too much memory. If this gets too big we'll sleep before asking for further objects. + + def put(self, item, block = True, timeout = None): + while self.curSize >= self.maxSize: + time.sleep(1) + with self.sizeLock: + self.curSize += len(item[1]) + Queue.Queue.put(self, item, block, timeout) + + def get(self, block = True, timeout = None): + item = Queue.Queue.get(self, block, timeout) + with self.sizeLock: + self.curSize -= len(item[1]) + return item diff --git a/src/class_singleCleaner.py b/src/class_singleCleaner.py index e3acff1d..e2cdbb89 100644 --- a/src/class_singleCleaner.py +++ b/src/class_singleCleaner.py @@ -1,58 +1,61 @@ """ -The `singleCleaner` class is a timer-driven thread that cleans data structures +The singleCleaner class is a timer-driven thread that cleans data structures to free memory, resends messages when a remote node doesn't respond, and sends pong messages to keep connections alive if the network isn't busy. - It cleans these data structures in memory: - - inventory (moves data to the on-disk sql database) - - inventorySets (clears then reloads data out of sql database) +inventory (moves data to the on-disk sql database) +inventorySets (clears then reloads data out of sql database) It cleans these tables on the disk: - - inventory (clears expired objects) - - pubkeys (clears pubkeys older than 4 weeks old which we have not used - personally) - - knownNodes (clears addresses which have not been online for over 3 days) +inventory (clears expired objects) +pubkeys (clears pubkeys older than 4 weeks old which we have not used + personally) +knownNodes (clears addresses which have not been online for over 3 days) It resends messages when there has been no response: - - resends getpubkey messages in 5 days (then 10 days, then 20 days, etc...) - - resends msg messages in 5 days (then 10 days, then 20 days, etc...) +resends getpubkey messages in 5 days (then 10 days, then 20 days, etc...) +resends msg messages in 5 days (then 10 days, then 20 days, etc...) """ -# pylint: disable=relative-import, protected-access + import gc import os -from datetime import datetime, timedelta -import time import shared +import threading +import time +import tr +from bmconfigparser import BMConfigParser +from helper_sql import sqlQuery, sqlExecute +from helper_threading import StoppableThread +from inventory import Inventory +from network.connectionpool import BMConnectionPool +from debug import logger import knownnodes import queues import state -import tr -from bmconfigparser import BMConfigParser -from helper_sql import sqlExecute, sqlQuery -from inventory import Inventory -from network import BMConnectionPool, StoppableThread -class singleCleaner(StoppableThread): - """The singleCleaner thread class""" - name = "singleCleaner" +class singleCleaner(threading.Thread, StoppableThread): cycleLength = 300 expireDiscoveredPeers = 300 - def run(self): # pylint: disable=too-many-branches + def __init__(self): + threading.Thread.__init__(self, name="singleCleaner") + self.initStop() + + def run(self): gc.disable() timeWeLastClearedInventoryAndPubkeysTables = 0 try: shared.maximumLengthOfTimeToBotherResendingMessages = ( float(BMConfigParser().get( - 'bitmessagesettings', 'stopresendingafterxdays')) - * 24 * 60 * 60 + 'bitmessagesettings', 'stopresendingafterxdays')) * + 24 * 60 * 60 ) + ( float(BMConfigParser().get( - 'bitmessagesettings', 'stopresendingafterxmonths')) - * (60 * 60 * 24 * 365) / 12) + 'bitmessagesettings', 'stopresendingafterxmonths')) * + (60 * 60 * 24 * 365) / 12) except: # Either the user hasn't set stopresendingafterxdays and # stopresendingafterxmonths yet or the options are missing @@ -74,7 +77,7 @@ class singleCleaner(StoppableThread): # 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? + # FIXME redundant? if shared.thisapp.daemon or not state.enableGUI: queues.UISignalQueue.queue.clear() if timeWeLastClearedInventoryAndPubkeysTables < \ @@ -94,12 +97,12 @@ class singleCleaner(StoppableThread): "SELECT toaddress, ackdata, status FROM sent" " WHERE ((status='awaitingpubkey' OR status='msgsent')" " AND folder='sent' AND sleeptill?)", - int(time.time()), int(time.time()) - - shared.maximumLengthOfTimeToBotherResendingMessages + int(time.time()), int(time.time()) - + shared.maximumLengthOfTimeToBotherResendingMessages ) for row in queryreturn: if len(row) < 2: - self.logger.error( + logger.error( 'Something went wrong in the singleCleaner thread:' ' a query did not return the requested fields. %r', row @@ -108,18 +111,17 @@ class singleCleaner(StoppableThread): break toAddress, ackData, status = row if status == 'awaitingpubkey': - self.resendPubkeyRequest(toAddress) + resendPubkeyRequest(toAddress) elif status == 'msgsent': - self.resendMsg(ackData) - deleteTrashMsgPermonantly() + resendMsg(ackData) + try: # Cleanup knownnodes and handle possible severe exception # while writing it to disk knownnodes.cleanupKnownNodes() except Exception as err: - # pylint: disable=protected-access if "Errno 28" in str(err): - self.logger.fatal( + logger.fatal( '(while writing knownnodes to disk)' ' Alert: Your disk or data storage volume is full.' ) @@ -130,13 +132,21 @@ class singleCleaner(StoppableThread): "MainWindow", 'Alert: Your disk or data storage volume' ' is full. Bitmessage will now exit.'), - True) + True) )) - if shared.thisapp.daemon or not state.enableGUI: + # FIXME redundant? + if shared.daemon or not state.enableGUI: os._exit(1) +# # clear download queues +# for thread in threading.enumerate(): +# if thread.isAlive() and hasattr(thread, 'downloadQueue'): +# thread.downloadQueue.clear() + # inv/object tracking - for connection in BMConnectionPool().connections(): + for connection in \ + BMConnectionPool().inboundConnections.values() + \ + BMConnectionPool().outboundConnections.values(): connection.clean() # discovery tracking @@ -147,58 +157,48 @@ class singleCleaner(StoppableThread): del state.discoveredPeers[k] except KeyError: pass - - # ..todo:: cleanup pending upload / download + # TODO: cleanup pending upload / download gc.collect() if state.shutdown == 0: self.stop.wait(singleCleaner.cycleLength) - def resendPubkeyRequest(self, address): - """Resend pubkey request for address""" - self.logger.debug( - 'It has been a long time and we haven\'t heard a response to our' - ' getpubkey request. Sending again.' - ) - try: - # We need to take this entry out of the neededPubkeys structure - # because the queues.workerQueue checks to see whether the entry - # is already present and will not do the POW and send the message - # because it assumes that it has already done it recently. - del state.neededPubkeys[address] - except: - pass - queues.UISignalQueue.put(( - 'updateStatusBar', - 'Doing work necessary to again attempt to request a public key...' - )) - sqlExecute( - '''UPDATE sent SET status='msgqueued' WHERE toaddress=?''', - address) - queues.workerQueue.put(('sendmessage', '')) +def resendPubkeyRequest(address): + logger.debug( + 'It has been a long time and we haven\'t heard a response to our' + ' getpubkey request. Sending again.' + ) + try: + # We need to take this entry out of the neededPubkeys structure + # because the queues.workerQueue checks to see whether the entry + # is already present and will not do the POW and send the message + # because it assumes that it has already done it recently. + del state.neededPubkeys[address] + except: + pass - def resendMsg(self, ackdata): - """Resend message by ackdata""" - self.logger.debug( - 'It has been a long time and we haven\'t heard an acknowledgement' - ' to our msg. Sending again.' - ) - sqlExecute( - '''UPDATE sent SET status='msgqueued' WHERE ackdata=?''', - ackdata) - queues.workerQueue.put(('sendmessage', '')) - queues.UISignalQueue.put(( - 'updateStatusBar', - 'Doing work necessary to again attempt to deliver a message...' - )) + queues.UISignalQueue.put(( + 'updateStatusBar', + 'Doing work necessary to again attempt to request a public key...' + )) + sqlExecute( + '''UPDATE sent SET status='msgqueued' WHERE toaddress=?''', + address) + queues.workerQueue.put(('sendmessage', '')) -def deleteTrashMsgPermonantly(): - """This method is used to delete old messages""" - ndays_before_time = datetime.now() - timedelta(days=30) - old_messages = time.mktime(ndays_before_time.timetuple()) - sqlExecute("delete from sent where folder = 'trash' and lastactiontime <= ?;", int(old_messages)) - sqlExecute("delete from inbox where folder = 'trash' and received <= ?;", int(old_messages)) - return +def resendMsg(ackdata): + logger.debug( + 'It has been a long time and we haven\'t heard an acknowledgement' + ' to our msg. Sending again.' + ) + sqlExecute( + '''UPDATE sent SET status='msgqueued' WHERE ackdata=?''', + ackdata) + queues.workerQueue.put(('sendmessage', '')) + queues.UISignalQueue.put(( + 'updateStatusBar', + 'Doing work necessary to again attempt to deliver a message...' + )) diff --git a/src/class_singleWorker.py b/src/class_singleWorker.py index 0bb110a7..d979ae19 100644 --- a/src/class_singleWorker.py +++ b/src/class_singleWorker.py @@ -1,13 +1,13 @@ """ -Thread for performing PoW +src/class_singleWorker.py +========================= """ -# pylint: disable=protected-access,too-many-branches,too-many-statements -# pylint: disable=no-self-use,too-many-lines,too-many-locals,relative-import - +# pylint: disable=protected-access,too-many-branches,too-many-statements,no-self-use,too-many-lines,too-many-locals from __future__ import division import hashlib +import threading import time from binascii import hexlify, unhexlify from struct import pack @@ -25,16 +25,12 @@ import queues import shared import state import tr -from addresses import ( - calculateInventoryHash, decodeAddress, decodeVarint, encodeVarint -) +from addresses import calculateInventoryHash, decodeAddress, decodeVarint, encodeVarint from bmconfigparser import BMConfigParser +from debug import logger from helper_sql import sqlExecute, sqlQuery +from helper_threading import StoppableThread from inventory import Inventory -from network import StoppableThread - -# This thread, of which there is only one, does the heavy lifting: -# calculating POWs. def sizeof_fmt(num, suffix='h/s'): @@ -47,11 +43,12 @@ def sizeof_fmt(num, suffix='h/s'): return "%.1f%s%s" % (num, 'Yi', suffix) -class singleWorker(StoppableThread): +class singleWorker(threading.Thread, StoppableThread): """Thread for performing PoW""" def __init__(self): - super(singleWorker, self).__init__(name="singleWorker") + threading.Thread.__init__(self, name="singleWorker") + self.initStop() proofofwork.init() def stopThread(self): @@ -74,7 +71,7 @@ class singleWorker(StoppableThread): # Initialize the neededPubkeys dictionary. queryreturn = sqlQuery( '''SELECT DISTINCT toaddress FROM sent''' - ''' WHERE (status='awaitingpubkey' AND folder LIKE '%sent%')''') + ''' WHERE (status='awaitingpubkey' AND folder='sent')''') for row in queryreturn: toAddress, = row # toStatus @@ -103,7 +100,7 @@ class singleWorker(StoppableThread): '''SELECT ackdata FROM sent WHERE status = 'msgsent' ''') for row in queryreturn: ackdata, = row - self.logger.info('Watching for ackdata %s', hexlify(ackdata)) + logger.info('Watching for ackdata %s', hexlify(ackdata)) shared.ackdataForWhichImWatching[ackdata] = 0 # Fix legacy (headerless) watched ackdata to include header @@ -178,14 +175,14 @@ class singleWorker(StoppableThread): self.busy = 0 return else: - self.logger.error( + logger.error( 'Probable programming error: The command sent' ' to the workerThread is weird. It is: %s\n', command ) queues.workerQueue.task_done() - self.logger.info("Quitting...") + logger.info("Quitting...") def _getKeysForAddress(self, address): privSigningKeyBase58 = BMConfigParser().get( @@ -222,34 +219,33 @@ class singleWorker(StoppableThread): )) / (2 ** 16)) )) initialHash = hashlib.sha512(payload).digest() - self.logger.info( + logger.info( '%s Doing proof of work... TTL set to %s', log_prefix, TTL) if log_time: start_time = time.time() trialValue, nonce = proofofwork.run(target, initialHash) - self.logger.info( + logger.info( '%s Found proof of work %s Nonce: %s', log_prefix, trialValue, nonce ) try: delta = time.time() - start_time - self.logger.info( + logger.info( 'PoW took %.1f seconds, speed %s.', delta, sizeof_fmt(nonce / delta) ) except: # NameError pass payload = pack('>Q', nonce) + payload + # inventoryHash = calculateInventoryHash(payload) return payload def doPOWForMyV2Pubkey(self, adressHash): - """ This function also broadcasts out the pubkey - message once it is done with the POW""" + """ This function also broadcasts out the pubkey message once it is done with the POW""" # Look up my stream number based on my address hash myAddress = shared.myAddressesByHash[adressHash] # status - _, addressVersionNumber, streamNumber, adressHash = ( - decodeAddress(myAddress)) + _, 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)) @@ -266,7 +262,7 @@ class singleWorker(StoppableThread): _, _, pubSigningKey, pubEncryptionKey = \ self._getKeysForAddress(myAddress) except Exception as err: - self.logger.error( + logger.error( 'Error within doPOWForMyV2Pubkey. Could not read' ' the keys from the keys.dat file for a requested' ' address. %s\n', err @@ -284,8 +280,7 @@ class singleWorker(StoppableThread): Inventory()[inventoryHash] = ( objectType, streamNumber, payload, embeddedTime, '') - self.logger.info( - 'broadcasting inv with hash: %s', hexlify(inventoryHash)) + logger.info('broadcasting inv with hash: %s', hexlify(inventoryHash)) queues.invQueue.put((streamNumber, inventoryHash)) queues.UISignalQueue.put(('updateStatusBar', '')) @@ -310,7 +305,7 @@ class singleWorker(StoppableThread): # The address has been deleted. return if BMConfigParser().safeGetBoolean(myAddress, 'chan'): - self.logger.info('This is a chan address. Not sending pubkey.') + logger.info('This is a chan address. Not sending pubkey.') return _, addressVersionNumber, streamNumber, adressHash = decodeAddress( myAddress) @@ -340,7 +335,7 @@ class singleWorker(StoppableThread): privSigningKeyHex, _, pubSigningKey, pubEncryptionKey = \ self._getKeysForAddress(myAddress) except Exception as err: - self.logger.error( + logger.error( 'Error within sendOutOrStoreMyV3Pubkey. Could not read' ' the keys from the keys.dat file for a requested' ' address. %s\n', err @@ -367,8 +362,7 @@ class singleWorker(StoppableThread): Inventory()[inventoryHash] = ( objectType, streamNumber, payload, embeddedTime, '') - self.logger.info( - 'broadcasting inv with hash: %s', hexlify(inventoryHash)) + logger.info('broadcasting inv with hash: %s', hexlify(inventoryHash)) queues.invQueue.put((streamNumber, inventoryHash)) queues.UISignalQueue.put(('updateStatusBar', '')) @@ -391,7 +385,7 @@ class singleWorker(StoppableThread): # The address has been deleted. return if shared.BMConfigParser().safeGetBoolean(myAddress, 'chan'): - self.logger.info('This is a chan address. Not sending pubkey.') + logger.info('This is a chan address. Not sending pubkey.') return _, addressVersionNumber, streamNumber, addressHash = decodeAddress( myAddress) @@ -410,7 +404,7 @@ class singleWorker(StoppableThread): privSigningKeyHex, _, pubSigningKey, pubEncryptionKey = \ self._getKeysForAddress(myAddress) except Exception as err: - self.logger.error( + logger.error( 'Error within sendOutOrStoreMyV4Pubkey. Could not read' ' the keys from the keys.dat file for a requested' ' address. %s\n', err @@ -458,8 +452,7 @@ class singleWorker(StoppableThread): doubleHashOfAddressData[32:] ) - self.logger.info( - 'broadcasting inv with hash: %s', hexlify(inventoryHash)) + logger.info('broadcasting inv with hash: %s', hexlify(inventoryHash)) queues.invQueue.put((streamNumber, inventoryHash)) queues.UISignalQueue.put(('updateStatusBar', '')) @@ -468,7 +461,7 @@ class singleWorker(StoppableThread): myAddress, 'lastpubkeysendtime', str(int(time.time()))) BMConfigParser().save() except Exception as err: - self.logger.error( + logger.error( 'Error: Couldn\'t add the lastpubkeysendtime' ' to the keys.dat file. Error message: %s', err ) @@ -485,7 +478,7 @@ class singleWorker(StoppableThread): embeddedTime = int(time.time() + TTL) streamNumber = 1 # Don't know yet what should be here objectType = protocol.OBJECT_ONIONPEER - # ..FIXME: ideally the objectPayload should be signed + # FIXME: ideally the objectPayload should be signed objectPayload = encodeVarint(peer.port) + protocol.encodeHost(peer.host) tag = calculateInventoryHash(objectPayload) @@ -506,7 +499,7 @@ class singleWorker(StoppableThread): objectType, streamNumber, buffer(payload), embeddedTime, buffer(tag) ) - self.logger.info( + logger.info( 'sending inv (within sendOnionPeerObj function) for object: %s', hexlify(inventoryHash)) queues.invQueue.put((streamNumber, inventoryHash)) @@ -521,7 +514,7 @@ class singleWorker(StoppableThread): queryreturn = sqlQuery( '''SELECT fromaddress, subject, message, ''' ''' ackdata, ttl, encodingtype FROM sent ''' - ''' WHERE status=? and folder LIKE '%sent%' ''', 'broadcastqueued') + ''' WHERE status=? and folder='sent' ''', 'broadcastqueued') for row in queryreturn: fromaddress, subject, body, ackdata, TTL, encoding = row @@ -529,7 +522,7 @@ class singleWorker(StoppableThread): _, addressVersionNumber, streamNumber, ripe = \ decodeAddress(fromaddress) if addressVersionNumber <= 1: - self.logger.error( + logger.error( 'Error: In the singleWorker thread, the ' ' sendBroadcast function doesn\'t understand' ' the address version.\n') @@ -645,7 +638,7 @@ class singleWorker(StoppableThread): # to not let the user try to send a message this large # until we implement message continuation. if len(payload) > 2 ** 18: # 256 KiB - self.logger.critical( + logger.critical( 'This broadcast object is too large to send.' ' This should never happen. Object size: %s', len(payload) @@ -656,7 +649,7 @@ class singleWorker(StoppableThread): objectType = 3 Inventory()[inventoryHash] = ( objectType, streamNumber, payload, embeddedTime, tag) - self.logger.info( + logger.info( 'sending inv (within sendBroadcast function)' ' for object: %s', hexlify(inventoryHash) @@ -691,7 +684,7 @@ class singleWorker(StoppableThread): '''SELECT toaddress, fromaddress, subject, message, ''' ''' ackdata, status, ttl, retrynumber, encodingtype FROM ''' ''' sent WHERE (status='msgqueued' or status='forcepow') ''' - ''' and folder LIKE '%sent%' ''') + ''' and folder='sent' ''') # while we have a msg that needs some work for row in queryreturn: toaddress, fromaddress, subject, message, \ @@ -876,8 +869,8 @@ class singleWorker(StoppableThread): "MainWindow", "Looking up the receiver\'s public key")) )) - self.logger.info('Sending a message.') - self.logger.debug( + logger.info('Sending a message.') + logger.debug( 'First 150 characters of message: %s', repr(message[:150]) ) @@ -921,7 +914,7 @@ class singleWorker(StoppableThread): if not shared.BMConfigParser().safeGetBoolean( 'bitmessagesettings', 'willinglysendtomobile' ): - self.logger.info( + logger.info( 'The receiver is a mobile user but the' ' sender (you) has not selected that you' ' are willing to send to mobiles. Aborting' @@ -987,7 +980,7 @@ class singleWorker(StoppableThread): defaults.networkDefaultPayloadLengthExtraBytes: requiredPayloadLengthExtraBytes = \ defaults.networkDefaultPayloadLengthExtraBytes - self.logger.debug( + logger.debug( 'Using averageProofOfWorkNonceTrialsPerByte: %s' ' and payloadLengthExtraBytes: %s.', requiredAverageProofOfWorkNonceTrialsPerByte, @@ -1052,9 +1045,8 @@ class singleWorker(StoppableThread): l10n.formatTimestamp())))) continue else: # if we are sending a message to ourselves or a chan.. - self.logger.info('Sending a message.') - self.logger.debug( - 'First 150 characters of message: %r', message[:150]) + logger.info('Sending a message.') + logger.debug('First 150 characters of message: %r', message[:150]) behaviorBitfield = protocol.getBitfield(fromaddress) try: @@ -1073,7 +1065,7 @@ class singleWorker(StoppableThread): " message. %1" ).arg(l10n.formatTimestamp())) )) - self.logger.error( + logger.error( 'Error within sendMsg. Could not read the keys' ' from the keys.dat file for our own address. %s\n', err) @@ -1149,14 +1141,14 @@ class singleWorker(StoppableThread): payload += encodeVarint(encodedMessage.length) payload += encodedMessage.data if BMConfigParser().has_section(toaddress): - self.logger.info( + logger.info( 'Not bothering to include ackdata because we are' ' sending to ourselves or a chan.' ) fullAckPayload = '' elif not protocol.checkBitfield( behaviorBitfield, protocol.BITFIELD_DOESACK): - self.logger.info( + logger.info( 'Not bothering to include ackdata because' ' the receiver said that they won\'t relay it anyway.' ) @@ -1209,7 +1201,7 @@ class singleWorker(StoppableThread): requiredPayloadLengthExtraBytes )) / (2 ** 16)) )) - self.logger.info( + logger.info( '(For msg message) Doing proof of work. Total required' ' difficulty: %f. Required small message difficulty: %f.', float(requiredAverageProofOfWorkNonceTrialsPerByte) / @@ -1221,13 +1213,12 @@ class singleWorker(StoppableThread): powStartTime = time.time() initialHash = hashlib.sha512(encryptedPayload).digest() trialValue, nonce = proofofwork.run(target, initialHash) - print("nonce calculated value#############################", nonce) - self.logger.info( + logger.info( '(For msg message) Found proof of work %s Nonce: %s', trialValue, nonce ) try: - self.logger.info( + logger.info( 'PoW took %.1f seconds, speed %s.', time.time() - powStartTime, sizeof_fmt(nonce / (time.time() - powStartTime)) @@ -1242,7 +1233,7 @@ class singleWorker(StoppableThread): # in the code to not let the user try to send a message # this large until we implement message continuation. if len(encryptedPayload) > 2 ** 18: # 256 KiB - self.logger.critical( + logger.critical( 'This msg object is too large to send. This should' ' never happen. Object size: %i', len(encryptedPayload) @@ -1273,7 +1264,7 @@ class singleWorker(StoppableThread): " Sent on %1" ).arg(l10n.formatTimestamp())) )) - self.logger.info( + logger.info( 'Broadcasting inv for my msg(within sendmsg function): %s', hexlify(inventoryHash) ) @@ -1326,7 +1317,7 @@ class singleWorker(StoppableThread): toStatus, addressVersionNumber, streamNumber, ripe = decodeAddress( toAddress) if toStatus != 'success': - self.logger.error( + logger.error( 'Very abnormal error occurred in requestPubKey.' ' toAddress is: %r. Please report this error to Atheros.', toAddress @@ -1340,7 +1331,7 @@ class singleWorker(StoppableThread): toAddress ) if not queryReturn: - self.logger.critical( + logger.critical( 'BUG: Why are we requesting the pubkey for %s' ' if there are no messages in the sent folder' ' to that address?', toAddress @@ -1388,11 +1379,11 @@ class singleWorker(StoppableThread): payload += encodeVarint(streamNumber) if addressVersionNumber <= 3: payload += ripe - self.logger.info( + logger.info( 'making request for pubkey with ripe: %s', hexlify(ripe)) else: payload += tag - self.logger.info( + logger.info( 'making request for v4 pubkey with tag: %s', hexlify(tag)) # print 'trial value', trialValue @@ -1413,7 +1404,7 @@ class singleWorker(StoppableThread): objectType = 1 Inventory()[inventoryHash] = ( objectType, streamNumber, payload, embeddedTime, '') - self.logger.info('sending inv (for the getpubkey message)') + logger.info('sending inv (for the getpubkey message)') queues.invQueue.put((streamNumber, inventoryHash)) # wait 10% past expiration diff --git a/src/class_smtpDeliver.py b/src/class_smtpDeliver.py index 4f8422cc..ef7a4363 100644 --- a/src/class_smtpDeliver.py +++ b/src/class_smtpDeliver.py @@ -1,9 +1,12 @@ """ -SMTP client thread for delivering emails +src/class_smtpDeliver.py +======================== """ # pylint: disable=unused-variable import smtplib +import sys +import threading import urlparse from email.header import Header from email.mime.text import MIMEText @@ -11,20 +14,23 @@ from email.mime.text import MIMEText import queues import state from bmconfigparser import BMConfigParser -from network.threads import StoppableThread +from debug import logger +from helper_threading import StoppableThread SMTPDOMAIN = "bmaddr.lan" -class smtpDeliver(StoppableThread): +class smtpDeliver(threading.Thread, StoppableThread): """SMTP client thread for delivery""" - name = "smtpDeliver" _instance = None + def __init__(self): + threading.Thread.__init__(self, name="smtpDeliver") + self.initStop() + def stopThread(self): - # pylint: disable=no-member try: - queues.UISignallerQueue.put(("stopThread", "data")) + queues.UISignallerQueue.put(("stopThread", "data")) # pylint: disable=no-member except: pass super(smtpDeliver, self).stopThread() @@ -38,7 +44,6 @@ class smtpDeliver(StoppableThread): def run(self): # pylint: disable=too-many-branches,too-many-statements,too-many-locals - # pylint: disable=deprecated-lambda while state.shutdown == 0: command, data = queues.UISignalQueue.get() if command == 'writeNewAddressToTable': @@ -61,9 +66,9 @@ class smtpDeliver(StoppableThread): msg = MIMEText(body, 'plain', 'utf-8') msg['Subject'] = Header(subject, 'utf-8') msg['From'] = fromAddress + '@' + SMTPDOMAIN - toLabel = map( + toLabel = map( # pylint: disable=deprecated-lambda lambda y: BMConfigParser().safeGet(y, "label"), - filter( + filter( # pylint: disable=deprecated-lambda lambda x: x == toAddress, BMConfigParser().addresses()) ) if toLabel: @@ -74,12 +79,10 @@ class smtpDeliver(StoppableThread): client.starttls() client.ehlo() client.sendmail(msg['From'], [to], msg.as_string()) - self.logger.info( - 'Delivered via SMTP to %s through %s:%i ...', - to, u.hostname, u.port) + logger.info("Delivered via SMTP to %s through %s:%i ...", to, u.hostname, u.port) client.quit() except: - self.logger.error('smtp delivery error', exc_info=True) + logger.error("smtp delivery error", exc_info=True) elif command == 'displayNewSentMessage': toAddress, fromLabel, fromAddress, subject, message, ackdata = data elif command == 'updateNetworkStatusTab': @@ -113,5 +116,5 @@ class smtpDeliver(StoppableThread): elif command == 'stopThread': break else: - self.logger.warning( - 'Command sent to smtpDeliver not recognized: %s', command) + sys.stderr.write( + 'Command sent to smtpDeliver not recognized: %s\n' % command) diff --git a/src/class_smtpServer.py b/src/class_smtpServer.py index 453ca640..216d35be 100644 --- a/src/class_smtpServer.py +++ b/src/class_smtpServer.py @@ -1,37 +1,28 @@ -""" -SMTP server thread -""" import asyncore import base64 import email -import logging +from email.parser import Parser +from email.header import decode_header import re import signal import smtpd import threading import time -from email.header import decode_header -from email.parser import Parser -import queues from addresses import decodeAddress from bmconfigparser import BMConfigParser -from helper_ackPayload import genAckPayload +from debug import logger from helper_sql import sqlExecute -from network.threads import StoppableThread +from helper_ackPayload import genAckPayload +from helper_threading import StoppableThread +import queues from version import softwareVersion SMTPDOMAIN = "bmaddr.lan" LISTENPORT = 8425 -logger = logging.getLogger('default') -# pylint: disable=attribute-defined-outside-init - - class smtpServerChannel(smtpd.SMTPChannel): - """Asyncore channel for SMTP protocol (server)""" def smtp_EHLO(self, arg): - """Process an EHLO""" if not arg: self.push('501 Syntax: HELO hostname') return @@ -39,17 +30,15 @@ class smtpServerChannel(smtpd.SMTPChannel): self.push('250 AUTH PLAIN') def smtp_AUTH(self, arg): - """Process AUTH""" if not arg or arg[0:5] not in ["PLAIN"]: self.push('501 Syntax: AUTH PLAIN') return authstring = arg[6:] try: decoded = base64.b64decode(authstring) - correctauth = "\x00" + BMConfigParser().safeGet( - "bitmessagesettings", "smtpdusername", "") + "\x00" + BMConfigParser().safeGet( - "bitmessagesettings", "smtpdpassword", "") - logger.debug('authstring: %s / %s', correctauth, decoded) + correctauth = "\x00" + 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') @@ -59,17 +48,14 @@ class smtpServerChannel(smtpd.SMTPChannel): self.push('501 Authentication fail') def smtp_DATA(self, arg): - """Process DATA""" if not hasattr(self, "auth") or not self.auth: - self.push('530 Authentication required') + self.push ("530 Authentication required") return smtpd.SMTPChannel.smtp_DATA(self, arg) class smtpServerPyBitmessage(smtpd.SMTPServer): - """Asyncore SMTP server class""" def handle_accept(self): - """Accept a connection""" pair = self.accept() if pair is not None: conn, addr = pair @@ -77,9 +63,7 @@ class smtpServerPyBitmessage(smtpd.SMTPServer): 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:] + status, addressVersionNumber, streamNumber, ripe = decodeAddress(toAddress) stealthLevel = BMConfigParser().safeGetInt('bitmessagesettings', 'ackstealthlevel') ackdata = genAckPayload(streamNumber, stealthLevel) sqlExecute( @@ -91,21 +75,19 @@ class smtpServerPyBitmessage(smtpd.SMTPServer): subject, message, ackdata, - int(time.time()), # sentTime (this will never change) - int(time.time()), # lastActionTime - 0, # sleepTill time. This will get set when the POW gets done. + int(time.time()), # sentTime (this will never change) + int(time.time()), # lastActionTime + 0, # sleepTill time. This will get set when the POW gets done. 'msgqueued', - 0, # retryNumber - 'sent', # folder - 2, # encodingtype - # not necessary to have a TTL higher than 2 days - min(BMConfigParser().getint('bitmessagesettings', 'ttl'), 86400 * 2) + 0, # retryNumber + 'sent', # folder + 2, # encodingtype + min(BMConfigParser().getint('bitmessagesettings', 'ttl'), 86400 * 2) # not necessary to have a TTL higher than 2 days ) queues.workerQueue.put(('sendmessage', toAddress)) def decode_header(self, hdr): - """Email header decoding""" ret = [] for h in decode_header(self.msg_headers[hdr]): if h[1]: @@ -115,38 +97,37 @@ class smtpServerPyBitmessage(smtpd.SMTPServer): return ret + def process_message(self, peer, mailfrom, rcpttos, data): - """Process an email""" - # pylint: disable=too-many-locals, too-many-branches - # print 'Receiving message from:', peer +# 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') + logger.error("Missing or invalid auth") return try: self.msg_headers = Parser().parsestr(data) except: - logger.error('Invalid headers') + logger.error("Invalid headers") return try: sender, domain = p.sub(r'\1', mailfrom).split("@") if domain != SMTPDOMAIN: - raise Exception("Bad domain %s" % domain) + raise Exception("Bad domain %s", domain) if sender not in BMConfigParser().addresses(): - raise Exception("Nonexisting user %s" % sender) + raise Exception("Nonexisting user %s", sender) except Exception as err: - logger.debug('Bad envelope from %s: %r', mailfrom, err) + logger.debug("Bad envelope from %s: %s", mailfrom, repr(err)) msg_from = self.decode_header("from") try: msg_from = p.sub(r'\1', self.decode_header("from")[0]) sender, domain = msg_from.split("@") if domain != SMTPDOMAIN: - raise Exception("Bad domain %s" % domain) + raise Exception("Bad domain %s", domain) if sender not in BMConfigParser().addresses(): - raise Exception("Nonexisting user %s" % sender) + raise Exception("Nonexisting user %s", sender) except Exception as err: - logger.error('Bad headers from %s: %r', msg_from, err) + logger.error("Bad headers from %s: %s", msg_from, repr(err)) return try: @@ -164,21 +145,19 @@ class smtpServerPyBitmessage(smtpd.SMTPServer): try: rcpt, domain = p.sub(r'\1', to).split("@") if domain != SMTPDOMAIN: - raise Exception("Bad domain %s" % domain) - logger.debug( - 'Sending %s to %s about %s', sender, rcpt, msg_subject) + raise Exception("Bad domain %s", domain) + logger.debug("Sending %s to %s about %s", sender, rcpt, msg_subject) self.send(sender, rcpt, msg_subject, body) - logger.info('Relayed %s to %s', sender, rcpt) + logger.info("Relayed %s to %s", sender, rcpt) except Exception as err: - logger.error('Bad to %s: %r', to, err) + logger.error( "Bad to %s: %s", to, repr(err)) continue return - -class smtpServer(StoppableThread): - """SMTP server thread""" - def __init__(self, _=None): - super(smtpServer, self).__init__(name="smtpServerThread") +class smtpServer(threading.Thread, StoppableThread): + def __init__(self, parent=None): + threading.Thread.__init__(self, name="smtpServerThread") + self.initStop() self.server = smtpServerPyBitmessage(('127.0.0.1', LISTENPORT), None) def stopThread(self): @@ -189,26 +168,21 @@ class smtpServer(StoppableThread): def run(self): asyncore.loop(1) - -def signals(_, __): - """Signal handler""" - logger.warning('Got signal, terminating') +def signals(signal, frame): + print "Got signal, terminating" for thread in threading.enumerate(): if thread.isAlive() and isinstance(thread, StoppableThread): thread.stopThread() - def runServer(): - """Run SMTP server as a standalone python process""" - logger.warning('Running SMTPd thread') + print "Running SMTPd thread" smtpThread = smtpServer() smtpThread.start() signal.signal(signal.SIGINT, signals) signal.signal(signal.SIGTERM, signals) - logger.warning('Processing') + print "Processing" smtpThread.join() - logger.warning('The end') - + print "The end" if __name__ == "__main__": runServer() diff --git a/src/class_sqlThread.py b/src/class_sqlThread.py index 7e9eb6c5..a45571e0 100644 --- a/src/class_sqlThread.py +++ b/src/class_sqlThread.py @@ -1,33 +1,29 @@ -""" -sqlThread is defined here -""" - -import os -import shutil # used for moving the messages.dat file -import sqlite3 -import sys import threading +from bmconfigparser import BMConfigParser +import sqlite3 import time - +import shutil # used for moving the messages.dat file +import sys +import os +from debug import logger 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 + +# This thread exists because SQLITE3 is so un-threadsafe that we must +# submit queries to it and it puts results back in a different queue. They +# won't let us just use locks. class sqlThread(threading.Thread): - """A thread for all SQL operations""" def __init__(self): threading.Thread.__init__(self, name="SQL") - def run(self): # pylint: disable=too-many-locals, too-many-branches, too-many-statements - """Process SQL queries from `.helper_sql.sqlSubmitQueue`""" + def run(self): self.conn = sqlite3.connect(state.appdata + 'messages.dat') self.conn.text_factory = str self.cur = self.conn.cursor() @@ -36,38 +32,30 @@ class sqlThread(threading.Thread): try: self.cur.execute( - '''CREATE TABLE inbox (msgid blob, toaddress text, fromaddress text, subject text,''' - ''' received text, message text, folder text, encodingtype int, read bool, sighash blob,''' - ''' UNIQUE(msgid) ON CONFLICT REPLACE)''') + '''CREATE TABLE inbox (msgid blob, toaddress text, fromaddress text, subject text, received text, message text, folder text, encodingtype int, read bool, sighash blob, UNIQUE(msgid) ON CONFLICT REPLACE)''' ) self.cur.execute( - '''CREATE TABLE sent (msgid blob, toaddress text, toripe blob, fromaddress text, subject text,''' - ''' message text, ackdata blob, senttime integer, lastactiontime integer,''' - ''' sleeptill integer, status text, retrynumber integer, folder text, encodingtype int, ttl int)''') + '''CREATE TABLE sent (msgid blob, toaddress text, toripe blob, fromaddress text, subject text, message text, ackdata blob, senttime integer, lastactiontime integer, sleeptill integer, status text, retrynumber integer, folder text, encodingtype int, ttl int)''' ) self.cur.execute( - '''CREATE TABLE subscriptions (label text, address text, enabled bool)''') + '''CREATE TABLE subscriptions (label text, address text, enabled bool)''' ) self.cur.execute( - '''CREATE TABLE addressbook (label text, address text)''') + '''CREATE TABLE addressbook (label text, address text)''' ) self.cur.execute( - '''CREATE TABLE blacklist (label text, address text, enabled bool)''') + '''CREATE TABLE blacklist (label text, address text, enabled bool)''' ) self.cur.execute( - '''CREATE TABLE whitelist (label text, address text, enabled bool)''') + '''CREATE TABLE whitelist (label text, address text, enabled bool)''' ) self.cur.execute( - '''CREATE TABLE pubkeys (address text, addressversion int, transmitdata blob, time int,''' - ''' usedpersonally text, UNIQUE(address) ON CONFLICT REPLACE)''') + '''CREATE TABLE pubkeys (address text, addressversion int, transmitdata blob, time int, usedpersonally text, UNIQUE(address) ON CONFLICT REPLACE)''' ) self.cur.execute( - '''CREATE TABLE inventory (hash blob, objecttype int, streamnumber int, payload blob,''' - ''' expirestime integer, tag blob, UNIQUE(hash) ON CONFLICT REPLACE)''') + '''CREATE TABLE inventory (hash blob, objecttype int, streamnumber int, payload blob, expirestime integer, tag blob, UNIQUE(hash) ON CONFLICT REPLACE)''' ) self.cur.execute( - '''INSERT INTO subscriptions VALUES''' - '''('Bitmessage new releases/announcements','BM-GtovgYdgs7qXPkoYaRgrLFuFKz1SFpsw',1)''') + '''INSERT INTO subscriptions VALUES('Bitmessage new releases/announcements','BM-GtovgYdgs7qXPkoYaRgrLFuFKz1SFpsw',1)''') self.cur.execute( - '''CREATE TABLE settings (key blob, value blob, UNIQUE(key) ON CONFLICT REPLACE)''') - self.cur.execute('''INSERT INTO settings VALUES('version','10')''') - self.cur.execute('''INSERT INTO settings VALUES('lastvacuumtime',?)''', ( + '''CREATE TABLE settings (key blob, value blob, UNIQUE(key) ON CONFLICT REPLACE)''' ) + self.cur.execute( '''INSERT INTO settings VALUES('version','10')''') + self.cur.execute( '''INSERT INTO settings VALUES('lastvacuumtime',?)''', ( int(time.time()),)) self.cur.execute( - '''CREATE TABLE objectprocessorqueue''' - ''' (objecttype int, data blob, UNIQUE(objecttype, data) ON CONFLICT REPLACE)''') + '''CREATE TABLE objectprocessorqueue (objecttype int, data blob, UNIQUE(objecttype, data) ON CONFLICT REPLACE)''' ) self.conn.commit() logger.info('Created messages database file') except Exception as err: @@ -132,38 +120,33 @@ class sqlThread(threading.Thread): logger.debug( "In messages.dat database, creating new 'settings' table.") self.cur.execute( - '''CREATE TABLE settings (key text, value blob, UNIQUE(key) ON CONFLICT REPLACE)''') - self.cur.execute('''INSERT INTO settings VALUES('version','1')''') - self.cur.execute('''INSERT INTO settings VALUES('lastvacuumtime',?)''', ( + '''CREATE TABLE settings (key text, value blob, UNIQUE(key) ON CONFLICT REPLACE)''' ) + self.cur.execute( '''INSERT INTO settings VALUES('version','1')''') + self.cur.execute( '''INSERT INTO settings VALUES('lastvacuumtime',?)''', ( int(time.time()),)) logger.debug('In messages.dat database, removing an obsolete field from the pubkeys table.') self.cur.execute( - '''CREATE TEMPORARY TABLE pubkeys_backup(hash blob, transmitdata blob, time int,''' - ''' usedpersonally text, UNIQUE(hash) ON CONFLICT REPLACE);''') + '''CREATE TEMPORARY TABLE pubkeys_backup(hash blob, transmitdata blob, time int, usedpersonally text, UNIQUE(hash) ON CONFLICT REPLACE);''') self.cur.execute( '''INSERT INTO pubkeys_backup SELECT hash, transmitdata, time, usedpersonally FROM pubkeys;''') - self.cur.execute('''DROP TABLE pubkeys''') + self.cur.execute( '''DROP TABLE pubkeys''') self.cur.execute( - '''CREATE TABLE pubkeys''' - ''' (hash blob, transmitdata blob, time int, usedpersonally text, UNIQUE(hash) ON CONFLICT REPLACE)''') + '''CREATE TABLE pubkeys (hash blob, transmitdata blob, time int, usedpersonally text, UNIQUE(hash) ON CONFLICT REPLACE)''' ) self.cur.execute( '''INSERT INTO pubkeys SELECT hash, transmitdata, time, usedpersonally FROM pubkeys_backup;''') - self.cur.execute('''DROP TABLE pubkeys_backup;''') - logger.debug( - 'Deleting all pubkeys from inventory.' - ' They will be redownloaded and then saved with the correct times.') + self.cur.execute( '''DROP TABLE pubkeys_backup;''') + logger.debug('Deleting all pubkeys from inventory. They will be redownloaded and then saved with the correct times.') self.cur.execute( '''delete from inventory where objecttype = 'pubkey';''') logger.debug('replacing Bitmessage announcements mailing list with a new one.') self.cur.execute( '''delete from subscriptions where address='BM-BbkPSZbzPwpVcYZpU4yHwf9ZPEapN5Zx' ''') self.cur.execute( - '''INSERT INTO subscriptions VALUES''' - '''('Bitmessage new releases/announcements','BM-GtovgYdgs7qXPkoYaRgrLFuFKz1SFpsw',1)''') + '''INSERT INTO subscriptions VALUES('Bitmessage new releases/announcements','BM-GtovgYdgs7qXPkoYaRgrLFuFKz1SFpsw',1)''') logger.debug('Commiting.') self.conn.commit() logger.debug('Vacuuming message.dat. You might notice that the file size gets much smaller.') - self.cur.execute(''' VACUUM ''') + self.cur.execute( ''' VACUUM ''') # After code refactoring, the possible status values for sent messages # have changed. @@ -187,21 +170,15 @@ class sqlThread(threading.Thread): 'In messages.dat database, removing an obsolete field from' ' the inventory table.') self.cur.execute( - '''CREATE TEMPORARY TABLE inventory_backup''' - '''(hash blob, objecttype text, streamnumber int, payload blob,''' - ''' receivedtime integer, UNIQUE(hash) ON CONFLICT REPLACE);''') + '''CREATE TEMPORARY TABLE inventory_backup(hash blob, objecttype text, streamnumber int, payload blob, receivedtime integer, UNIQUE(hash) ON CONFLICT REPLACE);''') self.cur.execute( - '''INSERT INTO inventory_backup SELECT hash, objecttype, streamnumber, payload, receivedtime''' - ''' FROM inventory;''') - self.cur.execute('''DROP TABLE inventory''') + '''INSERT INTO inventory_backup SELECT hash, objecttype, streamnumber, payload, receivedtime FROM inventory;''') + self.cur.execute( '''DROP TABLE inventory''') self.cur.execute( - '''CREATE TABLE inventory''' - ''' (hash blob, objecttype text, streamnumber int, payload blob, receivedtime integer,''' - ''' UNIQUE(hash) ON CONFLICT REPLACE)''') + '''CREATE TABLE inventory (hash blob, objecttype text, streamnumber int, payload blob, receivedtime integer, UNIQUE(hash) ON CONFLICT REPLACE)''' ) self.cur.execute( - '''INSERT INTO inventory SELECT hash, objecttype, streamnumber, payload, receivedtime''' - ''' FROM inventory_backup;''') - self.cur.execute('''DROP TABLE inventory_backup;''') + '''INSERT INTO inventory SELECT hash, objecttype, streamnumber, payload, receivedtime FROM inventory_backup;''') + self.cur.execute( '''DROP TABLE inventory_backup;''') item = '''update settings set value=? WHERE key='version';''' parameters = (3,) self.cur.execute(item, parameters) @@ -231,8 +208,7 @@ class sqlThread(threading.Thread): if currentVersion == 4: self.cur.execute('''DROP TABLE pubkeys''') self.cur.execute( - '''CREATE TABLE pubkeys (hash blob, addressversion int, transmitdata blob, time int,''' - '''usedpersonally text, UNIQUE(hash, addressversion) ON CONFLICT REPLACE)''') + '''CREATE TABLE pubkeys (hash blob, addressversion int, transmitdata blob, time int, usedpersonally text, UNIQUE(hash, addressversion) ON CONFLICT REPLACE)''') self.cur.execute( '''delete from inventory where objecttype = 'pubkey';''') item = '''update settings set value=? WHERE key='version';''' @@ -248,8 +224,7 @@ class sqlThread(threading.Thread): if currentVersion == 5: self.cur.execute('''DROP TABLE knownnodes''') self.cur.execute( - '''CREATE TABLE objectprocessorqueue''' - ''' (objecttype text, data blob, UNIQUE(objecttype, data) ON CONFLICT REPLACE)''') + '''CREATE TABLE objectprocessorqueue (objecttype text, data blob, UNIQUE(objecttype, data) ON CONFLICT REPLACE)''') item = '''update settings set value=? WHERE key='version';''' parameters = (6,) self.cur.execute(item, parameters) @@ -265,15 +240,10 @@ class sqlThread(threading.Thread): logger.debug( 'In messages.dat database, dropping and recreating' ' the inventory table.') - self.cur.execute('''DROP TABLE inventory''') - self.cur.execute( - '''CREATE TABLE inventory''' - ''' (hash blob, objecttype int, streamnumber int, payload blob, expirestime integer,''' - ''' tag blob, UNIQUE(hash) ON CONFLICT REPLACE)''') - self.cur.execute('''DROP TABLE objectprocessorqueue''') - self.cur.execute( - '''CREATE TABLE objectprocessorqueue''' - ''' (objecttype int, data blob, UNIQUE(objecttype, data) ON CONFLICT REPLACE)''') + self.cur.execute( '''DROP TABLE inventory''') + self.cur.execute( '''CREATE TABLE inventory (hash blob, objecttype int, streamnumber int, payload blob, expirestime integer, tag blob, UNIQUE(hash) ON CONFLICT REPLACE)''' ) + self.cur.execute( '''DROP TABLE objectprocessorqueue''') + self.cur.execute( '''CREATE TABLE objectprocessorqueue (objecttype int, data blob, UNIQUE(objecttype, data) ON CONFLICT REPLACE)''' ) item = '''update settings set value=? WHERE key='version';''' parameters = (7,) self.cur.execute(item, parameters) @@ -335,24 +305,15 @@ class sqlThread(threading.Thread): ' fields into the retrynumber field and adding the' ' sleeptill and ttl fields...') self.cur.execute( - '''CREATE TEMPORARY TABLE sent_backup''' - ''' (msgid blob, toaddress text, toripe blob, fromaddress text, subject text, message text,''' - ''' ackdata blob, lastactiontime integer, status text, retrynumber integer,''' - ''' folder text, encodingtype int)''') + '''CREATE TEMPORARY TABLE sent_backup (msgid blob, toaddress text, toripe blob, fromaddress text, subject text, message text, ackdata blob, lastactiontime integer, status text, retrynumber integer, folder text, encodingtype int)''' ) self.cur.execute( - '''INSERT INTO sent_backup SELECT msgid, toaddress, toripe, fromaddress,''' - ''' subject, message, ackdata, lastactiontime,''' - ''' status, 0, folder, encodingtype FROM sent;''') - self.cur.execute('''DROP TABLE sent''') + '''INSERT INTO sent_backup SELECT msgid, toaddress, toripe, fromaddress, subject, message, ackdata, lastactiontime, status, 0, folder, encodingtype FROM sent;''') + self.cur.execute( '''DROP TABLE sent''') self.cur.execute( - '''CREATE TABLE sent''' - ''' (msgid blob, toaddress text, toripe blob, fromaddress text, subject text, message text,''' - ''' ackdata blob, senttime integer, lastactiontime integer, sleeptill int, status text,''' - ''' retrynumber integer, folder text, encodingtype int, ttl int)''') + '''CREATE TABLE sent (msgid blob, toaddress text, toripe blob, fromaddress text, subject text, message text, ackdata blob, senttime integer, lastactiontime integer, sleeptill int, status text, retrynumber integer, folder text, encodingtype int, ttl int)''' ) self.cur.execute( - '''INSERT INTO sent SELECT msgid, toaddress, toripe, fromaddress, subject, message, ackdata,''' - ''' lastactiontime, lastactiontime, 0, status, 0, folder, encodingtype, 216000 FROM sent_backup;''') - self.cur.execute('''DROP TABLE sent_backup''') + '''INSERT INTO sent SELECT msgid, toaddress, toripe, fromaddress, subject, message, ackdata, lastactiontime, lastactiontime, 0, status, 0, folder, encodingtype, 216000 FROM sent_backup;''') + self.cur.execute( '''DROP TABLE sent_backup''') logger.info('In messages.dat database, finished making TTL-related changes.') logger.debug('In messages.dat database, adding address field to the pubkeys table.') # We're going to have to calculate the address for each row in the pubkeys @@ -369,24 +330,16 @@ class sqlThread(threading.Thread): self.cur.execute(item, parameters) # Now we can remove the hash field from the pubkeys table. self.cur.execute( - '''CREATE TEMPORARY TABLE pubkeys_backup''' - ''' (address text, addressversion int, transmitdata blob, time int,''' - ''' usedpersonally text, UNIQUE(address) ON CONFLICT REPLACE)''') + '''CREATE TEMPORARY TABLE pubkeys_backup (address text, addressversion int, transmitdata blob, time int, usedpersonally text, UNIQUE(address) ON CONFLICT REPLACE)''' ) self.cur.execute( - '''INSERT INTO pubkeys_backup''' - ''' SELECT address, addressversion, transmitdata, time, usedpersonally FROM pubkeys;''') - self.cur.execute('''DROP TABLE pubkeys''') + '''INSERT INTO pubkeys_backup SELECT address, addressversion, transmitdata, time, usedpersonally FROM pubkeys;''') + self.cur.execute( '''DROP TABLE pubkeys''') self.cur.execute( - '''CREATE TABLE pubkeys''' - ''' (address text, addressversion int, transmitdata blob, time int, usedpersonally text,''' - ''' UNIQUE(address) ON CONFLICT REPLACE)''') + '''CREATE TABLE pubkeys (address text, addressversion int, transmitdata blob, time int, usedpersonally text, UNIQUE(address) ON CONFLICT REPLACE)''' ) self.cur.execute( - '''INSERT INTO pubkeys SELECT''' - ''' address, addressversion, transmitdata, time, usedpersonally FROM pubkeys_backup;''') - self.cur.execute('''DROP TABLE pubkeys_backup''') - logger.debug( - 'In messages.dat database, done adding address field to the pubkeys table' - ' and removing the hash field.') + '''INSERT INTO pubkeys SELECT address, addressversion, transmitdata, time, usedpersonally FROM pubkeys_backup;''') + self.cur.execute( '''DROP TABLE pubkeys_backup''') + logger.debug('In messages.dat database, done adding address field to the pubkeys table and removing the hash field.') self.cur.execute('''update settings set value=10 WHERE key='version';''') # Are you hoping to add a new option to the keys.dat file of existing @@ -396,7 +349,7 @@ class sqlThread(threading.Thread): try: testpayload = '\x00\x00' t = ('1234', 1, testpayload, '12345678', 'no') - self.cur.execute('''INSERT INTO pubkeys VALUES(?,?,?,?,?)''', t) + self.cur.execute( '''INSERT INTO pubkeys VALUES(?,?,?,?,?)''', t) self.conn.commit() self.cur.execute( '''SELECT transmitdata FROM pubkeys WHERE address='1234' ''') @@ -406,29 +359,13 @@ class sqlThread(threading.Thread): self.cur.execute('''DELETE FROM pubkeys WHERE address='1234' ''') self.conn.commit() if transmitdata == '': - logger.fatal( - 'Problem: The version of SQLite you have cannot store Null values.' - ' Please download and install the latest revision of your version of Python' - ' (for example, the latest Python 2.7 revision) and try again.\n') - logger.fatal( - 'PyBitmessage will now exit very abruptly.' - ' You may now see threading errors related to this abrupt exit' - ' but the problem you need to solve is related to SQLite.\n\n') + logger.fatal('Problem: The version of SQLite you have cannot store Null values. Please download and install the latest revision of your version of Python (for example, the latest Python 2.7 revision) and try again.\n') + logger.fatal('PyBitmessage will now exit very abruptly. You may now see threading errors related to this abrupt exit but the problem you need to solve is related to SQLite.\n\n') os._exit(0) except Exception as err: if str(err) == 'database or disk is full': - logger.fatal( - '(While null value test) Alert: Your disk or data storage volume is full.' - ' sqlThread will now exit.') - queues.UISignalQueue.put(( - 'alert', ( - tr._translate( - "MainWindow", - "Disk full"), - tr._translate( - "MainWindow", - 'Alert: Your disk or data storage volume is full. Bitmessage will now exit.'), - True))) + logger.fatal('(While null value test) Alert: Your disk or data storage volume is full. sqlThread will now exit.') + queues.UISignalQueue.put(('alert', (tr._translate("MainWindow", "Disk full"), tr._translate("MainWindow", 'Alert: Your disk or data storage volume is full. Bitmessage will now exit.'), True))) os._exit(0) else: logger.error(err) @@ -444,21 +381,11 @@ class sqlThread(threading.Thread): if int(value) < int(time.time()) - 86400: logger.info('It has been a long time since the messages.dat file has been vacuumed. Vacuuming now...') try: - self.cur.execute(''' VACUUM ''') + self.cur.execute( ''' VACUUM ''') except Exception as err: if str(err) == 'database or disk is full': - logger.fatal( - '(While VACUUM) Alert: Your disk or data storage volume is full.' - ' sqlThread will now exit.') - queues.UISignalQueue.put(( - 'alert', ( - tr._translate( - "MainWindow", - "Disk full"), - tr._translate( - "MainWindow", - 'Alert: Your disk or data storage volume is full. Bitmessage will now exit.'), - True))) + logger.fatal('(While VACUUM) Alert: Your disk or data storage volume is full. sqlThread will now exit.') + queues.UISignalQueue.put(('alert', (tr._translate("MainWindow", "Disk full"), tr._translate("MainWindow", 'Alert: Your disk or data storage volume is full. Bitmessage will now exit.'), True))) os._exit(0) item = '''update settings set value=? WHERE key='lastvacuumtime';''' parameters = (int(time.time()),) @@ -473,18 +400,8 @@ class sqlThread(threading.Thread): self.conn.commit() except Exception as err: if str(err) == 'database or disk is full': - logger.fatal( - '(While committing) Alert: Your disk or data storage volume is full.' - ' sqlThread will now exit.') - queues.UISignalQueue.put(( - 'alert', ( - tr._translate( - "MainWindow", - "Disk full"), - tr._translate( - "MainWindow", - 'Alert: Your disk or data storage volume is full. Bitmessage will now exit.'), - True))) + logger.fatal('(While committing) Alert: Your disk or data storage volume is full. sqlThread will now exit.') + queues.UISignalQueue.put(('alert', (tr._translate("MainWindow", "Disk full"), tr._translate("MainWindow", 'Alert: Your disk or data storage volume is full. Bitmessage will now exit.'), True))) os._exit(0) elif item == 'exit': self.conn.close() @@ -498,18 +415,8 @@ class sqlThread(threading.Thread): self.conn.commit() except Exception as err: if str(err) == 'database or disk is full': - logger.fatal( - '(while movemessagstoprog) Alert: Your disk or data storage volume is full.' - ' sqlThread will now exit.') - queues.UISignalQueue.put(( - 'alert', ( - tr._translate( - "MainWindow", - "Disk full"), - tr._translate( - "MainWindow", - 'Alert: Your disk or data storage volume is full. Bitmessage will now exit.'), - True))) + logger.fatal('(while movemessagstoprog) Alert: Your disk or data storage volume is full. sqlThread will now exit.') + queues.UISignalQueue.put(('alert', (tr._translate("MainWindow", "Disk full"), tr._translate("MainWindow", 'Alert: Your disk or data storage volume is full. Bitmessage will now exit.'), True))) os._exit(0) self.conn.close() shutil.move( @@ -524,18 +431,8 @@ class sqlThread(threading.Thread): self.conn.commit() except Exception as err: if str(err) == 'database or disk is full': - logger.fatal( - '(while movemessagstoappdata) Alert: Your disk or data storage volume is full.' - ' sqlThread will now exit.') - queues.UISignalQueue.put(( - 'alert', ( - tr._translate( - "MainWindow", - "Disk full"), - tr._translate( - "MainWindow", - 'Alert: Your disk or data storage volume is full. Bitmessage will now exit.'), - True))) + logger.fatal('(while movemessagstoappdata) Alert: Your disk or data storage volume is full. sqlThread will now exit.') + queues.UISignalQueue.put(('alert', (tr._translate("MainWindow", "Disk full"), tr._translate("MainWindow", 'Alert: Your disk or data storage volume is full. Bitmessage will now exit.'), True))) os._exit(0) self.conn.close() shutil.move( @@ -548,21 +445,11 @@ class sqlThread(threading.Thread): self.cur.execute('''delete from sent where folder='trash' ''') self.conn.commit() try: - self.cur.execute(''' VACUUM ''') + self.cur.execute( ''' VACUUM ''') except Exception as err: if str(err) == 'database or disk is full': - logger.fatal( - '(while deleteandvacuume) Alert: Your disk or data storage volume is full.' - ' sqlThread will now exit.') - queues.UISignalQueue.put(( - 'alert', ( - tr._translate( - "MainWindow", - "Disk full"), - tr._translate( - "MainWindow", - 'Alert: Your disk or data storage volume is full. Bitmessage will now exit.'), - True))) + logger.fatal('(while deleteandvacuume) Alert: Your disk or data storage volume is full. sqlThread will now exit.') + queues.UISignalQueue.put(('alert', (tr._translate("MainWindow", "Disk full"), tr._translate("MainWindow", 'Alert: Your disk or data storage volume is full. Bitmessage will now exit.'), True))) os._exit(0) else: parameters = helper_sql.sqlSubmitQueue.get() @@ -574,30 +461,11 @@ class sqlThread(threading.Thread): rowcount = self.cur.rowcount except Exception as err: if str(err) == 'database or disk is full': - logger.fatal( - '(while cur.execute) Alert: Your disk or data storage volume is full.' - ' sqlThread will now exit.') - queues.UISignalQueue.put(( - 'alert', ( - tr._translate( - "MainWindow", - "Disk full"), - tr._translate( - "MainWindow", - 'Alert: Your disk or data storage volume is full. Bitmessage will now exit.'), - True))) + logger.fatal('(while cur.execute) Alert: Your disk or data storage volume is full. sqlThread will now exit.') + queues.UISignalQueue.put(('alert', (tr._translate("MainWindow", "Disk full"), tr._translate("MainWindow", 'Alert: Your disk or data storage volume is full. Bitmessage will now exit.'), True))) os._exit(0) else: - logger.fatal( - 'Major error occurred when trying to execute a SQL statement within the sqlThread.' - ' Please tell Atheros about this error message or post it in the forum!' - ' Error occurred while trying to execute statement: "%s" Here are the parameters;' - ' you might want to censor this data with asterisks (***)' - ' as it can contain private information: %s.' - ' Here is the actual error message thrown by the sqlThread: %s', - str(item), - str(repr(parameters)), - str(err)) + logger.fatal('Major error occurred when trying to execute a SQL statement within the sqlThread. Please tell Atheros about this error message or post it in the forum! Error occurred while trying to execute statement: "%s" Here are the parameters; you might want to censor this data with asterisks (***) as it can contain private information: %s. Here is the actual error message thrown by the sqlThread: %s', str(item), str(repr(parameters)), str(err)) logger.fatal('This program shall now abruptly exit!') os._exit(0) diff --git a/src/dave.png b/src/dave.png deleted file mode 100644 index e8d7bd0b..00000000 Binary files a/src/dave.png and /dev/null differ diff --git a/src/debug.py b/src/debug.py index b9aaf394..d3730d7f 100644 --- a/src/debug.py +++ b/src/debug.py @@ -1,38 +1,26 @@ """ Logging and debuging facility ------------------------------ +============================= Levels: - DEBUG - Detailed information, typically of interest only when diagnosing problems. - INFO - Confirmation that things are working as expected. - WARNING - An indication that something unexpected happened, or indicative of - some problem in the near future (e.g. 'disk space low'). The software - is still working as expected. - ERROR - Due to a more serious problem, the software has not been able to - perform some function. - CRITICAL - A serious error, indicating that the program itself may be unable to - continue running. + DEBUG + Detailed information, typically of interest only when diagnosing problems. + INFO + Confirmation that things are working as expected. + WARNING + An indication that something unexpected happened, or indicative of some problem in the + near future (e.g. 'disk space low'). The software is still working as expected. + ERROR + Due to a more serious problem, the software has not been able to perform some function. + CRITICAL + A serious error, indicating that the program itself may be unable to continue running. -There are three loggers by default: `console_only`, `file_only` and `both`. -You can configure logging in the logging.dat in the appdata dir. -It's format is described in the :func:`logging.config.fileConfig` doc. +There are three loggers: `console_only`, `file_only` and `both`. -Use: +Use: `from debug import logger` to import this facility into whatever module you wish to log messages from. + Logging is thread-safe so you don't have to worry about locks, just import and log. ->>> import logging ->>> logger = logging.getLogger('default') - -The old form: ``from debug import logger`` is also may be used, -but only in the top level modules. - -Logging is thread-safe so you don't have to worry about locks, -just import and log. """ import ConfigParser @@ -40,11 +28,11 @@ import logging import logging.config import os import sys - import helper_startup import state helper_startup.loadConfig() + # Now can be overriden from a config file, which uses standard python # logging.config.fileConfig interface # examples are here: @@ -53,22 +41,14 @@ log_level = 'WARNING' def log_uncaught_exceptions(ex_cls, ex, tb): - """The last resort logging function used for sys.excepthook""" logging.critical('Unhandled exception', exc_info=(ex_cls, ex, tb)) def configureLogging(): - """ - Configure logging, - using either logging.dat file in the state.appdata dir - or dictionary with hardcoded settings. - """ - sys.excepthook = log_uncaught_exceptions fail_msg = '' try: logging_config = os.path.join(state.appdata, 'logging.dat') - logging.config.fileConfig( - logging_config, disable_existing_loggers=False) + logging.config.fileConfig(logging_config) return ( False, 'Loaded logger configuration from %s' % logging_config @@ -80,11 +60,12 @@ def configureLogging(): ' logging config\n%s' % \ (logging_config, sys.exc_info()) else: - # no need to confuse the user if the logger config - # is missing entirely + # no need to confuse the user if the logger config is missing entirely fail_msg = 'Using default logger configuration' - logging_config = { + sys.excepthook = log_uncaught_exceptions + + logging.config.dictConfig({ 'version': 1, 'formatters': { 'default': { @@ -126,29 +107,34 @@ def configureLogging(): 'level': log_level, 'handlers': ['console'], }, - } - - logging_config['loggers']['default'] = logging_config['loggers'][ - 'file_only' if '-c' in sys.argv else 'both'] - logging.config.dictConfig(logging_config) + }) return True, fail_msg +def initLogging(): + preconfigured, msg = configureLogging() + if preconfigured: + if '-c' in sys.argv: + logger = logging.getLogger('file_only') + else: + logger = logging.getLogger('both') + else: + logger = logging.getLogger('default') + + if msg: + logger.log(logging.WARNING if preconfigured else logging.INFO, msg) + return logger + + def resetLogging(): - """Reconfigure logging in runtime when state.appdata dir changed""" - # pylint: disable=global-statement, used-before-assignment global logger - for i in logger.handlers: + for i in logger.handlers.iterkeys(): logger.removeHandler(i) i.flush() i.close() - configureLogging() - logger = logging.getLogger('default') + logger = initLogging() # ! -preconfigured, msg = configureLogging() -logger = logging.getLogger('default') -if msg: - logger.log(logging.WARNING if preconfigured else logging.INFO, msg) +logger = initLogging() diff --git a/src/defaults.py b/src/defaults.py index 32162b56..d10f9000 100644 --- a/src/defaults.py +++ b/src/defaults.py @@ -1,24 +1,24 @@ """ -Common default values +src/defaults.py +=============== """ -#: sanity check, prevent doing ridiculous PoW -#: 20 million PoWs equals approximately 2 days on dev's dual R9 290 +# sanity check, prevent doing ridiculous PoW +# 20 million PoWs equals approximately 2 days on dev's dual R9 290 ridiculousDifficulty = 20000000 -#: Remember here the RPC port read from namecoin.conf so we can restore to -#: it as default whenever the user changes the "method" selection for -#: namecoin integration to "namecoind". +# Remember here the RPC port read from namecoin.conf so we can restore to +# it as default whenever the user changes the "method" selection for +# namecoin integration to "namecoind". namecoinDefaultRpcPort = "8336" # If changed, these values will cause particularly unexpected behavior: # You won't be able to either send or receive messages because the proof # of work you do (or demand) won't match that done or demanded by others. # Don't change them! -#: The amount of work that should be performed (and demanded) per byte -#: of the payload. +# The amount of work that should be performed (and demanded) per byte of the payload. networkDefaultProofOfWorkNonceTrialsPerByte = 1000 -#: To make sending short messages a little more difficult, this value is -#: added to the payload length for use in calculating the proof of work -#: target. +# To make sending short messages a little more difficult, this value is +# added to the payload length for use in calculating the proof of work +# target. networkDefaultPayloadLengthExtraBytes = 1000 diff --git a/src/depends.py b/src/depends.py index 82529269..0114ec94 100755 --- a/src/depends.py +++ b/src/depends.py @@ -17,7 +17,7 @@ if not hasattr(sys, 'hexversion') or sys.hexversion < 0x20300F0: import logging import os from importlib import import_module -import state + # We can now use logging so set up a simple configuration formatter = logging.Formatter('%(levelname)s: %(message)s') handler = logging.StreamHandler(sys.stdout) @@ -113,7 +113,6 @@ PACKAGES = { def detectOS(): - """Finding out what Operating System is running""" if detectOS.result is not None: return detectOS.result if sys.platform.startswith('openbsd'): @@ -133,7 +132,6 @@ detectOS.result = None def detectOSRelease(): - """Detecting the release of OS""" with open("/etc/os-release", 'r') as osRelease: version = None for line in osRelease: @@ -150,7 +148,6 @@ def detectOSRelease(): def try_import(module, log_extra=False): - """Try to import the non imported packages""" try: return import_module(module) except ImportError: @@ -211,8 +208,10 @@ def check_sqlite(): ).fetchone()[0] logger.info('SQLite Library Source ID: %s', sqlite_source_id) if sqlite_version_number >= 3006023: - compile_options = ', '.join( - [row[0] for row in conn.execute('PRAGMA compile_options;')]) + compile_options = ', '.join(map( + lambda row: row[0], + conn.execute('PRAGMA compile_options;') + )) logger.info( 'SQLite Library Compile Options: %s', compile_options) # There is no specific version requirement as yet, so we just @@ -237,8 +236,7 @@ def check_openssl(): Here we are checking for openssl with its all dependent libraries and version checking. """ - # pylint: disable=too-many-branches, too-many-return-statements - # pylint: disable=protected-access, redefined-outer-name + ctypes = try_import('ctypes') if not ctypes: logger.error('Unable to check OpenSSL.') @@ -250,11 +248,8 @@ def check_openssl(): if getattr(sys, 'frozen', False): import os.path paths.insert(0, os.path.join(sys._MEIPASS, 'libeay32.dll')) - elif state.kivy: - return True else: paths = ['libcrypto.so', 'libcrypto.so.1.0.0'] - if sys.platform == 'darwin': paths.extend([ 'libcrypto.dylib', @@ -305,7 +300,7 @@ def check_openssl(): ' ECDH, and ECDSA enabled.') return False matches = cflags_regex.findall(openssl_cflags) - if matches: + if len(matches) > 0: logger.error( 'This OpenSSL library is missing the following required' ' features: %s. PyBitmessage requires OpenSSL 0.9.8b' @@ -316,13 +311,13 @@ def check_openssl(): return False -# ..todo:: The minimum versions of pythondialog and dialog need to be determined +# TODO: The minimum versions of pythondialog and dialog need to be determined def check_curses(): """Do curses dependency check. - Here we are checking for curses if available or not with check as interface - requires the `pythondialog `_ package - and the dialog utility. + Here we are checking for curses if available or not with check + as interface requires the pythondialog\ package and the dialog + utility. """ if sys.hexversion < 0x20600F0: logger.error( @@ -430,6 +425,7 @@ def check_dependencies(verbose=False, optional=False): check_functions = [check_ripemd160, check_sqlite, check_openssl] if optional: check_functions.extend([check_msgpack, check_pyqt, check_curses]) + # Unexpected exceptions are handled here for check in check_functions: try: diff --git a/src/eve.png b/src/eve.png deleted file mode 100644 index e11ff6f5..00000000 Binary files a/src/eve.png and /dev/null differ diff --git a/src/fallback/__init__.py b/src/fallback/__init__.py index 9a8d646f..d45c754d 100644 --- a/src/fallback/__init__.py +++ b/src/fallback/__init__.py @@ -1,19 +1,13 @@ """ -Fallback expressions help PyBitmessage modules to run without some external -dependencies. - - -RIPEMD160Hash -------------- - -We need to check :mod:`hashlib` for RIPEMD-160, as it won't be available -if OpenSSL is not linked against or the linked OpenSSL has RIPEMD disabled. -Try to use `pycryptodome `_ -in that case. +.. todo:: hello world """ import hashlib +# We need to check hashlib for RIPEMD-160, as it won't be available +# if OpenSSL is not linked against or the linked OpenSSL has RIPEMD +# disabled. + try: hashlib.new('ripemd160') except ValueError: diff --git a/src/helper_ackPayload.py b/src/helper_ackPayload.py index d30f4c0d..acdbadf7 100644 --- a/src/helper_ackPayload.py +++ b/src/helper_ackPayload.py @@ -1,28 +1,22 @@ -""" -This module is for generating ack payload -""" +"""This module is for generating ack payload.""" +import highlevelcrypto +import helper_random from binascii import hexlify from struct import pack - -import helper_random -import highlevelcrypto from addresses import encodeVarint +# This function generates payload objects for message acknowledgements +# Several stealth levels are available depending on the privacy needs; +# a higher level means better stealth, but also higher cost (size+POW) +# - level 0: a random 32-byte sequence with a message header appended +# - level 1: a getpubkey request for a (random) dummy key hash +# - level 2: a standard message, encrypted to a random pubkey + def genAckPayload(streamNumber=1, stealthLevel=0): - """ - Generate and return payload obj. - - This function generates payload objects for message acknowledgements - Several stealth levels are available depending on the privacy needs; - a higher level means better stealth, but also higher cost (size+POW) - - - level 0: a random 32-byte sequence with a message header appended - - level 1: a getpubkey request for a (random) dummy key hash - - level 2: a standard message, encrypted to a random pubkey - """ - if stealthLevel == 2: # Generate privacy-enhanced payload + """Generate and return payload obj.""" + if (stealthLevel == 2): # Generate privacy-enhanced payload # Generate a dummy privkey and derive the pubkey dummyPubKeyHex = highlevelcrypto.privToPub( hexlify(helper_random.randomBytes(32))) @@ -35,7 +29,7 @@ def genAckPayload(streamNumber=1, stealthLevel=0): acktype = 2 # message version = 1 - elif stealthLevel == 1: # Basic privacy payload (random getpubkey) + elif (stealthLevel == 1): # Basic privacy payload (random getpubkey) ackdata = helper_random.randomBytes(32) acktype = 0 # getpubkey version = 4 diff --git a/src/helper_bitcoin.py b/src/helper_bitcoin.py index d4f1d105..d56e395b 100644 --- a/src/helper_bitcoin.py +++ b/src/helper_bitcoin.py @@ -1,19 +1,10 @@ -""" -Calculates bitcoin and testnet address from pubkey -""" - import hashlib - -from debug import logger from pyelliptic import arithmetic - +# This function expects that pubkey begin with \x04 def calculateBitcoinAddressFromPubkey(pubkey): - """Calculate bitcoin address from given pubkey (65 bytes long hex string)""" if len(pubkey) != 65: - logger.error('Could not calculate Bitcoin address from pubkey because' - ' function was passed a pubkey that was' - ' %i bytes long rather than 65.', len(pubkey)) + print 'Could not calculate Bitcoin address from pubkey because function was passed a pubkey that was', len(pubkey), 'bytes long rather than 65.' return "error" ripe = hashlib.new('ripemd160') sha = hashlib.new('sha256') @@ -33,11 +24,8 @@ def calculateBitcoinAddressFromPubkey(pubkey): def calculateTestnetAddressFromPubkey(pubkey): - """This function expects that pubkey begin with the testnet prefix""" if len(pubkey) != 65: - logger.error('Could not calculate Bitcoin address from pubkey because' - ' function was passed a pubkey that was' - ' %i bytes long rather than 65.', len(pubkey)) + print 'Could not calculate Bitcoin address from pubkey because function was passed a pubkey that was', len(pubkey), 'bytes long rather than 65.' return "error" ripe = hashlib.new('ripemd160') sha = hashlib.new('sha256') diff --git a/src/helper_bootstrap.py b/src/helper_bootstrap.py new file mode 100644 index 00000000..1710b09c --- /dev/null +++ b/src/helper_bootstrap.py @@ -0,0 +1,84 @@ +import socket + +import knownnodes +import socks +import state +from bmconfigparser import BMConfigParser +from debug import logger + + +def dns(): + """ + DNS bootstrap. This could be programmed to use the SOCKS proxy to do the + DNS lookup some day but for now we will just rely on the entries in + defaultKnownNodes.py. Hopefully either they are up to date or the user + has run Bitmessage recently without SOCKS turned on and received good + bootstrap nodes using that method. + """ + + def try_add_known_node(stream, addr, port, method=''): + try: + socket.inet_aton(addr) + except (TypeError, socket.error): + return + logger.info( + 'Adding %s to knownNodes based on %s DNS bootstrap method', + addr, method) + knownnodes.addKnownNode(stream, state.Peer(addr, port)) + + proxy_type = BMConfigParser().get('bitmessagesettings', 'socksproxytype') + + if proxy_type == 'none': + for port in [8080, 8444]: + try: + for item in socket.getaddrinfo( + 'bootstrap%s.bitmessage.org' % port, 80): + try_add_known_node(1, item[4][0], port) + except: + logger.error( + 'bootstrap%s.bitmessage.org DNS bootstrapping failed.', + port, exc_info=True + ) + elif proxy_type == 'SOCKS5': + knownnodes.createDefaultKnownNodes(onion=True) + logger.debug('Adding default onion knownNodes.') + for port in [8080, 8444]: + logger.debug("Resolving %i through SOCKS...", port) + address_family = socket.AF_INET + sock = socks.socksocket(address_family, socket.SOCK_STREAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.settimeout(20) + proxytype = socks.PROXY_TYPE_SOCKS5 + sockshostname = BMConfigParser().get( + 'bitmessagesettings', 'sockshostname') + socksport = BMConfigParser().getint( + 'bitmessagesettings', 'socksport') + # Do domain name lookups through the proxy; + # though this setting doesn't really matter since we won't + # be doing any domain name lookups anyway. + rdns = True + if BMConfigParser().getboolean( + 'bitmessagesettings', 'socksauthentication'): + socksusername = BMConfigParser().get( + 'bitmessagesettings', 'socksusername') + sockspassword = BMConfigParser().get( + 'bitmessagesettings', 'sockspassword') + sock.setproxy( + proxytype, sockshostname, socksport, rdns, + socksusername, sockspassword) + else: + sock.setproxy( + proxytype, sockshostname, socksport, rdns) + try: + ip = sock.resolve("bootstrap" + str(port) + ".bitmessage.org") + sock.shutdown(socket.SHUT_RDWR) + sock.close() + except: + logger.error("SOCKS DNS resolving failed", exc_info=True) + else: + try_add_known_node(1, ip, port, 'SOCKS') + else: + logger.info( + 'DNS bootstrap skipped because the proxy type does not support' + ' DNS resolution.' + ) diff --git a/src/helper_generic.py b/src/helper_generic.py deleted file mode 100644 index 368a6c54..00000000 --- a/src/helper_generic.py +++ /dev/null @@ -1,114 +0,0 @@ -""" -Helper Generic perform generic operations for threading. - -Also perform some conversion operations. -""" - -import socket -import sys -import threading -import traceback -try: - import multiprocessing -except Exception as e: - pass -from binascii import hexlify, unhexlify - -import shared -import state -import queues -import shutdown -from debug import logger - - -def powQueueSize(): - curWorkerQueue = queues.workerQueue.qsize() - for thread in threading.enumerate(): - try: - if thread.name == "singleWorker": - curWorkerQueue += thread.busy - except Exception as err: - logger.info('Thread error %s', err) - return curWorkerQueue - - -def convertIntToString(n): - a = __builtins__.hex(n) - if a[-1:] == 'L': - a = a[:-1] - if (len(a) % 2) == 0: - return unhexlify(a[2:]) - else: - return unhexlify('0' + a[2:]) - - -def convertStringToInt(s): - return int(hexlify(s), 16) - - -def allThreadTraceback(frame): - id2name = dict([(th.ident, th.name) for th in threading.enumerate()]) - code = [] - for threadId, stack in sys._current_frames().items(): - code.append( - '\n# Thread: %s(%d)' % (id2name.get(threadId, ''), threadId)) - for filename, lineno, name, line in traceback.extract_stack(stack): - code.append( - 'File: "%s", line %d, in %s' % (filename, lineno, name)) - if line: - code.append(' %s' % (line.strip())) - print('\n'.join(code)) - - -def signal_handler(signal, frame): - try: - process = multiprocessing.current_process() - except Exception as e: - process = threading.current_thread() - logger.error( - 'Got signal %i in %s/%s', - signal, process.name, threading.current_thread().name - ) - if process.name == "RegExParser": - # on Windows this isn't triggered, but it's fine, - # it has its own process termination thing - raise SystemExit - if "PoolWorker" in process.name: - raise SystemExit - if threading.current_thread().name not in ("PyBitmessage", "MainThread"): - return - logger.error("Got signal %i", signal) - if shared.thisapp.daemon or not state.enableGUI: # FIXME redundant? - shutdown.doCleanShutdown() - else: - allThreadTraceback(frame) - print('Unfortunately you cannot use Ctrl+C when running the UI' - ' because the UI captures the signal.') - - -def isHostInPrivateIPRange(host): - if ":" in host: # IPv6 - hostAddr = socket.inet_pton(socket.AF_INET6, host) - if hostAddr == ('\x00' * 15) + '\x01': - return False - if hostAddr[0] == '\xFE' and (ord(hostAddr[1]) & 0xc0) == 0x80: - return False - if (ord(hostAddr[0]) & 0xfe) == 0xfc: - return False - elif ".onion" not in host: - if host[:3] == '10.': - return True - if host[:4] == '172.': - if host[6] == '.': - if int(host[4:6]) >= 16 and int(host[4:6]) <= 31: - return True - if host[:8] == '192.168.': - return True - # Multicast - if host[:3] >= 224 and host[:3] <= 239 and host[4] == '.': - return True - return False - - -def addDataPadding(data, desiredMsgLength=12, paddingChar='\x00'): - return data + paddingChar * (desiredMsgLength - len(data)) diff --git a/src/helper_inbox.py b/src/helper_inbox.py index 654dd59d..95214743 100644 --- a/src/helper_inbox.py +++ b/src/helper_inbox.py @@ -1,11 +1,10 @@ -"""Helper Inbox performs inbox messages related operations""" +"""Helper Inbox performs inbox messagese related operations.""" -import queues from helper_sql import sqlExecute, sqlQuery +import queues def insert(t): - """Perform an insert into the "inbox" table""" sqlExecute('''INSERT INTO inbox VALUES (?,?,?,?,?,?,?,?,?,?)''', *t) # shouldn't emit changedInboxUnread and displayNewInboxMessage # at the same time @@ -13,13 +12,11 @@ def insert(t): def trash(msgid): - """Mark a message in the `inbox` as `trash`""" sqlExecute('''UPDATE inbox SET folder='trash' WHERE msgid=?''', msgid) queues.UISignalQueue.put(('removeInboxRowByMsgid', msgid)) def isMessageAlreadyInInbox(sigHash): - """Check for previous instances of this message""" queryReturn = sqlQuery( '''SELECT COUNT(*) FROM inbox WHERE sighash=?''', sigHash) return queryReturn[0][0] != 0 diff --git a/src/helper_msgcoding.py b/src/helper_msgcoding.py index 76dad423..cc632ffa 100644 --- a/src/helper_msgcoding.py +++ b/src/helper_msgcoding.py @@ -5,11 +5,6 @@ Message encoding end decoding functions import string import zlib -import messagetypes -from bmconfigparser import BMConfigParser -from debug import logger -from tr import _translate - try: import msgpack except ImportError: @@ -18,6 +13,11 @@ except ImportError: except ImportError: import fallback.umsgpack.umsgpack as msgpack +import messagetypes +from bmconfigparser import BMConfigParser +from debug import logger +from tr import _translate + BITMESSAGE_ENCODING_IGNORE = 0 BITMESSAGE_ENCODING_TRIVIAL = 1 BITMESSAGE_ENCODING_SIMPLE = 2 @@ -25,24 +25,19 @@ BITMESSAGE_ENCODING_EXTENDED = 3 class MsgEncodeException(Exception): - """Exception during message encoding""" pass class MsgDecodeException(Exception): - """Exception during message decoding""" pass class DecompressionSizeException(MsgDecodeException): - # pylint: disable=super-init-not-called - """Decompression resulted in too much data (attack protection)""" def __init__(self, size): self.size = size class MsgEncode(object): - """Message encoder class""" def __init__(self, message, encoding=BITMESSAGE_ENCODING_SIMPLE): self.data = None self.encoding = encoding @@ -57,7 +52,6 @@ class MsgEncode(object): raise MsgEncodeException("Unknown encoding %i" % (encoding)) def encodeExtended(self, message): - """Handle extended encoding""" try: msgObj = messagetypes.message.Message() self.data = zlib.compress(msgpack.dumps(msgObj.encode(message)), 9) @@ -70,18 +64,15 @@ class MsgEncode(object): self.length = len(self.data) def encodeSimple(self, message): - """Handle simple encoding""" self.data = 'Subject:%(subject)s\nBody:%(body)s' % message self.length = len(self.data) def encodeTrivial(self, message): - """Handle trivial encoding""" self.data = message['body'] self.length = len(self.data) class MsgDecode(object): - """Message decoder class""" def __init__(self, encoding, data): self.encoding = encoding if self.encoding == BITMESSAGE_ENCODING_EXTENDED: @@ -97,7 +88,6 @@ class MsgDecode(object): self.subject = _translate("MsgDecode", "Unknown encoding") def decodeExtended(self, data): - """Handle extended encoding""" dc = zlib.decompressobj() tmp = "" while len(tmp) <= BMConfigParser().safeGetInt("zlib", "maxsize"): @@ -141,7 +131,6 @@ class MsgDecode(object): self.body = msgObj.body def decodeSimple(self, data): - """Handle simple encoding""" bodyPositionIndex = string.find(data, '\nBody:') if bodyPositionIndex > 1: subject = data[8:bodyPositionIndex] diff --git a/src/helper_random.py b/src/helper_random.py index 9a29d5e2..bb173d1b 100644 --- a/src/helper_random.py +++ b/src/helper_random.py @@ -2,17 +2,10 @@ import os import random - from pyelliptic.openssl import OpenSSL - NoneType = type(None) -def seed(): - """Initialize random number generator""" - random.seed() - - def randomBytes(n): """Method randomBytes.""" try: @@ -58,7 +51,8 @@ def randomrandrange(x, y=None): """ if isinstance(y, NoneType): return random.randrange(x) # nosec - return random.randrange(x, y) # nosec + else: + return random.randrange(x, y) # nosec def randomchoice(population): diff --git a/src/helper_search.py b/src/helper_search.py index 69acec43..d6704731 100644 --- a/src/helper_search.py +++ b/src/helper_search.py @@ -1,6 +1,6 @@ -"""Additional SQL helper for searching messages""" +#!/usr/bin/python2.7 -from helper_sql import sqlQuery +from helper_sql import * try: from PyQt4 import QtGui @@ -8,17 +8,13 @@ try: except ImportError: haveQt = False - -def search_translate(context, text): - """Translation wrapper""" +def search_translate (context, text): if haveQt: return QtGui.QApplication.translate(context, text) - return text.lower() + else: + return text.lower() - -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 +def search_sql(xAddress = "toaddress", account = None, folder = "inbox", where = None, what = None, unreadOnly = False): if what is not None and what != "": what = "%" + what + "%" if where == search_translate("MainWindow", "To"): @@ -36,7 +32,7 @@ def search_sql(xAddress="toaddress", account=None, folder="inbox", where=None, w if folder == "sent": sqlStatementBase = ''' - SELECT toaddress, fromaddress, subject, status, ackdata, lastactiontime + SELECT toaddress, fromaddress, subject, status, ackdata, lastactiontime FROM sent ''' else: sqlStatementBase = '''SELECT folder, msgid, toaddress, fromaddress, subject, received, read @@ -66,16 +62,13 @@ def search_sql(xAddress="toaddress", account=None, folder="inbox", where=None, w sqlArguments.append(what) if unreadOnly: sqlStatementParts.append("read = 0") - if sqlStatementParts: + if len(sqlStatementParts) > 0: 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)""" - # pylint: disable=too-many-arguments +def check_match(toAddress, fromAddress, subject, message, where = None, what = None): if what is not None and what != "": if where in (search_translate("MainWindow", "To"), search_translate("MainWindow", "All")): if what.lower() not in toAddress.lower(): diff --git a/src/helper_sent.py b/src/helper_sent.py index 60c4772d..8dde7215 100644 --- a/src/helper_sent.py +++ b/src/helper_sent.py @@ -1,15 +1,4 @@ -""" -Insert values into sent table -""" - -import uuid -from helper_sql import sqlExecute - +from helper_sql import * def insert(t): - """Perform an insert into the `sent` table""" - msgid = uuid.uuid4().bytes - temp = list(t) - temp[0] = msgid - t = tuple(temp) sqlExecute('''INSERT INTO sent VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''', *t) diff --git a/src/helper_sql.py b/src/helper_sql.py index 9b5dc29d..2b558f62 100644 --- a/src/helper_sql.py +++ b/src/helper_sql.py @@ -1,39 +1,17 @@ -""" -SQL-related functions defined here are really pass the queries (or other SQL -commands) to :class:`.threads.sqlThread` through `sqlSubmitQueue` queue and check -or return the result got from `sqlReturnQueue`. +"""Helper Sql performs sql operations.""" -This is done that way because :mod:`sqlite3` is so thread-unsafe that they -won't even let you call it from different threads using your own locks. -SQLite objects can only be used from one thread. - -.. note:: This actually only applies for certain deployments, and/or - really old version of sqlite. I haven't actually seen it anywhere. - Current versions do have support for threading and multiprocessing. - I don't see an urgent reason to refactor this, but it should be noted - in the comment that the problem is mostly not valid. Sadly, last time - I checked, there is no reliable way to check whether the library is - or isn't thread-safe. -""" - -import Queue import threading +import Queue sqlSubmitQueue = Queue.Queue() -"""the queue for SQL""" +# SQLITE3 is so thread-unsafe that they won't even let you call it from different threads using your own locks. +# SQL objects #can only be called from one thread. sqlReturnQueue = Queue.Queue() -"""the queue for results""" sqlLock = threading.Lock() def sqlQuery(sqlStatement, *args): - """ - Query sqlite and return results - - :param str sqlStatement: SQL statement string - :param list args: SQL query parameters - :rtype: list - """ + """SQLLITE execute statement and return query.""" sqlLock.acquire() sqlSubmitQueue.put(sqlStatement) @@ -50,7 +28,6 @@ def sqlQuery(sqlStatement, *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 sqlExecuteChunked.chunkSize = 999 @@ -81,7 +58,6 @@ def sqlExecuteChunked(sqlStatement, idCount, *args): def sqlExecute(sqlStatement, *args): - """Execute SQL statement (optionally with arguments)""" sqlLock.acquire() sqlSubmitQueue.put(sqlStatement) @@ -94,15 +70,13 @@ def sqlExecute(sqlStatement, *args): sqlLock.release() return rowcount - def sqlStoredProcedure(procName): - """Schedule procName to be run""" sqlLock.acquire() sqlSubmitQueue.put(procName) sqlLock.release() -class SqlBulkExecute(object): +class SqlBulkExecute: """This is used when you have to execute the same statement in a cycle.""" def __enter__(self): diff --git a/src/helper_startup.py b/src/helper_startup.py index 8374261d..1a1119f5 100644 --- a/src/helper_startup.py +++ b/src/helper_startup.py @@ -1,13 +1,16 @@ """ -Startup operations. +src/helper_startup.py +===================== + +Helper Start performs all the startup operations. """ # pylint: disable=too-many-branches,too-many-statements +from __future__ import print_function -import logging +import ConfigParser import os import platform import sys -import time from distutils.version import StrictVersion import defaults @@ -16,37 +19,45 @@ import paths import state from bmconfigparser import BMConfigParser -try: - from plugins.plugin import get_plugin -except ImportError: - get_plugin = None - - -logger = logging.getLogger('default') - # The user may de-select Portable Mode in the settings if they want # the config files to stay in the application data folder. StoreConfigFilesInSameDirectoryAsProgramByDefault = False +def _loadTrustedPeer(): + try: + trustedPeer = BMConfigParser().get('bitmessagesettings', 'trustedpeer') + except ConfigParser.Error: + # This probably means the trusted peer wasn't specified so we + # can just leave it as None + return + try: + host, port = trustedPeer.split(':') + except ValueError: + sys.exit( + 'Bad trustedpeer config setting! It should be set as' + ' trustedpeer=:' + ) + state.trustedPeer = state.Peer(host, int(port)) + + 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. needToCreateKeysFile = config.safeGet( 'bitmessagesettings', 'settingsversion') is None if not needToCreateKeysFile: - logger.info( + print( 'Loading config files from directory specified' - ' on startup: %s', state.appdata) + ' on startup: %s' % state.appdata) else: config.read(paths.lookupExeFolder() + 'keys.dat') try: config.get('bitmessagesettings', 'settingsversion') - logger.info('Loading config files from same directory as program.') + print('Loading config files from same directory as program.') needToCreateKeysFile = False state.appdata = paths.lookupExeFolder() except: @@ -57,8 +68,7 @@ def loadConfig(): needToCreateKeysFile = config.safeGet( 'bitmessagesettings', 'settingsversion') is None if not needToCreateKeysFile: - logger.info( - 'Loading existing config files from %s', state.appdata) + print('Loading existing config files from', state.appdata) if needToCreateKeysFile: @@ -113,10 +123,9 @@ def loadConfig(): # Just use the same directory as the program and forget about # the appdata folder state.appdata = '' - logger.info( - 'Creating new config files in same directory as program.') + print('Creating new config files in same directory as program.') else: - logger.info('Creating new config files in %s', state.appdata) + print('Creating new config files in', state.appdata) if not os.path.exists(state.appdata): os.makedirs(state.appdata) if not sys.platform.startswith('win'): @@ -125,6 +134,8 @@ def loadConfig(): else: updateConfig() + _loadTrustedPeer() + def updateConfig(): """Save the config""" @@ -266,7 +277,7 @@ def updateConfig(): 'bitmessagesettings', 'hidetrayconnectionnotifications', 'false') if config.safeGetInt('bitmessagesettings', 'maxoutboundconnections') < 1: config.set('bitmessagesettings', 'maxoutboundconnections', '8') - logger.warning('Your maximum outbound connections must be a number.') + print('WARNING: your maximum outbound connections must be a number.') # TTL is now user-specifiable. Let's add an option to save # whatever the user selects. @@ -289,26 +300,3 @@ def isOurOperatingSystemLimitedToHavingVeryFewHalfOpenConnections(): return False except Exception: pass - - -def start_proxyconfig(): - """Check socksproxytype and start any proxy configuration plugin""" - if not get_plugin: - return - config = BMConfigParser() - proxy_type = config.safeGet('bitmessagesettings', 'socksproxytype') - if proxy_type and proxy_type not in ('none', 'SOCKS4a', 'SOCKS5'): - try: - proxyconfig_start = time.time() - if not get_plugin('proxyconfig', name=proxy_type)(config): - raise TypeError() - except TypeError: - # cannot import shutdown here ): - logger.error( - 'Failed to run proxy config plugin %s', - proxy_type, exc_info=True) - os._exit(0) # pylint: disable=protected-access - else: - logger.info( - 'Started proxy config plugin %s in %s sec', - proxy_type, time.time() - proxyconfig_start) diff --git a/src/helper_threading.py b/src/helper_threading.py new file mode 100644 index 00000000..6b6a5e25 --- /dev/null +++ b/src/helper_threading.py @@ -0,0 +1,46 @@ +"""Helper threading perform all the threading operations.""" + +from contextlib import contextmanager +import threading + +try: + import prctl +except ImportError: + def set_thread_name(name): + """Set the thread name for external use (visible from the OS).""" + threading.current_thread().name = name +else: + def set_thread_name(name): + """Set a name for the thread for python internal use.""" + prctl.set_name(name) + + def _thread_name_hack(self): + set_thread_name(self.name) + threading.Thread.__bootstrap_original__(self) + + threading.Thread.__bootstrap_original__ = threading.Thread._Thread__bootstrap + threading.Thread._Thread__bootstrap = _thread_name_hack + + +class StoppableThread(object): + def initStop(self): + self.stop = threading.Event() + self._stopped = False + + def stopThread(self): + self._stopped = True + self.stop.set() + + +class BusyError(threading.ThreadError): + pass + +@contextmanager +def nonBlocking(lock): + locked = lock.acquire(False) + if not locked: + raise BusyError + try: + yield + finally: + lock.release() diff --git a/src/highlevelcrypto.py b/src/highlevelcrypto.py index f392fe4a..02fb85ab 100644 --- a/src/highlevelcrypto.py +++ b/src/highlevelcrypto.py @@ -1,10 +1,6 @@ """ -High level cryptographic functions based on `.pyelliptic` OpenSSL bindings. - -.. note:: - Upstream pyelliptic was upgraded from SHA1 to SHA256 for signing. - We must upgrade PyBitmessage gracefully. - `More discussion. `_ +src/highlevelcrypto.py +====================== """ from binascii import hexlify @@ -16,13 +12,12 @@ from pyelliptic import arithmetic as a def makeCryptor(privkey): - """Return a private `.pyelliptic.ECC` instance""" + """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) + cryptor = pyelliptic.ECC(curve='secp256k1', privkey=privkey_bin, pubkey=pubkey_bin) return cryptor @@ -34,7 +29,7 @@ def hexToPubkey(pubkey): def makePubCryptor(pubkey): - """Return a public `.pyelliptic.ECC` instance""" + """Return a public pyelliptic.ECC() instance""" pubkey_bin = hexToPubkey(pubkey) return pyelliptic.ECC(curve='secp256k1', pubkey=pubkey_bin) @@ -48,8 +43,7 @@ def privToPub(privkey): def encrypt(msg, hexPubkey): """Encrypts message with hex public key""" - return pyelliptic.ECC(curve='secp256k1').encrypt( - msg, hexToPubkey(hexPubkey)) + return pyelliptic.ECC(curve='secp256k1').encrypt(msg, hexToPubkey(hexPubkey)) def decrypt(msg, hexPrivkey): @@ -58,38 +52,36 @@ def decrypt(msg, hexPrivkey): def decryptFast(msg, cryptor): - """Decrypts message with an existing `.pyelliptic.ECC` object""" + """Decrypts message with an existing pyelliptic.ECC.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') + """Signs with hex private key""" + # pyelliptic is upgrading from SHA1 to SHA256 for signing. We must + # upgrade PyBitmessage gracefully. + # https://github.com/yann2192/pyelliptic/pull/33 + # More discussion: https://github.com/yann2192/pyelliptic/issues/32 + digestAlg = BMConfigParser().safeGet('bitmessagesettings', 'digestalg', 'sha1') if digestAlg == "sha1": # SHA1, this will eventually be deprecated - return makeCryptor(hexPrivkey).sign( - msg, digest_alg=OpenSSL.digest_ecdsa_sha1) + 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) + raise ValueError("Unknown digest algorithm %s" % (digestAlg)) def verify(msg, sig, hexPubkey): - """Verifies with hex public key using SHA1 or SHA256""" + """Verifies with hex public key""" # 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) + sigVerifyPassed = makePubCryptor(hexPubkey).verify(sig, msg, digest_alg=OpenSSL.digest_ecdsa_sha1) except: sigVerifyPassed = False if sigVerifyPassed: @@ -97,8 +89,7 @@ def verify(msg, sig, hexPubkey): 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) + return makePubCryptor(hexPubkey).verify(sig, msg, digest_alg=OpenSSL.EVP_sha256) except: return False @@ -109,14 +100,13 @@ def pointMult(secret): Evidently, this type of error can occur very rarely: - >>> File "highlevelcrypto.py", line 54, in pointMult - >>> group = OpenSSL.EC_KEY_get0_group(k) - >>> WindowsError: exception: access violation reading 0x0000000000000008 + File "highlevelcrypto.py", line 54, in pointMult + group = OpenSSL.EC_KEY_get0_group(k) + WindowsError: exception: access violation reading 0x0000000000000008 """ while True: try: - k = OpenSSL.EC_KEY_new_by_curve_name( - OpenSSL.get_curve('secp256k1')) + k = OpenSSL.EC_KEY_new_by_curve_name(OpenSSL.get_curve('secp256k1')) priv_key = OpenSSL.BN_bin2bn(secret, 32, None) group = OpenSSL.EC_KEY_get0_group(k) pub_key = OpenSSL.EC_POINT_new(group) diff --git a/src/image.svg b/src/image.svg deleted file mode 100644 index bef2b802..00000000 Binary files a/src/image.svg and /dev/null differ diff --git a/src/images/3.zip b/src/images/3.zip deleted file mode 100644 index 34d555fe..00000000 Binary files a/src/images/3.zip and /dev/null differ diff --git a/src/images/account_multiple.png b/src/images/account_multiple.png deleted file mode 100644 index 11271619..00000000 Binary files a/src/images/account_multiple.png and /dev/null differ diff --git a/src/images/addressbookadd.png b/src/images/addressbookadd.png deleted file mode 100644 index 82af2f63..00000000 Binary files a/src/images/addressbookadd.png and /dev/null differ diff --git a/src/images/avatar.png b/src/images/avatar.png deleted file mode 100644 index b006bfa2..00000000 Binary files a/src/images/avatar.png and /dev/null differ diff --git a/src/images/back-button.png b/src/images/back-button.png deleted file mode 100644 index e260b08b..00000000 Binary files a/src/images/back-button.png and /dev/null differ diff --git a/src/images/black_cross.png b/src/images/black_cross.png deleted file mode 100644 index a71b611b..00000000 Binary files a/src/images/black_cross.png and /dev/null differ diff --git a/src/images/copy_text.png b/src/images/copy_text.png deleted file mode 100644 index dcc63611..00000000 Binary files a/src/images/copy_text.png and /dev/null differ diff --git a/src/images/down-arrow.png b/src/images/down-arrow.png deleted file mode 100644 index bf3e864c..00000000 Binary files a/src/images/down-arrow.png and /dev/null differ diff --git a/src/images/drawer_logo1.png b/src/images/drawer_logo1.png deleted file mode 100644 index 4152cc40..00000000 Binary files a/src/images/drawer_logo1.png and /dev/null differ diff --git a/src/images/left_arrow.png b/src/images/left_arrow.png deleted file mode 100644 index 0f01c425..00000000 Binary files a/src/images/left_arrow.png and /dev/null differ diff --git a/src/images/loader.gif b/src/images/loader.gif deleted file mode 100644 index 29064d57..00000000 Binary files a/src/images/loader.gif and /dev/null differ diff --git a/src/images/loader.zip b/src/images/loader.zip deleted file mode 100644 index c80a3a17..00000000 Binary files a/src/images/loader.zip and /dev/null differ diff --git a/src/images/red.png b/src/images/red.png deleted file mode 100644 index 2570e26a..00000000 Binary files a/src/images/red.png and /dev/null differ diff --git a/src/images/right-arrow.png b/src/images/right-arrow.png deleted file mode 100644 index 8f136a77..00000000 Binary files a/src/images/right-arrow.png and /dev/null differ diff --git a/src/images/search.png b/src/images/search.png deleted file mode 100644 index 42a1e45a..00000000 Binary files a/src/images/search.png and /dev/null differ diff --git a/src/images/search_mail.png b/src/images/search_mail.png deleted file mode 100644 index 7b38e9b4..00000000 Binary files a/src/images/search_mail.png and /dev/null differ diff --git a/src/images/text_images/!.png b/src/images/text_images/!.png deleted file mode 100644 index bac2f246..00000000 Binary files a/src/images/text_images/!.png and /dev/null differ diff --git a/src/images/text_images/0.png b/src/images/text_images/0.png deleted file mode 100644 index 2b8b63e3..00000000 Binary files a/src/images/text_images/0.png and /dev/null differ diff --git a/src/images/text_images/1.png b/src/images/text_images/1.png deleted file mode 100644 index 3918f6d3..00000000 Binary files a/src/images/text_images/1.png and /dev/null differ diff --git a/src/images/text_images/2.png b/src/images/text_images/2.png deleted file mode 100644 index 0cf202e9..00000000 Binary files a/src/images/text_images/2.png and /dev/null differ diff --git a/src/images/text_images/3.png b/src/images/text_images/3.png deleted file mode 100644 index f9d612dd..00000000 Binary files a/src/images/text_images/3.png and /dev/null differ diff --git a/src/images/text_images/4.png b/src/images/text_images/4.png deleted file mode 100644 index f2ab33e1..00000000 Binary files a/src/images/text_images/4.png and /dev/null differ diff --git a/src/images/text_images/5.png b/src/images/text_images/5.png deleted file mode 100644 index 09d6e56e..00000000 Binary files a/src/images/text_images/5.png and /dev/null differ diff --git a/src/images/text_images/6.png b/src/images/text_images/6.png deleted file mode 100644 index e385a954..00000000 Binary files a/src/images/text_images/6.png and /dev/null differ diff --git a/src/images/text_images/7.png b/src/images/text_images/7.png deleted file mode 100644 index 55fc4f77..00000000 Binary files a/src/images/text_images/7.png and /dev/null differ diff --git a/src/images/text_images/8.png b/src/images/text_images/8.png deleted file mode 100644 index 2a3fa76f..00000000 Binary files a/src/images/text_images/8.png and /dev/null differ diff --git a/src/images/text_images/9.png b/src/images/text_images/9.png deleted file mode 100644 index 81ad9084..00000000 Binary files a/src/images/text_images/9.png and /dev/null differ diff --git a/src/images/text_images/A.png b/src/images/text_images/A.png deleted file mode 100644 index 64ed6110..00000000 Binary files a/src/images/text_images/A.png and /dev/null differ diff --git a/src/images/text_images/B.png b/src/images/text_images/B.png deleted file mode 100644 index 2db56c1f..00000000 Binary files a/src/images/text_images/B.png and /dev/null differ diff --git a/src/images/text_images/C.png b/src/images/text_images/C.png deleted file mode 100644 index 47a4052c..00000000 Binary files a/src/images/text_images/C.png and /dev/null differ diff --git a/src/images/text_images/D.png b/src/images/text_images/D.png deleted file mode 100644 index 2549ffc2..00000000 Binary files a/src/images/text_images/D.png and /dev/null differ diff --git a/src/images/text_images/E.png b/src/images/text_images/E.png deleted file mode 100644 index 5d631611..00000000 Binary files a/src/images/text_images/E.png and /dev/null differ diff --git a/src/images/text_images/F.png b/src/images/text_images/F.png deleted file mode 100644 index 43086f38..00000000 Binary files a/src/images/text_images/F.png and /dev/null differ diff --git a/src/images/text_images/G.png b/src/images/text_images/G.png deleted file mode 100644 index 32d1709d..00000000 Binary files a/src/images/text_images/G.png and /dev/null differ diff --git a/src/images/text_images/H.png b/src/images/text_images/H.png deleted file mode 100644 index 279bd1ce..00000000 Binary files a/src/images/text_images/H.png and /dev/null differ diff --git a/src/images/text_images/I.png b/src/images/text_images/I.png deleted file mode 100644 index c88f048d..00000000 Binary files a/src/images/text_images/I.png and /dev/null differ diff --git a/src/images/text_images/J.png b/src/images/text_images/J.png deleted file mode 100644 index 15331171..00000000 Binary files a/src/images/text_images/J.png and /dev/null differ diff --git a/src/images/text_images/K.png b/src/images/text_images/K.png deleted file mode 100644 index 9afcadd7..00000000 Binary files a/src/images/text_images/K.png and /dev/null differ diff --git a/src/images/text_images/L.png b/src/images/text_images/L.png deleted file mode 100644 index e841b9d9..00000000 Binary files a/src/images/text_images/L.png and /dev/null differ diff --git a/src/images/text_images/M.png b/src/images/text_images/M.png deleted file mode 100644 index 10de35e9..00000000 Binary files a/src/images/text_images/M.png and /dev/null differ diff --git a/src/images/text_images/N.png b/src/images/text_images/N.png deleted file mode 100644 index 2d235d06..00000000 Binary files a/src/images/text_images/N.png and /dev/null differ diff --git a/src/images/text_images/O.png b/src/images/text_images/O.png deleted file mode 100644 index c0cc972a..00000000 Binary files a/src/images/text_images/O.png and /dev/null differ diff --git a/src/images/text_images/P.png b/src/images/text_images/P.png deleted file mode 100644 index 57ec5012..00000000 Binary files a/src/images/text_images/P.png and /dev/null differ diff --git a/src/images/text_images/Q.png b/src/images/text_images/Q.png deleted file mode 100644 index 27ffd18b..00000000 Binary files a/src/images/text_images/Q.png and /dev/null differ diff --git a/src/images/text_images/R.png b/src/images/text_images/R.png deleted file mode 100644 index 090646f5..00000000 Binary files a/src/images/text_images/R.png and /dev/null differ diff --git a/src/images/text_images/S.png b/src/images/text_images/S.png deleted file mode 100644 index 444419cf..00000000 Binary files a/src/images/text_images/S.png and /dev/null differ diff --git a/src/images/text_images/T.png b/src/images/text_images/T.png deleted file mode 100644 index ace7b36b..00000000 Binary files a/src/images/text_images/T.png and /dev/null differ diff --git a/src/images/text_images/U.png b/src/images/text_images/U.png deleted file mode 100644 index a47f326e..00000000 Binary files a/src/images/text_images/U.png and /dev/null differ diff --git a/src/images/text_images/V.png b/src/images/text_images/V.png deleted file mode 100644 index da07d0ac..00000000 Binary files a/src/images/text_images/V.png and /dev/null differ diff --git a/src/images/text_images/W.png b/src/images/text_images/W.png deleted file mode 100644 index a00f9d7c..00000000 Binary files a/src/images/text_images/W.png and /dev/null differ diff --git a/src/images/text_images/X.png b/src/images/text_images/X.png deleted file mode 100644 index be919fc4..00000000 Binary files a/src/images/text_images/X.png and /dev/null differ diff --git a/src/images/text_images/Y.png b/src/images/text_images/Y.png deleted file mode 100644 index 4819bbd1..00000000 Binary files a/src/images/text_images/Y.png and /dev/null differ diff --git a/src/images/text_images/Z.png b/src/images/text_images/Z.png deleted file mode 100644 index 7d1c8e01..00000000 Binary files a/src/images/text_images/Z.png and /dev/null differ diff --git a/src/images/transparent.png b/src/images/transparent.png deleted file mode 100644 index 26529139..00000000 Binary files a/src/images/transparent.png and /dev/null differ diff --git a/src/images/white.png b/src/images/white.png deleted file mode 100644 index 0542f499..00000000 Binary files a/src/images/white.png and /dev/null differ diff --git a/src/inventory.py b/src/inventory.py index fc06e455..4b9ad226 100644 --- a/src/inventory.py +++ b/src/inventory.py @@ -1,8 +1,8 @@ """The Inventory singleton""" # TODO make this dynamic, and watch out for frozen, like with messagetypes -import storage.filesystem import storage.sqlite +import storage.filesystem from bmconfigparser import BMConfigParser from singleton import Singleton 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/knownnodes.py b/src/knownnodes.py index bb588fcb..5d1c003d 100644 --- a/src/knownnodes.py +++ b/src/knownnodes.py @@ -3,7 +3,6 @@ Manipulations with knownNodes dictionary. """ import json -import logging import os import pickle import threading @@ -11,33 +10,33 @@ import time import state from bmconfigparser import BMConfigParser -from network.node import Peer +from debug import logger +from helper_bootstrap import dns knownNodesLock = threading.Lock() -"""Thread lock for knownnodes modification""" knownNodes = {stream: {} for stream in range(1, 4)} -"""The dict of known nodes for each stream""" knownNodesTrimAmount = 2000 -"""trim stream knownnodes dict to this length""" +# forget a node after rating is this low knownNodesForgetRating = -0.5 -"""forget a node after rating is this low""" knownNodesActual = False -logger = logging.getLogger('default') - DEFAULT_NODES = ( - Peer('5.45.99.75', 8444), - Peer('75.167.159.54', 8444), - Peer('95.165.168.168', 8444), - Peer('85.180.139.241', 8444), - Peer('158.222.217.190', 8080), - Peer('178.62.12.187', 8448), - Peer('24.188.198.204', 8111), - Peer('109.147.204.113', 1195), - Peer('178.11.46.221', 8444) + state.Peer('5.45.99.75', 8444), + state.Peer('75.167.159.54', 8444), + state.Peer('95.165.168.168', 8444), + state.Peer('85.180.139.241', 8444), + state.Peer('158.222.217.190', 8080), + state.Peer('178.62.12.187', 8448), + state.Peer('24.188.198.204', 8111), + state.Peer('109.147.204.113', 1195), + state.Peer('178.11.46.221', 8444) +) + +DEFAULT_NODES_ONION = ( + state.Peer('quzwelsuziwqgpt2.onion', 8444), ) @@ -63,17 +62,20 @@ def json_deserialize_knownnodes(source): for node in json.load(source): peer = node['peer'] info = node['info'] - peer = Peer(str(peer['host']), peer.get('port', 8444)) + peer = state.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: + if ( + not (knownNodesActual or info.get('self')) and + peer not in DEFAULT_NODES and + peer not in DEFAULT_NODES_ONION + ): knownNodesActual = True def pickle_deserialize_old_knownnodes(source): """ - Unpickle source and reorganize knownnodes dict if it has old format + Unpickle source and reorganize knownnodes dict if it's in old format the old format was {Peer:lastseen, ...} the new format is {Peer:{"lastseen":i, "rating":f}} """ @@ -86,7 +88,6 @@ def pickle_deserialize_old_knownnodes(source): def saveKnownNodes(dirName=None): - """Save knownnodes to filesystem""" if dirName is None: dirName = state.appdata with knownNodesLock: @@ -95,24 +96,21 @@ def saveKnownNodes(dirName=None): def addKnownNode(stream, peer, lastseen=None, is_self=False): - """Add a new node to the dict""" knownNodes[stream][peer] = { "lastseen": lastseen or time.time(), - "rating": 1 if is_self else 0, + "rating": 0, "self": is_self, } -def createDefaultKnownNodes(): - """Creating default Knownnodes""" +def createDefaultKnownNodes(onion=False): past = time.time() - 2418600 # 28 days - 10 min - for peer in DEFAULT_NODES: + for peer in DEFAULT_NODES_ONION if onion else DEFAULT_NODES: addKnownNode(1, peer, past) saveKnownNodes() def readKnownNodes(): - """Load knownnodes from filesystem""" try: with open(state.appdata + 'knownnodes.dat', 'rb') as source: with knownNodesLock: @@ -133,13 +131,12 @@ def readKnownNodes(): if onionhostname and ".onion" in onionhostname: onionport = config.safeGetInt('bitmessagesettings', 'onionport') if onionport: - self_peer = Peer(onionhostname, onionport) + self_peer = state.Peer(onionhostname, onionport) addKnownNode(1, self_peer, is_self=True) state.ownAddresses[self_peer] = True def increaseRating(peer): - """Increase rating of a peer node""" increaseAmount = 0.1 maxRating = 1 with knownNodesLock: @@ -154,7 +151,6 @@ def increaseRating(peer): def decreaseRating(peer): - """Decrease rating of a peer node""" decreaseAmount = 0.1 minRating = -1 with knownNodesLock: @@ -169,7 +165,6 @@ def decreaseRating(peer): def trimKnownNodes(recAddrStream=1): - """Triming Knownnodes""" if len(knownNodes[recAddrStream]) < \ BMConfigParser().safeGetInt("knownnodes", "maxnodes"): return @@ -182,38 +177,40 @@ def trimKnownNodes(recAddrStream=1): del knownNodes[recAddrStream][oldest] -def dns(): - """Add DNS names to knownnodes""" - for port in [8080, 8444]: - addKnownNode( - 1, Peer('bootstrap%s.bitmessage.org' % port, port)) - - def cleanupKnownNodes(): """ Cleanup knownnodes: remove old nodes and nodes with low rating """ now = int(time.time()) needToWriteKnownNodesToDisk = False + dns_done = False + spawnConnections = not BMConfigParser().safeGetBoolean( + 'bitmessagesettings', 'dontconnect' + ) and BMConfigParser().safeGetBoolean( + 'bitmessagesettings', 'sendoutgoingconnections') with knownNodesLock: for stream in knownNodes: if stream not in state.streamsInWhichIAmParticipating: continue keys = knownNodes[stream].keys() + if len(keys) <= 1: # leave at least one node + if not dns_done and spawnConnections: + dns() + dns_done = True + continue for node in keys: - if len(knownNodes[stream]) <= 1: # leave at least one node - break try: - age = now - knownNodes[stream][node]["lastseen"] - # scrap old nodes (age > 28 days) - if age > 2419200: + # scrap old nodes + if (now - knownNodes[stream][node]["lastseen"] > + 2419200): # 28 days needToWriteKnownNodesToDisk = True del knownNodes[stream][node] continue - # scrap old nodes (age > 3 hours) with low rating - if (age > 10800 and knownNodes[stream][node]["rating"] - <= knownNodesForgetRating): + # scrap old nodes with low rating + if (now - knownNodes[stream][node]["lastseen"] > 10800 and + knownNodes[stream][node]["rating"] <= + knownNodesForgetRating): needToWriteKnownNodesToDisk = True del knownNodes[stream][node] continue diff --git a/src/l10n.py b/src/l10n.py index 7a78525b..b3b16341 100644 --- a/src/l10n.py +++ b/src/l10n.py @@ -1,13 +1,13 @@ -""" -Localization -""" + import logging import os import time from bmconfigparser import BMConfigParser -logger = logging.getLogger('default') + +#logger = logging.getLogger(__name__) +logger = logging.getLogger('file_only') DEFAULT_ENCODING = 'ISO8859-1' @@ -50,7 +50,7 @@ except: if BMConfigParser().has_option('bitmessagesettings', 'timeformat'): time_format = BMConfigParser().get('bitmessagesettings', 'timeformat') - # Test the format string + #Test the format string try: time.strftime(time_format) except: @@ -59,52 +59,48 @@ if BMConfigParser().has_option('bitmessagesettings', 'timeformat'): else: time_format = DEFAULT_TIME_FORMAT -# It seems some systems lie about the encoding they use so we perform -# comprehensive decoding tests +#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 + #Check day names for i in xrange(7): unicode(time.strftime(time_format, (0, 0, 0, 0, 0, 0, i, 0, 0)), encoding) - # Check month names + #Check month names for i in xrange(1, 13): unicode(time.strftime(time_format, (0, i, 0, 0, 0, 0, 0, 0, 0)), encoding) - # Check AM/PM + #Check AM/PM unicode(time.strftime(time_format, (0, 0, 0, 11, 0, 0, 0, 0, 0)), encoding) unicode(time.strftime(time_format, (0, 0, 0, 13, 0, 0, 0, 0, 0)), encoding) - # Check DST + #Check DST 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 encoding = DEFAULT_ENCODING - def setlocale(category, newlocale): - """Set the locale""" 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, as_unicode=True): - """Return a formatted timestamp""" - # For some reason some timestamps are strings so we need to sanitize. +def formatTimestamp(timestamp = None, as_unicode = True): + #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: timestamp = None - # timestamp can't be less than 0. + #timestamp can't be less than 0. if timestamp is not None and timestamp < 0: timestamp = None if timestamp is None: timestring = time.strftime(time_format) else: - # In case timestamp is too far in the future + #In case timestamp is too far in the future try: timestring = time.strftime(time_format, time.localtime(timestamp)) except ValueError: @@ -114,21 +110,17 @@ def formatTimestamp(timestamp=None, as_unicode=True): return unicode(timestring, encoding) return timestring - def getTranslationLanguage(): - """Return the user's language choice""" - userlocale = BMConfigParser().safeGet( - 'bitmessagesettings', 'userlocale', 'system') - return userlocale if userlocale and userlocale != 'system' else language + userlocale = None + if BMConfigParser().has_option('bitmessagesettings', 'userlocale'): + userlocale = BMConfigParser().get('bitmessagesettings', 'userlocale') + if userlocale in [None, '', 'system']: + return language + return userlocale + def getWindowsLocale(posixLocale): - """ - Get the Windows locale - Technically this converts the locale string from UNIX to Windows format, - because they use different ones in their - libraries. E.g. "en_EN.UTF-8" to "english". - """ if posixLocale in windowsLanguageMap: return windowsLanguageMap[posixLocale] if "." in posixLocale: diff --git a/src/main.py b/src/main.py index 22ea7c3e..969dbe56 100644 --- a/src/main.py +++ b/src/main.py @@ -1,8 +1,8 @@ """This module is for thread start.""" +from bitmessagemain import main import state if __name__ == '__main__': state.kivy = True - print("Kivy Loading for PyBitmessage......") - from bitmessagemain import main + print("Kivy Loading......") main() diff --git a/src/message_data_reader.py b/src/message_data_reader.py new file mode 100644 index 00000000..de6ed6c0 --- /dev/null +++ b/src/message_data_reader.py @@ -0,0 +1,136 @@ +# pylint: disable=too-many-locals +""" +This program can be used to print out everything in your Inbox or Sent folders and also take things out of the trash. +Scroll down to the bottom to see the functions that you can uncomment. Save then run this file. +The functions which only read the database file seem to function just +fine even if you have Bitmessage running but you should definitly close +it before running the functions that make changes (like taking items out +of the trash). +""" + +from __future__ import absolute_import + +import sqlite3 +from binascii import hexlify +from time import strftime, localtime + +import paths +import queues + + +appdata = paths.lookupAppdataFolder() + +conn = sqlite3.connect(appdata + 'messages.dat') +conn.text_factory = str +cur = conn.cursor() + + +def readInbox(): + """Print each row from inbox table""" + print 'Printing everything in inbox table:' + item = '''select * from inbox''' + parameters = '' + cur.execute(item, parameters) + output = cur.fetchall() + for row in output: + print row + + +def readSent(): + """Print each row from sent table""" + print 'Printing everything in Sent table:' + item = '''select * from sent where folder !='trash' ''' + parameters = '' + cur.execute(item, parameters) + output = cur.fetchall() + for row in output: + (msgid, toaddress, toripe, fromaddress, subject, message, ackdata, lastactiontime, + sleeptill, status, retrynumber, folder, encodingtype, ttl) = row # pylint: disable=unused-variable + print(hexlify(msgid), toaddress, 'toripe:', hexlify(toripe), 'fromaddress:', fromaddress, 'ENCODING TYPE:', + encodingtype, 'SUBJECT:', repr(subject), 'MESSAGE:', repr(message), 'ACKDATA:', hexlify(ackdata), + lastactiontime, status, retrynumber, folder) + + +def readSubscriptions(): + """Print each row from subscriptions table""" + print 'Printing everything in subscriptions table:' + item = '''select * from subscriptions''' + parameters = '' + cur.execute(item, parameters) + output = cur.fetchall() + for row in output: + print row + + +def readPubkeys(): + """Print each row from pubkeys table""" + print 'Printing everything in pubkeys table:' + item = '''select address, transmitdata, time, usedpersonally from pubkeys''' + parameters = '' + cur.execute(item, parameters) + output = cur.fetchall() + for row in output: + address, transmitdata, time, usedpersonally = row + print( + 'Address:', address, '\tTime first broadcast:', unicode( + strftime('%a, %d %b %Y %I:%M %p', localtime(time)), 'utf-8'), + '\tUsed by me personally:', usedpersonally, '\tFull pubkey message:', hexlify(transmitdata), + ) + + +def readInventory(): + """Print each row from inventory table""" + print 'Printing everything in inventory table:' + item = '''select hash, objecttype, streamnumber, payload, expirestime from inventory''' + parameters = '' + cur.execute(item, parameters) + output = cur.fetchall() + for row in output: + obj_hash, objecttype, streamnumber, payload, expirestime = row + print 'Hash:', hexlify(obj_hash), objecttype, streamnumber, '\t', hexlify(payload), '\t', unicode( + strftime('%a, %d %b %Y %I:%M %p', localtime(expirestime)), 'utf-8') + + +def takeInboxMessagesOutOfTrash(): + """Update all inbox messages with folder=trash to have folder=inbox""" + item = '''update inbox set folder='inbox' where folder='trash' ''' + parameters = '' + cur.execute(item, parameters) + _ = cur.fetchall() + conn.commit() + print 'done' + + +def takeSentMessagesOutOfTrash(): + """Update all sent messages with folder=trash to have folder=sent""" + item = '''update sent set folder='sent' where folder='trash' ''' + parameters = '' + cur.execute(item, parameters) + _ = cur.fetchall() + conn.commit() + print 'done' + + +def markAllInboxMessagesAsUnread(): + """Update all messages in inbox to have read=0""" + item = '''update inbox set read='0' ''' + parameters = '' + cur.execute(item, parameters) + _ = cur.fetchall() + conn.commit() + queues.UISignalQueue.put(('changedInboxUnread', None)) + print 'done' + + +def vacuum(): + """Perform a vacuum on the database""" + item = '''VACUUM''' + parameters = '' + cur.execute(item, parameters) + _ = cur.fetchall() + conn.commit() + print 'done' + + +if __name__ == '__main__': + readInbox() diff --git a/src/messagetypes/__init__.py b/src/messagetypes/__init__.py index 97ce693a..06783eac 100644 --- a/src/messagetypes/__init__.py +++ b/src/messagetypes/__init__.py @@ -1,26 +1,17 @@ -import logging from importlib import import_module -from os import listdir, path +from os import path, listdir from string import lower -try: - from kivy.utils import platform -except: - platform = '' +from debug import logger 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): +class MsgBase(object): + def encode(self): self.data = {"": lower(type(self).__name__)} def constructObject(data): - """Constructing an object""" whitelist = ["message"] if data[""] not in whitelist: return None @@ -41,8 +32,7 @@ def constructObject(data): else: return returnObj - -if paths.frozen is not None or platform == "android": +if paths.frozen is not None: import messagetypes.message import messagetypes.vote else: diff --git a/src/messagetypes/message.py b/src/messagetypes/message.py index 27bba13a..f52c6b35 100644 --- a/src/messagetypes/message.py +++ b/src/messagetypes/message.py @@ -1,30 +1,24 @@ -import logging - +from debug import logger from messagetypes import MsgBase -# pylint: disable=attribute-defined-outside-init - -logger = logging.getLogger('default') class Message(MsgBase): - """Encapsulate a message""" - # pylint: disable=attribute-defined-outside-init + def __init__(self): + return def decode(self, data): - """Decode a message""" # UTF-8 and variable type validator - if isinstance(data["subject"], str): + if type(data["subject"]) is str: self.subject = unicode(data["subject"], 'utf-8', 'replace') else: self.subject = unicode(str(data["subject"]), 'utf-8', 'replace') - if isinstance(data["body"], str): + if type(data["body"]) is 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__() + super(Message, self).encode() try: self.data["subject"] = data["subject"] self.data["body"] = data["body"] @@ -33,6 +27,5 @@ class Message(MsgBase): return self.data def process(self): - """Process a message""" logger.debug("Subject: %i bytes", len(self.subject)) logger.debug("Body: %i bytes", len(self.body)) diff --git a/src/messagetypes/vote.py b/src/messagetypes/vote.py index d20f5cd6..df8d267f 100644 --- a/src/messagetypes/vote.py +++ b/src/messagetypes/vote.py @@ -1,31 +1,23 @@ -import logging - +from debug import logger from messagetypes import MsgBase -# pylint: disable=attribute-defined-outside-init - -logger = logging.getLogger('default') - class Vote(MsgBase): - """Module used to vote""" + def __init__(self): + return def decode(self, data): - """decode a vote""" - # pylint: disable=attribute-defined-outside-init self.msgid = data["msgid"] self.vote = data["vote"] def encode(self, data): - """Encode a vote""" - super(Vote, self).__init__() + super(Vote, self).encode() try: self.data["msgid"] = data["msgid"] self.data["vote"] = data["vote"] except KeyError as e: - logger.error("Missing key %s", e) + logger.error("Missing key %s", e.name) return self.data def process(self): - """Encode a vote""" logger.debug("msgid: %s", self.msgid) logger.debug("vote: %s", self.vote) diff --git a/src/multiqueue.py b/src/multiqueue.py index d7c10847..8c64d33d 100644 --- a/src/multiqueue.py +++ b/src/multiqueue.py @@ -1,6 +1,6 @@ """ -A queue with multiple internal subqueues. -Elements are added into a random subqueue, and retrieval rotates +src/multiqueue.py +================= """ import Queue diff --git a/src/namecoin.py b/src/namecoin.py index ae2bde79..6674bdcd 100644 --- a/src/namecoin.py +++ b/src/namecoin.py @@ -1,7 +1,31 @@ -""" -Namecoin queries -""" # pylint: disable=too-many-branches,protected-access +""" +Copyright (C) 2013 by Daniel Kraft +This file is part of the Bitmessage project. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +.. todo:: from debug import logger crashes PyBitmessage due to a circular dependency. The debug module will also +override/disable logging.getLogger() # loggers so module level logging functions are used instead +""" + +from __future__ import absolute_import import base64 import httplib @@ -10,11 +34,11 @@ import os import socket import sys +from addresses import decodeAddress +from debug import logger import defaults import tr # translate -from addresses import decodeAddress from bmconfigparser import BMConfigParser -from debug import logger configSection = "bitmessagesettings" @@ -234,7 +258,7 @@ class namecoinConnection(object): resp = self.con.getresponse() result = resp.read() if resp.status != 200: - raise Exception("Namecoin returned status %i: %s" % (resp.status, resp.reason)) + raise Exception("Namecoin returned status %i: %s" % resp.status, resp.reason) except: logger.info("HTTP receive error") except: @@ -264,7 +288,7 @@ class namecoinConnection(object): return result except socket.error as exc: - raise Exception("Socket error in RPC connection: %s" % exc) + raise Exception("Socket error in RPC connection: %s" % str(exc)) def lookupNamecoinFolder(): 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 70613539..e69de29b 100644 --- a/src/network/__init__.py +++ b/src/network/__init__.py @@ -1,20 +0,0 @@ -""" -Network subsystem packages -""" -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 - - -__all__ = [ - "BMConnectionPool", "Dandelion", - "AddrThread", "AnnounceThread", "BMNetworkThread", "DownloadThread", - "InvThread", "ReceiveQueueThread", "UploadThread", "StoppableThread" -] diff --git a/src/network/addrthread.py b/src/network/addrthread.py index 3bf448d8..5b0ea638 100644 --- a/src/network/addrthread.py +++ b/src/network/addrthread.py @@ -1,19 +1,18 @@ -""" -Announce addresses as they are received from other hosts -""" import Queue +import threading -import state -from helper_random import randomshuffle -from network.assemble import assemble_addr +import addresses +from helper_threading import StoppableThread from network.connectionpool import BMConnectionPool from queues import addrQueue -from threads import StoppableThread +import protocol +import state - -class AddrThread(StoppableThread): - """(Node) address broadcasting thread""" - name = "AddrBroadcaster" +class AddrThread(threading.Thread, StoppableThread): + def __init__(self): + threading.Thread.__init__(self, name="AddrBroadcaster") + self.initStop() + self.name = "AddrBroadcaster" def run(self): while not state.shutdown: @@ -21,26 +20,15 @@ class AddrThread(StoppableThread): while True: try: data = addrQueue.get(False) - chunk.append(data) + chunk.append((data[0], data[1])) + if len(data) > 2: + source = BMConnectionPool().getConnectionByAddr(data[2]) except Queue.Empty: break + except KeyError: + continue - if chunk: - # Choose peers randomly - connections = BMConnectionPool().establishedConnections() - randomshuffle(connections) - for i in connections: - randomshuffle(chunk) - filtered = [] - for stream, peer, seen, destination in chunk: - # peer's own address or address received from peer - if i.destination in (peer, destination): - continue - if stream not in i.streams: - continue - filtered.append((stream, peer, seen)) - if filtered: - i.append_write_buf(assemble_addr(filtered)) + #finish addrQueue.iterate() for i in range(len(chunk)): diff --git a/src/network/advanceddispatcher.py b/src/network/advanceddispatcher.py index 982be819..c8f125f0 100644 --- a/src/network/advanceddispatcher.py +++ b/src/network/advanceddispatcher.py @@ -1,19 +1,21 @@ """ -Improved version of asyncore dispatcher +src/network/advanceddispatcher.py +================================= """ # pylint: disable=attribute-defined-outside-init + import socket import threading import time import network.asyncore_pollchoose as asyncore import state -from threads import BusyError, nonBlocking +from debug import logger +from helper_threading import BusyError, nonBlocking class ProcessingError(Exception): - """General class for protocol parser exception, - use as a base for others.""" + """General class for protocol parser exception, use as a base for others.""" pass @@ -23,8 +25,7 @@ class UnknownStateError(ProcessingError): class AdvancedDispatcher(asyncore.dispatcher): - """Improved version of asyncore dispatcher, - with buffers and protocol state.""" + """Improved version of asyncore dispatcher, with buffers and protocol state.""" # pylint: disable=too-many-instance-attributes _buf_len = 131072 # 128kB @@ -72,8 +73,7 @@ class AdvancedDispatcher(asyncore.dispatcher): del self.read_buf[0:length] def process(self): - """Process (parse) data that's in the buffer, - as long as there is enough data and the connection is open.""" + """Process (parse) data that's in the buffer, as long as there is enough data and the connection is open.""" while self.connected and not state.shutdown: try: with nonBlocking(self.processingLock): @@ -84,8 +84,7 @@ class AdvancedDispatcher(asyncore.dispatcher): try: cmd = getattr(self, "state_" + str(self.state)) except AttributeError: - self.logger.error( - 'Unknown state %s', self.state, exc_info=True) + logger.error("Unknown state %s", self.state, exc_info=True) raise UnknownStateError(self.state) if not cmd(): break @@ -105,9 +104,8 @@ class AdvancedDispatcher(asyncore.dispatcher): if asyncore.maxUploadRate > 0: self.uploadChunk = int(asyncore.uploadBucket) self.uploadChunk = min(self.uploadChunk, len(self.write_buf)) - return asyncore.dispatcher.writable(self) and ( - self.connecting or ( - self.connected and self.uploadChunk > 0)) + return asyncore.dispatcher.writable(self) and \ + (self.connecting or (self.connected and self.uploadChunk > 0)) def readable(self): """Is the read buffer ready to accept data from the network?""" @@ -116,15 +114,13 @@ class AdvancedDispatcher(asyncore.dispatcher): self.downloadChunk = int(asyncore.downloadBucket) try: if self.expectBytes > 0 and not self.fullyEstablished: - self.downloadChunk = min( - self.downloadChunk, self.expectBytes - len(self.read_buf)) + self.downloadChunk = min(self.downloadChunk, self.expectBytes - len(self.read_buf)) if self.downloadChunk < 0: self.downloadChunk = 0 except AttributeError: pass - return asyncore.dispatcher.readable(self) and ( - self.connecting or self.accepting or ( - self.connected and self.downloadChunk > 0)) + return asyncore.dispatcher.readable(self) and \ + (self.connecting or self.accepting or (self.connected and self.downloadChunk > 0)) def handle_read(self): """Append incoming data to the read buffer.""" @@ -148,21 +144,20 @@ class AdvancedDispatcher(asyncore.dispatcher): try: asyncore.dispatcher.handle_connect_event(self) except socket.error as e: - # pylint: disable=protected-access - if e.args[0] not in asyncore._DISCONNECTED: + if e.args[0] not in asyncore._DISCONNECTED: # pylint: disable=protected-access raise def handle_connect(self): """Method for handling connection established implementations.""" self.lastTx = time.time() - def state_close(self): # pylint: disable=no-self-use + def state_close(self): """Signal to the processing loop to end.""" + # pylint: disable=no-self-use return False def handle_close(self): - """Callback for connection being closed, - but can also be called directly when you want connection to close.""" + """Callback for connection being closed, but can also be called directly when you want connection to close.""" with self.readLock: self.read_buf = bytearray() with self.writeLock: diff --git a/src/network/announcethread.py b/src/network/announcethread.py index 19038ab6..a94eeb36 100644 --- a/src/network/announcethread.py +++ b/src/network/announcethread.py @@ -1,20 +1,20 @@ -""" -Announce myself (node address) -""" +import threading import time -import state from bmconfigparser import BMConfigParser -from network.assemble import assemble_addr +from debug import logger +from helper_threading import StoppableThread +from network.bmproto import BMProto from network.connectionpool import BMConnectionPool from network.udp import UDPSocket -from node import Peer -from threads import StoppableThread +import state - -class AnnounceThread(StoppableThread): - """A thread to manage regular announcing of this node""" - name = "Announcer" +class AnnounceThread(threading.Thread, StoppableThread): + def __init__(self): + threading.Thread.__init__(self, name="Announcer") + self.initStop() + self.name = "Announcer" + logger.info("init announce thread") def run(self): lastSelfAnnounced = 0 @@ -26,18 +26,10 @@ class AnnounceThread(StoppableThread): if processed == 0: self.stop.wait(10) - @staticmethod - def announceSelf(): - """Announce our presence""" + def announceSelf(self): for connection in BMConnectionPool().udpSockets.values(): if not connection.announcing: continue for stream in state.streamsInWhichIAmParticipating: - addr = ( - stream, - Peer( - '127.0.0.1', - BMConfigParser().safeGetInt( - 'bitmessagesettings', 'port')), - time.time()) - connection.append_write_buf(assemble_addr([addr])) + addr = (stream, state.Peer('127.0.0.1', BMConfigParser().safeGetInt("bitmessagesettings", "port")), time.time()) + connection.append_write_buf(BMProto.assembleAddr([addr])) diff --git a/src/network/assemble.py b/src/network/assemble.py deleted file mode 100644 index 32fad3e4..00000000 --- a/src/network/assemble.py +++ /dev/null @@ -1,31 +0,0 @@ -""" -Create bitmessage protocol command packets -""" -import struct - -import addresses -from network.constants import MAX_ADDR_COUNT -from network.node import Peer -from protocol import CreatePacket, encodeHost - - -def assemble_addr(peerList): - """Create address command""" - if isinstance(peerList, Peer): - peerList = [peerList] - if not peerList: - return b'' - retval = b'' - for i in range(0, len(peerList), MAX_ADDR_COUNT): - payload = addresses.encodeVarint(len(peerList[i:i + MAX_ADDR_COUNT])) - for stream, peer, timestamp in peerList[i:i + MAX_ADDR_COUNT]: - # 64-bit time - payload += struct.pack('>Q', timestamp) - payload += struct.pack('>I', stream) - # service bit flags offered by this node - payload += struct.pack('>q', 1) - payload += encodeHost(peer.host) - # remote port - payload += struct.pack('>H', peer.port) - retval += CreatePacket('addr', payload) - return retval diff --git a/src/network/asyncore_pollchoose.py b/src/network/asyncore_pollchoose.py index 41757f37..3337c0f0 100644 --- a/src/network/asyncore_pollchoose.py +++ b/src/network/asyncore_pollchoose.py @@ -1,11 +1,56 @@ -""" -Basic infrastructure for asynchronous socket service clients and servers. -""" # -*- Mode: Python -*- # Id: asyncore.py,v 2.51 2000/09/07 22:29:26 rushing Exp # Author: Sam Rushing -# pylint: disable=too-many-branches,too-many-lines,global-statement -# pylint: disable=redefined-builtin,no-self-use +# pylint: disable=too-many-statements,too-many-branches,no-self-use,too-many-lines,attribute-defined-outside-init +# pylint: disable=global-statement +""" +src/network/asyncore_pollchoose.py +================================== + +# ====================================================================== +# Copyright 1996 by Sam Rushing +# +# All Rights Reserved +# +# Permission to use, copy, modify, and distribute this software and +# its documentation for any purpose and without fee is hereby +# granted, provided that the above copyright notice appear in all +# copies and that both that copyright notice and this permission +# notice appear in supporting documentation, and that the name of Sam +# Rushing not be used in advertising or publicity pertaining to +# distribution of the software without specific, written prior +# permission. +# +# SAM RUSHING DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN +# NO EVENT SHALL SAM RUSHING BE LIABLE FOR ANY SPECIAL, INDIRECT OR +# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# ====================================================================== + +Basic infrastructure for asynchronous socket service clients and servers. + +There are only two ways to have a program on a single processor do "more +than one thing at a time". Multi-threaded programming is the simplest and +most popular way to do it, but there is another very different technique, +that lets you have nearly all the advantages of multi-threading, without +actually using multiple threads. it's really only practical if your program +is largely I/O bound. If your program is CPU bound, then pre-emptive +scheduled threads are probably what you really need. Network servers are +rarely CPU-bound, however. + +If your operating system supports the select() system call in its I/O +library (and nearly all do), then you can use it to juggle multiple +communication channels at once; doing other work while your I/O is taking +place in the "background." Although this strategy can seem strange and +complex, especially at first, it is in many ways easier to understand and +control than multi-threaded programming. The module documented here solves +many of the difficult problems for you, making the task of building +sophisticated high-performance network servers and clients a snap. +""" + import os import select import socket @@ -13,9 +58,8 @@ import sys import time import warnings from errno import ( - EADDRINUSE, EAGAIN, EALREADY, EBADF, ECONNABORTED, ECONNREFUSED, - ECONNRESET, EHOSTUNREACH, EINPROGRESS, EINTR, EINVAL, EISCONN, ENETUNREACH, - ENOTCONN, ENOTSOCK, EPIPE, ESHUTDOWN, ETIMEDOUT, EWOULDBLOCK, errorcode + EADDRINUSE, EAGAIN, EALREADY, EBADF, ECONNABORTED, ECONNREFUSED, ECONNRESET, EHOSTUNREACH, EINPROGRESS, EINTR, + EINVAL, EISCONN, ENETUNREACH, ENOTCONN, ENOTSOCK, EPIPE, ESHUTDOWN, ETIMEDOUT, EWOULDBLOCK, errorcode ) from threading import current_thread @@ -63,8 +107,7 @@ def _strerror(err): class ExitNow(Exception): - """We don't use directly but may be necessary as we replace - asyncore due to some library raising or expecting it""" + """We don't use directly but may be necessary as we replace asyncore due to some library raising or expecting it""" pass @@ -109,8 +152,7 @@ def write(obj): def set_rates(download, upload): """Set throttling rates""" - global maxDownloadRate, maxUploadRate, downloadBucket - global uploadBucket, downloadTimestamp, uploadTimestamp + global maxDownloadRate, maxUploadRate, downloadBucket, uploadBucket, downloadTimestamp, uploadTimestamp maxDownloadRate = float(download) * 1024 maxUploadRate = float(upload) * 1024 @@ -140,8 +182,7 @@ def update_received(download=0): currentTimestamp = time.time() receivedBytes += download if maxDownloadRate > 0: - bucketIncrease = \ - maxDownloadRate * (currentTimestamp - downloadTimestamp) + bucketIncrease = maxDownloadRate * (currentTimestamp - downloadTimestamp) downloadBucket += bucketIncrease if downloadBucket > maxDownloadRate: downloadBucket = int(maxDownloadRate) @@ -201,6 +242,7 @@ def readwrite(obj, flags): def select_poller(timeout=0.0, map=None): """A poller which uses select(), available on most platforms.""" + # pylint: disable=redefined-builtin if map is None: map = socket_map @@ -256,6 +298,7 @@ def select_poller(timeout=0.0, map=None): def poll_poller(timeout=0.0, map=None): """A poller which uses poll(), available on most UNIXen.""" + # pylint: disable=redefined-builtin if map is None: map = socket_map @@ -313,6 +356,7 @@ poll2 = poll3 = poll_poller def epoll_poller(timeout=0.0, map=None): """A poller which uses epoll(), supported on Linux 2.5.44 and newer.""" + # pylint: disable=redefined-builtin if map is None: map = socket_map @@ -368,7 +412,7 @@ def epoll_poller(timeout=0.0, map=None): def kqueue_poller(timeout=0.0, map=None): """A poller which uses kqueue(), BSD specific.""" - # pylint: disable=no-member,too-many-statements + # pylint: disable=redefined-builtin,no-member if map is None: map = socket_map @@ -396,20 +440,14 @@ def kqueue_poller(timeout=0.0, map=None): poller_flags |= select.KQ_EV_ENABLE else: poller_flags |= select.KQ_EV_DISABLE - updates.append( - select.kevent( - fd, filter=select.KQ_FILTER_READ, - flags=poller_flags)) + updates.append(select.kevent(fd, filter=select.KQ_FILTER_READ, flags=poller_flags)) if kq_filter & 2 != obj.poller_filter & 2: poller_flags = select.KQ_EV_ADD if kq_filter & 2: poller_flags |= select.KQ_EV_ENABLE else: poller_flags |= select.KQ_EV_DISABLE - updates.append( - select.kevent( - fd, filter=select.KQ_FILTER_WRITE, - flags=poller_flags)) + updates.append(select.kevent(fd, filter=select.KQ_FILTER_WRITE, flags=poller_flags)) obj.poller_filter = kq_filter if not selectables: @@ -443,6 +481,7 @@ def kqueue_poller(timeout=0.0, map=None): def loop(timeout=30.0, use_poll=False, map=None, count=None, poller=None): """Poll in a loop, until count or timeout is reached""" + # pylint: disable=redefined-builtin if map is None: map = socket_map @@ -481,9 +520,9 @@ def loop(timeout=30.0, use_poll=False, map=None, count=None, poller=None): count = count - 1 -class dispatcher(object): +class dispatcher: """Dispatcher for socket objects""" - # pylint: disable=too-many-public-methods,too-many-instance-attributes + # pylint: disable=too-many-public-methods,too-many-instance-attributes,old-style-class debug = False connected = False @@ -498,6 +537,7 @@ class dispatcher(object): minTx = 1500 def __init__(self, sock=None, map=None): + # pylint: disable=redefined-builtin if map is None: self._map = socket_map else: @@ -546,7 +586,8 @@ class dispatcher(object): def add_channel(self, map=None): """Add a channel""" - # pylint: disable=attribute-defined-outside-init + # pylint: disable=redefined-builtin + if map is None: map = self._map map[self._fileno] = self @@ -555,6 +596,8 @@ class dispatcher(object): def del_channel(self, map=None): """Delete a channel""" + # pylint: disable=redefined-builtin + fd = self._fileno if map is None: map = self._map @@ -562,14 +605,12 @@ class dispatcher(object): del map[fd] if self._fileno: try: - kqueue_poller.pollster.control([select.kevent( - fd, select.KQ_FILTER_READ, select.KQ_EV_DELETE)], 0) - except(AttributeError, KeyError, TypeError, IOError, OSError): + kqueue_poller.pollster.control([select.kevent(fd, select.KQ_FILTER_READ, select.KQ_EV_DELETE)], 0) + except (AttributeError, KeyError, TypeError, IOError, OSError): pass try: - kqueue_poller.pollster.control([select.kevent( - fd, select.KQ_FILTER_WRITE, select.KQ_EV_DELETE)], 0) - except(AttributeError, KeyError, TypeError, IOError, OSError): + kqueue_poller.pollster.control([select.kevent(fd, select.KQ_FILTER_WRITE, select.KQ_EV_DELETE)], 0) + except (AttributeError, KeyError, TypeError, IOError, OSError): pass try: epoll_poller.pollster.unregister(fd) @@ -586,10 +627,8 @@ class dispatcher(object): self.poller_filter = 0 self.poller_registered = False - def create_socket( - self, family=socket.AF_INET, socket_type=socket.SOCK_STREAM): + def create_socket(self, family=socket.AF_INET, socket_type=socket.SOCK_STREAM): """Create a socket""" - # pylint: disable=attribute-defined-outside-init self.family_and_type = family, socket_type sock = socket.socket(family, socket_type) sock.setblocking(0) @@ -597,16 +636,20 @@ class dispatcher(object): def set_socket(self, sock, map=None): """Set socket""" + # pylint: disable=redefined-builtin + self.socket = sock self._fileno = sock.fileno() self.add_channel(map) def set_reuse_addr(self): """try to re-use a server port if possible""" + try: self.socket.setsockopt( - socket.SOL_SOCKET, socket.SO_REUSEADDR, self.socket.getsockopt( - socket.SOL_SOCKET, socket.SO_REUSEADDR) | 1 + socket.SOL_SOCKET, socket.SO_REUSEADDR, + self.socket.getsockopt(socket.SOL_SOCKET, + socket.SO_REUSEADDR) | 1 ) except socket.error: pass @@ -661,16 +704,13 @@ class dispatcher(object): raise socket.error(err, errorcode[err]) def accept(self): - """Accept incoming connections. - Returns either an address pair or None.""" + """Accept incoming connections. Returns either an address pair or None.""" try: conn, addr = self.socket.accept() except TypeError: return None except socket.error as why: - if why.args[0] in ( - EWOULDBLOCK, WSAEWOULDBLOCK, ECONNABORTED, - EAGAIN, ENOTCONN): + if why.args[0] in (EWOULDBLOCK, WSAEWOULDBLOCK, ECONNABORTED, EAGAIN, ENOTCONN): return None else: raise @@ -729,12 +769,11 @@ class dispatcher(object): try: retattr = getattr(self.socket, attr) except AttributeError: - raise AttributeError( - "%s instance has no attribute '%s'" - % (self.__class__.__name__, attr)) + raise AttributeError("%s instance has no attribute '%s'" + % (self.__class__.__name__, attr)) else: - msg = "%(me)s.%(attr)s is deprecated; use %(me)s.socket.%(attr)s"\ - " instead" % {'me': self.__class__.__name__, 'attr': attr} + msg = "%(me)s.%(attr)s is deprecated; use %(me)s.socket.%(attr)s " \ + "instead" % {'me': self.__class__.__name__, 'attr': attr} warnings.warn(msg, DeprecationWarning, stacklevel=2) return retattr @@ -816,8 +855,13 @@ class dispatcher(object): self.log_info( 'uncaptured python exception, closing channel %s (%s:%s %s)' % ( - self_repr, t, v, tbinfo), - 'error') + self_repr, + t, + v, + tbinfo + ), + 'error' + ) self.handle_close() def handle_accept(self): @@ -858,8 +902,11 @@ class dispatcher_with_send(dispatcher): adds simple buffered output capability, useful for simple clients. [for more sophisticated usage use asynchat.async_chat] """ + # pylint: disable=redefined-builtin def __init__(self, sock=None, map=None): + # pylint: disable=redefined-builtin + dispatcher.__init__(self, sock, map) self.out_buffer = b'' @@ -894,8 +941,7 @@ def compact_traceback(): """Return a compact traceback""" t, v, tb = sys.exc_info() tbinfo = [] - # Must have a traceback - if not tb: + if not tb: # Must have a traceback raise AssertionError("traceback does not exist") while tb: tbinfo.append(( @@ -915,6 +961,7 @@ def compact_traceback(): def close_all(map=None, ignore_all=False): """Close all connections""" + # pylint: disable=redefined-builtin if map is None: map = socket_map @@ -951,13 +998,13 @@ def close_all(map=None, ignore_all=False): if os.name == 'posix': import fcntl - class file_wrapper: # pylint: disable=old-style-class + class file_wrapper: """ - Here we override just enough to make a file look - like a socket for the purposes of asyncore. + Here we override just enough to make a file look like a socket for the purposes of asyncore. The passed fd is automatically os.dup()'d """ + # pylint: disable=old-style-class def __init__(self, fd): self.fd = os.dup(fd) @@ -972,11 +1019,12 @@ if os.name == 'posix': def getsockopt(self, level, optname, buflen=None): """Fake getsockopt()""" - if (level == socket.SOL_SOCKET and optname == socket.SO_ERROR and + if (level == socket.SOL_SOCKET and + optname == socket.SO_ERROR and not buflen): return 0 - raise NotImplementedError( - "Only asyncore specific behaviour implemented.") + raise NotImplementedError("Only asyncore specific behaviour " + "implemented.") read = recv write = send @@ -993,6 +1041,8 @@ if os.name == 'posix': """A dispatcher for file_wrapper objects""" def __init__(self, fd, map=None): + # pylint: disable=redefined-builtin + dispatcher.__init__(self, None, map) self.connected = True try: diff --git a/src/network/bmobject.py b/src/network/bmobject.py index 12b997d7..0a4c12b7 100644 --- a/src/network/bmobject.py +++ b/src/network/bmobject.py @@ -1,27 +1,26 @@ """ -BMObject and it's exceptions. +src/network/bmobject.py +====================== + """ -import logging + import time import protocol import state from addresses import calculateInventoryHash +from debug import logger from inventory import Inventory from network.dandelion import Dandelion -logger = logging.getLogger('default') - class BMObjectInsufficientPOWError(Exception): - """Exception indicating the object - doesn't have sufficient proof of work.""" + """Exception indicating the object doesn't have sufficient proof of work.""" errorCodes = ("Insufficient proof of work") class BMObjectInvalidDataError(Exception): - """Exception indicating the data being parsed - does not match the specification.""" + """Exception indicating the data being parsed does not match the specification.""" errorCodes = ("Data invalid") @@ -31,8 +30,7 @@ class BMObjectExpiredError(Exception): class BMObjectUnwantedStreamError(Exception): - """Exception indicating the object is in a stream - we didn't advertise as being interested in.""" + """Exception indicating the object is in a stream we didn't advertise as being interested in.""" errorCodes = ("Object in unwanted stream") @@ -46,8 +44,9 @@ class BMObjectAlreadyHaveError(Exception): errorCodes = ("Already have this object") -class BMObject(object): # pylint: disable=too-many-instance-attributes +class BMObject(object): """Bitmessage Object as a class.""" + # pylint: disable=too-many-instance-attributes # max TTL, 28 days and 3 hours maxTTL = 28 * 24 * 60 * 60 + 10800 @@ -82,36 +81,31 @@ class BMObject(object): # pylint: disable=too-many-instance-attributes raise BMObjectInsufficientPOWError() def checkEOLSanity(self): - """Check if object's lifetime - isn't ridiculously far in the past or future.""" + """Check if object's lifetime isn't ridiculously far in the past or future.""" # EOL sanity check if self.expiresTime - int(time.time()) > BMObject.maxTTL: logger.info( - 'This object\'s End of Life time is too far in the future.' - ' Ignoring it. Time is %i', self.expiresTime) + 'This object\'s End of Life time is too far in the future. Ignoring it. Time is %i', + self.expiresTime) # .. todo:: remove from download queue raise BMObjectExpiredError() if self.expiresTime - int(time.time()) < BMObject.minTTL: logger.info( - 'This object\'s End of Life time was too long ago.' - ' Ignoring the object. Time is %i', self.expiresTime) + 'This object\'s End of Life time was too long ago. Ignoring the object. Time is %i', + self.expiresTime) # .. todo:: remove from download queue raise BMObjectExpiredError() def checkStream(self): """Check if object's stream matches streams we are interested in""" if self.streamNumber not in state.streamsInWhichIAmParticipating: - logger.debug( - 'The streamNumber %i isn\'t one we are interested in.', - self.streamNumber) + logger.debug('The streamNumber %i isn\'t one we are interested in.', self.streamNumber) raise BMObjectUnwantedStreamError() def checkAlreadyHave(self): """ - Check if we already have the object - (so that we don't duplicate it in inventory - or advertise it unnecessarily) + Check if we already have the object (so that we don't duplicate it in inventory or advertise it unnecessarily) """ # if it's a stem duplicate, pretend we don't have it if Dandelion().hasHash(self.inventoryHash): @@ -120,8 +114,7 @@ class BMObject(object): # pylint: disable=too-many-instance-attributes raise BMObjectAlreadyHaveError() def checkObjectByType(self): - """Call a object type specific check - (objects can have additional checks based on their types)""" + """Call a object type specific check (objects can have additional checks based on their types)""" if self.objectType == protocol.OBJECT_GETPUBKEY: self.checkGetpubkey() elif self.objectType == protocol.OBJECT_PUBKEY: @@ -132,21 +125,20 @@ class BMObject(object): # pylint: disable=too-many-instance-attributes self.checkBroadcast() # other objects don't require other types of tests - def checkMessage(self): # pylint: disable=no-self-use + def checkMessage(self): """"Message" object type checks.""" + # pylint: disable=no-self-use return def checkGetpubkey(self): """"Getpubkey" object type checks.""" if len(self.data) < 42: - logger.info( - 'getpubkey message doesn\'t contain enough data. Ignoring.') + logger.info('getpubkey message doesn\'t contain enough data. Ignoring.') raise BMObjectInvalidError() def checkPubkey(self): """"Pubkey" object type checks.""" - # sanity check - if len(self.data) < 146 or len(self.data) > 440: + if len(self.data) < 146 or len(self.data) > 440: # sanity check logger.info('pubkey object too short or too long. Ignoring.') raise BMObjectInvalidError() @@ -154,9 +146,8 @@ class BMObject(object): # pylint: disable=too-many-instance-attributes """"Broadcast" object type checks.""" if len(self.data) < 180: logger.debug( - 'The payload length of this broadcast' - ' packet is unreasonably low. Someone is probably' - ' trying funny business. Ignoring message.') + 'The payload length of this broadcast packet is unreasonably low.' + ' Someone is probably trying funny business. Ignoring message.') raise BMObjectInvalidError() # this isn't supported anymore diff --git a/src/network/bmproto.py b/src/network/bmproto.py index 64bde74c..c8efe91e 100644 --- a/src/network/bmproto.py +++ b/src/network/bmproto.py @@ -1,10 +1,5 @@ -""" -Bitmessage Protocol -""" -# pylint: disable=attribute-defined-outside-init, too-few-public-methods import base64 import hashlib -import logging import socket import struct import time @@ -16,26 +11,20 @@ import knownnodes import protocol import state from bmconfigparser import BMConfigParser +from debug import logger from inventory import Inventory from network.advanceddispatcher import AdvancedDispatcher -from network.bmobject import ( - BMObject, BMObjectAlreadyHaveError, BMObjectExpiredError, - 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.bmobject import ( + BMObject, BMObjectInsufficientPOWError, BMObjectInvalidDataError, + BMObjectExpiredError, BMObjectUnwantedStreamError, + BMObjectInvalidError, BMObjectAlreadyHaveError) +from network.node import Node from network.proxy import ProxyError -from node import Node, Peer -from objectracker import ObjectTracker, missingObjects -from queues import invQueue, objectProcessorQueue, portCheckerQueue +from objectracker import missingObjects, ObjectTracker +from queues import objectProcessorQueue, portCheckerQueue, invQueue, addrQueue from randomtrackingdict import RandomTrackingDict -logger = logging.getLogger('default') - class BMProtoError(ProxyError): """A Bitmessage Protocol Base Error""" @@ -54,18 +43,26 @@ class BMProtoExcessiveDataError(BMProtoError): class BMProto(AdvancedDispatcher, ObjectTracker): """A parser for the Bitmessage Protocol""" - # pylint: disable=too-many-instance-attributes, too-many-public-methods + # ~1.6 MB which is the maximum possible size of an inv message. + maxMessageSize = 1600100 + # 2**18 = 256kB is the maximum size of an object payload + maxObjectPayloadSize = 2**18 + # protocol specification says max 1000 addresses in one addr command + maxAddrCount = 1000 + # protocol specification says max 50000 objects in one inv command + maxObjectCount = 50000 + # address is online if online less than this many seconds ago + addressAlive = 10800 + # maximum time offset + maxTimeOffset = 3600 timeOffsetWrongCount = 0 def __init__(self, address=None, sock=None): - # pylint: disable=unused-argument, super-init-not-called AdvancedDispatcher.__init__(self, sock) self.isOutbound = False # packet/connection from a local IP self.local = False self.pendingUpload = RandomTrackingDict() - # canonical identifier of network group - self.network_group = None def bm_proto_reset(self): """Reset the bitmessage object parser""" @@ -93,14 +90,14 @@ class BMProto(AdvancedDispatcher, ObjectTracker): self.close_reason = "Bad magic" self.set_state("close") return False - if self.payloadLength > MAX_MESSAGE_SIZE: + if self.payloadLength > BMProto.maxMessageSize: 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): """Process incoming command""" self.payload = self.read_buf[:self.payloadLength] if self.checksum != hashlib.sha512(self.payload).digest()[0:4]: @@ -162,8 +159,7 @@ class BMProto(AdvancedDispatcher, ObjectTracker): def decode_payload_varint(self): """Decode a varint from the payload""" - value, offset = addresses.decodeVarint( - self.payload[self.payloadOffset:]) + value, offset = addresses.decodeVarint(self.payload[self.payloadOffset:]) self.payloadOffset += offset return value @@ -185,7 +181,6 @@ class BMProto(AdvancedDispatcher, ObjectTracker): return Node(services, host, port) - # pylint: disable=too-many-branches, too-many-statements def decode_payload_content(self, pattern="v"): """ Decode the payload depending on pattern: @@ -202,7 +197,6 @@ 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": @@ -236,7 +230,7 @@ class BMProto(AdvancedDispatcher, ObjectTracker): while True: i = parserStack[-1][3][parserStack[-1][4]] if i in "0123456789" and ( - size is None or parserStack[-1][3][parserStack[-1][4] - 1] + size is None or parserStack[-1][3][parserStack[-1][4] - 1] not in "lL"): try: size = size * 10 + int(i) @@ -257,7 +251,6 @@ class BMProto(AdvancedDispatcher, ObjectTracker): 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, [] @@ -313,11 +306,8 @@ class BMProto(AdvancedDispatcher, ObjectTracker): def bm_command_error(self): """Decode an error message and log it""" - err_values = self.decode_payload_content("vvlsls") - fatalStatus = err_values[0] - # banTime = err_values[1] - # inventoryVector = err_values[2] - errorText = err_values[3] + fatalStatus, banTime, inventoryVector, errorText = \ + self.decode_payload_content("vvlsls") logger.error( '%s:%i error: %i, %s', self.destination.host, self.destination.port, fatalStatus, errorText) @@ -341,7 +331,7 @@ class BMProto(AdvancedDispatcher, ObjectTracker): def _command_inv(self, dandelion=False): items = self.decode_payload_content("l32s") - if len(items) > MAX_OBJECT_COUNT: + if len(items) > BMProto.maxObjectCount: logger.error( 'Too many items in %sinv message!', 'd' if dandelion else '') raise BMProtoExcessiveDataError() @@ -376,7 +366,7 @@ class BMProto(AdvancedDispatcher, ObjectTracker): nonce, expiresTime, objectType, version, streamNumber, self.payload, self.payloadOffset) - if len(self.payload) - self.payloadOffset > MAX_OBJECT_PAYLOAD_SIZE: + if len(self.payload) - self.payloadOffset > BMProto.maxObjectPayloadSize: logger.info( 'The payload length of this object is too large (%d bytes).' ' Ignoring it.', len(self.payload) - self.payloadOffset) @@ -412,10 +402,8 @@ class BMProto(AdvancedDispatcher, ObjectTracker): except KeyError: pass - if self.object.inventoryHash in Inventory() and Dandelion().hasHash( - self.object.inventoryHash): - Dandelion().removeHash( - self.object.inventoryHash, "cycle detection") + if self.object.inventoryHash in Inventory() and Dandelion().hasHash(self.object.inventoryHash): + Dandelion().removeHash(self.object.inventoryHash, "cycle detection") Inventory()[self.object.inventoryHash] = ( self.object.objectType, self.object.streamNumber, @@ -434,46 +422,39 @@ class BMProto(AdvancedDispatcher, ObjectTracker): def bm_command_addr(self): """Incoming addresses, process them""" - # pylint: disable=redefined-outer-name addresses = self._decode_addr() - for seenTime, stream, _, ip, port in addresses: + for i in addresses: + seenTime, stream, services, ip, port = i decodedIP = protocol.checkIPAddress(str(ip)) if stream not in state.streamsInWhichIAmParticipating: continue if ( decodedIP and time.time() - seenTime > 0 and - seenTime > time.time() - ADDRESS_ALIVE and + seenTime > time.time() - BMProto.addressAlive and port > 0 ): - peer = Peer(decodedIP, port) + peer = state.Peer(decodedIP, port) try: - if knownnodes.knownNodes[stream][peer]["lastseen"] > \ - seenTime: + if knownnodes.knownNodes[stream][peer]["lastseen"] > seenTime: continue except KeyError: pass - if len(knownnodes.knownNodes[stream]) < \ - BMConfigParser().safeGetInt("knownnodes", "maxnodes"): + if len(knownnodes.knownNodes[stream]) < BMConfigParser().safeGetInt("knownnodes", "maxnodes"): with knownnodes.knownNodesLock: try: - knownnodes.knownNodes[stream][peer]["lastseen"] = \ - seenTime + 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)) + addrQueue.put((stream, peer, self.destination)) return True def bm_command_portcheck(self): """Incoming port check request, queue it.""" - portCheckerQueue.put(Peer(self.destination, self.peerNode.port)) + portCheckerQueue.put(state.Peer(self.destination, self.peerNode.port)) return True def bm_command_ping(self): @@ -481,7 +462,7 @@ class BMProto(AdvancedDispatcher, ObjectTracker): self.append_write_buf(protocol.CreatePacket('pong')) return True - def bm_command_pong(self): # pylint: disable=no-self-use + def bm_command_pong(self): """ Incoming pong. Ignore it. PyBitmessage pings connections after about 5 minutes @@ -519,7 +500,7 @@ class BMProto(AdvancedDispatcher, ObjectTracker): self.timeOffset = self.timestamp - int(time.time()) logger.debug('remoteProtocolVersion: %i', self.remoteProtocolVersion) logger.debug('services: 0x%08X', self.services) - logger.debug('time offset: %i', self.timeOffset) + logger.debug('time offset: %i', self.timestamp - int(time.time())) logger.debug('my external IP: %s', self.sockNode.host) logger.debug( 'remote node incoming address: %s:%i', @@ -549,7 +530,6 @@ class BMProto(AdvancedDispatcher, ObjectTracker): length=self.payloadLength, expectBytes=0) return False - # pylint: disable=too-many-return-statements def peerValidityChecks(self): """Check the validity of the peer""" if self.remoteProtocolVersion < 3: @@ -560,16 +540,16 @@ class BMProto(AdvancedDispatcher, ObjectTracker): 'Closing connection to old protocol version %s, node: %s', self.remoteProtocolVersion, self.destination) return False - if self.timeOffset > MAX_TIME_OFFSET: + if self.timeOffset > BMProto.maxTimeOffset: self.append_write_buf(protocol.assembleErrorMessage( - errorText="Your time is too far in the future" - " compared to mine. Closing connection.", fatal=2)) + 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) BMProto.timeOffsetWrongCount += 1 return False - elif self.timeOffset < -MAX_TIME_OFFSET: + elif self.timeOffset < -BMProto.maxTimeOffset: self.append_write_buf(protocol.assembleErrorMessage( errorText="Your time is too far in the past compared to mine." " Closing connection.", fatal=2)) @@ -585,8 +565,8 @@ class BMProto(AdvancedDispatcher, ObjectTracker): errorText="We don't have shared stream interests." " Closing connection.", fatal=2)) logger.debug( - 'Closed connection to %s because there is no overlapping' - ' interest in streams.', self.destination) + 'Closed connection to %s because there is no overlapping interest' + ' in streams.', self.destination) return False if self.destination in connectionpool.BMConnectionPool().inboundConnections: try: @@ -595,8 +575,8 @@ class BMProto(AdvancedDispatcher, ObjectTracker): errorText="Too many connections from your IP." " Closing connection.", fatal=2)) logger.debug( - 'Closed connection to %s because we are already' - ' connected to that IP.', self.destination) + 'Closed connection to %s because we are already connected' + ' to that IP.', self.destination) return False except: pass @@ -604,14 +584,12 @@ class BMProto(AdvancedDispatcher, ObjectTracker): # incoming from a peer we're connected to as outbound, # or server full report the same error to counter deanonymisation if ( - Peer(self.destination.host, self.peerNode.port) - in connectionpool.BMConnectionPool().inboundConnections - or len(connectionpool.BMConnectionPool().inboundConnections) - + len(connectionpool.BMConnectionPool().outboundConnections) - > BMConfigParser().safeGetInt( - 'bitmessagesettings', 'maxtotalconnections') - + BMConfigParser().safeGetInt( - 'bitmessagesettings', 'maxbootstrapconnections') + state.Peer(self.destination.host, self.peerNode.port) in + connectionpool.BMConnectionPool().inboundConnections or + len(connectionpool.BMConnectionPool().inboundConnections) + + len(connectionpool.BMConnectionPool().outboundConnections) > + BMConfigParser().safeGetInt("bitmessagesettings", "maxtotalconnections") + + BMConfigParser().safeGetInt("bitmessagesettings", "maxbootstrapconnections") ): self.append_write_buf(protocol.assembleErrorMessage( errorText="Server full, please try again later.", fatal=2)) @@ -631,10 +609,36 @@ class BMProto(AdvancedDispatcher, ObjectTracker): return True + @staticmethod + def assembleAddr(peerList): + """Build up a packed address""" + if isinstance(peerList, state.Peer): + peerList = (peerList) + if not peerList: + return b'' + retval = b'' + for i in range(0, len(peerList), BMProto.maxAddrCount): + payload = addresses.encodeVarint( + len(peerList[i:i + BMProto.maxAddrCount])) + for address in peerList[i:i + BMProto.maxAddrCount]: + stream, peer, timestamp = address + payload += struct.pack( + '>Q', timestamp) # 64-bit time + payload += struct.pack('>I', stream) + payload += struct.pack( + '>q', 1) # service bit flags offered by this node + payload += protocol.encodeHost(peer.host) + payload += struct.pack('>H', peer.port) # remote port + retval += protocol.CreatePacket('addr', payload) + return retval + @staticmethod def stopDownloadingObject(hashId, forwardAnyway=False): """Stop downloading an object""" - for connection in connectionpool.BMConnectionPool().connections(): + for connection in ( + connectionpool.BMConnectionPool().inboundConnections.values() + + connectionpool.BMConnectionPool().outboundConnections.values() + ): try: del connection.objectsNewToMe[hashId] except KeyError: @@ -675,7 +679,7 @@ class BMStringParser(BMProto): """ def __init__(self): super(BMStringParser, self).__init__() - self.destination = Peer('127.0.0.1', 8444) + self.destination = state.Peer('127.0.0.1', 8444) self.payload = None ObjectTracker.__init__(self) diff --git a/src/network/connectionchooser.py b/src/network/connectionchooser.py index badd98b7..e116ec53 100644 --- a/src/network/connectionchooser.py +++ b/src/network/connectionchooser.py @@ -1,21 +1,14 @@ -""" -Select which node to connect to -""" -# pylint: disable=too-many-branches -import logging import random # nosec import knownnodes import protocol import state from bmconfigparser import BMConfigParser +from debug import logger from queues import Queue, portCheckerQueue -logger = logging.getLogger('default') - def getDiscoveredPeer(): - """Get a peer from the local peer discovery list""" try: peer = random.choice(state.discoveredPeers.keys()) except (IndexError, KeyError): @@ -28,11 +21,10 @@ def getDiscoveredPeer(): def chooseConnection(stream): - """Returns an appropriate connection""" haveOnion = BMConfigParser().safeGet( "bitmessagesettings", "socksproxytype")[0:5] == 'SOCKS' - onionOnly = BMConfigParser().safeGetBoolean( - "bitmessagesettings", "onionservicesonly") + if state.trustedPeer: + return state.trustedPeer try: retval = portCheckerQueue.get(False) portCheckerQueue.task_done() @@ -46,23 +38,15 @@ def chooseConnection(stream): for _ in range(50): peer = random.choice(knownnodes.knownNodes[stream].keys()) try: - peer_info = knownnodes.knownNodes[stream][peer] - if peer_info.get('self'): - continue - rating = peer_info["rating"] + rating = knownnodes.knownNodes[stream][peer]['rating'] except TypeError: logger.warning('Error in %s', peer) rating = 0 if haveOnion: - # do not connect to raw IP addresses - # --keep all traffic within Tor overlay - if onionOnly and not peer.host.endswith('.onion'): - continue # onion addresses have a higher priority when SOCKS if peer.host.endswith('.onion') and rating > 0: rating = 1 - # TODO: need better check - elif not peer.host.startswith('bootstrap'): + else: encodedAddr = protocol.encodeHost(peer.host) # don't connect to local IPs when using SOCKS if not protocol.checkIPAddress(encodedAddr, False): diff --git a/src/network/connectionpool.py b/src/network/connectionpool.py index 6264191d..077f44a5 100644 --- a/src/network/connectionpool.py +++ b/src/network/connectionpool.py @@ -1,49 +1,27 @@ -""" -`BMConnectionPool` class definition -""" import errno -import logging import re import socket -import sys import time import asyncore_pollchoose as asyncore +import helper_bootstrap import helper_random import knownnodes import protocol import state from bmconfigparser import BMConfigParser from connectionchooser import chooseConnection -from node import Peer +from debug import logger from proxy import Proxy from singleton import Singleton from tcp import ( - bootstrap, Socks4aBMConnection, Socks5BMConnection, - TCPConnection, TCPServer) + TCPServer, Socks5BMConnection, Socks4aBMConnection, TCPConnection) from udp import UDPSocket -logger = logging.getLogger('default') - @Singleton class BMConnectionPool(object): """Pool of all existing connections""" - # pylint: disable=too-many-instance-attributes - - trustedPeer = None - """ - If the trustedpeer option is specified in keys.dat then this will - contain a Peer which will be connected to instead of using the - addresses advertised by other peers. - - The expected use case is where the user has a trusted server where - they run a Bitmessage daemon permanently. If they then run a second - instance of the client on a local machine periodically when they want - to check for messages it will sync with the network a lot faster - without compromising security. - """ - def __init__(self): asyncore.set_rates( BMConfigParser().safeGetInt( @@ -56,33 +34,9 @@ class BMConnectionPool(object): self.listeningSockets = {} self.udpSockets = {} self.streams = [] - self._lastSpawned = 0 - self._spawnWait = 2 - self._bootstrapped = False - - trustedPeer = BMConfigParser().safeGet( - 'bitmessagesettings', 'trustedpeer') - try: - if trustedPeer: - host, port = trustedPeer.split(':') - self.trustedPeer = Peer(host, int(port)) - except ValueError: - sys.exit( - 'Bad trustedpeer config setting! It should be set as' - ' trustedpeer=:' - ) - - def connections(self): - """ - Shortcut for combined list of connections from - `inboundConnections` and `outboundConnections` dicts - """ - return self.inboundConnections.values() + self.outboundConnections.values() - - def establishedConnections(self): - """Shortcut for list of connections having fullyEstablished == True""" - return [ - x for x in self.connections() if x.fullyEstablished] + self.lastSpawned = 0 + self.spawnWait = 2 + self.bootstrapped = False def connectToStream(self, streamNumber): """Connect to a bitmessage stream""" @@ -113,7 +67,10 @@ class BMConnectionPool(object): def isAlreadyConnected(self, nodeid): """Check if we're already connected to this peer""" - for i in self.connections(): + for i in ( + self.inboundConnections.values() + + self.outboundConnections.values() + ): try: if nodeid == i.nodeid: return True @@ -139,7 +96,7 @@ class BMConnectionPool(object): if isinstance(connection, UDPSocket): del self.udpSockets[connection.listening.host] elif isinstance(connection, TCPServer): - del self.listeningSockets[Peer( + del self.listeningSockets[state.Peer( connection.destination.host, connection.destination.port)] elif connection.isOutbound: try: @@ -156,8 +113,7 @@ class BMConnectionPool(object): pass connection.handle_close() - @staticmethod - def getListeningIP(): + def getListeningIP(self): """What IP are we supposed to be listening on?""" if BMConfigParser().safeGet( "bitmessagesettings", "onionhostname").endswith(".onion"): @@ -165,11 +121,10 @@ class BMConnectionPool(object): "bitmessagesettings", "onionbindip") else: host = '127.0.0.1' - if ( - BMConfigParser().safeGetBoolean("bitmessagesettings", "sockslisten") - or BMConfigParser().safeGet("bitmessagesettings", "socksproxytype") - == "none" - ): + if (BMConfigParser().safeGetBoolean( + "bitmessagesettings", "sockslisten") or + BMConfigParser().safeGet( + "bitmessagesettings", "socksproxytype") == "none"): # python doesn't like bind + INADDR_ANY? # host = socket.INADDR_ANY host = BMConfigParser().get("network", "bind") @@ -199,37 +154,8 @@ class BMConnectionPool(object): udpSocket = UDPSocket(host=bind, announcing=True) self.udpSockets[udpSocket.listening.host] = udpSocket - def startBootstrappers(self): - """Run the process of resolving bootstrap hostnames""" - proxy_type = BMConfigParser().safeGet( - 'bitmessagesettings', 'socksproxytype') - # A plugins may be added here - hostname = None - if not proxy_type or proxy_type == 'none': - connection_base = TCPConnection - elif proxy_type == 'SOCKS5': - connection_base = Socks5BMConnection - hostname = helper_random.randomchoice([ - 'quzwelsuziwqgpt2.onion', None - ]) - elif proxy_type == 'SOCKS4a': - connection_base = Socks4aBMConnection # FIXME: I cannot test - else: - # This should never happen because socksproxytype setting - # is handled in bitmessagemain before starting the connectionpool - return - - bootstrapper = bootstrap(connection_base) - if not hostname: - port = helper_random.randomchoice([8080, 8444]) - hostname = 'bootstrap%s.bitmessage.org' % port - else: - port = 8444 - self.addConnection(bootstrapper(hostname, port)) - - def loop(self): # pylint: disable=too-many-branches,too-many-statements + def loop(self): """Main Connectionpool's loop""" - # pylint: disable=too-many-locals # defaults to empty loop if outbound connections are maxed spawnConnections = False acceptConnections = True @@ -243,22 +169,18 @@ class BMConnectionPool(object): 'bitmessagesettings', 'socksproxytype', '') onionsocksproxytype = BMConfigParser().safeGet( 'bitmessagesettings', 'onionsocksproxytype', '') - if ( - socksproxytype[:5] == 'SOCKS' - and not BMConfigParser().safeGetBoolean( - 'bitmessagesettings', 'sockslisten') - and '.onion' not in BMConfigParser().safeGet( - 'bitmessagesettings', 'onionhostname', '') - ): + if (socksproxytype[:5] == 'SOCKS' and + not BMConfigParser().safeGetBoolean( + 'bitmessagesettings', 'sockslisten') and + '.onion' not in BMConfigParser().safeGet( + 'bitmessagesettings', 'onionhostname', '')): acceptConnections = False - # pylint: disable=too-many-nested-blocks if spawnConnections: if not knownnodes.knownNodesActual: - self.startBootstrappers() - knownnodes.knownNodesActual = True - if not self._bootstrapped: - self._bootstrapped = True + helper_bootstrap.dns() + if not self.bootstrapped: + self.bootstrapped = True Proxy.proxy = ( BMConfigParser().safeGet( 'bitmessagesettings', 'sockshostname'), @@ -287,7 +209,7 @@ class BMConnectionPool(object): for i in range( state.maximumNumberOfHalfOpenConnections - pending): try: - chosen = self.trustedPeer or chooseConnection( + chosen = chooseConnection( helper_random.randomchoice(self.streams)) except ValueError: continue @@ -298,22 +220,10 @@ class BMConnectionPool(object): # don't connect to self if chosen in state.ownAddresses: continue - # don't connect to the hosts from the same - # network group, defense against sibyl attacks - host_network_group = protocol.network_group( - chosen.host) - same_group = False - for j in self.outboundConnections.values(): - if host_network_group == j.network_group: - same_group = True - if chosen.host == j.destination.host: - knownnodes.decreaseRating(chosen) - break - if same_group: - continue try: - if chosen.host.endswith(".onion") and Proxy.onion_proxy: + if (chosen.host.endswith(".onion") and + Proxy.onion_proxy is not None): if onionsocksproxytype == "SOCKS5": self.addConnection(Socks5BMConnection(chosen)) elif onionsocksproxytype == "SOCKS4a": @@ -328,9 +238,12 @@ class BMConnectionPool(object): if e.errno == errno.ENETUNREACH: continue - self._lastSpawned = time.time() + self.lastSpawned = time.time() else: - for i in self.connections(): + for i in ( + self.inboundConnections.values() + + self.outboundConnections.values() + ): # FIXME: rating will be increased after next connection i.handle_close() @@ -340,7 +253,7 @@ class BMConnectionPool(object): self.startListening() else: for bind in re.sub( - r'[^\w.]+', ' ', + "[^\w.]+", " ", BMConfigParser().safeGet('network', 'bind') ).split(): self.startListening(bind) @@ -350,7 +263,7 @@ class BMConnectionPool(object): self.startUDPSocket() else: for bind in re.sub( - r'[^\w.]+', ' ', + "[^\w.]+", " ", BMConfigParser().safeGet('network', 'bind') ).split(): self.startUDPSocket(bind) @@ -368,13 +281,16 @@ class BMConnectionPool(object): i.accepting = i.connecting = i.connected = False logger.info('Stopped udp sockets.') - loopTime = float(self._spawnWait) - if self._lastSpawned < time.time() - self._spawnWait: + loopTime = float(self.spawnWait) + if self.lastSpawned < time.time() - self.spawnWait: loopTime = 2.0 asyncore.loop(timeout=loopTime, count=1000) reaper = [] - for i in self.connections(): + for i in ( + self.inboundConnections.values() + + self.outboundConnections.values() + ): minTx = time.time() - 20 if i.fullyEstablished: minTx -= 300 - 20 @@ -386,8 +302,10 @@ class BMConnectionPool(object): time.time() - i.lastTx) i.set_state("close") for i in ( - self.connections() - + self.listeningSockets.values() + self.udpSockets.values() + self.inboundConnections.values() + + self.outboundConnections.values() + + self.listeningSockets.values() + + self.udpSockets.values() ): if not (i.accepting or i.connecting or i.connected): reaper.append(i) diff --git a/src/network/constants.py b/src/network/constants.py deleted file mode 100644 index f8f4120f..00000000 --- a/src/network/constants.py +++ /dev/null @@ -1,17 +0,0 @@ -""" -Network protocol constants -""" - - -#: address is online if online less than this many seconds ago -ADDRESS_ALIVE = 10800 -#: protocol specification says max 1000 addresses in one addr command -MAX_ADDR_COUNT = 1000 -#: ~1.6 MB which is the maximum possible size of an inv message. -MAX_MESSAGE_SIZE = 1600100 -#: 2**18 = 256kB is the maximum size of an object payload -MAX_OBJECT_PAYLOAD_SIZE = 2**18 -#: protocol specification says max 50000 objects in one inv command -MAX_OBJECT_COUNT = 50000 -#: maximum time offset -MAX_TIME_OFFSET = 3600 diff --git a/src/network/dandelion.py b/src/network/dandelion.py index 03f45bd7..2678ca57 100644 --- a/src/network/dandelion.py +++ b/src/network/dandelion.py @@ -1,14 +1,11 @@ -""" -Dandelion class definition, tracks stages -""" -import logging from collections import namedtuple -from random import choice, expovariate, sample +from random import choice, sample, expovariate from threading import RLock from time import time import connectionpool import state +from debug import logging from queues import invQueue from singleton import Singleton @@ -23,11 +20,9 @@ MAX_STEMS = 2 Stem = namedtuple('Stem', ['child', 'stream', 'timeout']) -logger = logging.getLogger('default') - @Singleton -class Dandelion: # pylint: disable=old-style-class +class Dandelion(): """Dandelion class for tracking stem/fluff stages.""" def __init__(self): # currently assignable child stems @@ -40,8 +35,7 @@ class Dandelion: # pylint: disable=old-style-class self.refresh = time() + REASSIGN_INTERVAL self.lock = RLock() - @staticmethod - def poissonTimeout(start=None, average=0): + def poissonTimeout(self, start=None, average=0): """Generate deadline using Poisson distribution""" if start is None: start = time() @@ -73,10 +67,9 @@ class Dandelion: # pylint: disable=old-style-class def removeHash(self, hashId, reason="no reason specified"): """Switch inventory vector from stem to fluff mode""" - if logger.isEnabledFor(logging.DEBUG): - logger.debug( - '%s entering fluff mode due to %s.', - ''.join('%02x' % ord(i) for i in hashId), reason) + logging.debug( + "%s entering fluff mode due to %s.", + ''.join('%02x' % ord(i) for i in hashId), reason) with self.lock: try: del self.hashMap[hashId] @@ -104,8 +97,8 @@ class Dandelion: # pylint: disable=old-style-class for k in (k for k, v in self.nodeMap.iteritems() if v is None): self.nodeMap[k] = connection for k, v in { - k: v for k, v in self.hashMap.iteritems() - if v.child is None + k: v for k, v in self.hashMap.iteritems() + if v.child is None }.iteritems(): self.hashMap[k] = Stem( connection, v.stream, self.poissonTimeout()) @@ -122,13 +115,12 @@ class Dandelion: # pylint: disable=old-style-class self.stem.remove(connection) # active mappings to pointing to the removed node for k in ( - k for k, v in self.nodeMap.iteritems() - if v == connection + k for k, v in self.nodeMap.iteritems() if v == connection ): self.nodeMap[k] = None for k, v in { - k: v for k, v in self.hashMap.iteritems() - if v.child == connection + k: v for k, v in self.hashMap.iteritems() + if v.child == connection }.iteritems(): self.hashMap[k] = Stem( None, v.stream, self.poissonTimeout()) diff --git a/src/network/downloadthread.py b/src/network/downloadthread.py index 0ae83b5b..babce5da 100644 --- a/src/network/downloadthread.py +++ b/src/network/downloadthread.py @@ -1,20 +1,18 @@ -""" -`DownloadThread` class definition -""" +import threading import time import addresses import helper_random import protocol from dandelion import Dandelion +from debug import logger +from helper_threading import StoppableThread from inventory import Inventory from network.connectionpool import BMConnectionPool from objectracker import missingObjects -from threads import StoppableThread -class DownloadThread(StoppableThread): - """Thread-based class for downloading from connections""" +class DownloadThread(threading.Thread, StoppableThread): minPending = 200 maxRequestChunk = 1000 requestTimeout = 60 @@ -22,16 +20,16 @@ class DownloadThread(StoppableThread): requestExpires = 3600 def __init__(self): - super(DownloadThread, self).__init__(name="Downloader") + threading.Thread.__init__(self, name="Downloader") + self.initStop() + self.name = "Downloader" + logger.info("init download thread") self.lastCleaned = time.time() def cleanPending(self): - """Expire pending downloads eventually""" - deadline = time.time() - self.requestExpires + deadline = time.time() - DownloadThread.requestExpires try: - toDelete = [ - k for k, v in missingObjects.iteritems() - if v < deadline] + toDelete = [k for k, v in missingObjects.iteritems() if v < deadline] except RuntimeError: pass else: @@ -43,12 +41,12 @@ class DownloadThread(StoppableThread): while not self._stopped: requested = 0 # Choose downloading peers randomly - connections = BMConnectionPool().establishedConnections() + connections = [x for x in BMConnectionPool().inboundConnections.values() + BMConnectionPool().outboundConnections.values() if x.fullyEstablished] helper_random.randomshuffle(connections) - requestChunk = max(int( - min(self.maxRequestChunk, len(missingObjects)) - / len(connections)), 1) if connections else 1 - + try: + requestChunk = max(int(min(DownloadThread.maxRequestChunk, len(missingObjects)) / len(connections)), 1) + except ZeroDivisionError: + requestChunk = 1 for i in connections: now = time.time() # avoid unnecessary delay @@ -74,11 +72,9 @@ class DownloadThread(StoppableThread): continue payload[0:0] = addresses.encodeVarint(chunkCount) i.append_write_buf(protocol.CreatePacket('getdata', payload)) - self.logger.debug( - '%s:%i Requesting %i objects', - i.destination.host, i.destination.port, chunkCount) + logger.debug("%s:%i Requesting %i objects", i.destination.host, i.destination.port, chunkCount) requested += chunkCount - if time.time() >= self.lastCleaned + self.cleanInterval: + if time.time() >= self.lastCleaned + DownloadThread.cleanInterval: self.cleanPending() if not requested: self.stop.wait(1) diff --git a/src/network/http_old.py b/src/network/http-old.py similarity index 80% rename from src/network/http_old.py rename to src/network/http-old.py index 64d09983..56d24915 100644 --- a/src/network/http_old.py +++ b/src/network/http-old.py @@ -8,7 +8,6 @@ duration = 60 class HTTPClient(asyncore.dispatcher): - """An asyncore dispatcher""" port = 12345 def __init__(self, host, path, connect=True): @@ -20,33 +19,31 @@ class HTTPClient(asyncore.dispatcher): 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) +# print self.recv(8192) self.recv(8192) def writable(self): - return len(self.buffer) > 0 + 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: + 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) + asyncore.loop(count=len(asyncore.socket_map)/2) if requestCount % 100 == 0: print "Processed %i total messages" % (requestCount) diff --git a/src/network/http.py b/src/network/http.py index 8bba38ac..55cb81a1 100644 --- a/src/network/http.py +++ b/src/network/http.py @@ -2,17 +2,15 @@ import socket from advanceddispatcher import AdvancedDispatcher import asyncore_pollchoose as asyncore -from proxy import ProxyError -from socks5 import Socks5Connection, Socks5Resolver -from socks4a import Socks4aConnection, Socks4aResolver +from proxy import Proxy, ProxyError, GeneralProxyError +from socks5 import Socks5Connection, Socks5Resolver, Socks5AuthError, Socks5Error +from socks4a import Socks4aConnection, Socks4aResolver, Socks4aError - -class HttpError(ProxyError): - pass +class HttpError(ProxyError): pass class HttpConnection(AdvancedDispatcher): - def __init__(self, host, path="/"): # pylint: disable=redefined-outer-name + def __init__(self, host, path="/"): AdvancedDispatcher.__init__(self) self.path = path self.destination = (host, 80) @@ -21,15 +19,13 @@ class HttpConnection(AdvancedDispatcher): 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)) + self.append_write_buf("GET %s HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n" % (self.path, self.destination[0])) + print "Sending %ib" % (len(self.write_buf)) self.set_state("http_request_sent", 0) return False def state_http_request_sent(self): - if self.read_buf: + if len(self.read_buf) > 0: print "Received %ib" % (len(self.read_buf)) self.read_buf = b"" if not self.connected: @@ -38,7 +34,7 @@ class HttpConnection(AdvancedDispatcher): class Socks5HttpConnection(Socks5Connection, HttpConnection): - def __init__(self, host, path="/"): # pylint: disable=super-init-not-called, redefined-outer-name + def __init__(self, host, path="/"): self.path = path Socks5Connection.__init__(self, address=(host, 80)) @@ -48,7 +44,7 @@ class Socks5HttpConnection(Socks5Connection, HttpConnection): class Socks4aHttpConnection(Socks4aConnection, HttpConnection): - def __init__(self, host, path="/"): # pylint: disable=super-init-not-called, redefined-outer-name + def __init__(self, host, path="/"): Socks4aConnection.__init__(self, address=(host, 80)) self.path = path @@ -59,31 +55,32 @@ class Socks4aHttpConnection(Socks4aConnection, HttpConnection): if __name__ == "__main__": # initial fill + for host in ("bootstrap8080.bitmessage.org", "bootstrap8444.bitmessage.org"): proxy = Socks5Resolver(host=host) - while asyncore.socket_map: + while len(asyncore.socket_map) > 0: 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: + while len(asyncore.socket_map) > 0: print "loop %s, len %i" % (proxy.state, len(asyncore.socket_map)) asyncore.loop(timeout=1, count=1) proxy.resolved() for host in ("bitmessage.org",): direct = HttpConnection(host) - while asyncore.socket_map: - # print "loop, state = %s" % (direct.state) + while len(asyncore.socket_map) > 0: +# print "loop, state = %s" % (direct.state) asyncore.loop(timeout=1, count=1) proxy = Socks5HttpConnection(host) - while asyncore.socket_map: - # print "loop, state = %s" % (proxy.state) + while len(asyncore.socket_map) > 0: +# print "loop, state = %s" % (proxy.state) asyncore.loop(timeout=1, count=1) proxy = Socks4aHttpConnection(host) - while asyncore.socket_map: - # print "loop, state = %s" % (proxy.state) + while len(asyncore.socket_map) > 0: +# print "loop, state = %s" % (proxy.state) asyncore.loop(timeout=1, count=1) diff --git a/src/network/httpd.py b/src/network/httpd.py index b69ffa99..b8b6ba21 100644 --- a/src/network/httpd.py +++ b/src/network/httpd.py @@ -1,34 +1,28 @@ -""" -src/network/httpd.py -======================= -""" import asyncore import socket from tls import TLSHandshake - class HTTPRequestHandler(asyncore.dispatcher): - """Handling HTTP request""" response = """HTTP/1.0 200 OK\r - Date: Sun, 23 Oct 2016 18:02:00 GMT\r - Content-Type: text/html; charset=UTF-8\r - Content-Encoding: UTF-8\r - Content-Length: 136\r - Last-Modified: Wed, 08 Jan 2003 23:11:55 GMT\r - Server: Apache/1.3.3.7 (Unix) (Red-Hat/Linux)\r - ETag: "3f80f-1b6-3e1cb03b"\r - Accept-Ranges: bytes\r - Connection: close\r - \r - - - An Example Page - - - Hello World, this is a very simple HTML document. - - """ +Date: Sun, 23 Oct 2016 18:02:00 GMT\r +Content-Type: text/html; charset=UTF-8\r +Content-Encoding: UTF-8\r +Content-Length: 136\r +Last-Modified: Wed, 08 Jan 2003 23:11:55 GMT\r +Server: Apache/1.3.3.7 (Unix) (Red-Hat/Linux)\r +ETag: "3f80f-1b6-3e1cb03b"\r +Accept-Ranges: bytes\r +Connection: close\r +\r + + + An Example Page + + + Hello World, this is a very simple HTML document. + +""" def __init__(self, sock): if not hasattr(self, '_map'): @@ -68,17 +62,11 @@ class HTTPRequestHandler(asyncore.dispatcher): class HTTPSRequestHandler(HTTPRequestHandler, TLSHandshake): - """Handling HTTPS request""" def __init__(self, sock): if not hasattr(self, '_map'): - asyncore.dispatcher.__init__(self, sock) # pylint: disable=non-parent-init-called - # self.tlsDone = False - TLSHandshake.__init__( - self, - sock=sock, - certfile='/home/shurdeek/src/PyBitmessage/src/sslkeys/cert.pem', - keyfile='/home/shurdeek/src/PyBitmessage/src/sslkeys/key.pem', - server_side=True) + asyncore.dispatcher.__init__(self, sock) +# self.tlsDone = False + TLSHandshake.__init__(self, sock=sock, certfile='/home/shurdeek/src/PyBitmessage/src/sslkeys/cert.pem', keyfile='/home/shurdeek/src/PyBitmessage/src/sslkeys/key.pem', server_side=True) HTTPRequestHandler.__init__(self, sock) def handle_connect(self): @@ -93,7 +81,8 @@ class HTTPSRequestHandler(HTTPRequestHandler, TLSHandshake): def readable(self): if self.tlsDone: return HTTPRequestHandler.readable(self) - return TLSHandshake.readable(self) + else: + return TLSHandshake.readable(self) def handle_read(self): if self.tlsDone: @@ -104,7 +93,8 @@ class HTTPSRequestHandler(HTTPRequestHandler, TLSHandshake): def writable(self): if self.tlsDone: return HTTPRequestHandler.writable(self) - return TLSHandshake.writable(self) + else: + return TLSHandshake.writable(self) def handle_write(self): if self.tlsDone: @@ -114,7 +104,6 @@ class HTTPSRequestHandler(HTTPRequestHandler, TLSHandshake): class HTTPServer(asyncore.dispatcher): - """Handling HTTP Server""" port = 12345 def __init__(self): @@ -130,15 +119,14 @@ class HTTPServer(asyncore.dispatcher): pair = self.accept() if pair is not None: sock, addr = pair - # print 'Incoming connection from %s' % repr(addr) +# print 'Incoming connection from %s' % repr(addr) self.connections += 1 - # if self.connections % 1000 == 0: - # print "Processed %i connections, active %i" % (self.connections, len(asyncore.socket_map)) +# if self.connections % 1000 == 0: +# print "Processed %i connections, active %i" % (self.connections, len(asyncore.socket_map)) HTTPRequestHandler(sock) class HTTPSServer(HTTPServer): - """Handling HTTPS Server""" port = 12345 def __init__(self): @@ -149,13 +137,12 @@ class HTTPSServer(HTTPServer): pair = self.accept() if pair is not None: sock, addr = pair - # print 'Incoming connection from %s' % repr(addr) +# print 'Incoming connection from %s' % repr(addr) self.connections += 1 - # if self.connections % 1000 == 0: - # print "Processed %i connections, active %i" % (self.connections, len(asyncore.socket_map)) +# if self.connections % 1000 == 0: +# print "Processed %i connections, active %i" % (self.connections, len(asyncore.socket_map)) HTTPSRequestHandler(sock) - if __name__ == "__main__": client = HTTPSServer() asyncore.loop() diff --git a/src/network/https.py b/src/network/https.py index a7b8b57c..151efcb8 100644 --- a/src/network/https.py +++ b/src/network/https.py @@ -1,18 +1,10 @@ import asyncore from http import HTTPClient +import paths from tls import TLSHandshake -""" -self.sslSock = ssl.wrap_socket( - self.sock, - keyfile=os.path.join(paths.codePath(), 'sslkeys', 'key.pem'), - certfile=os.path.join(paths.codePath(), 'sslkeys', 'cert.pem'), - server_side=not self.initiatedConnection, - ssl_version=ssl.PROTOCOL_TLSv1, - do_handshake_on_connect=False, - ciphers='AECDH-AES256-SHA') -""" +# self.sslSock = ssl.wrap_socket(self.sock, keyfile = os.path.join(paths.codePath(), 'sslkeys', 'key.pem'), certfile = os.path.join(paths.codePath(), 'sslkeys', 'cert.pem'), server_side = not self.initiatedConnection, ssl_version=ssl.PROTOCOL_TLSv1, do_handshake_on_connect=False, ciphers='AECDH-AES256-SHA') class HTTPSClient(HTTPClient, TLSHandshake): @@ -20,15 +12,7 @@ class HTTPSClient(HTTPClient, TLSHandshake): if not hasattr(self, '_map'): asyncore.dispatcher.__init__(self) self.tlsDone = False - """ - TLSHandshake.__init__( - self, - address=(host, 443), - certfile='/home/shurdeek/src/PyBitmessage/sslsrc/keys/cert.pem', - keyfile='/home/shurdeek/src/PyBitmessage/src/sslkeys/key.pem', - server_side=False, - ciphers='AECDH-AES256-SHA') - """ +# TLSHandshake.__init__(self, address=(host, 443), certfile='/home/shurdeek/src/PyBitmessage/sslsrc/keys/cert.pem', keyfile='/home/shurdeek/src/PyBitmessage/src/sslkeys/key.pem', server_side=False, ciphers='AECDH-AES256-SHA') HTTPClient.__init__(self, host, path, connect=False) TLSHandshake.__init__(self, address=(host, 443), server_side=False) @@ -65,7 +49,6 @@ class HTTPSClient(HTTPClient, TLSHandshake): else: TLSHandshake.handle_write(self) - if __name__ == "__main__": client = HTTPSClient('anarchy.economicsofbitcoin.com', '/') asyncore.loop() diff --git a/src/network/invthread.py b/src/network/invthread.py index e68b7692..6f6f1364 100644 --- a/src/network/invthread.py +++ b/src/network/invthread.py @@ -1,17 +1,16 @@ -""" -Thread to send inv annoucements -""" import Queue -import random +from random import randint, shuffle +import threading from time import time import addresses -import protocol -import state +from bmconfigparser import BMConfigParser +from helper_threading import StoppableThread from network.connectionpool import BMConnectionPool from network.dandelion import Dandelion from queues import invQueue -from threads import StoppableThread +import protocol +import state def handleExpiredDandelion(expired): @@ -19,7 +18,9 @@ def handleExpiredDandelion(expired): the object""" if not expired: return - for i in BMConnectionPool().connections(): + for i in \ + BMConnectionPool().inboundConnections.values() + \ + BMConnectionPool().outboundConnections.values(): if not i.fullyEstablished: continue for x in expired: @@ -32,23 +33,23 @@ def handleExpiredDandelion(expired): i.objectsNewToThem[hashid] = time() -class InvThread(StoppableThread): - """Main thread that sends inv annoucements""" +class InvThread(threading.Thread, StoppableThread): + def __init__(self): + threading.Thread.__init__(self, name="InvBroadcaster") + self.initStop() + self.name = "InvBroadcaster" - name = "InvBroadcaster" - - @staticmethod - def handleLocallyGenerated(stream, hashId): - """Locally generated inventory items require special handling""" + def handleLocallyGenerated(self, stream, hashId): Dandelion().addHash(hashId, stream=stream) - for connection in BMConnectionPool().connections(): - if state.dandelion and connection != \ - Dandelion().objectChildStem(hashId): - continue - connection.objectsNewToThem[hashId] = time() + for connection in \ + BMConnectionPool().inboundConnections.values() + \ + BMConnectionPool().outboundConnections.values(): + if state.dandelion and connection != Dandelion().objectChildStem(hashId): + continue + connection.objectsNewToThem[hashId] = time() - def run(self): # pylint: disable=too-many-branches - while not state.shutdown: # pylint: disable=too-many-nested-blocks + def run(self): + while not state.shutdown: chunk = [] while True: # Dandelion fluff trigger by expiration @@ -63,7 +64,8 @@ class InvThread(StoppableThread): break if chunk: - for connection in BMConnectionPool().connections(): + for connection in BMConnectionPool().inboundConnections.values() + \ + BMConnectionPool().outboundConnections.values(): fluffs = [] stems = [] for inv in chunk: @@ -78,7 +80,7 @@ class InvThread(StoppableThread): 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) >= state.dandelion: + if 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: @@ -89,20 +91,16 @@ class InvThread(StoppableThread): fluffs.append(inv[1]) if fluffs: - random.shuffle(fluffs) - connection.append_write_buf(protocol.CreatePacket( - 'inv', - addresses.encodeVarint( - len(fluffs)) + ''.join(fluffs))) + shuffle(fluffs) + connection.append_write_buf(protocol.CreatePacket('inv', \ + addresses.encodeVarint(len(fluffs)) + "".join(fluffs))) if stems: - random.shuffle(stems) - connection.append_write_buf(protocol.CreatePacket( - 'dinv', - addresses.encodeVarint( - len(stems)) + ''.join(stems))) + shuffle(stems) + connection.append_write_buf(protocol.CreatePacket('dinv', \ + addresses.encodeVarint(len(stems)) + "".join(stems))) invQueue.iterate() - for _ in range(len(chunk)): + for i in range(len(chunk)): invQueue.task_done() if Dandelion().refresh < time(): diff --git a/src/network/networkthread.py b/src/network/networkthread.py index 61ff6c09..9ceb856b 100644 --- a/src/network/networkthread.py +++ b/src/network/networkthread.py @@ -1,16 +1,19 @@ -""" -A thread to handle network concerns -""" +import threading + import network.asyncore_pollchoose as asyncore import state +from debug import logger +from helper_threading import StoppableThread from network.connectionpool import BMConnectionPool from queues import excQueue -from threads import StoppableThread -class BMNetworkThread(StoppableThread): - """Main network thread""" - name = "Asyncore" +class BMNetworkThread(threading.Thread, StoppableThread): + def __init__(self): + threading.Thread.__init__(self, name="Asyncore") + self.initStop() + self.name = "Asyncore" + logger.info("init asyncore thread") def run(self): try: diff --git a/src/network/node.py b/src/network/node.py index 4c532b81..ab9f5fbe 100644 --- a/src/network/node.py +++ b/src/network/node.py @@ -1,7 +1,3 @@ -""" -Named tuples representing the network peers -""" import collections -Peer = collections.namedtuple('Peer', ['host', 'port']) Node = collections.namedtuple('Node', ['services', 'host', 'port']) diff --git a/src/network/objectracker.py b/src/network/objectracker.py index ca29c023..f119b2d8 100644 --- a/src/network/objectracker.py +++ b/src/network/objectracker.py @@ -1,6 +1,3 @@ -""" -Module for tracking objects -""" import time from threading import RLock @@ -30,7 +27,6 @@ missingObjects = {} class ObjectTracker(object): - """Object tracker mixin""" invCleanPeriod = 300 invInitialCapacity = 50000 invErrorRate = 0.03 @@ -46,47 +42,37 @@ class ObjectTracker(object): self.lastCleaned = time.time() def initInvBloom(self): - """Init bloom filter for tracking. WIP.""" if haveBloom: # lock? - self.invBloom = BloomFilter( - capacity=ObjectTracker.invInitialCapacity, - error_rate=ObjectTracker.invErrorRate) + self.invBloom = BloomFilter(capacity=ObjectTracker.invInitialCapacity, + error_rate=ObjectTracker.invErrorRate) def initAddrBloom(self): - """Init bloom filter for tracking addrs, WIP. - This either needs to be moved to addrthread.py or removed.""" if haveBloom: # lock? - self.addrBloom = BloomFilter( - capacity=ObjectTracker.invInitialCapacity, - error_rate=ObjectTracker.invErrorRate) + self.addrBloom = BloomFilter(capacity=ObjectTracker.invInitialCapacity, + error_rate=ObjectTracker.invErrorRate) def clean(self): - """Clean up tracking to prevent memory bloat""" if self.lastCleaned < time.time() - ObjectTracker.invCleanPeriod: if haveBloom: - if missingObjects == 0: + if len(missingObjects) == 0: self.initInvBloom() self.initAddrBloom() else: # release memory deadline = time.time() - ObjectTracker.trackingExpires with self.objectsNewToThemLock: - self.objectsNewToThem = { - k: v - for k, v in self.objectsNewToThem.iteritems() - if v >= deadline} + self.objectsNewToThem = {k: v for k, v in self.objectsNewToThem.iteritems() if v >= deadline} self.lastCleaned = time.time() def hasObj(self, hashid): - """Do we already have object?""" if haveBloom: return hashid in self.invBloom - return hashid in self.objectsNewToMe + else: + return hashid in self.objectsNewToMe def handleReceivedInventory(self, hashId): - """Handling received inventory""" if haveBloom: self.invBloom.add(hashId) try: @@ -99,20 +85,18 @@ class ObjectTracker(object): self.objectsNewToMe[hashId] = True def handleReceivedObject(self, streamNumber, hashid): - """Handling received object""" - for i in network.connectionpool.BMConnectionPool().connections(): + for i in network.connectionpool.BMConnectionPool().inboundConnections.values() + network.connectionpool.BMConnectionPool().outboundConnections.values(): if not i.fullyEstablished: continue try: del i.objectsNewToMe[hashid] except KeyError: - if streamNumber in i.streams and ( - not Dandelion().hasHash(hashid) or - Dandelion().objectChildStem(hashid) == i): + if streamNumber in i.streams and \ + (not Dandelion().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 + # update stream number, which we didn't have when we just received the dinv # also resets expiration of the stem mode Dandelion().setHashStream(hashid, streamNumber) @@ -125,12 +109,23 @@ class ObjectTracker(object): self.objectsNewToMe.setLastObject() def hasAddr(self, addr): - """WIP, should be moved to addrthread.py or removed""" if haveBloom: return addr in self.invBloom - return None def addAddr(self, hashid): - """WIP, should be moved to addrthread.py or removed""" if haveBloom: self.addrBloom.add(hashid) + +# addr sending -> per node upload queue, and flush every minute or so +# inv sending -> if not in bloom, inv immediately, otherwise put into a per node upload queue and flush every minute or so +# data sending -> a simple queue + +# no bloom +# - if inv arrives +# - if we don't have it, add tracking and download queue +# - if we do have it, remove from tracking +# tracking downloads +# - per node hash of items the node has but we don't +# tracking inv +# - per node hash of items that neither the remote node nor we have +# diff --git a/src/network/proxy.py b/src/network/proxy.py index 38676d66..04f0ac13 100644 --- a/src/network/proxy.py +++ b/src/network/proxy.py @@ -1,17 +1,11 @@ -""" -Set proxy if avaiable otherwise exception -""" -# pylint: disable=protected-access -import logging import socket import time import asyncore_pollchoose as asyncore +import state from advanceddispatcher import AdvancedDispatcher from bmconfigparser import BMConfigParser -from node import Peer - -logger = logging.getLogger('default') +from debug import logger class ProxyError(Exception): @@ -62,7 +56,7 @@ class Proxy(AdvancedDispatcher): 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[0], str) or not isinstance(address[1], int)): raise ValueError self.__class__._proxy = address @@ -89,10 +83,9 @@ class Proxy(AdvancedDispatcher): def onion_proxy(self, address): """Set onion proxy address""" if address is not None and ( - not isinstance(address, tuple) or len(address) < 2 - or not isinstance(address[0], str) - or not isinstance(address[1], int) - ): + not isinstance(address, tuple) or len(address) < 2 or + not isinstance(address[0], str) or + not isinstance(address[1], int)): raise ValueError self.__class__._onion_proxy = address @@ -107,13 +100,12 @@ class Proxy(AdvancedDispatcher): self.__class__._onion_auth = authTuple def __init__(self, address): - if not isinstance(address, Peer): + if not isinstance(address, state.Peer): raise ValueError AdvancedDispatcher.__init__(self) self.destination = address self.isOutbound = True self.fullyEstablished = False - self.connectedAt = 0 self.create_socket(socket.AF_INET, socket.SOCK_STREAM) if BMConfigParser().safeGetBoolean( "bitmessagesettings", "socksauthentication"): @@ -121,7 +113,8 @@ class Proxy(AdvancedDispatcher): BMConfigParser().safeGet( "bitmessagesettings", "socksusername"), BMConfigParser().safeGet( - "bitmessagesettings", "sockspassword")) + "bitmessagesettings", "sockspassword") + ) else: self.auth = None self.connect( @@ -145,6 +138,5 @@ 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/network/receivequeuethread.py b/src/network/receivequeuethread.py index bf1d8300..0a7562cb 100644 --- a/src/network/receivequeuethread.py +++ b/src/network/receivequeuethread.py @@ -1,22 +1,28 @@ -""" -Process data incoming from network -""" import errno import Queue import socket +import sys +import threading +import time -import state -from network.advanceddispatcher import UnknownStateError +import addresses +from bmconfigparser import BMConfigParser +from debug import logger +from helper_threading import StoppableThread +from inventory import Inventory from network.connectionpool import BMConnectionPool +from network.bmproto import BMProto +from network.advanceddispatcher import UnknownStateError from queues import receiveDataQueue -from threads import StoppableThread +import protocol +import state - -class ReceiveQueueThread(StoppableThread): - """This thread processes data received from the network - (which is done by the asyncore thread)""" +class ReceiveQueueThread(threading.Thread, StoppableThread): def __init__(self, num=0): - super(ReceiveQueueThread, self).__init__(name="ReceiveQueue_%i" % num) + threading.Thread.__init__(self, name="ReceiveQueue_%i" %(num)) + self.initStop() + self.name = "ReceiveQueue_%i" % (num) + logger.info("init receive queue thread %i", num) def run(self): while not self._stopped and state.shutdown == 0: @@ -29,28 +35,27 @@ class ReceiveQueueThread(StoppableThread): break # cycle as long as there is data - # methods should return False if there isn't enough data, - # or the connection is to be aborted + # methods should return False if there isn't enough data, or the connection is to be aborted - # state_* methods should return False if there isn't - # enough data, or the connection is to be aborted + # state_* methods should return False if there isn't enough data, + # or the connection is to be aborted try: connection = BMConnectionPool().getConnectionByAddr(dest) - # connection object not found + # KeyError = connection object not found except KeyError: receiveDataQueue.task_done() continue try: connection.process() - # state isn't implemented - except UnknownStateError: + # UnknownStateError = state isn't implemented + except (UnknownStateError): pass except socket.error as err: if err.errno == errno.EBADF: connection.set_state("close", 0) else: - self.logger.error('Socket error: %s', err) + logger.error("Socket error: %s", str(err)) except: - self.logger.error('Error processing', exc_info=True) + logger.error("Error processing", exc_info=True) receiveDataQueue.task_done() diff --git a/src/network/socks4a.py b/src/network/socks4a.py index 0d4310bc..bdf59944 100644 --- a/src/network/socks4a.py +++ b/src/network/socks4a.py @@ -1,11 +1,7 @@ -""" -SOCKS4a proxy module -""" -# pylint: disable=attribute-defined-outside-init import socket import struct -from proxy import GeneralProxyError, Proxy, ProxyError +from proxy import Proxy, ProxyError, GeneralProxyError class Socks4aError(ProxyError): @@ -86,7 +82,7 @@ class Socks4aConnection(Socks4a): self.append_write_buf(self.ipaddr) except socket.error: # Well it's not an IP number, so it's probably a DNS name. - if self._remote_dns: + if Proxy._remote_dns: # Resolve remotely rmtrslv = True self.ipaddr = None @@ -122,7 +118,6 @@ class Socks4aResolver(Socks4a): Socks4a.__init__(self, address=(self.host, self.port)) def state_auth_done(self): - """Request connection to be made""" # Now we can request the actual connection self.append_write_buf( struct.pack('>BBH', 0x04, 0xF0, self.destination[1])) diff --git a/src/network/socks5.py b/src/network/socks5.py index fc33f4df..2e0821da 100644 --- a/src/network/socks5.py +++ b/src/network/socks5.py @@ -1,12 +1,13 @@ """ -SOCKS5 proxy module +src/network/socks5.py +===================== + """ # pylint: disable=attribute-defined-outside-init import socket import struct -from node import Peer from proxy import GeneralProxyError, Proxy, ProxyError @@ -65,9 +66,10 @@ class Socks5(Proxy): elif ret[1] == 2: # username/password self.append_write_buf( - struct.pack( - 'BB', 1, len(self._auth[0])) + self._auth[0] + struct.pack( - 'B', len(self._auth[1])) + self._auth[1]) + struct.pack('BB', 1, len(self._auth[0])) + + self._auth[0] + struct.pack('B', len(self._auth[1])) + + self._auth[1] + ) self.set_state("auth_needed", length=2, expectBytes=2) else: if ret[1] == 0xff: @@ -153,13 +155,15 @@ class Socks5(Proxy): return True def proxy_sock_name(self): - """Handle return value when using SOCKS5 - for DNS resolving instead of connecting.""" + """Handle return value when using SOCKS5 for DNS resolving instead of connecting.""" return socket.inet_ntoa(self.__proxysockname[0]) class Socks5Connection(Socks5): """Child socks5 class used for making outbound connections.""" + def __init__(self, address): + Socks5.__init__(self, address=address) + def state_auth_done(self): """Request connection to be made""" # Now we can request the actual connection @@ -169,13 +173,16 @@ class Socks5Connection(Socks5): try: self.ipaddr = socket.inet_aton(self.destination[0]) self.append_write_buf(chr(0x01).encode() + self.ipaddr) - except socket.error: # may be IPv6! + except socket.error: # Well it's not an IP number, so it's probably a DNS name. - if self._remote_dns: + if Proxy._remote_dns: # pylint: disable=protected-access # Resolve remotely self.ipaddr = None - self.append_write_buf(chr(0x03).encode() + chr( - len(self.destination[0])).encode() + self.destination[0]) + self.append_write_buf( + chr(0x03).encode() + + chr(len(self.destination[0])).encode() + + self.destination[0] + ) else: # Resolve locally self.ipaddr = socket.inet_aton( @@ -199,14 +206,16 @@ class Socks5Resolver(Socks5): def __init__(self, host): self.host = host self.port = 8444 - Socks5.__init__(self, address=Peer(self.host, self.port)) + Socks5.__init__(self, address=(self.host, self.port)) def state_auth_done(self): """Perform resolving""" # Now we can request the actual connection self.append_write_buf(struct.pack('BBB', 0x05, 0xF0, 0x00)) - self.append_write_buf(chr(0x03).encode() + chr( - len(self.host)).encode() + str(self.host)) + self.append_write_buf( + chr(0x03).encode() + chr(len(self.host)).encode() + + str(self.host) + ) self.append_write_buf(struct.pack(">H", self.port)) self.set_state("pre_connect", length=0, expectBytes=4) return True diff --git a/src/network/stats.py b/src/network/stats.py index 82e6c87f..5a0f8064 100644 --- a/src/network/stats.py +++ b/src/network/stats.py @@ -1,13 +1,9 @@ -""" -Network statistics -""" import time import asyncore_pollchoose as asyncore from network.connectionpool import BMConnectionPool from objectracker import missingObjects - lastReceivedTimestamp = time.time() lastReceivedBytes = 0 currentReceivedSpeed = 0 @@ -15,64 +11,60 @@ lastSentTimestamp = time.time() lastSentBytes = 0 currentSentSpeed = 0 - def connectedHostsList(): - """List of all the connected hosts""" - return BMConnectionPool().establishedConnections() - + retval = [] + for i in BMConnectionPool().inboundConnections.values() + \ + BMConnectionPool().outboundConnections.values(): + if not i.fullyEstablished: + continue + try: + retval.append(i) + except AttributeError: + pass + return retval def sentBytes(): - """Sending Bytes""" return asyncore.sentBytes - def uploadSpeed(): - """Getting upload speed""" - # pylint: disable=global-statement global lastSentTimestamp, lastSentBytes, currentSentSpeed currentTimestamp = time.time() if int(lastSentTimestamp) < int(currentTimestamp): currentSentBytes = asyncore.sentBytes - currentSentSpeed = int( - (currentSentBytes - lastSentBytes) / ( - currentTimestamp - lastSentTimestamp)) + currentSentSpeed = int((currentSentBytes - lastSentBytes) / (currentTimestamp - lastSentTimestamp)) lastSentBytes = currentSentBytes lastSentTimestamp = currentTimestamp return currentSentSpeed - def receivedBytes(): - """Receiving Bytes""" return asyncore.receivedBytes - def downloadSpeed(): - """Getting download speed""" - # pylint: disable=global-statement global lastReceivedTimestamp, lastReceivedBytes, currentReceivedSpeed currentTimestamp = time.time() if int(lastReceivedTimestamp) < int(currentTimestamp): currentReceivedBytes = asyncore.receivedBytes - currentReceivedSpeed = int( - (currentReceivedBytes - lastReceivedBytes) / ( - currentTimestamp - lastReceivedTimestamp)) + currentReceivedSpeed = int((currentReceivedBytes - lastReceivedBytes) / + (currentTimestamp - lastReceivedTimestamp)) lastReceivedBytes = currentReceivedBytes lastReceivedTimestamp = currentTimestamp return currentReceivedSpeed - def pendingDownload(): - """Getting pending downloads""" return len(missingObjects) - + #tmp = {} + #for connection in BMConnectionPool().inboundConnections.values() + \ + # BMConnectionPool().outboundConnections.values(): + # for k in connection.objectsNewToMe.keys(): + # tmp[k] = True + #return len(tmp) def pendingUpload(): - """Getting pending uploads""" - # tmp = {} - # for connection in BMConnectionPool().inboundConnections.values() + \ - # BMConnectionPool().outboundConnections.values(): - # for k in connection.objectsNewToThem.keys(): - # tmp[k] = True - # This probably isn't the correct logic so it's disabled - # return len(tmp) + #tmp = {} + #for connection in BMConnectionPool().inboundConnections.values() + \ + # BMConnectionPool().outboundConnections.values(): + # for k in connection.objectsNewToThem.keys(): + # tmp[k] = True + #This probably isn't the correct logic so it's disabled + #return len(tmp) return 0 diff --git a/src/network/tcp.py b/src/network/tcp.py index d611b1ca..5ebd6a21 100644 --- a/src/network/tcp.py +++ b/src/network/tcp.py @@ -1,8 +1,9 @@ -""" -TCP protocol handler -""" # pylint: disable=too-many-ancestors -import logging +""" +src/network/tcp.py +================== +""" + import math import random import socket @@ -17,26 +18,23 @@ import protocol import shared import state from bmconfigparser import BMConfigParser +from debug import logger 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') +from queues import UISignalQueue, invQueue, receiveDataQueue class TCPConnection(BMProto, TLSDispatcher): # pylint: disable=too-many-instance-attributes """ + .. todo:: Look to understand and/or fix the non-parent-init-called """ @@ -49,7 +47,7 @@ class TCPConnection(BMProto, TLSDispatcher): self.connectedAt = 0 self.skipUntil = 0 if address is None and sock is not None: - self.destination = Peer(*sock.getpeername()) + self.destination = state.Peer(*sock.getpeername()) self.isOutbound = False TLSDispatcher.__init__(self, sock, server_side=True) self.connectedAt = time.time() @@ -75,16 +73,11 @@ class TCPConnection(BMProto, TLSDispatcher): logger.debug( 'Connecting to %s:%i', self.destination.host, self.destination.port) - try: - self.local = ( - protocol.checkIPAddress( - protocol.encodeHost(self.destination.host), True) and - not protocol.checkSocksIP(self.destination.host) - ) - except socket.error: - # it's probably a hostname - pass - self.network_group = protocol.network_group(self.destination.host) + encodedAddr = protocol.encodeHost(self.destination.host) + self.local = all([ + protocol.checkIPAddress(encodedAddr, True), + not protocol.checkSocksIP(self.destination.host) + ]) ObjectTracker.__init__(self) # pylint: disable=non-parent-init-called self.bm_proto_reset() self.set_state("bm_header", expectBytes=protocol.Header.size) @@ -138,9 +131,10 @@ class TCPConnection(BMProto, TLSDispatcher): if not self.isOutbound and not self.local: 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 if self.isOutbound: @@ -182,7 +176,7 @@ class TCPConnection(BMProto, TLSDispatcher): for peer, params in addrs[substream]: templist.append((substream, peer, params["lastseen"])) if templist: - self.append_write_buf(assemble_addr(templist)) + self.append_write_buf(BMProto.assembleAddr(templist)) def sendBigInv(self): """ @@ -212,8 +206,8 @@ class TCPConnection(BMProto, TLSDispatcher): bigInvList[objHash] = 0 objectCount = 0 payload = b'' - # Now let us start appending all of these hashes together. - # They will be sent out in a big inv message to our new peer. + # Now let us start appending all of these hashes together. They will be + # sent out in a big inv message to our new peer. for obj_hash, _ in bigInvList.items(): payload += obj_hash objectCount += 1 @@ -221,7 +215,7 @@ class TCPConnection(BMProto, TLSDispatcher): # Remove -1 below when sufficient time has passed for users to # upgrade to versions of PyBitmessage that accept inv with 50,000 # items - if objectCount >= MAX_OBJECT_COUNT - 1: + if objectCount >= BMProto.maxObjectCount - 1: sendChunk() payload = b'' objectCount = 0 @@ -328,39 +322,6 @@ class Socks4aBMConnection(Socks4aConnection, TCPConnection): return True -def bootstrap(connection_class): - """Make bootstrapper class for connection type (connection_class)""" - class Bootstrapper(connection_class): - """Base class for bootstrappers""" - _connection_base = connection_class - - def __init__(self, host, port): - self._connection_base.__init__(self, Peer(host, port)) - self.close_reason = self._succeed = False - - def bm_command_addr(self): - """ - Got addr message - the bootstrap succeed. - Let BMProto process the addr message and switch state to 'close' - """ - BMProto.bm_command_addr(self) - self._succeed = True - # pylint: disable=attribute-defined-outside-init - self.close_reason = "Thanks for bootstrapping!" - self.set_state("close") - - def handle_close(self): - """ - After closing the connection switch knownnodes.knownNodesActual - back to False if the bootstrapper failed. - """ - self._connection_base.handle_close(self) - if not self._succeed: - knownnodes.knownNodesActual = False - - return Bootstrapper - - class TCPServer(AdvancedDispatcher): """TCP connection server for Bitmessage protocol""" @@ -372,7 +333,6 @@ class TCPServer(AdvancedDispatcher): for attempt in range(50): try: if attempt > 0: - logger.warning('Failed to bind on port %s', port) port = random.randint(32767, 65535) self.bind((host, port)) except socket.error as e: @@ -380,12 +340,11 @@ class TCPServer(AdvancedDispatcher): continue else: if attempt > 0: - logger.warning('Setting port to %s', port) BMConfigParser().set( 'bitmessagesettings', 'port', str(port)) BMConfigParser().save() break - self.destination = Peer(host, port) + self.destination = state.Peer(host, port) self.bound = True self.listen(5) @@ -403,7 +362,7 @@ class TCPServer(AdvancedDispatcher): except (TypeError, IndexError): return - state.ownAddresses[Peer(*sock.getsockname())] = True + state.ownAddresses[state.Peer(*sock.getsockname())] = True if ( len(connectionpool.BMConnectionPool().inboundConnections) + len(connectionpool.BMConnectionPool().outboundConnections) > diff --git a/src/network/threads.py b/src/network/threads.py deleted file mode 100644 index 9bdaa85d..00000000 --- a/src/network/threads.py +++ /dev/null @@ -1,49 +0,0 @@ -"""Threading primitives for the network package""" - -import logging -import random -import threading -from contextlib import contextmanager - - -class StoppableThread(threading.Thread): - """Base class for application threads with stopThread method""" - name = None - logger = logging.getLogger('default') - - def __init__(self, name=None): - if name: - self.name = name - super(StoppableThread, self).__init__(name=self.name) - self.stop = threading.Event() - self._stopped = False - random.seed() - self.logger.info('Init thread %s', self.name) - - def stopThread(self): - """Stop the thread""" - self._stopped = True - self.stop.set() - - -class BusyError(threading.ThreadError): - """ - Thread error raised when another connection holds the lock - we are trying to acquire. - """ - pass - - -@contextmanager -def nonBlocking(lock): - """ - A context manager which acquires given lock non-blocking - and raises BusyError if failed to acquire. - """ - locked = lock.acquire(False) - if not locked: - raise BusyError - try: - yield - finally: - lock.release() diff --git a/src/network/tls.py b/src/network/tls.py index 1b325696..c643f46e 100644 --- a/src/network/tls.py +++ b/src/network/tls.py @@ -1,18 +1,18 @@ """ SSL/TLS negotiation. """ -import logging + import os import socket import ssl import sys -import network.asyncore_pollchoose as asyncore -import paths +from debug import logger from network.advanceddispatcher import AdvancedDispatcher +import network.asyncore_pollchoose as asyncore from queues import receiveDataQueue +import paths -logger = logging.getLogger('default') _DISCONNECTED_SSL = frozenset((ssl.SSL_ERROR_EOF,)) @@ -23,8 +23,7 @@ if sys.version_info >= (2, 7, 13): # ssl.PROTOCOL_TLS1.2 sslProtocolVersion = ssl.PROTOCOL_TLS # pylint: disable=no-member elif sys.version_info >= (2, 7, 9): - # this means any SSL/TLS. - # SSLv2 and 3 are excluded with an option after context is created + # this means any SSL/TLS. SSLv2 and 3 are excluded with an option after context is created sslProtocolVersion = ssl.PROTOCOL_SSLv23 else: # this means TLSv1, there is no way to set "TLSv1 or higher" or @@ -33,28 +32,24 @@ else: # ciphers -if ssl.OPENSSL_VERSION_NUMBER >= 0x10100000 and not \ - ssl.OPENSSL_VERSION.startswith("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" class TLSDispatcher(AdvancedDispatcher): - """TLS functionality for classes derived from AdvancedDispatcher""" - # pylint: disable=too-many-instance-attributes, too-many-arguments - # pylint: disable=super-init-not-called - def __init__(self, _=None, sock=None, certfile=None, keyfile=None, - server_side=False, ciphers=sslProtocolCiphers): + def __init__( + self, address=None, sock=None, certfile=None, keyfile=None, + server_side=False, ciphers=sslProtocolCiphers + ): self.want_read = self.want_write = True if certfile is None: - self.certfile = os.path.join( - paths.codePath(), 'sslkeys', 'cert.pem') + 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') + self.keyfile = os.path.join(paths.codePath(), 'sslkeys', 'key.pem') else: self.keyfile = keyfile self.server_side = server_side @@ -65,27 +60,19 @@ class TLSDispatcher(AdvancedDispatcher): self.isSSL = False 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, - # it's safe to wrap the socket. - if sys.version_info >= (2, 7, 9): - context = ssl.create_default_context( - purpose=ssl.Purpose.SERVER_AUTH - if self.server_side else ssl.Purpose.CLIENT_AUTH) + # Once the connection has been established, it's safe to wrap the + # socket. + if sys.version_info >= (2,7,9): + context = ssl.create_default_context(purpose = ssl.Purpose.SERVER_AUTH if self.server_side else ssl.Purpose.CLIENT_AUTH) context.set_ciphers(self.ciphers) context.set_ecdh_curve("secp256k1") context.check_hostname = False context.verify_mode = ssl.CERT_NONE # also exclude TLSv1 and TLSv1.1 in the future - context.options = ssl.OP_ALL | ssl.OP_NO_SSLv2 |\ - ssl.OP_NO_SSLv3 | ssl.OP_SINGLE_ECDH_USE |\ - ssl.OP_CIPHER_SERVER_PREFERENCE - self.sslSocket = context.wrap_socket( - self.socket, server_side=self.server_side, - do_handshake_on_connect=False) + context.options = ssl.OP_ALL | ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_SINGLE_ECDH_USE | ssl.OP_CIPHER_SERVER_PREFERENCE + self.sslSocket = context.wrap_socket(self.socket, server_side = self.server_side, do_handshake_on_connect=False) else: self.sslSocket = ssl.wrap_socket( self.socket, server_side=self.server_side, @@ -99,16 +86,10 @@ class TLSDispatcher(AdvancedDispatcher): # if hasattr(self.socket, "context"): # self.socket.context.set_ecdh_curve("secp256k1") - @staticmethod - def state_tls_handshake(): - """ - Do nothing while TLS handshake is pending, as during this phase - we need to react to callbacks instead - """ + def state_tls_handshake(self): return False def writable(self): - """Handle writable checks for TLS-enabled sockets""" try: if self.tlsStarted and not self.tlsDone and not self.write_buf: return self.want_write @@ -117,39 +98,26 @@ class TLSDispatcher(AdvancedDispatcher): return AdvancedDispatcher.writable(self) def readable(self): - """Handle readable check for TLS-enabled sockets""" try: - # during TLS handshake, and after flushing write buffer, - # return status of last handshake attempt + # during TLS handshake, and after flushing write buffer, return status of last handshake attempt if self.tlsStarted and not self.tlsDone and not self.write_buf: - # print "tls readable, %r" % (self.want_read) + #print "tls readable, %r" % (self.want_read) return self.want_read - # prior to TLS handshake, - # receiveDataThread should emulate synchronous behaviour - elif not self.fullyEstablished and ( - self.expectBytes == 0 or not self.write_buf_empty()): + # prior to TLS handshake, receiveDataThread should emulate synchronous behaviour + elif not self.fullyEstablished and (self.expectBytes == 0 or not self.write_buf_empty()): return False return AdvancedDispatcher.readable(self) except AttributeError: return AdvancedDispatcher.readable(self) - 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 - and normal reads must be ignored. - """ + def handle_read(self): 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) + #logger.debug("%s:%i TLS handshaking (read)", self.destination.host, self.destination.port) self.tls_handshake() else: - # logger.debug( - # "%s:%i Not TLS handshaking (read)", self.destination.host, - # self.destination.port) + #logger.debug("%s:%i Not TLS handshaking (read)", self.destination.host, self.destination.port) return AdvancedDispatcher.handle_read(self) except AttributeError: return AdvancedDispatcher.handle_read(self) @@ -163,23 +131,14 @@ class TLSDispatcher(AdvancedDispatcher): self.handle_close() return - 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 - and normal writes must be ignored. - """ + def handle_write(self): 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) + #logger.debug("%s:%i TLS handshaking (write)", self.destination.host, self.destination.port) self.tls_handshake() else: - # logger.debug( - # "%s:%i Not TLS handshaking (write)", self.destination.host, - # self.destination.port) + #logger.debug("%s:%i Not TLS handshaking (write)", self.destination.host, self.destination.port) return AdvancedDispatcher.handle_write(self) except AttributeError: return AdvancedDispatcher.handle_write(self) @@ -194,28 +153,25 @@ class TLSDispatcher(AdvancedDispatcher): return def tls_handshake(self): - """Perform TLS handshake and handle its stages""" # wait for flush if self.write_buf: return False # Perform the handshake. try: - # print "handshaking (internal)" + #print "handshaking (internal)" self.sslSocket.do_handshake() except ssl.SSLError as err: - # print "%s:%i: handshake fail" % ( - # self.destination.host, self.destination.port) + #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: - # print "want read" + #print "want read" self.want_read = True if err.args[0] == ssl.SSL_ERROR_WANT_WRITE: - # print "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.handle_close() else: @@ -223,15 +179,11 @@ class TLSDispatcher(AdvancedDispatcher): else: if sys.version_info >= (2, 7, 9): self.tlsVersion = self.sslSocket.version() - logger.debug( - '%s:%i: TLS handshake success, TLS protocol version: %s', - self.destination.host, self.destination.port, - self.tlsVersion) + logger.debug("%s:%i: TLS handshake success, TLS protocol version: %s", + self.destination.host, self.destination.port, self.sslSocket.version()) else: self.tlsVersion = "TLSv1" - logger.debug( - '%s:%i: TLS handshake success', - self.destination.host, self.destination.port) + logger.debug("%s:%i: TLS handshake success", self.destination.host, self.destination.port) # The handshake has completed, so remove this channel and... self.del_channel() self.set_socket(self.sslSocket) diff --git a/src/network/udp.py b/src/network/udp.py index d5f1cccd..225aabf3 100644 --- a/src/network/udp.py +++ b/src/network/udp.py @@ -1,31 +1,23 @@ -""" -UDP protocol handler -""" -import logging -import socket import time +import socket -import protocol import state +import protocol from bmproto import BMProto -from node import Peer +from debug import logger from objectracker import ObjectTracker from queues import receiveDataQueue -logger = logging.getLogger('default') - -class UDPSocket(BMProto): # pylint: disable=too-many-instance-attributes - """Bitmessage protocol over UDP (class)""" +class UDPSocket(BMProto): port = 8444 announceInterval = 60 def __init__(self, host=None, sock=None, announcing=False): - # pylint: disable=bad-super-call super(BMProto, self).__init__(sock=sock) self.verackReceived = True self.verackSent = True - # .. todo:: sort out streams + # TODO sort out streams self.streams = [1] self.fullyEstablished = True self.connectedAt = 0 @@ -43,8 +35,8 @@ class UDPSocket(BMProto): # pylint: disable=too-many-instance-attributes else: self.socket = sock self.set_socket_reuse() - self.listening = Peer(*self.socket.getsockname()) - self.destination = Peer(*self.socket.getsockname()) + self.listening = state.Peer(*self.socket.getsockname()) + self.destination = state.Peer(*self.socket.getsockname()) ObjectTracker.__init__(self) self.connecting = False self.connected = True @@ -52,7 +44,6 @@ class UDPSocket(BMProto): # pylint: disable=too-many-instance-attributes self.set_state("bm_header", expectBytes=protocol.Header.size) def set_socket_reuse(self): - """Set socket reuse option""" self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: @@ -78,12 +69,12 @@ class UDPSocket(BMProto): # pylint: disable=too-many-instance-attributes if not self.local: return True remoteport = False - for seenTime, stream, _, ip, port in addresses: + for seenTime, stream, services, ip, port in addresses: decodedIP = protocol.checkIPAddress(str(ip)) if stream not in state.streamsInWhichIAmParticipating: continue - if (seenTime < time.time() - self.maxTimeOffset - or seenTime > time.time() + self.maxTimeOffset): + 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 @@ -95,8 +86,9 @@ class UDPSocket(BMProto): # pylint: disable=too-many-instance-attributes "received peer discovery from %s:%i (port %i):", self.destination.host, self.destination.port, remoteport) if self.local: - state.discoveredPeers[Peer(self.destination.host, remoteport)] = \ - time.time() + state.discoveredPeers[ + state.Peer(self.destination.host, remoteport) + ] = time.time() return True def bm_command_portcheck(self): @@ -130,9 +122,12 @@ class UDPSocket(BMProto): # pylint: disable=too-many-instance-attributes logger.error("socket error: %s", e) return - self.destination = Peer(*addr) + self.destination = state.Peer(*addr) encodedAddr = protocol.encodeHost(addr[0]) - self.local = bool(protocol.checkIPAddress(encodedAddr, True)) + if protocol.checkIPAddress(encodedAddr, True): + self.local = True + else: + self.local = False # overwrite the old buffer to avoid mixing data and so that # self.local works correctly self.read_buf[0:] = recdata @@ -144,9 +139,6 @@ class UDPSocket(BMProto): # pylint: disable=too-many-instance-attributes retval = self.socket.sendto( self.write_buf, ('', self.port)) except socket.error as e: - logger.error("socket error on sendto: %s", e) - if e.errno == 101: - self.announcing = False - self.socket.close() + logger.error("socket error on sendato: %s", e) retval = 0 self.slice_write_buf(retval) diff --git a/src/network/uploadthread.py b/src/network/uploadthread.py index 7d80d789..61ee6fab 100644 --- a/src/network/uploadthread.py +++ b/src/network/uploadthread.py @@ -1,40 +1,46 @@ """ -`UploadThread` class definition +src/network/uploadthread.py """ +# pylint: disable=unsubscriptable-object +import threading import time import helper_random import protocol +from debug import logger +from helper_threading import StoppableThread from inventory import Inventory from network.connectionpool import BMConnectionPool from network.dandelion import Dandelion from randomtrackingdict import RandomTrackingDict -from threads import StoppableThread -class UploadThread(StoppableThread): - """ - This is a thread that uploads the objects that the peers requested from me - """ +class UploadThread(threading.Thread, StoppableThread): + """This is a thread that uploads the objects that the peers requested from me """ maxBufSize = 2097152 # 2MB - name = "Uploader" + + def __init__(self): + threading.Thread.__init__(self, name="Uploader") + self.initStop() + self.name = "Uploader" + logger.info("init upload thread") def run(self): while not self._stopped: uploaded = 0 - # Choose uploading peers randomly - connections = BMConnectionPool().establishedConnections() + # Choose downloading peers randomly + connections = [x for x in BMConnectionPool().inboundConnections.values() + + BMConnectionPool().outboundConnections.values() if x.fullyEstablished] helper_random.randomshuffle(connections) for i in connections: now = time.time() # avoid unnecessary delay if i.skipUntil >= now: continue - if len(i.write_buf) > self.maxBufSize: + if len(i.write_buf) > UploadThread.maxBufSize: continue try: - request = i.pendingUpload.randomKeys( - RandomTrackingDict.maxPending) + request = i.pendingUpload.randomKeys(RandomTrackingDict.maxPending) except KeyError: continue payload = bytearray() @@ -44,26 +50,22 @@ class UploadThread(StoppableThread): 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.', - i.destination) + logger.info('%s asked for a stem object we didn\'t offer to it.', + i.destination) break try: - payload.extend(protocol.CreatePacket( - 'object', Inventory()[chunk].payload)) + payload.extend(protocol.CreatePacket('object', + Inventory()[chunk].payload)) chunk_count += 1 except KeyError: i.antiIntersectionDelay() - self.logger.info( - '%s asked for an object we don\'t have.', - i.destination) + logger.info('%s asked for an object we don\'t have.', i.destination) break if not chunk_count: continue i.append_write_buf(payload) - self.logger.debug( - '%s:%i Uploading %i objects', - i.destination.host, i.destination.port, chunk_count) + logger.debug("%s:%i Uploading %i objects", + i.destination.host, i.destination.port, chunk_count) uploaded += chunk_count if not uploaded: self.stop.wait(1) diff --git a/src/nohup.out b/src/nohup.out deleted file mode 100644 index 1e343a48..00000000 --- a/src/nohup.out +++ /dev/null @@ -1,3 +0,0 @@ -python: can't open file 'main.py.py': [Errno 2] No such file or directory -[WARNING] [Config ] Older configuration version detected (21 instead of 20) -[WARNING] [Config ] Upgrading configuration in progress. diff --git a/src/openclpow.py b/src/openclpow.py index 35bf46d2..eb91a07f 100644 --- a/src/openclpow.py +++ b/src/openclpow.py @@ -1,15 +1,14 @@ #!/usr/bin/env python2.7 -""" -Module for Proof of Work using OpenCL -""" -import hashlib -import os from struct import pack, unpack +import time +import hashlib +import random +import os -import paths from bmconfigparser import BMConfigParser -from debug import logger +import paths from state import shutdown +from debug import logger libAvailable = True ctx = False @@ -28,8 +27,6 @@ except ImportError: def initCL(): - """Initlialise OpenCL engine""" - # pylint: disable=global-statement global ctx, queue, program, hash_dt, libAvailable if libAvailable is False: return @@ -43,13 +40,12 @@ def initCL(): for platform in cl.get_platforms(): gpus.extend(platform.get_devices(device_type=cl.device_type.GPU)) if BMConfigParser().safeGet("bitmessagesettings", "opencl") == platform.vendor: - enabledGpus.extend(platform.get_devices( - device_type=cl.device_type.GPU)) + enabledGpus.extend(platform.get_devices(device_type=cl.device_type.GPU)) if platform.vendor not in vendors: vendors.append(platform.vendor) except: pass - if enabledGpus: + if (len(enabledGpus) > 0): ctx = cl.Context(devices=enabledGpus) queue = cl.CommandQueue(ctx) f = open(os.path.join(paths.codePath(), "bitmsghash", 'bitmsghash.cl'), 'r') @@ -59,29 +55,23 @@ def initCL(): else: logger.info("No OpenCL GPUs found") del enabledGpus[:] - except Exception: + except Exception as e: logger.error("OpenCL fail: ", exc_info=True) del enabledGpus[:] - def openclAvailable(): - """Are there any OpenCL GPUs available?""" - return bool(gpus) - + return (len(gpus) > 0) def openclEnabled(): - """Is OpenCL enabled (and available)?""" - return bool(enabledGpus) + return (len(enabledGpus) > 0) - -def do_opencl_pow(hash_, target): - """Perform PoW using OpenCL""" +def do_opencl_pow(hash, target): output = numpy.zeros(1, dtype=[('v', numpy.uint64, 1)]) - if not enabledGpus: + if (len(enabledGpus) == 0): return output[0][0] data = numpy.zeros(1, dtype=hash_dt, order='C') - data[0]['v'] = ("0000000000000000" + hash_).decode("hex") + data[0]['v'] = ("0000000000000000" + hash).decode("hex") data[0]['target'] = target hash_buf = cl.Buffer(ctx, cl.mem_flags.READ_ONLY | cl.mem_flags.COPY_HOST_PTR, hostbuf=data) @@ -93,8 +83,9 @@ def do_opencl_pow(hash_, target): kernel.set_arg(0, hash_buf) kernel.set_arg(1, dest_buf) + start = time.time() progress = 0 - globamt = worksize * 2000 + globamt = worksize*2000 while output[0][0] == 0 and shutdown == 0: kernel.set_arg(2, pack("Q', hashlib.sha512(hashlib.sha512(pack('>Q', nonce) + initialHash).digest()).digest()[0:8]) - print "{} - value {} < {}".format(nonce, trialValue, target_) + target = 54227212183L + initialHash = "3758f55b5a8d902fd3597e4ce6a2d3f23daff735f65d9698c270987f4e67ad590b93f3ffeba0ef2fd08a8dc2f87b68ae5a0dc819ab57f22ad2c4c9c8618a43b3".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/paths.py b/src/paths.py index ac90da63..325fcd8b 100644 --- a/src/paths.py +++ b/src/paths.py @@ -1,94 +1,76 @@ -""" -Path related functions -""" -# pylint: disable=import-error -import logging -import os -import re +from os import environ, path import sys +import re from datetime import datetime -from shutil import move -from kivy.utils import platform - -logger = logging.getLogger('default') # When using py2exe or py2app, the variable frozen is added to the sys -# namespace. This can be used to setup a different code path for +# namespace. This can be used to setup a different code path for # binary distributions vs source distributions. -frozen = getattr(sys, 'frozen', None) - +frozen = getattr(sys,'frozen', None) def lookupExeFolder(): - """Returns executable folder path""" if frozen: - exeFolder = ( + if frozen == "macosx_app": # targetdir/Bitmessage.app/Contents/MacOS/Bitmessage - os.path.dirname(sys.executable).split(os.path.sep)[0] + os.path.sep - if frozen == "macosx_app" else - os.path.dirname(sys.executable) + os.path.sep) + exeFolder = path.dirname(path.dirname(path.dirname(path.dirname(sys.executable)))) + path.sep + else: + exeFolder = path.dirname(sys.executable) + path.sep elif __file__: - exeFolder = os.path.dirname(__file__) + os.path.sep + exeFolder = path.dirname(__file__) + path.sep else: exeFolder = '' return exeFolder - def lookupAppdataFolder(): - """Returns path of the folder where application data is stored""" APPNAME = "PyBitmessage" - dataFolder = os.environ.get('BITMESSAGE_HOME') - if dataFolder: - if dataFolder[-1] not in (os.path.sep, os.path.altsep): - dataFolder += os.path.sep + if "BITMESSAGE_HOME" in environ: + dataFolder = environ["BITMESSAGE_HOME"] + if dataFolder[-1] not in [path.sep, path.altsep]: + dataFolder += path.sep elif sys.platform == 'darwin': - try: - dataFolder = os.path.join( - os.environ['HOME'], - 'Library/Application Support/', APPNAME - ) + '/' + if "HOME" in environ: + dataFolder = path.join(environ["HOME"], "Library/Application Support/", APPNAME) + '/' + else: + stringToLog = 'Could not find home folder, please report this message and your OS X version to the BitMessage Github.' + if 'logger' in globals(): + logger.critical(stringToLog) + else: + print stringToLog + sys.exit() - except KeyError: - sys.exit( - 'Could not find home folder, please report this message' - ' and your OS X version to the BitMessage Github.') - elif platform == 'android': - dataFolder = os.path.join(os.environ['ANDROID_PRIVATE'] + '/', APPNAME) + '/' 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 + dataFolder = path.join(environ['APPDATA'].decode(sys.getfilesystemencoding(), 'ignore'), APPNAME) + path.sep else: + from shutil import move try: - dataFolder = os.path.join(os.environ['XDG_CONFIG_HOME'], APPNAME) + dataFolder = path.join(environ["XDG_CONFIG_HOME"], APPNAME) except KeyError: - dataFolder = os.path.join(os.environ['HOME'], '.config', APPNAME) + dataFolder = path.join(environ["HOME"], ".config", APPNAME) - # Migrate existing data to the proper location - # if this is an existing install + # Migrate existing data to the proper location if this is an existing install try: - move(os.path.join(os.environ['HOME'], '.%s' % APPNAME), dataFolder) - logger.info('Moving data folder to %s', dataFolder) + move(path.join(environ["HOME"], ".%s" % APPNAME), dataFolder) + stringToLog = "Moving data folder to %s" % (dataFolder) + if 'logger' in globals(): + logger.info(stringToLog) + else: + print stringToLog except IOError: # Old directory may not exist. pass - dataFolder = dataFolder + os.path.sep + dataFolder = dataFolder + '/' return dataFolder - - + def codePath(): - """Returns path to the program sources""" - # pylint: disable=protected-access - if not frozen: - return os.path.dirname(__file__) - return ( - os.environ.get('RESOURCEPATH') - # pylint: disable=protected-access - if frozen == "macosx_app" else sys._MEIPASS) - + if frozen == "macosx_app": + codePath = environ.get("RESOURCEPATH") + elif frozen: # windows + codePath = sys._MEIPASS + else: + codePath = path.dirname(__file__) + return codePath def tail(f, lines=20): - """Returns last lines in the f file object""" total_lines_wanted = lines BLOCK_SIZE = 1024 @@ -96,17 +78,16 @@ def tail(f, lines=20): block_end_byte = f.tell() lines_to_go = total_lines_wanted block_number = -1 - blocks = [] - # blocks of size BLOCK_SIZE, in reverse order starting - # from the end of the file + blocks = [] # blocks of size BLOCK_SIZE, in reverse order starting + # from the end of the file while lines_to_go > 0 and block_end_byte > 0: - if block_end_byte - BLOCK_SIZE > 0: + if (block_end_byte - BLOCK_SIZE > 0): # read the last block we haven't yet read - f.seek(block_number * BLOCK_SIZE, 2) + f.seek(block_number*BLOCK_SIZE, 2) blocks.append(f.read(BLOCK_SIZE)) else: # file too small, start from begining - f.seek(0, 0) + f.seek(0,0) # only read what was not read blocks.append(f.read(block_end_byte)) lines_found = blocks[-1].count('\n') @@ -118,12 +99,9 @@ def tail(f, lines=20): def lastCommit(): - """ - Returns last commit information as dict with 'commit' and 'time' keys - """ - githeadfile = os.path.join(codePath(), '..', '.git', 'logs', 'HEAD') + githeadfile = path.join(codePath(), '..', '.git', 'logs', 'HEAD') result = {} - if os.path.isfile(githeadfile): + if path.isfile(githeadfile): try: with open(githeadfile, 'rt') as githead: line = tail(githead, 1) diff --git a/src/plugins/__init__.py b/src/plugins/__init__.py index 285009df..e69de29b 100644 --- a/src/plugins/__init__.py +++ b/src/plugins/__init__.py @@ -1,7 +0,0 @@ -""" -Simple plugin system based on setuptools ----------------------------------------- - - -""" -# .. include:: pybitmessage.plugins.plugin.rst diff --git a/src/plugins/indicator_libmessaging.py b/src/plugins/indicator_libmessaging.py index 60bf5e7e..36178663 100644 --- a/src/plugins/indicator_libmessaging.py +++ b/src/plugins/indicator_libmessaging.py @@ -1,7 +1,4 @@ # -*- coding: utf-8 -*- -""" -Indicator plugin using libmessaging -""" import gi gi.require_version('MessagingMenu', '1.0') # noqa:E402 @@ -12,7 +9,6 @@ from pybitmessage.tr import _translate class IndicatorLibmessaging(object): - """Plugin for libmessage indicator""" def __init__(self, form): try: self.app = MessagingMenu.App(desktop_id='pybitmessage.desktop') @@ -36,18 +32,15 @@ class IndicatorLibmessaging(object): if self.app: self.app.unregister() - def activate(self, app, source): # pylint: disable=unused-argument - """Activate the libmessaging indicator plugin""" + def activate(self, app, source): self.form.appIndicatorInbox( self.new_message_item if source == 'messages' else self.new_broadcast_item ) + # show the number of unread messages and subscriptions + # on the messaging menu def show_unread(self, draw_attention=False): - """ - show the number of unread messages and subscriptions - on the messaging menu - """ for source, count in zip( ('messages', 'subscriptions'), self.form.getUnread() diff --git a/src/plugins/menu_qrcode.py b/src/plugins/menu_qrcode.py index 4cdedb2d..7f21c6b8 100644 --- a/src/plugins/menu_qrcode.py +++ b/src/plugins/menu_qrcode.py @@ -6,17 +6,15 @@ A menu plugin showing QR-Code for bitmessage address in modal dialog. import urllib import qrcode -from PyQt4 import QtCore, QtGui +from PyQt4 import QtGui, QtCore from pybitmessage.tr import _translate # http://stackoverflow.com/questions/20452486 -class Image(qrcode.image.base.BaseImage): # pylint: disable=abstract-method +class Image(qrcode.image.base.BaseImage): """Image output class for qrcode using QPainter""" - def __init__(self, border, width, box_size): - # pylint: disable=super-init-not-called self.border = border self.width = width self.box_size = box_size @@ -39,7 +37,7 @@ class Image(qrcode.image.base.BaseImage): # pylint: disable=abstract-method QtCore.Qt.black) -class QRCodeDialog(QtGui.QDialog): +class QRCodeDialog(QtGui.QDialog): """The dialog""" def __init__(self, parent): super(QRCodeDialog, self).__init__(parent) diff --git a/src/plugins/notification_notify2.py b/src/plugins/notification_notify2.py index 84ecbdde..3fd935c4 100644 --- a/src/plugins/notification_notify2.py +++ b/src/plugins/notification_notify2.py @@ -1,7 +1,4 @@ # -*- coding: utf-8 -*- -""" -Notification plugin using notify2 -""" import gi gi.require_version('Notify', '0.7') @@ -9,13 +6,10 @@ from gi.repository import Notify Notify.init('pybitmessage') - -def connect_plugin(title, subtitle, category, _, icon): - """Plugin for notify2""" +def connect_plugin(title, subtitle, category, label, icon): if not icon: icon = 'mail-message-new' if category == 2 else 'pybitmessage' connect_plugin.notification.update(title, subtitle, icon) connect_plugin.notification.show() - connect_plugin.notification = Notify.Notification.new("Init", "Init") diff --git a/src/plugins/plugin.py b/src/plugins/plugin.py index 629de0a6..6601adaf 100644 --- a/src/plugins/plugin.py +++ b/src/plugins/plugin.py @@ -1,28 +1,16 @@ # -*- coding: utf-8 -*- -""" -Operating with plugins -""" -import logging import pkg_resources -logger = logging.getLogger('default') - - def get_plugins(group, point='', name=None, fallback=None): """ - :param str group: plugin group - :param str point: plugin name prefix - :param name: exact plugin name - :param fallback: fallback plugin name - - Iterate through plugins (``connect_plugin`` attribute of entry point) - which name starts with ``point`` or equals to ``name``. - If ``fallback`` kwarg specified, plugin with that name yield last. + Iterate through plugins (`connect_plugin` attribute of entry point) + which name starts with `point` or equals to `name`. + If `fallback` kwarg specified, plugin with that name yield last. """ for ep in pkg_resources.iter_entry_points('bitmessage.' + group): - if name and ep.name == name or not point or ep.name.startswith(point): + if name and ep.name == name or ep.name.startswith(point): try: plugin = ep.load().connect_plugin if ep.name == fallback: @@ -34,8 +22,6 @@ def get_plugins(group, point='', name=None, fallback=None): ValueError, pkg_resources.DistributionNotFound, pkg_resources.UnknownExtra): - logger.debug( - 'Problem while loading %s', ep.name, exc_info=True) continue try: yield _fallback @@ -44,8 +30,6 @@ def get_plugins(group, point='', name=None, fallback=None): def get_plugin(*args, **kwargs): - """ - :return: first available plugin from :func:`get_plugins` if any. - """ + """Returns first available plugin `from get_plugins()` if any.""" for plugin in get_plugins(*args, **kwargs): return plugin diff --git a/src/plugins/proxyconfig_stem.py b/src/plugins/proxyconfig_stem.py deleted file mode 100644 index 7e8dc089..00000000 --- a/src/plugins/proxyconfig_stem.py +++ /dev/null @@ -1,140 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Configure tor proxy and hidden service with -`stem `_ depending on *bitmessagesettings*: - - * try to start own tor instance on *socksport* if *sockshostname* - is unset or set to localhost; - * if *socksport* is already in use that instance is used only for - hidden service (if *sockslisten* is also set True); - * create ephemeral hidden service v3 if there is already *onionhostname*; - * otherwise use stem's 'BEST' version and save onion keys to the new - section using *onionhostname* as name for future use. -""" -import logging -import os -import random # noseq -import tempfile - -import stem -import stem.control -import stem.process -import stem.version - - -class DebugLogger(object): # pylint: disable=too-few-public-methods - """Safe logger wrapper for tor and plugin's logs""" - def __init__(self): - self._logger = logging.getLogger('default') - self._levels = { - 'err': 40, - 'warn': 30, - 'notice': 20 - } - - def __call__(self, line): - try: - level, line = line.split('[', 1)[1].split(']') - except IndexError: - # Plugin's debug or unexpected log line from tor - self._logger.debug(line) - else: - self._logger.log(self._levels.get(level, 10), '(tor) %s', line) - - -def connect_plugin(config): # pylint: disable=too-many-branches - """ - Run stem proxy configurator - - :param config: current configuration instance - :type config: :class:`pybitmessage.bmconfigparser.BMConfigParser` - :return: True if configuration was done successfully - """ - logwrite = DebugLogger() - if config.safeGet('bitmessagesettings', 'sockshostname', '') not in ( - 'localhost', '127.0.0.1', '' - ): - # remote proxy is choosen for outbound connections, - # nothing to do here, but need to set socksproxytype to SOCKS5! - config.set('bitmessagesettings', 'socksproxytype', 'SOCKS5') - logwrite( - 'sockshostname is set to remote address,' - ' aborting stem proxy configuration') - return - - datadir = tempfile.mkdtemp() - 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) - tor_config['SocksPort'] = str(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=20, - init_msg_handler=logwrite) - except OSError: - if not attempt: - try: - stem.version.get_system_tor_version() - except IOError: - return - continue - else: - logwrite('Started tor on port %s' % port) - break - - config.setTemp('bitmessagesettings', 'socksproxytype', 'SOCKS5') - - if config.safeGetBoolean('bitmessagesettings', 'sockslisten'): - # need a hidden service for inbound connections - try: - controller = stem.control.Controller.from_socket_file( - control_socket) - controller.authenticate() - except stem.SocketError: - # something goes wrong way - logwrite('Failed to instantiate or authenticate on controller') - return - - onionhostname = config.safeGet('bitmessagesettings', 'onionhostname') - onionkey = config.safeGet(onionhostname, 'privsigningkey') - if onionhostname and not onionkey: - logwrite('The hidden service found in config ): %s' % - onionhostname) - onionkeytype = config.safeGet(onionhostname, 'keytype') - - response = controller.create_ephemeral_hidden_service( - {config.safeGetInt('bitmessagesettings', 'onionport', 8444): - config.safeGetInt('bitmessagesettings', 'port', 8444)}, - key_type=(onionkeytype or 'NEW'), - key_content=(onionkey or onionhostname and 'ED25519-V3' or 'BEST') - ) - - if not response.is_ok(): - logwrite('Bad response from controller ):') - return - - if not onionkey: - logwrite('Started hidden service %s.onion' % response.service_id) - # only save new service keys - # if onionhostname was not set previously - if not onionhostname: - onionhostname = response.service_id + '.onion' - config.set( - 'bitmessagesettings', 'onionhostname', onionhostname) - config.add_section(onionhostname) - config.set( - onionhostname, 'privsigningkey', response.private_key) - config.set( - onionhostname, 'keytype', response.private_key_type) - config.save() - - return True diff --git a/src/plugins/sound_canberra.py b/src/plugins/sound_canberra.py index 9fea8197..094901ed 100644 --- a/src/plugins/sound_canberra.py +++ b/src/plugins/sound_canberra.py @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- -""" -Sound theme plugin using pycanberra -""" + +from pybitmessage.bitmessageqt import sound import pycanberra -from pybitmessage.bitmessageqt import sound _canberra = pycanberra.Canberra() @@ -16,8 +14,7 @@ _theme = { } -def connect_plugin(category, label=None): # pylint: disable=unused-argument - """This function implements the entry point.""" +def connect_plugin(category, label=None): try: _canberra.play(0, pycanberra.CA_PROP_EVENT_ID, _theme[category], None) except (KeyError, pycanberra.CanberraException): diff --git a/src/plugins/sound_gstreamer.py b/src/plugins/sound_gstreamer.py index 8f3606dd..062da3f9 100644 --- a/src/plugins/sound_gstreamer.py +++ b/src/plugins/sound_gstreamer.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -""" -Sound notification plugin using gstreamer -""" + import gi gi.require_version('Gst', '1.0') from gi.repository import Gst # noqa: E402 @@ -11,7 +9,6 @@ _player = Gst.ElementFactory.make("playbin", "player") def connect_plugin(sound_file): - """Entry point for sound file""" _player.set_state(Gst.State.NULL) _player.set_property("uri", "file://" + sound_file) _player.set_state(Gst.State.PLAYING) diff --git a/src/plugins/sound_playfile.py b/src/plugins/sound_playfile.py index e36d9922..c8216d07 100644 --- a/src/plugins/sound_playfile.py +++ b/src/plugins/sound_playfile.py @@ -1,13 +1,10 @@ # -*- coding: utf-8 -*- -""" -Sound notification plugin using external executable or winsound (on Windows) -""" + try: import winsound def connect_plugin(sound_file): - """Plugin's entry point""" winsound.PlaySound(sound_file, winsound.SND_FILENAME) except ImportError: import os @@ -21,8 +18,7 @@ except ImportError: args, stdout=FNULL, stderr=subprocess.STDOUT, close_fds=True) def connect_plugin(sound_file): - """This function implements the entry point.""" - global play_cmd # pylint: disable=global-statement + global play_cmd ext = os.path.splitext(sound_file)[-1] try: diff --git a/src/proofofwork.py b/src/proofofwork.py index be53c373..bb16951c 100644 --- a/src/proofofwork.py +++ b/src/proofofwork.py @@ -1,6 +1,7 @@ # pylint: disable=too-many-branches,too-many-statements,protected-access """ -Proof of work calculation +src/proofofwork.py +================== """ import ctypes @@ -18,8 +19,6 @@ import state import tr from bmconfigparser import BMConfigParser from debug import logger -from kivy.utils import platform - bitmsglib = 'bitmsghash.so' bmpow = None @@ -293,7 +292,8 @@ def init(): global bitmsglib, bmpow openclpow.initCL() - if "win32" == sys.platform: + + if sys.platform == "win32": if ctypes.sizeof(ctypes.c_voidp) == 4: bitmsglib = 'bitmsghash32.dll' else: @@ -319,12 +319,6 @@ def init(): except: logger.error("C PoW test fail.", exc_info=True) bso = None - elif platform == "android": - try: - bso = ctypes.CDLL('libbitmsghash.so') - except Exception as e: - bso = None - else: try: bso = ctypes.CDLL(os.path.join(paths.codePath(), "bitmsghash", bitmsglib)) diff --git a/src/protocol.py b/src/protocol.py index 4f2d0856..c2bf3021 100644 --- a/src/protocol.py +++ b/src/protocol.py @@ -1,8 +1,12 @@ +# pylint: disable=too-many-boolean-expressions,too-many-return-statements,too-many-locals,too-many-statements """ +protocol.py +=========== + Low-level protocol-related functions. """ -# pylint: disable=too-many-boolean-expressions,too-many-return-statements -# pylint: disable=too-many-locals,too-many-statements + +from __future__ import absolute_import import base64 import hashlib @@ -10,8 +14,9 @@ import random import socket import sys import time +import traceback from binascii import hexlify -from struct import Struct, pack, unpack +from struct import pack, unpack, Struct import defaults import highlevelcrypto @@ -24,18 +29,10 @@ from fallback import RIPEMD160Hash from helper_sql import sqlExecute from version import softwareVersion + # Service flags -#: This is a normal network node NODE_NETWORK = 1 -#: This node supports SSL/TLS in the current connect (python < 2.7.9 -#: only supports an SSL client, so in that case it would only have this -#: on when the connection is a client). NODE_SSL = 2 -# (Proposal) This node may do PoW on behalf of some its peers -# (PoW offloading/delegating), but it doesn't have to. Clients may have -# to meet additional requirements (e.g. TLS authentication) -# NODE_POW = 4 -#: Node supports dandelion NODE_DANDELION = 8 # Bitfield flags @@ -97,8 +94,7 @@ def isBitSetWithinBitfield(fourByteString, n): def encodeHost(host): """Encode a given host to be used in low-level socket operations""" if host.find('.onion') > -1: - return '\xfd\x87\xd8\x7e\xeb\x43' + base64.b32decode( - host.split(".")[0], True) + return '\xfd\x87\xd8\x7e\xeb\x43' + base64.b32decode(host.split(".")[0], True) elif host.find(':') == -1: return '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFF' + \ socket.inet_aton(host) @@ -114,39 +110,8 @@ def networkType(host): return 'IPv6' -def network_group(host): - """Canonical identifier of network group - simplified, borrowed from - GetGroup() in src/netaddresses.cpp in bitcoin core""" - if not isinstance(host, str): - return None - network_type = networkType(host) - try: - raw_host = encodeHost(host) - except socket.error: - return host - if network_type == 'IPv4': - decoded_host = checkIPv4Address(raw_host[12:], True) - if decoded_host: - # /16 subnet - return raw_host[12:14] - elif network_type == 'IPv6': - decoded_host = checkIPv6Address(raw_host, True) - if decoded_host: - # /32 subnet - return raw_host[0:12] - else: - # just host, e.g. for tor - return host - # global network type group for local, private, unroutable - return network_type - - def checkIPAddress(host, private=False): - """ - Returns hostStandardFormat if it is a valid IP address, - otherwise returns False - """ + """Returns hostStandardFormat if it is a valid IP address, otherwise returns False""" 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) @@ -162,46 +127,35 @@ def checkIPAddress(host, private=False): except ValueError: return False if hostStandardFormat == "": - # This can happen on Windows systems which are - # not 64-bit compatible so let us drop the IPv6 address. + # This can happen on Windows systems which are not 64-bit compatible + # so let us drop the IPv6 address. return False return checkIPv6Address(host, hostStandardFormat, private) def checkIPv4Address(host, hostStandardFormat, private=False): - """ - Returns hostStandardFormat if it is an IPv4 address, - otherwise returns False - """ + """Returns hostStandardFormat if it is an IPv4 address, otherwise returns False""" if host[0] == '\x7F': # 127/8 if not private: - logger.debug( - 'Ignoring IP address in loopback range: %s', - hostStandardFormat) + logger.debug('Ignoring IP address in loopback range: %s', hostStandardFormat) return hostStandardFormat if private else False if host[0] == '\x0A': # 10/8 if not private: - logger.debug( - 'Ignoring IP address in private range: %s', hostStandardFormat) + logger.debug('Ignoring IP address in private range: %s', hostStandardFormat) return hostStandardFormat if private else False if host[0:2] == '\xC0\xA8': # 192.168/16 if not private: - logger.debug( - 'Ignoring IP address in private range: %s', hostStandardFormat) + logger.debug('Ignoring IP address in private range: %s', hostStandardFormat) return hostStandardFormat if private else False if host[0:2] >= '\xAC\x10' and host[0:2] < '\xAC\x20': # 172.16/12 if not private: - logger.debug( - 'Ignoring IP address in private range: %s', hostStandardFormat) + logger.debug('Ignoring IP address in private range: %s', hostStandardFormat) return hostStandardFormat if private else False return False if private else hostStandardFormat def checkIPv6Address(host, hostStandardFormat, private=False): - """ - Returns hostStandardFormat if it is an IPv6 address, - otherwise returns False - """ + """Returns hostStandardFormat if it is an IPv6 address, otherwise returns False""" if host == ('\x00' * 15) + '\x01': if not private: logger.debug('Ignoring loopback address: %s', hostStandardFormat) @@ -212,8 +166,7 @@ def checkIPv6Address(host, hostStandardFormat, private=False): return hostStandardFormat if private else False if (ord(host[0]) & 0xfe) == 0xfc: if not private: - logger.debug( - 'Ignoring unique local address: %s', hostStandardFormat) + logger.debug('Ignoring unique local address: %s', hostStandardFormat) return hostStandardFormat if private else False return False if private else hostStandardFormat @@ -234,27 +187,28 @@ def haveSSL(server=False): def checkSocksIP(host): """Predicate to check if we're using a SOCKS proxy""" - sockshostname = BMConfigParser().safeGet( - 'bitmessagesettings', 'sockshostname') try: - if not state.socksIP: - state.socksIP = socket.gethostbyname(sockshostname) - except NameError: # uninitialised - state.socksIP = socket.gethostbyname(sockshostname) - except (TypeError, socket.gaierror): # None, resolving failure - state.socksIP = sockshostname + if state.socksIP is None or not state.socksIP: + state.socksIP = socket.gethostbyname(BMConfigParser().get("bitmessagesettings", "sockshostname")) + # uninitialised + except NameError: + state.socksIP = socket.gethostbyname(BMConfigParser().get("bitmessagesettings", "sockshostname")) + # resolving failure + except socket.gaierror: + state.socksIP = BMConfigParser().get("bitmessagesettings", "sockshostname") return state.socksIP == host -def isProofOfWorkSufficient( - data, nonceTrialsPerByte=0, payloadLengthExtraBytes=0, recvTime=0): +def isProofOfWorkSufficient(data, + nonceTrialsPerByte=0, + payloadLengthExtraBytes=0, + recvTime=0): """ - Validate an object's Proof of Work using method described - `here `_ - + Validate an object's Proof of Work using method described in: + https://bitmessage.org/wiki/Proof_of_work Arguments: - int nonceTrialsPerByte (default: from `.defaults`) - int payloadLengthExtraBytes (default: from `.defaults`) + int nonceTrialsPerByte (default: from default.py) + int payloadLengthExtraBytes (default: from default.py) float recvTime (optional) UNIX epoch time when object was received from the network (default: current system time) Returns: @@ -268,20 +222,18 @@ def isProofOfWorkSufficient( TTL = endOfLifeTime - (int(recvTime) if recvTime else int(time.time())) if TTL < 300: TTL = 300 - POW, = unpack('>Q', hashlib.sha512(hashlib.sha512( - data[:8] + hashlib.sha512(data[8:]).digest() - ).digest()).digest()[0:8]) - return POW <= 2 ** 64 / ( - nonceTrialsPerByte * ( - len(data) + payloadLengthExtraBytes + - ((TTL * (len(data) + payloadLengthExtraBytes)) / (2 ** 16)))) + POW, = unpack('>Q', 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)))) # Packet creation def CreatePacket(command, payload=''): - """Construct and return a packet""" + """Construct and return a number of bytes from a payload""" payload_length = len(payload) checksum = hashlib.sha512(payload).digest()[0:4] @@ -291,13 +243,8 @@ def CreatePacket(command, payload=''): return bytes(b) -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 - """ +def assembleVersionMessage(remoteHost, remotePort, participatingStreams, server=False, nodeid=None): + """Construct the payload of a version message, return the resultng bytes of running CreatePacket() on it""" payload = '' payload += pack('>L', 3) # protocol version. # bitflags of the services I offer. @@ -309,19 +256,15 @@ def assembleVersionMessage( ) payload += pack('>q', int(time.time())) - # boolservices of remote connection; ignored by the remote host. - payload += pack('>q', 1) - if checkSocksIP(remoteHost) and server: - # prevent leaking of tor outbound IP + payload += pack( + '>q', 1) # boolservices of remote connection; ignored by the remote host. + if checkSocksIP(remoteHost) and server: # prevent leaking of tor outbound IP payload += encodeHost('127.0.0.1') payload += pack('>H', 8444) else: # use first 16 bytes if host data is longer # for example in case of onion v3 service - try: - payload += encodeHost(remoteHost)[:16] - except socket.error: - payload += encodeHost('127.0.0.1') + payload += encodeHost(remoteHost)[:16] payload += pack('>H', remotePort) # remote IPv6 and port # bitflags of the services I offer. @@ -331,26 +274,23 @@ def assembleVersionMessage( (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 += '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFF' + pack( - '>L', 2130706433) + # = 127.0.0.1. This will be ignored by the remote host. The actual remote connected IP will be used. + 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 = BMConfigParser().safeGetInt('bitmessagesettings', 'extport') if ( extport and ((server and not checkSocksIP(remoteHost)) or ( - BMConfigParser().get('bitmessagesettings', 'socksproxytype') - == 'none' and not server)) + BMConfigParser().get('bitmessagesettings', 'socksproxytype') == + 'none' and not server)) ): payload += pack('>H', extport) elif checkSocksIP(remoteHost) and server: # incoming connection over Tor - payload += pack( - '>H', BMConfigParser().getint('bitmessagesettings', 'onionport')) + payload += pack('>H', BMConfigParser().getint('bitmessagesettings', 'onionport')) else: # no extport and not incoming over Tor - payload += pack( - '>H', BMConfigParser().getint('bitmessagesettings', 'port')) + payload += pack('>H', BMConfigParser().getint('bitmessagesettings', 'port')) + random.seed() if nodeid is not None: payload += nodeid[0:8] else: @@ -373,10 +313,7 @@ def assembleVersionMessage( def assembleErrorMessage(fatal=0, banTime=0, inventoryVector='', errorText=''): - """ - Construct the payload of an error message, - return the resulting bytes of running `CreatePacket` on it - """ + """Construct the payload of an error message, return the resultng bytes of running CreatePacket() on it""" payload = encodeVarint(fatal) payload += encodeVarint(banTime) payload += encodeVarint(len(inventoryVector)) @@ -513,7 +450,7 @@ def decryptAndCheckPubkeyPayload(data, address): except Exception: logger.critical( 'Pubkey decryption was UNsuccessful because of' - ' an unhandled exception! This is definitely a bug!', - exc_info=True + ' an unhandled exception! This is definitely a bug! \n%s', + traceback.format_exc() ) return 'failed' diff --git a/src/pyelliptic/__init__.py b/src/pyelliptic/__init__.py index dbc1b2af..761d08af 100644 --- a/src/pyelliptic/__init__.py +++ b/src/pyelliptic/__init__.py @@ -1,28 +1,19 @@ -""" -Copyright (C) 2010 -Author: Yann GUIBET -Contact: - -Python OpenSSL wrapper. -For modern cryptography with ECC, AES, HMAC, Blowfish, ... - -This is an abandoned package maintained inside of the PyBitmessage. -""" - -from .cipher import Cipher -from .ecc import ECC -from .eccblind import ECCBlind -from .hash import hmac_sha256, hmac_sha512, pbkdf2 -from .openssl import OpenSSL +# Copyright (C) 2010 +# Author: Yann GUIBET +# Contact: __version__ = '1.3' __all__ = [ 'OpenSSL', 'ECC', - 'ECCBlind', 'Cipher', 'hmac_sha256', 'hmac_sha512', 'pbkdf2' ] + +from .openssl import OpenSSL +from .ecc import ECC +from .cipher import Cipher +from .hash import hmac_sha256, hmac_sha512, pbkdf2 diff --git a/src/pyelliptic/arithmetic.py b/src/pyelliptic/arithmetic.py index 83e634ad..95c85b93 100644 --- a/src/pyelliptic/arithmetic.py +++ b/src/pyelliptic/arithmetic.py @@ -1,6 +1,5 @@ -""" -Arithmetic Expressions -""" +# pylint: disable=missing-docstring,too-many-function-args + import hashlib import re @@ -12,7 +11,6 @@ G = (Gx, Gy) def inv(a, n): - """Inversion""" lm, hm = 1, 0 low, high = a % n, n while low > 1: @@ -23,7 +21,6 @@ def inv(a, n): def get_code_string(base): - """Returns string according to base value""" if base == 2: return '01' elif base == 10: @@ -39,7 +36,6 @@ def get_code_string(base): def encode(val, base, minlen=0): - """Returns the encoded string""" code_string = get_code_string(base) result = "" while val > 0: @@ -51,7 +47,6 @@ def encode(val, base, minlen=0): def decode(string, base): - """Returns the decoded string""" code_string = get_code_string(base) result = 0 if base == 16: @@ -64,13 +59,10 @@ def decode(string, base): def changebase(string, frm, to, minlen=0): - """Change base of the string""" return encode(decode(string, frm), to, minlen) def base10_add(a, b): - """Adding the numbers that are of base10""" - # pylint: disable=too-many-function-args if a is None: return b[0], b[1] if b is None: @@ -86,7 +78,6 @@ def base10_add(a, b): def base10_double(a): - """Double the numbers that are of base10""" if a is None: return None m = ((3 * a[0] * a[0] + A) * inv(2 * a[1], P)) % P @@ -96,7 +87,6 @@ def base10_double(a): def base10_multiply(a, n): - """Multiply the numbers that are of base10""" if n == 0: return G if n == 1: @@ -109,35 +99,28 @@ def base10_multiply(a, n): def hex_to_point(h): - """Converting hexadecimal to point value""" return (decode(h[2:66], 16), decode(h[66:], 16)) def point_to_hex(p): - """Converting point value to hexadecimal""" return '04' + encode(p[0], 16, 64) + encode(p[1], 16, 64) def multiply(privkey, pubkey): - """Multiplying keys""" - return point_to_hex(base10_multiply( - hex_to_point(pubkey), decode(privkey, 16))) + return point_to_hex(base10_multiply(hex_to_point(pubkey), decode(privkey, 16))) def privtopub(privkey): - """Converting key from private to public""" return point_to_hex(base10_multiply(G, decode(privkey, 16))) def add(p1, p2): - """Adding two public keys""" if len(p1) == 32: return encode(decode(p1, 16) + decode(p2, 16) % P, 16, 32) return point_to_hex(base10_add(hex_to_point(p1), hex_to_point(p2))) def hash_160(string): - """Hashed version of public key""" intermed = hashlib.sha256(string).digest() ripemd160 = hashlib.new('ripemd160') ripemd160.update(intermed) @@ -145,18 +128,17 @@ def hash_160(string): def dbl_sha256(string): - """Double hashing (SHA256)""" return hashlib.sha256(hashlib.sha256(string).digest()).digest() def bin_to_b58check(inp): - """Convert binary to base58""" inp_fmtd = '\x00' + inp leadingzbytes = len(re.match('^\x00*', inp_fmtd).group(0)) checksum = dbl_sha256(inp_fmtd)[:4] return '1' * leadingzbytes + changebase(inp_fmtd + checksum, 256, 58) +# Convert a public key (in hex) to a Bitcoin address + def pubkey_to_address(pubkey): - """Convert a public key (in hex) to a Bitcoin address""" return bin_to_b58check(hash_160(changebase(pubkey, 16, 256))) diff --git a/src/pyelliptic/cipher.py b/src/pyelliptic/cipher.py index 4057e169..b597cafa 100644 --- a/src/pyelliptic/cipher.py +++ b/src/pyelliptic/cipher.py @@ -1,18 +1,15 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -""" -Symmetric Encryption -""" + # Copyright (C) 2011 Yann GUIBET # See LICENSE for details. -from openssl import OpenSSL +from pyelliptic.openssl import OpenSSL -# pylint: disable=redefined-builtin -class Cipher(object): +class Cipher: """ - Main class for encryption + Symmetric encryption import pyelliptic iv = pyelliptic.Cipher.gen_IV('aes-256-cfb') @@ -47,34 +44,30 @@ class Cipher(object): @staticmethod def get_blocksize(ciphername): - """This Method returns cipher blocksize""" cipher = OpenSSL.get_cipher(ciphername) return cipher.get_blocksize() @staticmethod def gen_IV(ciphername): - """Generate random initialization vector""" cipher = OpenSSL.get_cipher(ciphername) return OpenSSL.rand(cipher.get_blocksize()) def update(self, input): - """Update result with more data""" i = OpenSSL.c_int(0) buffer = OpenSSL.malloc(b"", len(input) + self.cipher.get_blocksize()) inp = OpenSSL.malloc(input, len(input)) if OpenSSL.EVP_CipherUpdate(self.ctx, OpenSSL.byref(buffer), OpenSSL.byref(i), inp, len(input)) == 0: raise Exception("[OpenSSL] EVP_CipherUpdate FAIL ...") - return buffer.raw[0:i.value] # pylint: disable=invalid-slice-index + return buffer.raw[0:i.value] def final(self): - """Returning the final value""" i = OpenSSL.c_int(0) buffer = OpenSSL.malloc(b"", self.cipher.get_blocksize()) if (OpenSSL.EVP_CipherFinal_ex(self.ctx, OpenSSL.byref(buffer), OpenSSL.byref(i))) == 0: raise Exception("[OpenSSL] EVP_CipherFinal_ex FAIL ...") - return buffer.raw[0:i.value] # pylint: disable=invalid-slice-index + return buffer.raw[0:i.value] def ciphering(self, input): """ @@ -84,7 +77,6 @@ class Cipher(object): return buff + self.final() def __del__(self): - # pylint: disable=protected-access if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: OpenSSL.EVP_CIPHER_CTX_reset(self.ctx) else: diff --git a/src/pyelliptic/ecc.py b/src/pyelliptic/ecc.py index a7f5a6b7..fb0d6773 100644 --- a/src/pyelliptic/ecc.py +++ b/src/pyelliptic/ecc.py @@ -1,18 +1,20 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- """ -Asymmetric cryptography using elliptic curves +src/pyelliptic/ecc.py +===================== """ -# pylint: disable=protected-access, too-many-branches, too-many-locals +# pylint: disable=protected-access + # Copyright (C) 2011 Yann GUIBET # See LICENSE for details. from hashlib import sha512 from struct import pack, unpack -from cipher import Cipher -from hash import equals, hmac_sha256 -from openssl import OpenSSL +from pyelliptic.cipher import Cipher +from pyelliptic.hash import equals, hmac_sha256 +from pyelliptic.openssl import OpenSSL class ECC(object): @@ -171,8 +173,7 @@ class ECC(object): if OpenSSL.EC_POINT_get_affine_coordinates_GFp( group, pub_key, pub_key_x, pub_key_y, 0) == 0: - raise Exception( - "[OpenSSL] EC_POINT_get_affine_coordinates_GFp FAIL ...") + raise Exception("[OpenSSL] EC_POINT_get_affine_coordinates_GFp FAIL ...") privkey = OpenSSL.malloc(0, OpenSSL.BN_num_bytes(priv_key)) pubkeyx = OpenSSL.malloc(0, OpenSSL.BN_num_bytes(pub_key_x)) @@ -275,6 +276,7 @@ class ECC(object): def raw_check_key(self, privkey, pubkey_x, pubkey_y, curve=None): """Check key validity, key is supplied as binary data""" + # pylint: disable=too-many-branches if curve is None: curve = self.curve elif isinstance(curve, str): @@ -322,6 +324,7 @@ class ECC(object): """ Sign the input with ECDSA method and returns the signature """ + # pylint: disable=too-many-branches,too-many-locals try: size = len(inputb) buff = OpenSSL.malloc(inputb, size) @@ -391,6 +394,7 @@ class ECC(object): Verify the signature with the input and the local public key. Returns a boolean """ + # pylint: disable=too-many-branches try: bsig = OpenSSL.malloc(sig, len(sig)) binputb = OpenSSL.malloc(inputb, len(inputb)) @@ -433,13 +437,10 @@ class ECC(object): 0, digest, dgst_len.contents, bsig, len(sig), key) if ret == -1: - # Fail to Check - return False + return False # Fail to Check if ret == 0: - # Bad signature ! - return False - # Good - return True + return False # Bad signature ! + return True # Good finally: OpenSSL.EC_KEY_free(key) @@ -487,6 +488,7 @@ class ECC(object): """ Decrypt data with ECIES method using the local private key """ + # pylint: disable=too-many-locals blocksize = OpenSSL.get_cipher(ciphername).get_blocksize() iv = data[:blocksize] i = blocksize diff --git a/src/pyelliptic/eccblind.py b/src/pyelliptic/eccblind.py deleted file mode 100644 index 5b9b045e..00000000 --- a/src/pyelliptic/eccblind.py +++ /dev/null @@ -1,210 +0,0 @@ -#!/usr/bin/env python -""" -ECC blind signature functionality based on -"An Efficient Blind Signature Scheme -Based on the Elliptic CurveDiscrete Logarithm Problem" by Morteza Nikooghadama - and Ali Zakerolhosseini , -http://www.isecure-journal.com/article_39171_47f9ec605dd3918c2793565ec21fcd7a.pdf -""" - -# variable names are based on the math in the paper, so they don't conform -# to PEP8 - -from .openssl import OpenSSL - - -class ECCBlind(object): # pylint: disable=too-many-instance-attributes - """ - Class for ECC blind signature functionality - """ - - # init - k = None - R = None - keypair = None - F = None - Q = None - a = None - b = None - c = None - binv = None - r = None - m = None - m_ = None - s_ = None - signature = None - - @staticmethod - def ec_get_random(group, ctx): - """ - Random point from finite field - """ - order = OpenSSL.BN_new() - OpenSSL.EC_GROUP_get_order(group, order, ctx) - OpenSSL.BN_rand(order, OpenSSL.BN_num_bits(order), 0, 0) - return order - - @staticmethod - def ec_invert(group, a, ctx): - """ - ECC inversion - """ - order = OpenSSL.BN_new() - OpenSSL.EC_GROUP_get_order(group, order, ctx) - inverse = OpenSSL.BN_mod_inverse(0, a, order, ctx) - return inverse - - @staticmethod - def ec_gen_keypair(group, ctx): - """ - Generate an ECC keypair - """ - d = ECCBlind.ec_get_random(group, ctx) - Q = OpenSSL.EC_POINT_new(group) - OpenSSL.EC_POINT_mul(group, Q, d, 0, 0, 0) - return (d, Q) - - @staticmethod - def ec_Ftor(F, group, ctx): - """ - x0 coordinate of F - """ - # F = (x0, y0) - x0 = OpenSSL.BN_new() - y0 = OpenSSL.BN_new() - OpenSSL.EC_POINT_get_affine_coordinates_GFp(group, F, x0, y0, ctx) - return x0 - - def __init__(self, curve="secp256k1", pubkey=None): - self.ctx = OpenSSL.BN_CTX_new() - - if pubkey: - self.group, self.G, self.n, self.Q = pubkey - else: - self.group = OpenSSL.EC_GROUP_new_by_curve_name( - OpenSSL.get_curve(curve)) - # Order n - self.n = OpenSSL.BN_new() - OpenSSL.EC_GROUP_get_order(self.group, self.n, self.ctx) - - # Generator G - self.G = OpenSSL.EC_GROUP_get0_generator(self.group) - - # new keypair - self.keypair = ECCBlind.ec_gen_keypair(self.group, self.ctx) - - self.Q = self.keypair[1] - - self.pubkey = (self.group, self.G, self.n, self.Q) - - # Identity O (infinity) - self.iO = OpenSSL.EC_POINT_new(self.group) - OpenSSL.EC_POINT_set_to_infinity(self.group, self.iO) - - def signer_init(self): - """ - Init signer - """ - # Signer: Random integer k - self.k = ECCBlind.ec_get_random(self.group, self.ctx) - - # R = kG - self.R = OpenSSL.EC_POINT_new(self.group) - OpenSSL.EC_POINT_mul(self.group, self.R, self.k, 0, 0, 0) - - return self.R - - def create_signing_request(self, R, msg): - """ - Requester creates a new signing request - """ - self.R = R - - # Requester: 3 random blinding factors - self.F = OpenSSL.EC_POINT_new(self.group) - OpenSSL.EC_POINT_set_to_infinity(self.group, self.F) - temp = OpenSSL.EC_POINT_new(self.group) - abinv = OpenSSL.BN_new() - - # F != O - while OpenSSL.EC_POINT_cmp(self.group, self.F, self.iO, self.ctx) == 0: - self.a = ECCBlind.ec_get_random(self.group, self.ctx) - self.b = ECCBlind.ec_get_random(self.group, self.ctx) - self.c = ECCBlind.ec_get_random(self.group, self.ctx) - - # F = b^-1 * R... - self.binv = ECCBlind.ec_invert(self.group, self.b, self.ctx) - 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, 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, 0, self.G, self.c, 0) - OpenSSL.EC_POINT_add(self.group, self.F, self.F, temp, 0) - - # F = (x0, y0) - self.r = ECCBlind.ec_Ftor(self.F, self.group, self.ctx) - - # Requester: Blinding (m' = br(m) + a) - self.m = OpenSSL.BN_new() - OpenSSL.BN_bin2bn(msg, len(msg), self.m) - - self.m_ = OpenSSL.BN_new() - OpenSSL.BN_mod_mul(self.m_, self.b, self.r, self.n, self.ctx) - OpenSSL.BN_mod_mul(self.m_, self.m_, self.m, self.n, self.ctx) - OpenSSL.BN_mod_add(self.m_, self.m_, self.a, self.n, self.ctx) - return self.m_ - - def blind_sign(self, m_): - """ - Signer blind-signs the request - """ - self.m_ = m_ - self.s_ = OpenSSL.BN_new() - OpenSSL.BN_mod_mul(self.s_, self.keypair[0], self.m_, self.n, self.ctx) - OpenSSL.BN_mod_add(self.s_, self.s_, self.k, self.n, self.ctx) - return self.s_ - - def unblind(self, s_): - """ - Requester unblinds the signature - """ - self.s_ = s_ - s = OpenSSL.BN_new() - OpenSSL.BN_mod_mul(s, self.binv, self.s_, self.n, self.ctx) - OpenSSL.BN_mod_add(s, s, self.c, self.n, self.ctx) - self.signature = (s, self.F) - return self.signature - - def verify(self, msg, signature): - """ - Verify signature with certifier's pubkey - """ - - # convert msg to BIGNUM - self.m = OpenSSL.BN_new() - OpenSSL.BN_bin2bn(msg, len(msg), self.m) - - # init - s, self.F = signature - if self.r is None: - self.r = ECCBlind.ec_Ftor(self.F, self.group, self.ctx) - - lhs = OpenSSL.EC_POINT_new(self.group) - rhs = OpenSSL.EC_POINT_new(self.group) - - OpenSSL.EC_POINT_mul(self.group, lhs, s, 0, 0, 0) - - OpenSSL.EC_POINT_mul(self.group, 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) - if retval == -1: - raise RuntimeError("EC_POINT_cmp returned an error") - else: - return retval == 0 diff --git a/src/pyelliptic/hash.py b/src/pyelliptic/hash.py index f098d631..fb910dd4 100644 --- a/src/pyelliptic/hash.py +++ b/src/pyelliptic/hash.py @@ -1,10 +1,10 @@ -""" -Wrappers for hash functions from OpenSSL. -""" +#!/usr/bin/env python +# -*- coding: utf-8 -*- + # Copyright (C) 2011 Yann GUIBET # See LICENSE for details. -from openssl import OpenSSL +from pyelliptic.openssl import OpenSSL # For python3 @@ -27,10 +27,10 @@ def _equals_str(a, b): def equals(a, b): - """Compare two strings or bytearrays""" if isinstance(a, str): return _equals_str(a, b) - return _equals_bytes(a, b) + else: + return _equals_bytes(a, b) def hmac_sha256(k, m): @@ -58,7 +58,6 @@ def hmac_sha512(k, m): def pbkdf2(password, salt=None, i=10000, keylen=64): - """Key derivation function using SHA256""" if salt is None: salt = OpenSSL.rand(8) p_password = OpenSSL.malloc(password, len(password)) diff --git a/src/pyelliptic/openssl.py b/src/pyelliptic/openssl.py index 7cf1e2c5..115bdc08 100644 --- a/src/pyelliptic/openssl.py +++ b/src/pyelliptic/openssl.py @@ -1,53 +1,42 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + # Copyright (C) 2011 Yann GUIBET # See LICENSE for details. # # Software slightly changed by Jonathan Warren -# pylint: disable=protected-access, import-error -""" -This module loads openssl libs with ctypes and incapsulates -needed openssl functionality in class _OpenSSL. -""" -import ctypes -from kivy.utils import platform + import sys -# pylint: disable=protected-access +import ctypes OpenSSL = None -class CipherName(object): - """Class returns cipher name, pointer and blocksize""" - +class CipherName: def __init__(self, name, pointer, blocksize): self._name = name self._pointer = pointer self._blocksize = blocksize def __str__(self): - return "Cipher : " + self._name + \ - " | Blocksize : " + str(self._blocksize) + \ - " | Function pointer : " + str(self._pointer) + return "Cipher : " + self._name + " | Blocksize : " + str(self._blocksize) + " | Fonction pointer : " + str(self._pointer) def get_pointer(self): - """Method returns the pointer""" return self._pointer() def get_name(self): - """This method returns cipher name""" return self._name def get_blocksize(self): - """This method returns cipher blocksize""" return self._blocksize def get_version(library): - """Method returns the version of the OpenSSL Library""" version = None hexversion = None cflags = None try: - # OpenSSL 1.1 + #OpenSSL 1.1 OPENSSL_VERSION = 0 OPENSSL_CFLAGS = 1 library.OpenSSL_version.argtypes = [ctypes.c_int] @@ -58,7 +47,7 @@ def get_version(library): hexversion = library.OpenSSL_version_num() except AttributeError: try: - # OpenSSL 1.0 + #OpenSSL 1.0 SSLEAY_VERSION = 0 SSLEAY_CFLAGS = 2 library.SSLeay.restype = ctypes.c_long @@ -68,18 +57,19 @@ def get_version(library): cflags = library.SSLeay_version(SSLEAY_CFLAGS) hexversion = library.SSLeay() except AttributeError: - # raise NotImplementedError('Cannot determine version of this OpenSSL library.') + #raise NotImplementedError('Cannot determine version of this OpenSSL library.') pass return (version, hexversion, cflags) -class _OpenSSL(object): +class _OpenSSL: """ Wrapper for OpenSSL using ctypes """ - # pylint: disable=too-many-statements, too-many-instance-attributes def __init__(self, library): - """Build the wrapper""" + """ + Build the wrapper + """ self._lib = ctypes.CDLL(library) self._version, self._hexversion, self._cflags = get_version(self._lib) self._libreSSL = self._version.startswith("LibreSSL") @@ -138,14 +128,9 @@ class _OpenSSL(object): self.EC_KEY_get0_group.restype = ctypes.c_void_p self.EC_KEY_get0_group.argtypes = [ctypes.c_void_p] - self.EC_POINT_get_affine_coordinates_GFp = \ - self._lib.EC_POINT_get_affine_coordinates_GFp + self.EC_POINT_get_affine_coordinates_GFp = self._lib.EC_POINT_get_affine_coordinates_GFp self.EC_POINT_get_affine_coordinates_GFp.restype = ctypes.c_int - self.EC_POINT_get_affine_coordinates_GFp.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_void_p] + self.EC_POINT_get_affine_coordinates_GFp.argtypes = [ctypes.c_void_p, ctypes.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 @@ -159,17 +144,11 @@ class _OpenSSL(object): self.EC_KEY_set_group = self._lib.EC_KEY_set_group self.EC_KEY_set_group.restype = ctypes.c_int - self.EC_KEY_set_group.argtypes = [ctypes.c_void_p, - ctypes.c_void_p] + self.EC_KEY_set_group.argtypes = [ctypes.c_void_p, ctypes.c_void_p] - self.EC_POINT_set_affine_coordinates_GFp = \ - self._lib.EC_POINT_set_affine_coordinates_GFp + self.EC_POINT_set_affine_coordinates_GFp = self._lib.EC_POINT_set_affine_coordinates_GFp self.EC_POINT_set_affine_coordinates_GFp.restype = ctypes.c_int - self.EC_POINT_set_affine_coordinates_GFp.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_void_p] + self.EC_POINT_set_affine_coordinates_GFp.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] self.EC_POINT_new = self._lib.EC_POINT_new self.EC_POINT_new.restype = ctypes.c_void_p @@ -185,11 +164,7 @@ class _OpenSSL(object): self.EC_POINT_mul = self._lib.EC_POINT_mul self.EC_POINT_mul.restype = None - self.EC_POINT_mul.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_void_p] + self.EC_POINT_mul.argtypes = [ctypes.c_void_p, 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 @@ -200,11 +175,10 @@ class _OpenSSL(object): self.EC_KEY_OpenSSL = self._lib.EC_KEY_OpenSSL self._lib.EC_KEY_OpenSSL.restype = ctypes.c_void_p self._lib.EC_KEY_OpenSSL.argtypes = [] - + self.EC_KEY_set_method = self._lib.EC_KEY_set_method self._lib.EC_KEY_set_method.restype = ctypes.c_int - self._lib.EC_KEY_set_method.argtypes = [ctypes.c_void_p, - ctypes.c_void_p] + self._lib.EC_KEY_set_method.argtypes = [ctypes.c_void_p, ctypes.c_void_p] else: self.ECDH_OpenSSL = self._lib.ECDH_OpenSSL self._lib.ECDH_OpenSSL.restype = ctypes.c_void_p @@ -212,8 +186,7 @@ class _OpenSSL(object): self.ECDH_set_method = self._lib.ECDH_set_method self._lib.ECDH_set_method.restype = ctypes.c_int - self._lib.ECDH_set_method.argtypes = [ctypes.c_void_p, - ctypes.c_void_p] + self._lib.ECDH_set_method.argtypes = [ctypes.c_void_p, ctypes.c_void_p] self.BN_CTX_new = self._lib.BN_CTX_new self._lib.BN_CTX_new.restype = ctypes.c_void_p @@ -222,15 +195,12 @@ class _OpenSSL(object): self.ECDH_compute_key = self._lib.ECDH_compute_key self.ECDH_compute_key.restype = ctypes.c_int self.ECDH_compute_key.argtypes = [ctypes.c_void_p, - ctypes.c_int, - ctypes.c_void_p, - ctypes.c_void_p] + ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p] self.EVP_CipherInit_ex = self._lib.EVP_CipherInit_ex self.EVP_CipherInit_ex.restype = ctypes.c_int self.EVP_CipherInit_ex.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_void_p] + ctypes.c_void_p, ctypes.c_void_p] self.EVP_CIPHER_CTX_new = self._lib.EVP_CIPHER_CTX_new self.EVP_CIPHER_CTX_new.restype = ctypes.c_void_p @@ -253,13 +223,13 @@ class _OpenSSL(object): self.EVP_aes_256_cbc.restype = ctypes.c_void_p self.EVP_aes_256_cbc.argtypes = [] - # self.EVP_aes_128_ctr = self._lib.EVP_aes_128_ctr - # self.EVP_aes_128_ctr.restype = ctypes.c_void_p - # self.EVP_aes_128_ctr.argtypes = [] + #self.EVP_aes_128_ctr = self._lib.EVP_aes_128_ctr + #self.EVP_aes_128_ctr.restype = ctypes.c_void_p + #self.EVP_aes_128_ctr.argtypes = [] - # self.EVP_aes_256_ctr = self._lib.EVP_aes_256_ctr - # self.EVP_aes_256_ctr.restype = ctypes.c_void_p - # self.EVP_aes_256_ctr.argtypes = [] + #self.EVP_aes_256_ctr = self._lib.EVP_aes_256_ctr + #self.EVP_aes_256_ctr.restype = ctypes.c_void_p + #self.EVP_aes_256_ctr.argtypes = [] self.EVP_aes_128_ofb = self._lib.EVP_aes_128_ofb self.EVP_aes_128_ofb.restype = ctypes.c_void_p @@ -280,7 +250,7 @@ class _OpenSSL(object): self.EVP_rc4 = self._lib.EVP_rc4 self.EVP_rc4.restype = ctypes.c_void_p self.EVP_rc4.argtypes = [] - + if self._hexversion >= 0x10100000 and not self._libreSSL: self.EVP_CIPHER_CTX_reset = self._lib.EVP_CIPHER_CTX_reset self.EVP_CIPHER_CTX_reset.restype = ctypes.c_int @@ -297,8 +267,7 @@ class _OpenSSL(object): self.EVP_CipherUpdate = self._lib.EVP_CipherUpdate self.EVP_CipherUpdate.restype = ctypes.c_int self.EVP_CipherUpdate.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, ctypes.c_void_p, - ctypes.c_void_p, ctypes.c_int] + ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int] self.EVP_CipherFinal_ex = self._lib.EVP_CipherFinal_ex self.EVP_CipherFinal_ex.restype = ctypes.c_int @@ -312,7 +281,7 @@ class _OpenSSL(object): self.EVP_DigestInit_ex = self._lib.EVP_DigestInit_ex self.EVP_DigestInit_ex.restype = ctypes.c_int self._lib.EVP_DigestInit_ex.argtypes = 3 * [ctypes.c_void_p] - + self.EVP_DigestUpdate = self._lib.EVP_DigestUpdate self.EVP_DigestUpdate.restype = ctypes.c_int self.EVP_DigestUpdate.argtypes = [ctypes.c_void_p, @@ -327,24 +296,22 @@ class _OpenSSL(object): self.EVP_DigestFinal_ex.restype = ctypes.c_int self.EVP_DigestFinal_ex.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] - + self.ECDSA_sign = self._lib.ECDSA_sign self.ECDSA_sign.restype = ctypes.c_int self.ECDSA_sign.argtypes = [ctypes.c_int, ctypes.c_void_p, - ctypes.c_int, ctypes.c_void_p, - ctypes.c_void_p, ctypes.c_void_p] + ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] self.ECDSA_verify = self._lib.ECDSA_verify self.ECDSA_verify.restype = ctypes.c_int self.ECDSA_verify.argtypes = [ctypes.c_int, ctypes.c_void_p, - ctypes.c_int, ctypes.c_void_p, - ctypes.c_int, ctypes.c_void_p] + ctypes.c_int, ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p] if self._hexversion >= 0x10100000 and not self._libreSSL: self.EVP_MD_CTX_new = self._lib.EVP_MD_CTX_new self.EVP_MD_CTX_new.restype = ctypes.c_void_p self.EVP_MD_CTX_new.argtypes = [] - + self.EVP_MD_CTX_reset = self._lib.EVP_MD_CTX_reset self.EVP_MD_CTX_reset.restype = None self.EVP_MD_CTX_reset.argtypes = [ctypes.c_void_p] @@ -362,11 +329,11 @@ class _OpenSSL(object): self.EVP_MD_CTX_create = self._lib.EVP_MD_CTX_create self.EVP_MD_CTX_create.restype = ctypes.c_void_p self.EVP_MD_CTX_create.argtypes = [] - + self.EVP_MD_CTX_init = self._lib.EVP_MD_CTX_init self.EVP_MD_CTX_init.restype = None self.EVP_MD_CTX_init.argtypes = [ctypes.c_void_p] - + self.EVP_MD_CTX_destroy = self._lib.EVP_MD_CTX_destroy self.EVP_MD_CTX_destroy.restype = None self.EVP_MD_CTX_destroy.argtypes = [ctypes.c_void_p] @@ -396,167 +363,36 @@ class _OpenSSL(object): self.HMAC = self._lib.HMAC self.HMAC.restype = ctypes.c_void_p self.HMAC.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int, - ctypes.c_void_p, ctypes.c_int, - ctypes.c_void_p, ctypes.c_void_p] + 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: # The above is not compatible with all versions of OSX. self.PKCS5_PBKDF2_HMAC = self._lib.PKCS5_PBKDF2_HMAC_SHA1 - + self.PKCS5_PBKDF2_HMAC.restype = ctypes.c_int self.PKCS5_PBKDF2_HMAC.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p, ctypes.c_int, ctypes.c_int, ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p] - # Blind signature requirements - self.BN_CTX_new = self._lib.BN_CTX_new - self.BN_CTX_new.restype = ctypes.c_void_p - self.BN_CTX_new.argtypes = [] - - self.BN_dup = self._lib.BN_dup - self.BN_dup.restype = ctypes.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.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.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.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.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.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.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.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.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.c_void_p, - ctypes.c_void_p] - - self.BN_bn2dec = self._lib.BN_bn2dec - self.BN_bn2dec.restype = ctypes.c_char_p - self.BN_bn2dec.argtypes = [ctypes.c_void_p] - - self.BN_CTX_free = self._lib.BN_CTX_free - self.BN_CTX_free.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 - self.EC_GROUP_new_by_curve_name.argtypes = [ctypes.c_int] - - self.EC_GROUP_get_order = self._lib.EC_GROUP_get_order - self.EC_GROUP_get_order.restype = ctypes.c_int - self.EC_GROUP_get_order.argtypes = [ctypes.c_void_p, - ctypes.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.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.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.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.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.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.c_void_p] - self._set_ciphers() self._set_curves() def _set_ciphers(self): self.cipher_algo = { - 'aes-128-cbc': CipherName( - 'aes-128-cbc', self.EVP_aes_128_cbc, 16), - 'aes-256-cbc': CipherName( - 'aes-256-cbc', self.EVP_aes_256_cbc, 16), - 'aes-128-cfb': CipherName( - 'aes-128-cfb', self.EVP_aes_128_cfb128, 16), - 'aes-256-cfb': CipherName( - 'aes-256-cfb', self.EVP_aes_256_cfb128, 16), - 'aes-128-ofb': CipherName( - 'aes-128-ofb', self._lib.EVP_aes_128_ofb, 16), - 'aes-256-ofb': CipherName( - 'aes-256-ofb', self._lib.EVP_aes_256_ofb, 16), - # 'aes-128-ctr': CipherName( - # 'aes-128-ctr', self._lib.EVP_aes_128_ctr, 16), - # 'aes-256-ctr': CipherName( - # 'aes-256-ctr', self._lib.EVP_aes_256_ctr, 16), - 'bf-cfb': CipherName( - 'bf-cfb', self.EVP_bf_cfb64, 8), - 'bf-cbc': CipherName( - 'bf-cbc', self.EVP_bf_cbc, 8), - # 128 is the initialisation size not block size - 'rc4': CipherName( - 'rc4', self.EVP_rc4, 128), + 'aes-128-cbc': CipherName('aes-128-cbc', self.EVP_aes_128_cbc, 16), + 'aes-256-cbc': CipherName('aes-256-cbc', self.EVP_aes_256_cbc, 16), + 'aes-128-cfb': CipherName('aes-128-cfb', self.EVP_aes_128_cfb128, 16), + 'aes-256-cfb': CipherName('aes-256-cfb', self.EVP_aes_256_cfb128, 16), + 'aes-128-ofb': CipherName('aes-128-ofb', self._lib.EVP_aes_128_ofb, 16), + 'aes-256-ofb': CipherName('aes-256-ofb', self._lib.EVP_aes_256_ofb, 16), + #'aes-128-ctr': CipherName('aes-128-ctr', self._lib.EVP_aes_128_ctr, 16), + #'aes-256-ctr': CipherName('aes-256-ctr', self._lib.EVP_aes_256_ctr, 16), + 'bf-cfb': CipherName('bf-cfb', self.EVP_bf_cfb64, 8), + 'bf-cbc': CipherName('bf-cbc', self.EVP_bf_cbc, 8), + 'rc4': CipherName('rc4', self.EVP_rc4, 128), # 128 is the initialisation size not block size } def _set_curves(self): @@ -616,13 +452,13 @@ class _OpenSSL(object): raise Exception("Unknown curve") return self.curves[name] - def get_curve_by_id(self, id_): + def get_curve_by_id(self, id): """ returns the name of a elliptic curve with his id """ res = None for i in self.curves: - if self.curves[i] == id_: + if self.curves[i] == id: res = i break if res is None: @@ -633,64 +469,47 @@ class _OpenSSL(object): """ OpenSSL random function """ - buffer_ = self.malloc(0, size) - # This pyelliptic library, by default, didn't check the return value - # of RAND_bytes. It is evidently possible that it returned an error - # and not-actually-random data. However, in tests on various - # operating systems, while generating hundreds of gigabytes of random - # strings of various sizes I could not get an error to occur. - # Also Bitcoin doesn't check the return value of RAND_bytes either. + buffer = self.malloc(0, size) + # This pyelliptic library, by default, didn't check the return value of RAND_bytes. It is + # evidently possible that it returned an error and not-actually-random data. However, in + # tests on various operating systems, while generating hundreds of gigabytes of random + # strings of various sizes I could not get an error to occur. Also Bitcoin doesn't check + # the return value of RAND_bytes either. # Fixed in Bitmessage version 0.4.2 (in source code on 2013-10-13) - while self.RAND_bytes(buffer_, size) != 1: + while self.RAND_bytes(buffer, size) != 1: import time time.sleep(1) - return buffer_.raw + return buffer.raw def malloc(self, data, size): """ returns a create_string_buffer (ctypes) """ - buffer_ = None + buffer = None if data != 0: if sys.version_info.major == 3 and isinstance(data, type('')): data = data.encode() - buffer_ = self.create_string_buffer(data, size) + buffer = self.create_string_buffer(data, size) else: - buffer_ = self.create_string_buffer(size) - return buffer_ - + buffer = self.create_string_buffer(size) + return buffer def loadOpenSSL(): - """Method find and load the OpenSSL library""" - # pylint: disable=global-statement, protected-access, too-many-branches - global OpenSSL from os import path, environ from ctypes.util import find_library - + libdir = [] - if getattr(sys, 'frozen', None): + if getattr(sys,'frozen', None): if 'darwin' in sys.platform: libdir.extend([ - path.join( - environ['RESOURCEPATH'], '..', - 'Frameworks', 'libcrypto.dylib'), - path.join( - environ['RESOURCEPATH'], '..', - 'Frameworks', 'libcrypto.1.1.0.dylib'), - path.join( - environ['RESOURCEPATH'], '..', - 'Frameworks', 'libcrypto.1.0.2.dylib'), - path.join( - environ['RESOURCEPATH'], '..', - 'Frameworks', 'libcrypto.1.0.1.dylib'), - path.join( - environ['RESOURCEPATH'], '..', - 'Frameworks', 'libcrypto.1.0.0.dylib'), - path.join( - environ['RESOURCEPATH'], '..', - 'Frameworks', 'libcrypto.0.9.8.dylib'), - ]) + path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.dylib'), + path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.1.1.0.dylib'), + path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.1.0.2.dylib'), + path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.1.0.1.dylib'), + path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.1.0.0.dylib'), + path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.0.9.8.dylib'), + ]) elif 'win32' in sys.platform or 'win64' in sys.platform: libdir.append(path.join(sys._MEIPASS, 'libeay32.dll')) else: @@ -709,25 +528,16 @@ def loadOpenSSL(): path.join(sys._MEIPASS, 'libssl.so.0.9.8'), ]) if 'darwin' in sys.platform: - libdir.extend([ - 'libcrypto.dylib', '/usr/local/opt/openssl/lib/libcrypto.dylib']) + libdir.extend(['libcrypto.dylib', '/usr/local/opt/openssl/lib/libcrypto.dylib']) elif 'win32' in sys.platform or 'win64' in sys.platform: libdir.append('libeay32.dll') - elif platform == "android": - libdir.append('libcrypto1.0.2p.so') - libdir.append('libssl1.0.2p.so') - libdir.append('libcrypto1.1.so') - libdir.append('libssl1.1.so') else: libdir.append('libcrypto.so') libdir.append('libssl.so') libdir.append('libcrypto.so.1.0.0') libdir.append('libssl.so.1.0.0') if 'linux' in sys.platform or 'darwin' in sys.platform or 'bsd' in sys.platform: - try: - libdir.append(find_library('ssl')) - except OSError: - pass + libdir.append(find_library('ssl')) elif 'win32' in sys.platform or 'win64' in sys.platform: libdir.append(find_library('libeay32')) for library in libdir: @@ -736,8 +546,6 @@ def loadOpenSSL(): return except: pass - raise Exception( - "Couldn't find and load the OpenSSL library. You must install it.") - + raise Exception("Couldn't find and load the OpenSSL library. You must install it.") loadOpenSSL() diff --git a/src/qidenticon.py b/src/qidenticon.py index 6eab09cd..8db8430a 100644 --- a/src/qidenticon.py +++ b/src/qidenticon.py @@ -1,18 +1,22 @@ +#!/usr/bin/env python +# -*- coding:utf-8 -*- # pylint: disable=too-many-locals,too-many-arguments,too-many-function-args """ -Usage ------ += usage = +== python == >>> import qtidenticon >>> qtidenticon.render_identicon(code, size) Return a PIL Image class instance which have generated identicon image. -``size`` specifies `patch size`. Generated image size is 3 * ``size``. +```size``` specifies `patch size`. Generated image size is 3 * ```size```. """ from PyQt4 import QtGui -from PyQt4.QtCore import QPointF, QSize, Qt -from PyQt4.QtGui import QPainter, QPixmap, QPolygonF +from PyQt4.QtCore import QSize, QPointF, Qt +from PyQt4.QtGui import QPixmap, QPainter, QPolygonF + +__all__ = ['render_identicon', 'IdenticonRendererBase'] class IdenticonRendererBase(object): @@ -22,7 +26,7 @@ class IdenticonRendererBase(object): def __init__(self, code): """ - :param code: code for icon + @param code code for icon """ if not isinstance(code, int): code = int(code) @@ -32,8 +36,8 @@ class IdenticonRendererBase(object): """ render identicon to QPicture - :param size: identicon patchsize. (image size is 3 * [size]) - :returns: :class:`QPicture` + @param size identicon patchsize. (image size is 3 * [size]) + @return QPicture """ # decode the code @@ -75,7 +79,7 @@ class IdenticonRendererBase(object): def drawPatchQt(self, pos, turn, invert, patch_type, image, size, foreColor, backColor, penwidth): # pylint: disable=unused-argument """ - :param size: patch size + @param size patch size """ path = self.PATH_SET[patch_type] if not path: @@ -130,7 +134,7 @@ class IdenticonRendererBase(object): class DonRenderer(IdenticonRendererBase): """ Don Park's implementation of identicon - see: http://www.docuverse.com/blog/donpark/2007/01/19/identicon-updated-and-source-released + see : http://www.docuverse.com/blog/donpark/2007/01/19/identicon-updated-and-source-released """ PATH_SET = [ diff --git a/src/queues.py b/src/queues.py index 7d9e284a..7b6bbade 100644 --- a/src/queues.py +++ b/src/queues.py @@ -1,51 +1,20 @@ -"""Most of the queues used by bitmessage threads are defined here.""" - import Queue -import threading -import time +from class_objectProcessorQueue import ObjectProcessorQueue from multiqueue import MultiQueue - -class ObjectProcessorQueue(Queue.Queue): - """Special queue class using lock for `.threads.objectProcessor`""" - - maxSize = 32000000 - - def __init__(self): - Queue.Queue.__init__(self) - self.sizeLock = threading.Lock() - #: in Bytes. We maintain this to prevent nodes from flooding us - #: with objects which take up too much memory. If this gets - #: too big we'll sleep before asking for further objects. - self.curSize = 0 - - def put(self, item, block=True, timeout=None): - while self.curSize >= self.maxSize: - time.sleep(1) - with self.sizeLock: - self.curSize += len(item[1]) - Queue.Queue.put(self, item, block, timeout) - - def get(self, block=True, timeout=None): - item = Queue.Queue.get(self, block, timeout) - with self.sizeLock: - self.curSize -= len(item[1]) - return item - - workerQueue = Queue.Queue() UISignalQueue = Queue.Queue() addressGeneratorQueue = Queue.Queue() -#: `.network.ReceiveQueueThread` instances dump objects they hear -#: on the network into this queue to be processed. +# receiveDataThreads 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. +# The address generator thread uses this queue to get information back +# to the API thread. apiAddressGeneratorReturnQueue = Queue.Queue() -#: for exceptions +# Exceptions excQueue = Queue.Queue() diff --git a/src/network/randomtrackingdict.py b/src/randomtrackingdict.py similarity index 85% rename from src/network/randomtrackingdict.py rename to src/randomtrackingdict.py index e87bf156..6c3300ab 100644 --- a/src/network/randomtrackingdict.py +++ b/src/randomtrackingdict.py @@ -1,6 +1,8 @@ """ -Track randomize ordered dict +src/randomtrackingdict.py +========================= """ + import random from threading import RLock from time import time @@ -12,12 +14,10 @@ class RandomTrackingDict(object): """ Dict with randomised order and tracking. - Keeps a track of how many items have been requested from the dict, - and timeouts. Resets after all objects have been retrieved and timed out. - The main purpose of this isn't as much putting related code together - as performance optimisation and anonymisation of downloading of objects - from other peers. If done using a standard dict or array, it takes - too much CPU (and looks convoluted). Randomisation helps with anonymity. + Keeps a track of how many items have been requested from the dict, and timeouts. Resets after all objects have been + retrieved and timed out. The main purpose of this isn't as much putting related code together as performance + optimisation and anonymisation of downloading of objects from other peers. If done using a standard dict or array, + it takes too much CPU (and looks convoluted). Randomisation helps with anonymity. """ # pylint: disable=too-many-instance-attributes maxPending = 10 @@ -85,14 +85,13 @@ class RandomTrackingDict(object): def setMaxPending(self, maxPending): """ - Sets maximum number of objects that can be retrieved from the class - simultaneously as long as there is no timeout + Sets maximum number of objects that can be retrieved from the class simultaneously as long as there is no + timeout """ self.maxPending = maxPending def setPendingTimeout(self, pendingTimeout): - """Sets how long to wait for a timeout if max pending is reached - (or all objects have been retrieved)""" + """Sets how long to wait for a timeout if max pending is reached (or all objects have been retrieved)""" self.pendingTimeout = pendingTimeout def setLastObject(self): @@ -100,8 +99,7 @@ class RandomTrackingDict(object): self.lastObject = time() def randomKeys(self, count=1): - """Retrieve count random keys from the dict - that haven't already been retrieved""" + """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()): @@ -111,15 +109,13 @@ class RandomTrackingDict(object): with self.lock: # reset if we've requested all # and if last object received too long time ago - if self.pendingLen == self.len and self.lastObject + \ - self.pendingTimeout < time(): + if self.pendingLen == self.len and self.lastObject + self.pendingTimeout < time(): self.pendingLen = 0 self.setLastObject() available = self.len - self.pendingLen if count > available: count = available - randomIndex = helper_random.randomsample( - range(self.len - self.pendingLen), count) + randomIndex = helper_random.randomsample(range(self.len - self.pendingLen), count) retval = [self.indexDict[i] for i in randomIndex] for i in sorted(randomIndex, reverse=True): diff --git a/src/semaphores.py b/src/semaphores.py deleted file mode 100644 index 04120fe7..00000000 --- a/src/semaphores.py +++ /dev/null @@ -1,3 +0,0 @@ -from threading import Semaphore - -kivyuisignaler = Semaphore(0) \ No newline at end of file diff --git a/src/shared.py b/src/shared.py index cabc0f40..6d03bcca 100644 --- a/src/shared.py +++ b/src/shared.py @@ -1,33 +1,23 @@ -""" -Some shared functions - -.. deprecated:: 0.6.3 - Should be moved to different places and this file removed, - but it needs refactoring. -""" -from __future__ import division +from __future__ import division # Libraries. -import hashlib import os -import stat -import subprocess import sys +import stat import threading +import hashlib +import subprocess from binascii import hexlify from pyelliptic import arithmetic -from kivy.utils import platform # Project imports. -import highlevelcrypto import state -from addresses import decodeAddress, encodeVarint +import highlevelcrypto from bmconfigparser import BMConfigParser from debug import logger +from addresses import decodeAddress, encodeVarint from helper_sql import sqlQuery -from pyelliptic import arithmetic - verbose = 1 # This is obsolete with the change to protocol v3 @@ -66,7 +56,6 @@ maximumLengthOfTimeToBotherResendingMessages = 0 def isAddressInMyAddressBook(address): - """Is address in my addressbook?""" queryreturn = sqlQuery( '''select address from addressbook where address=?''', address) @@ -75,7 +64,6 @@ def isAddressInMyAddressBook(address): # At this point we should really just have a isAddressInMy(book, address)... def isAddressInMySubscriptionsList(address): - """Am I subscribed to this address?""" queryreturn = sqlQuery( '''select * from subscriptions where address=?''', str(address)) @@ -83,9 +71,6 @@ def isAddressInMySubscriptionsList(address): def isAddressInMyAddressBookSubscriptionsListOrWhitelist(address): - """ - Am I subscribed to this address, is it in my addressbook or whitelist? - """ if isAddressInMyAddressBook(address): return True @@ -106,11 +91,6 @@ def isAddressInMyAddressBookSubscriptionsListOrWhitelist(address): 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:] != \ @@ -121,7 +101,7 @@ def decodeWalletImportFormat(WIFstring): ' 6 characters of the PRIVATE key: %s', str(WIFstring)[:6] ) - os._exit(0) # pylint: disable=protected-access + os._exit(0) # return "" elif privkey[0] == '\x80': # checksum passed return privkey[1:] @@ -131,57 +111,54 @@ def decodeWalletImportFormat(WIFstring): ' 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 + os._exit(0) def reloadMyAddressHashes(): - """Reload keys for user's addresses from the config file""" logger.debug('reloading keys from keys.dat file') - myECCryptorObjects.clear() myAddressesByHash.clear() myAddressesByTag.clear() # myPrivateKeys.clear() - keyfileSecure = checkSensitiveFilePermissions(os.path.join( - state.appdata, 'keys.dat')) + keyfileSecure = checkSensitiveFilePermissions(state.appdata + 'keys.dat') hasEnabledKeys = False for addressInKeysFile in BMConfigParser().addresses(): isEnabled = BMConfigParser().getboolean(addressInKeysFile, 'enabled') if isEnabled: hasEnabledKeys = True # status - addressVersionNumber, streamNumber, hashobj = decodeAddress(addressInKeysFile)[1:] + _, addressVersionNumber, streamNumber, hash = \ + decodeAddress(addressInKeysFile) if addressVersionNumber in (2, 3, 4): # Returns a simple 32 bytes of information encoded # in 64 Hex characters, or null if there was an error. privEncryptionKey = hexlify(decodeWalletImportFormat( - BMConfigParser().get(addressInKeysFile, 'privencryptionkey'))) + BMConfigParser().get(addressInKeysFile, 'privencryptionkey')) + ) + # It is 32 bytes encoded as 64 hex characters if len(privEncryptionKey) == 64: - myECCryptorObjects[hashobj] = \ + myECCryptorObjects[hash] = \ highlevelcrypto.makeCryptor(privEncryptionKey) - myAddressesByHash[hashobj] = addressInKeysFile + myAddressesByHash[hash] = addressInKeysFile tag = hashlib.sha512(hashlib.sha512( encodeVarint(addressVersionNumber) + - encodeVarint(streamNumber) + hashobj).digest()).digest()[32:] + encodeVarint(streamNumber) + hash).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 platform == "android": - if not keyfileSecure: - fixSensitiveFilePermissions(state.appdata + 'keys.dat', hasEnabledKeys) + if not keyfileSecure: + fixSensitiveFilePermissions(state.appdata + 'keys.dat', hasEnabledKeys) def reloadBroadcastSendersForWhichImWatching(): - """ - Reinitialize runtime data for the broadcasts I'm subscribed to - from the config file - """ broadcastSendersForWhichImWatching.clear() MyECSubscriptionCryptorObjects.clear() queryreturn = sqlQuery('SELECT address FROM subscriptions where enabled=1') @@ -189,9 +166,9 @@ def reloadBroadcastSendersForWhichImWatching(): for row in queryreturn: address, = row # status - addressVersionNumber, streamNumber, hashobj = decodeAddress(address)[1:] + _, addressVersionNumber, streamNumber, hash = decodeAddress(address) if addressVersionNumber == 2: - broadcastSendersForWhichImWatching[hashobj] = 0 + broadcastSendersForWhichImWatching[hash] = 0 # Now, for all addresses, even version 2 addresses, # we should create Cryptor objects in a dictionary which we will # use to attempt to decrypt encrypted broadcast messages. @@ -199,14 +176,14 @@ def reloadBroadcastSendersForWhichImWatching(): if addressVersionNumber <= 3: privEncryptionKey = hashlib.sha512( encodeVarint(addressVersionNumber) + - encodeVarint(streamNumber) + hashobj + encodeVarint(streamNumber) + hash ).digest()[:32] - MyECSubscriptionCryptorObjects[hashobj] = \ + MyECSubscriptionCryptorObjects[hash] = \ highlevelcrypto.makeCryptor(hexlify(privEncryptionKey)) else: doubleHashOfAddressData = hashlib.sha512(hashlib.sha512( encodeVarint(addressVersionNumber) + - encodeVarint(streamNumber) + hashobj + encodeVarint(streamNumber) + hash ).digest()).digest() tag = doubleHashOfAddressData[32:] privEncryptionKey = doubleHashOfAddressData[:32] @@ -215,22 +192,21 @@ def reloadBroadcastSendersForWhichImWatching(): def fixPotentiallyInvalidUTF8Data(text): - """Sanitise invalid UTF-8 strings""" try: unicode(text, 'utf-8') return text except: return 'Part of the message is corrupt. The message cannot be' \ - ' displayed the normal way.\n\n' + repr(text) + ' displayed the normal way.\n\n' + repr(text) +# Checks sensitive file permissions for inappropriate umask +# during keys.dat creation. (Or unwise subsequent chmod.) +# +# Returns true iff file appears to have appropriate permissions. def checkSensitiveFilePermissions(filename): - """ - :param str filename: path to the file - :return: True if file appears to have appropriate permissions. - """ if sys.platform == 'win32': - # .. todo:: This might deserve extra checks by someone familiar with + # TODO: This might deserve extra checks by someone familiar with # Windows systems. return True elif sys.platform[:7] == 'freebsd': @@ -238,30 +214,30 @@ def checkSensitiveFilePermissions(filename): present_permissions = os.stat(filename)[0] disallowed_permissions = stat.S_IRWXG | stat.S_IRWXO return present_permissions & disallowed_permissions == 0 - try: - # Skip known problems for non-Win32 filesystems - # without POSIX permissions. - fstype = subprocess.check_output( - 'stat -f -c "%%T" %s' % (filename), - shell=True, - stderr=subprocess.STDOUT - ) - if 'fuseblk' in fstype: - logger.info( - 'Skipping file permissions check for %s.' - ' Filesystem fuseblk detected.', filename) - return True - except: - # Swallow exception here, but we might run into trouble later! - logger.error('Could not determine filesystem type. %s', filename) - present_permissions = os.stat(filename)[0] - disallowed_permissions = stat.S_IRWXG | stat.S_IRWXO - return present_permissions & disallowed_permissions == 0 + else: + try: + # Skip known problems for non-Win32 filesystems + # without POSIX permissions. + fstype = subprocess.check_output( + 'stat -f -c "%%T" %s' % (filename), + shell=True, + stderr=subprocess.STDOUT + ) + if 'fuseblk' in fstype: + logger.info( + 'Skipping file permissions check for %s.' + ' Filesystem fuseblk detected.', filename) + return True + except: + # Swallow exception here, but we might run into trouble later! + logger.error('Could not determine filesystem type. %s', filename) + present_permissions = os.stat(filename)[0] + disallowed_permissions = stat.S_IRWXG | stat.S_IRWXO + return present_permissions & disallowed_permissions == 0 # Fixes permissions on a sensitive file. def fixSensitiveFilePermissions(filename, hasEnabledKeys): - """Try to change file permissions to be more restrictive""" if hasEnabledKeys: logger.warning( 'Keyfile had insecure permissions, and there were enabled' @@ -286,7 +262,6 @@ def fixSensitiveFilePermissions(filename, hasEnabledKeys): def openKeysFile(): - """Open keys file with an external editor""" if 'linux' in sys.platform: subprocess.call(["xdg-open", state.appdata + 'keys.dat']) else: diff --git a/src/shutdown.py b/src/shutdown.py index dbc2af04..f136ac75 100644 --- a/src/shutdown.py +++ b/src/shutdown.py @@ -1,24 +1,22 @@ -"""shutdown function""" import os import Queue import threading import time -import shared -import state from debug import logger from helper_sql import sqlQuery, sqlStoredProcedure -from inventory import Inventory +from helper_threading import StoppableThread from knownnodes import saveKnownNodes -from network import StoppableThread +from inventory import Inventory from queues import ( addressGeneratorQueue, objectProcessorQueue, UISignalQueue, workerQueue) +import shared +import state def doCleanShutdown(): - """ - Used to tell all the treads to finish work and exit. - """ + # Used to tell proof of work worker threads + # and the objectProcessorThread to exit. state.shutdown = 1 objectProcessorQueue.put(('checkShutdownVariable', 'no data')) @@ -54,11 +52,9 @@ def doCleanShutdown(): time.sleep(.25) for thread in threading.enumerate(): - if ( - thread is not threading.currentThread() - and isinstance(thread, StoppableThread) - and thread.name != 'SQL' - ): + if (thread is not threading.currentThread() and + isinstance(thread, StoppableThread) and + thread.name != 'SQL'): logger.debug("Waiting for thread %s", thread.name) thread.join() @@ -80,10 +76,10 @@ def doCleanShutdown(): except Queue.Empty: break - if shared.thisapp.daemon or not state.enableGUI: # ..fixme:: redundant? + if shared.thisapp.daemon or not state.enableGUI: # FIXME redundant? logger.info('Clean shutdown complete.') shared.thisapp.cleanup() - os._exit(0) # pylint: disable=protected-access + os._exit(0) else: logger.info('Core shutdown complete.') for thread in threading.enumerate(): diff --git a/src/singleinstance.py b/src/singleinstance.py index b061cbff..c2def912 100644 --- a/src/singleinstance.py +++ b/src/singleinstance.py @@ -1,13 +1,8 @@ -""" -This is based upon the singleton class from -`tendo `_ -which is under the Python Software Foundation License version 2 -""" +#! /usr/bin/env python import atexit import os import sys - import state try: @@ -16,10 +11,14 @@ except ImportError: pass -class singleinstance(object): +class singleinstance: """ Implements a single instance application by creating a lock file at appdata. + + This is based upon the singleton class from tendo + https://github.com/pycontribs/tendo + which is under the Python Software Foundation License version 2 """ def __init__(self, flavor_id="", daemon=False): self.initialized = False @@ -29,7 +28,7 @@ class singleinstance(object): self.lockfile = os.path.normpath( os.path.join(state.appdata, 'singleton%s.lock' % flavor_id)) - if state.enableGUI and not state.kivy and not self.daemon and not state.curses: + if state.enableGUI and not self.daemon and not state.curses: # Tells the already running (if any) application to get focus. import bitmessageqt bitmessageqt.init() @@ -40,7 +39,6 @@ class singleinstance(object): atexit.register(self.cleanup) def lock(self): - """Obtain single instance lock""" if self.lockPid is None: self.lockPid = os.getpid() if sys.platform == 'win32': @@ -53,7 +51,8 @@ class singleinstance(object): self.lockfile, os.O_CREAT | os.O_EXCL | os.O_RDWR | os.O_TRUNC ) - except OSError as e: + except OSError: + type, e, tb = sys.exc_info() if e.errno == 13: print( 'Another instance of this application' @@ -84,7 +83,6 @@ class singleinstance(object): self.fp.flush() def cleanup(self): - """Release single instance lock""" if not self.initialized: return if self.daemon and self.lockPid == os.getpid(): @@ -95,7 +93,7 @@ class singleinstance(object): os.close(self.fd) else: fcntl.lockf(self.fp, fcntl.LOCK_UN) - except Exception: + except Exception, e: pass return diff --git a/src/singleton.py b/src/singleton.py index 5c6c43be..1eef08e1 100644 --- a/src/singleton.py +++ b/src/singleton.py @@ -1,21 +1,6 @@ -""" -Singleton decorator definition -""" - -from functools import wraps - - def Singleton(cls): - """ - Decorator implementing the singleton pattern: - it restricts the instantiation of a class to one "single" instance. - """ instances = {} - - # https://github.com/sphinx-doc/sphinx/issues/3783 - @wraps(cls) def getinstance(): - """Find an instance or save newly created one""" if cls not in instances: instances[cls] = cls() return instances[cls] diff --git a/src/socks/BUGS b/src/socks/BUGS new file mode 100644 index 00000000..fa8ccfad --- /dev/null +++ b/src/socks/BUGS @@ -0,0 +1,25 @@ +SocksiPy version 1.00 +A Python SOCKS module. +(C) 2006 Dan-Haim. All rights reserved. +See LICENSE file for details. + + +KNOWN BUGS AND ISSUES +---------------------- + +There are no currently known bugs in this module. +There are some limits though: + +1) Only outgoing connections are supported - This module currently only supports +outgoing TCP connections, though some servers may support incoming connections +as well. UDP is not supported either. + +2) GSSAPI Socks5 authenticaion is not supported. + + +If you find any new bugs, please contact the author at: + +negativeiq@users.sourceforge.net + + +Thank you! diff --git a/src/socks/LICENSE b/src/socks/LICENSE new file mode 100644 index 00000000..04b6b1f3 --- /dev/null +++ b/src/socks/LICENSE @@ -0,0 +1,22 @@ +Copyright 2006 Dan-Haim. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +3. Neither the name of Dan Haim nor the names of his contributors may be used + to endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE. diff --git a/src/socks/README b/src/socks/README new file mode 100644 index 00000000..a52f55f3 --- /dev/null +++ b/src/socks/README @@ -0,0 +1,201 @@ +SocksiPy version 1.00 +A Python SOCKS module. +(C) 2006 Dan-Haim. All rights reserved. +See LICENSE file for details. + + +WHAT IS A SOCKS PROXY? +A SOCKS proxy is a proxy server at the TCP level. In other words, it acts as +a tunnel, relaying all traffic going through it without modifying it. +SOCKS proxies can be used to relay traffic using any network protocol that +uses TCP. + +WHAT IS SOCKSIPY? +This Python module allows you to create TCP connections through a SOCKS +proxy without any special effort. + +PROXY COMPATIBILITY +SocksiPy is compatible with three different types of proxies: +1. SOCKS Version 4 (Socks4), including the Socks4a extension. +2. SOCKS Version 5 (Socks5). +3. HTTP Proxies which support tunneling using the CONNECT method. + +SYSTEM REQUIREMENTS +Being written in Python, SocksiPy can run on any platform that has a Python +interpreter and TCP/IP support. +This module has been tested with Python 2.3 and should work with greater versions +just as well. + + +INSTALLATION +------------- + +Simply copy the file "socks.py" to your Python's lib/site-packages directory, +and you're ready to go. + + +USAGE +------ + +First load the socks module with the command: + +>>> import socks +>>> + +The socks module provides a class called "socksocket", which is the base to +all of the module's functionality. +The socksocket object has the same initialization parameters as the normal socket +object to ensure maximal compatibility, however it should be noted that socksocket +will only function with family being AF_INET and type being SOCK_STREAM. +Generally, it is best to initialize the socksocket object with no parameters + +>>> s = socks.socksocket() +>>> + +The socksocket object has an interface which is very similiar to socket's (in fact +the socksocket class is derived from socket) with a few extra methods. +To select the proxy server you would like to use, use the setproxy method, whose +syntax is: + +setproxy(proxytype, addr[, port[, rdns[, username[, password]]]]) + +Explaination of the parameters: + +proxytype - The type of the proxy server. This can be one of three possible +choices: PROXY_TYPE_SOCKS4, PROXY_TYPE_SOCKS5 and PROXY_TYPE_HTTP for Socks4, +Socks5 and HTTP servers respectively. + +addr - The IP address or DNS name of the proxy server. + +port - The port of the proxy server. Defaults to 1080 for socks and 8080 for http. + +rdns - This is a boolean flag than modifies the behavior regarding DNS resolving. +If it is set to True, DNS resolving will be preformed remotely, on the server. +If it is set to False, DNS resolving will be preformed locally. Please note that +setting this to True with Socks4 servers actually use an extension to the protocol, +called Socks4a, which may not be supported on all servers (Socks5 and http servers +always support DNS). The default is True. + +username - For Socks5 servers, this allows simple username / password authentication +with the server. For Socks4 servers, this parameter will be sent as the userid. +This parameter is ignored if an HTTP server is being used. If it is not provided, +authentication will not be used (servers may accept unauthentication requests). + +password - This parameter is valid only for Socks5 servers and specifies the +respective password for the username provided. + +Example of usage: + +>>> s.setproxy(socks.PROXY_TYPE_SOCKS5,"socks.example.com") +>>> + +After the setproxy method has been called, simply call the connect method with the +traditional parameters to establish a connection through the proxy: + +>>> s.connect(("www.sourceforge.net",80)) +>>> + +Connection will take a bit longer to allow negotiation with the proxy server. +Please note that calling connect without calling setproxy earlier will connect +without a proxy (just like a regular socket). + +Errors: Any errors in the connection process will trigger exceptions. The exception +may either be generated by the underlying socket layer or may be custom module +exceptions, whose details follow: + +class ProxyError - This is a base exception class. It is not raised directly but +rather all other exception classes raised by this module are derived from it. +This allows an easy way to catch all proxy-related errors. + +class GeneralProxyError - When thrown, it indicates a problem which does not fall +into another category. The parameter is a tuple containing an error code and a +description of the error, from the following list: +1 - invalid data - This error means that unexpected data has been received from +the server. The most common reason is that the server specified as the proxy is +not really a Socks4/Socks5/HTTP proxy, or maybe the proxy type specified is wrong. +4 - bad proxy type - This will be raised if the type of the proxy supplied to the +setproxy function was not PROXY_TYPE_SOCKS4/PROXY_TYPE_SOCKS5/PROXY_TYPE_HTTP. +5 - bad input - This will be raised if the connect method is called with bad input +parameters. + +class Socks5AuthError - This indicates that the connection through a Socks5 server +failed due to an authentication problem. The parameter is a tuple containing a +code and a description message according to the following list: + +1 - authentication is required - This will happen if you use a Socks5 server which +requires authentication without providing a username / password at all. +2 - all offered authentication methods were rejected - This will happen if the proxy +requires a special authentication method which is not supported by this module. +3 - unknown username or invalid password - Self descriptive. + +class Socks5Error - This will be raised for Socks5 errors which are not related to +authentication. The parameter is a tuple containing a code and a description of the +error, as given by the server. The possible errors, according to the RFC are: + +1 - General SOCKS server failure - If for any reason the proxy server is unable to +fulfill your request (internal server error). +2 - connection not allowed by ruleset - If the address you're trying to connect to +is blacklisted on the server or requires authentication. +3 - Network unreachable - The target could not be contacted. A router on the network +had replied with a destination net unreachable error. +4 - Host unreachable - The target could not be contacted. A router on the network +had replied with a destination host unreachable error. +5 - Connection refused - The target server has actively refused the connection +(the requested port is closed). +6 - TTL expired - The TTL value of the SYN packet from the proxy to the target server +has expired. This usually means that there are network problems causing the packet +to be caught in a router-to-router "ping-pong". +7 - Command not supported - The client has issued an invalid command. When using this +module, this error should not occur. +8 - Address type not supported - The client has provided an invalid address type. +When using this module, this error should not occur. + +class Socks4Error - This will be raised for Socks4 errors. The parameter is a tuple +containing a code and a description of the error, as given by the server. The +possible error, according to the specification are: + +1 - Request rejected or failed - Will be raised in the event of an failure for any +reason other then the two mentioned next. +2 - request rejected because SOCKS server cannot connect to identd on the client - +The Socks server had tried an ident lookup on your computer and has failed. In this +case you should run an identd server and/or configure your firewall to allow incoming +connections to local port 113 from the remote server. +3 - request rejected because the client program and identd report different user-ids - +The Socks server had performed an ident lookup on your computer and has received a +different userid than the one you have provided. Change your userid (through the +username parameter of the setproxy method) to match and try again. + +class HTTPError - This will be raised for HTTP errors. The parameter is a tuple +containing the HTTP status code and the description of the server. + + +After establishing the connection, the object behaves like a standard socket. +Call the close method to close the connection. + +In addition to the socksocket class, an additional function worth mentioning is the +setdefaultproxy function. The parameters are the same as the setproxy method. +This function will set default proxy settings for newly created socksocket objects, +in which the proxy settings haven't been changed via the setproxy method. +This is quite useful if you wish to force 3rd party modules to use a socks proxy, +by overriding the socket object. +For example: + +>>> socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5,"socks.example.com") +>>> socket.socket = socks.socksocket +>>> urllib.urlopen("http://www.sourceforge.net/") + + +PROBLEMS +--------- + +If you have any problems using this module, please first refer to the BUGS file +(containing current bugs and issues). If your problem is not mentioned you may +contact the author at the following E-Mail address: + +negativeiq@users.sourceforge.net + +Please allow some time for your question to be received and handled. + + +Dan-Haim, +Author. diff --git a/src/socks/__init__.py b/src/socks/__init__.py new file mode 100644 index 00000000..7fd2cba3 --- /dev/null +++ b/src/socks/__init__.py @@ -0,0 +1,476 @@ +"""SocksiPy - Python SOCKS module. +Version 1.00 + +Copyright 2006 Dan-Haim. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + 3. Neither the name of Dan Haim nor the names of his contributors may be used + to endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE. + + +This module provides a standard socket-like interface for Python +for tunneling connections through SOCKS proxies. + +""" + +""" + +Minor modifications made by Christopher Gilbert (http://motomastyle.com/) +for use in PyLoris (http://pyloris.sourceforge.net/) + +Minor modifications made by Mario Vilas (http://breakingcode.wordpress.com/) +mainly to merge bug fixes found in Sourceforge + +""" + +import socket +import struct +import sys + +PROXY_TYPE_SOCKS4 = 1 +PROXY_TYPE_SOCKS5 = 2 +PROXY_TYPE_HTTP = 3 + +_defaultproxy = None +_orgsocket = socket.socket + +class ProxyError(Exception): pass +class GeneralProxyError(ProxyError): pass +class Socks5AuthError(ProxyError): pass +class Socks5Error(ProxyError): pass +class Socks4Error(ProxyError): pass +class HTTPError(ProxyError): pass + +_generalerrors = ("success", + "invalid data", + "not connected", + "not available", + "bad proxy type", + "bad input", + "timed out", + "network unreachable", + "connection refused", + "host unreachable") + +_socks5errors = ("succeeded", + "general SOCKS server failure", + "connection not allowed by ruleset", + "Network unreachable", + "Host unreachable", + "Connection refused", + "TTL expired", + "Command not supported", + "Address type not supported", + "Unknown error") + +_socks5autherrors = ("succeeded", + "authentication is required", + "all offered authentication methods were rejected", + "unknown username or invalid password", + "unknown error") + +_socks4errors = ("request granted", + "request rejected or failed", + "request rejected because SOCKS server cannot connect to identd on the client", + "request rejected because the client program and identd report different user-ids", + "unknown error") + +def setdefaultproxy(proxytype=None, addr=None, port=None, rdns=True, username=None, password=None): + """setdefaultproxy(proxytype, addr[, port[, rdns[, username[, password]]]]) + Sets a default proxy which all further socksocket objects will use, + unless explicitly changed. + """ + global _defaultproxy + _defaultproxy = (proxytype, addr, port, rdns, username, password) + +def wrapmodule(module): + """wrapmodule(module) + Attempts to replace a module's socket library with a SOCKS socket. Must set + a default proxy using setdefaultproxy(...) first. + This will only work on modules that import socket directly into the namespace; + most of the Python Standard Library falls into this category. + """ + if _defaultproxy != None: + module.socket.socket = socksocket + else: + raise GeneralProxyError((4, "no proxy specified")) + +class socksocket(socket.socket): + """socksocket([family[, type[, proto]]]) -> socket object + Open a SOCKS enabled socket. The parameters are the same as + those of the standard socket init. In order for SOCKS to work, + you must specify family=AF_INET, type=SOCK_STREAM and proto=0. + """ + + def __init__(self, family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0, _sock=None): + _orgsocket.__init__(self, family, type, proto, _sock) + if _defaultproxy != None: + self.__proxy = _defaultproxy + else: + self.__proxy = (None, None, None, None, None, None) + self.__proxysockname = None + self.__proxypeername = None + + def __recvall(self, count): + """__recvall(count) -> data + Receive EXACTLY the number of bytes requested from the socket. + Blocks until the required number of bytes have been received. + """ + try: + data = self.recv(count) + except socket.timeout: + raise GeneralProxyError((6, "timed out")) + while len(data) < count: + d = self.recv(count-len(data)) + if not d: raise GeneralProxyError((0, "connection closed unexpectedly")) + data = data + d + return data + + def setproxy(self, proxytype=None, addr=None, port=None, rdns=True, username=None, password=None): + """setproxy(proxytype, addr[, port[, rdns[, username[, password]]]]) + Sets the proxy to be used. + + proxytype + The type of the proxy to be used. Three types + are supported: PROXY_TYPE_SOCKS4 (including socks4a), + PROXY_TYPE_SOCKS5 and PROXY_TYPE_HTTP + + addr + The address of the server (IP or DNS). + + port + The port of the server. Defaults to 1080 for SOCKS + servers and 8080 for HTTP proxy servers. + + rdns + Should DNS queries be preformed on the remote side + (rather than the local side). The default is True. + Note: This has no effect with SOCKS4 servers. + + username + Username to authenticate with to the server. + The default is no authentication. + + password + Password to authenticate with to the server. + Only relevant when username is also provided. + + """ + self.__proxy = (proxytype, addr, port, rdns, username, password) + + def __negotiatesocks5(self): + """__negotiatesocks5(self,destaddr,destport) + Negotiates a connection through a SOCKS5 server. + """ + # First we'll send the authentication packages we support. + if (self.__proxy[4]!=None) and (self.__proxy[5]!=None): + # The username/password details were supplied to the + # setproxy method so we support the USERNAME/PASSWORD + # authentication (in addition to the standard none). + self.sendall(struct.pack('BBBB', 0x05, 0x02, 0x00, 0x02)) + else: + # No username/password were entered, therefore we + # only support connections with no authentication. + self.sendall(struct.pack('BBB', 0x05, 0x01, 0x00)) + # We'll receive the server's response to determine which + # method was selected + chosenauth = self.__recvall(2) + if chosenauth[0:1] != chr(0x05).encode(): + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + # Check the chosen authentication method + if chosenauth[1:2] == chr(0x00).encode(): + # No authentication is required + pass + elif chosenauth[1:2] == chr(0x02).encode(): + # Okay, we need to perform a basic username/password + # authentication. + self.sendall(chr(0x01).encode() + chr(len(self.__proxy[4])) + self.__proxy[4] + chr(len(self.__proxy[5])) + self.__proxy[5]) + authstat = self.__recvall(2) + if authstat[0:1] != chr(0x01).encode(): + # Bad response + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + if authstat[1:2] != chr(0x00).encode(): + # Authentication failed + self.close() + raise Socks5AuthError((3, _socks5autherrors[3])) + # Authentication succeeded + else: + # Reaching here is always bad + self.close() + if chosenauth[1] == chr(0xFF).encode(): + raise Socks5AuthError((2, _socks5autherrors[2])) + else: + raise GeneralProxyError((1, _generalerrors[1])) + + def __connectsocks5(self, destaddr, destport): + # Now we can request the actual connection + req = struct.pack('BBB', 0x05, 0x01, 0x00) + # If the given destination address is an IP address, we'll + # use the IPv4 address request even if remote resolving was specified. + try: + ipaddr = socket.inet_aton(destaddr) + req = req + chr(0x01).encode() + ipaddr + except socket.error: + # Well it's not an IP number, so it's probably a DNS name. + if self.__proxy[3]: + # Resolve remotely + ipaddr = None + req = req + chr(0x03).encode() + chr(len(destaddr)).encode() + destaddr + else: + # Resolve locally + ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) + req = req + chr(0x01).encode() + ipaddr + req = req + struct.pack(">H", destport) + self.sendall(req) + # Get the response + resp = self.__recvall(4) + if resp[0:1] != chr(0x05).encode(): + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + elif resp[1:2] != chr(0x00).encode(): + # Connection failed + self.close() + if ord(resp[1:2])<=8: + raise Socks5Error((ord(resp[1:2]), _socks5errors[ord(resp[1:2])])) + else: + raise Socks5Error((9, _socks5errors[9])) + # Get the bound address/port + elif resp[3:4] == chr(0x01).encode(): + boundaddr = self.__recvall(4) + elif resp[3:4] == chr(0x03).encode(): + resp = resp + self.recv(1) + boundaddr = self.__recvall(ord(resp[4:5])) + else: + self.close() + raise GeneralProxyError((1,_generalerrors[1])) + boundport = struct.unpack(">H", self.__recvall(2))[0] + self.__proxysockname = (boundaddr, boundport) + if ipaddr != None: + self.__proxypeername = (socket.inet_ntoa(ipaddr), destport) + else: + self.__proxypeername = (destaddr, destport) + + def __resolvesocks5(self, host): + # Now we can request the actual connection + req = struct.pack('BBB', 0x05, 0xF0, 0x00) + req += chr(0x03).encode() + chr(len(host)).encode() + host + req = req + struct.pack(">H", 8444) + self.sendall(req) + # Get the response + ip = "" + resp = self.__recvall(4) + if resp[0:1] != chr(0x05).encode(): + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + elif resp[1:2] != chr(0x00).encode(): + # Connection failed + self.close() + if ord(resp[1:2])<=8: + raise Socks5Error((ord(resp[1:2]), _socks5errors[ord(resp[1:2])])) + else: + raise Socks5Error((9, _socks5errors[9])) + # Get the bound address/port + elif resp[3:4] == chr(0x01).encode(): + ip = socket.inet_ntoa(self.__recvall(4)) + elif resp[3:4] == chr(0x03).encode(): + resp = resp + self.recv(1) + ip = self.__recvall(ord(resp[4:5])) + else: + self.close() + raise GeneralProxyError((1,_generalerrors[1])) + boundport = struct.unpack(">H", self.__recvall(2))[0] + return ip + + def getproxysockname(self): + """getsockname() -> address info + Returns the bound IP address and port number at the proxy. + """ + return self.__proxysockname + + def getproxypeername(self): + """getproxypeername() -> address info + Returns the IP and port number of the proxy. + """ + return _orgsocket.getpeername(self) + + def getpeername(self): + """getpeername() -> address info + Returns the IP address and port number of the destination + machine (note: getproxypeername returns the proxy) + """ + return self.__proxypeername + + def getproxytype(self): + return self.__proxy[0] + + def __negotiatesocks4(self,destaddr,destport): + """__negotiatesocks4(self,destaddr,destport) + Negotiates a connection through a SOCKS4 server. + """ + # Check if the destination address provided is an IP address + rmtrslv = False + try: + ipaddr = socket.inet_aton(destaddr) + except socket.error: + # It's a DNS name. Check where it should be resolved. + if self.__proxy[3]: + ipaddr = struct.pack("BBBB", 0x00, 0x00, 0x00, 0x01) + rmtrslv = True + else: + ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) + # Construct the request packet + req = struct.pack(">BBH", 0x04, 0x01, destport) + ipaddr + # The username parameter is considered userid for SOCKS4 + if self.__proxy[4] != None: + req = req + self.__proxy[4] + req = req + chr(0x00).encode() + # DNS name if remote resolving is required + # NOTE: This is actually an extension to the SOCKS4 protocol + # called SOCKS4A and may not be supported in all cases. + if rmtrslv: + req = req + destaddr + chr(0x00).encode() + self.sendall(req) + # Get the response from the server + resp = self.__recvall(8) + if resp[0:1] != chr(0x00).encode(): + # Bad data + self.close() + raise GeneralProxyError((1,_generalerrors[1])) + if resp[1:2] != chr(0x5A).encode(): + # Server returned an error + self.close() + if ord(resp[1:2]) in (91, 92, 93): + self.close() + raise Socks4Error((ord(resp[1:2]), _socks4errors[ord(resp[1:2]) - 90])) + else: + raise Socks4Error((94, _socks4errors[4])) + # Get the bound address/port + self.__proxysockname = (socket.inet_ntoa(resp[4:]), struct.unpack(">H", resp[2:4])[0]) + if rmtrslv != None: + self.__proxypeername = (socket.inet_ntoa(ipaddr), destport) + else: + self.__proxypeername = (destaddr, destport) + + def __negotiatehttp(self, destaddr, destport): + """__negotiatehttp(self,destaddr,destport) + Negotiates a connection through an HTTP server. + """ + # If we need to resolve locally, we do this now + if not self.__proxy[3]: + addr = socket.gethostbyname(destaddr) + else: + addr = destaddr + self.sendall(("CONNECT " + addr + ":" + str(destport) + " HTTP/1.1\r\n" + "Host: " + destaddr + "\r\n\r\n").encode()) + # We read the response until we get the string "\r\n\r\n" + resp = self.recv(1) + while resp.find("\r\n\r\n".encode()) == -1: + resp = resp + self.recv(1) + # We just need the first line to check if the connection + # was successful + statusline = resp.splitlines()[0].split(" ".encode(), 2) + if statusline[0] not in ("HTTP/1.0".encode(), "HTTP/1.1".encode()): + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + try: + statuscode = int(statusline[1]) + except ValueError: + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + if statuscode != 200: + self.close() + raise HTTPError((statuscode, statusline[2])) + self.__proxysockname = ("0.0.0.0", 0) + self.__proxypeername = (addr, destport) + + def connect(self, destpair): + """connect(self, despair) + Connects to the specified destination through a proxy. + destpar - A tuple of the IP/DNS address and the port number. + (identical to socket's connect). + To select the proxy server use setproxy(). + """ + # Do a minimal input check first + if (not type(destpair) in (list,tuple)) or (len(destpair) < 2) or (type(destpair[0]) != type('')) or (type(destpair[1]) != int): + raise GeneralProxyError((5, _generalerrors[5])) + if self.__proxy[0] == PROXY_TYPE_SOCKS5: + if self.__proxy[2] != None: + portnum = self.__proxy[2] + else: + portnum = 1080 + try: + _orgsocket.connect(self, (self.__proxy[1], portnum)) + except socket.error as e: + # ENETUNREACH, WSAENETUNREACH + if e[0] in [101, 10051]: + raise GeneralProxyError((7, _generalerrors[7])) + # ECONNREFUSED, WSAECONNREFUSED + if e[0] in [111, 10061]: + raise GeneralProxyError((8, _generalerrors[8])) + # EHOSTUNREACH, WSAEHOSTUNREACH + if e[0] in [113, 10065]: + raise GeneralProxyError((9, _generalerrors[9])) + raise + self.__negotiatesocks5() + self.__connectsocks5(destpair[0], destpair[1]) + elif self.__proxy[0] == PROXY_TYPE_SOCKS4: + if self.__proxy[2] != None: + portnum = self.__proxy[2] + else: + portnum = 1080 + _orgsocket.connect(self,(self.__proxy[1], portnum)) + self.__negotiatesocks4(destpair[0], destpair[1]) + elif self.__proxy[0] == PROXY_TYPE_HTTP: + if self.__proxy[2] != None: + portnum = self.__proxy[2] + else: + portnum = 8080 + try: + _orgsocket.connect(self,(self.__proxy[1], portnum)) + except socket.error as e: + # ENETUNREACH, WSAENETUNREACH + if e[0] in [101, 10051]: + raise GeneralProxyError((7, _generalerrors[7])) + # ECONNREFUSED, WSAECONNREFUSED + if e[0] in [111, 10061]: + raise GeneralProxyError((8, _generalerrors[8])) + # EHOSTUNREACH, WSAEHOSTUNREACH + if e[0] in [113, 10065]: + raise GeneralProxyError((9, _generalerrors[9])) + raise + self.__negotiatehttp(destpair[0], destpair[1]) + elif self.__proxy[0] == None: + _orgsocket.connect(self, (destpair[0], destpair[1])) + else: + raise GeneralProxyError((4, _generalerrors[4])) + + def resolve(self, host): + if self.__proxy[0] == PROXY_TYPE_SOCKS5: + if self.__proxy[2] != None: + portnum = self.__proxy[2] + else: + portnum = 1080 + _orgsocket.connect(self, (self.__proxy[1], portnum)) + self.__negotiatesocks5() + return self.__resolvesocks5(host) + else: + return None diff --git a/src/state.py b/src/state.py index 16bc016e..2cbc3a7c 100644 --- a/src/state.py +++ b/src/state.py @@ -1,49 +1,43 @@ -""" -Global runtime variables. -""" import collections neededPubkeys = {} streamsInWhichIAmParticipating = [] +# For UPnP extPort = None -"""For UPnP""" +# for Tor hidden service socksIP = None -"""for Tor hidden service""" -appdata = '' -"""holds the location of the application data storage directory""" +# Network protocols availability, initialised below +networkProtocolAvailability = None +appdata = '' # holds the location of the application data storage directory + +# Set to 1 by the doCleanShutdown function. +# Used to tell the proof of work worker threads to exit. shutdown = 0 -""" -Set to 1 by the `.shutdown.doCleanShutdown` function. -Used to tell the threads to exit. -""" # Component control flags - set on startup, do not change during runtime # The defaults are for standalone GUI (default operating mode) -enableNetwork = True -"""enable network threads""" -enableObjProc = True -"""enable object processing thread""" -enableAPI = True -"""enable API (if configured)""" -enableGUI = True -"""enable GUI (QT or ncurses)""" -enableSTDIO = False -"""enable STDIO threads""" +enableNetwork = True # enable network threads +enableObjProc = True # enable object processing threads +enableAPI = True # enable API (if configured) +enableGUI = True # enable GUI (QT or ncurses) +enableSTDIO = False # enable STDIO threads curses = False -sqlReady = False -"""set to true by `.threads.sqlThread` when ready for processing""" +sqlReady = False # set to true by sqlTread when ready for processing maximumNumberOfHalfOpenConnections = 0 + invThread = None addrThread = None downloadThread = None uploadThread = None + ownAddresses = {} + # If the trustedpeer option is specified in keys.dat then this will # contain a Peer which will be connected to instead of using the # addresses advertised by other peers. The client will only connect to @@ -55,21 +49,19 @@ ownAddresses = {} # it will sync with the network a lot faster without compromising # security. trustedPeer = None + discoveredPeers = {} + Peer = collections.namedtuple('Peer', ['host', 'port']) def resetNetworkProtocolAvailability(): - """This method helps to reset the availability of network protocol""" - # pylint: disable=global-statement global networkProtocolAvailability networkProtocolAvailability = {'IPv4': None, 'IPv6': None, 'onion': None} resetNetworkProtocolAvailability() -discoveredPeers = {} - dandelion = 0 testmode = False @@ -77,49 +69,3 @@ testmode = False kivy = False association = '' - -kivyapp = None - -navinstance = None - -mail_id = 0 - -myAddressObj = None - -detailPageType = None - -ackdata = None - -status = None - -screen_density = None - -msg_counter_objs = None - -check_sent_acc = None - -sent_count = 0 - -inbox_count = 0 - -trash_count = 0 - -draft_count = 0 - -all_count = 0 - -searcing_text = '' - -search_screen = '' - -send_draft_mail = None - -is_allmail = False - -in_composer = False - -availabe_credit = 0 - -in_sent_method = False - -in_search_mode = False diff --git a/src/storage/filesystem.py b/src/storage/filesystem.py index b19d9272..d64894a9 100644 --- a/src/storage/filesystem.py +++ b/src/storage/filesystem.py @@ -1,145 +1,94 @@ -""" -Module for using filesystem (directory with files) for inventory storage -""" -import string -import time from binascii import hexlify, unhexlify from os import listdir, makedirs, path, remove, rmdir +import string from threading import RLock +import time +import traceback from paths import lookupAppdataFolder -from storage import InventoryItem, InventoryStorage - +from storage import InventoryStorage, InventoryItem class FilesystemInventory(InventoryStorage): - """Filesystem for inventory storage""" - # pylint: disable=too-many-ancestors, abstract-method topDir = "inventory" objectDir = "objects" metadataFilename = "metadata" dataFilename = "data" def __init__(self): - super(FilesystemInventory, self).__init__() - self.baseDir = path.join( - lookupAppdataFolder(), FilesystemInventory.topDir) + super(self.__class__, self).__init__() + self.baseDir = path.join(lookupAppdataFolder(), FilesystemInventory.topDir) for createDir in [self.baseDir, path.join(self.baseDir, "objects")]: if path.exists(createDir): if not path.isdir(createDir): - raise IOError( - "%s exists but it's not a directory" % createDir) + raise IOError("%s exists but it's not a directory" % (createDir)) else: makedirs(createDir) - # Guarantees that two receiveDataThreads - # don't receive and process the same message - # concurrently (probably sent by a malicious individual) - self.lock = RLock() + self.lock = RLock() # Guarantees that two receiveDataThreads don't receive and process the same message concurrently (probably sent by a malicious individual) self._inventory = {} self._load() - def __contains__(self, hashval): + def __contains__(self, hash): + retval = False for streamDict in self._inventory.values(): - if hashval in streamDict: + if hash in streamDict: return True return False - def __getitem__(self, hashval): + def __getitem__(self, hash): for streamDict in self._inventory.values(): try: - retval = streamDict[hashval] + retval = streamDict[hash] except KeyError: continue if retval.payload is None: - retval = InventoryItem( - retval.type, - retval.stream, - self.getData(hashval), - retval.expires, - retval.tag) + retval = InventoryItem(retval.type, retval.stream, self.getData(hash), retval.expires, retval.tag) return retval - raise KeyError(hashval) + raise KeyError(hash) - def __setitem__(self, hashval, value): + def __setitem__(self, hash, value): with self.lock: value = InventoryItem(*value) try: - makedirs(path.join( - self.baseDir, - FilesystemInventory.objectDir, - hexlify(hashval))) + makedirs(path.join(self.baseDir, FilesystemInventory.objectDir, hexlify(hash))) except OSError: pass try: - with open( - path.join( - self.baseDir, - FilesystemInventory.objectDir, - hexlify(hashval), - FilesystemInventory.metadataFilename, - ), - "w", - ) as f: - f.write("%s,%s,%s,%s," % ( - value.type, - value.stream, - value.expires, - hexlify(value.tag))) - with open( - path.join( - self.baseDir, - FilesystemInventory.objectDir, - hexlify(hashval), - FilesystemInventory.dataFilename, - ), - "w", - ) as f: + with open(path.join(self.baseDir, FilesystemInventory.objectDir, hexlify(hash), FilesystemInventory.metadataFilename), 'w') as f: + f.write("%s,%s,%s,%s," % (value.type, value.stream, value.expires, hexlify(value.tag))) + with open(path.join(self.baseDir, FilesystemInventory.objectDir, hexlify(hash), FilesystemInventory.dataFilename), 'w') as f: f.write(value.payload) except IOError: raise KeyError try: - self._inventory[value.stream][hashval] = value + self._inventory[value.stream][hash] = value except KeyError: self._inventory[value.stream] = {} - self._inventory[value.stream][hashval] = value + self._inventory[value.stream][hash] = value - def delHashId(self, hashval): - """Remove object from inventory""" - for stream in self._inventory: + def delHashId(self, hash): + for stream in self._inventory.keys(): try: - del self._inventory[stream][hashval] + del self._inventory[stream][hash] except KeyError: pass with self.lock: try: - remove( - path.join( - self.baseDir, - FilesystemInventory.objectDir, - hexlify(hashval), - FilesystemInventory.metadataFilename)) + remove(path.join(self.baseDir, FilesystemInventory.objectDir, hexlify(hash), FilesystemInventory.metadataFilename)) except IOError: pass try: - remove( - path.join( - self.baseDir, - FilesystemInventory.objectDir, - hexlify(hashval), - FilesystemInventory.dataFilename)) + remove(path.join(self.baseDir, FilesystemInventory.objectDir, hexlify(hash), FilesystemInventory.dataFilename)) except IOError: pass try: - rmdir(path.join( - self.baseDir, - FilesystemInventory.objectDir, - hexlify(hashval))) + rmdir(path.join(self.baseDir, FilesystemInventory.objectDir, hexlify(hash))) except IOError: pass def __iter__(self): elems = [] for streamDict in self._inventory.values(): - elems.extend(streamDict.keys()) + elems.extend (streamDict.keys()) return elems.__iter__() def __len__(self): @@ -152,112 +101,73 @@ class FilesystemInventory(InventoryStorage): newInventory = {} for hashId in self.object_list(): try: - objectType, streamNumber, expiresTime, tag = self.getMetadata( - hashId) + objectType, streamNumber, expiresTime, tag = self.getMetadata(hashId) try: - newInventory[streamNumber][hashId] = InventoryItem( - objectType, streamNumber, None, expiresTime, tag) + newInventory[streamNumber][hashId] = InventoryItem(objectType, streamNumber, None, expiresTime, tag) except KeyError: newInventory[streamNumber] = {} - newInventory[streamNumber][hashId] = InventoryItem( - objectType, streamNumber, None, expiresTime, tag) + newInventory[streamNumber][hashId] = InventoryItem(objectType, streamNumber, None, expiresTime, tag) except KeyError: print "error loading %s" % (hexlify(hashId)) + pass self._inventory = newInventory # for i, v in self._inventory.items(): # print "loaded stream: %s, %i items" % (i, len(v)) def stream_list(self): - """Return list of streams""" return self._inventory.keys() def object_list(self): - """Return inventory vectors (hashes) from a directory""" - return [unhexlify(x) for x in listdir(path.join( - self.baseDir, FilesystemInventory.objectDir))] + return [unhexlify(x) for x in listdir(path.join(self.baseDir, FilesystemInventory.objectDir))] def getData(self, hashId): - """Get object data""" try: - with open( - path.join( - self.baseDir, - FilesystemInventory.objectDir, - hexlify(hashId), - FilesystemInventory.dataFilename, - ), - "r", - ) as f: + with open(path.join(self.baseDir, FilesystemInventory.objectDir, hexlify(hashId), FilesystemInventory.dataFilename), 'r') as f: return f.read() except IOError: raise AttributeError def getMetadata(self, hashId): - """Get object metadata""" try: - with open( - path.join( - self.baseDir, - FilesystemInventory.objectDir, - hexlify(hashId), - FilesystemInventory.metadataFilename, - ), - "r", - ) as f: - objectType, streamNumber, expiresTime, tag = string.split( - f.read(), ",", 4)[:4] - return [ - int(objectType), - int(streamNumber), - int(expiresTime), - unhexlify(tag)] + with open(path.join(self.baseDir, FilesystemInventory.objectDir, hexlify(hashId), FilesystemInventory.metadataFilename), 'r') as f: + objectType, streamNumber, expiresTime, tag, undef = string.split(f.read(), ",", 4) + return [int(objectType), int(streamNumber), int(expiresTime), unhexlify(tag)] except IOError: raise KeyError def by_type_and_tag(self, objectType, tag): - """Get a list of objects filtered by object type and tag""" retval = [] - for streamDict in self._inventory.values(): + for stream, streamDict in self._inventory: for hashId, item in streamDict: if item.type == objectType and item.tag == tag: - try: + try: if item.payload is None: item.payload = self.getData(hashId) except IOError: continue - retval.append(InventoryItem( - item.type, - item.stream, - item.payload, - item.expires, - item.tag)) + retval.append(InventoryItem(item.type, item.stream, item.payload, item.expires, item.tag)) return retval def hashes_by_stream(self, stream): - """Return inventory vectors (hashes) for a stream""" try: return self._inventory[stream].keys() except KeyError: return [] def unexpired_hashes_by_stream(self, stream): - """Return unexpired hashes in the inventory for a particular stream""" t = int(time.time()) try: - return [x for x, value in self._inventory[stream].items() - if value.expires > t] + return [x for x, value in self._inventory[stream].items() if value.expires > t] except KeyError: return [] def flush(self): - """Flush the inventory and create a new, empty one""" self._load() def clean(self): - """Clean out old items from the inventory""" minTime = int(time.time()) - (60 * 60 * 30) deletes = [] - for streamDict in self._inventory.values(): + for stream, streamDict in self._inventory.items(): for hashId, item in streamDict.items(): if item.expires < minTime: deletes.append(hashId) diff --git a/src/storage/sqlite.py b/src/storage/sqlite.py index 0992c00e..438cbdcb 100644 --- a/src/storage/sqlite.py +++ b/src/storage/sqlite.py @@ -1,62 +1,45 @@ -""" -Sqlite Inventory -""" +import collections +from threading import current_thread, enumerate as threadingEnumerate, RLock +import Queue import sqlite3 import time -from threading import RLock -from helper_sql import SqlBulkExecute, sqlExecute, sqlQuery -from storage import InventoryItem, InventoryStorage +from helper_sql import * +from storage import InventoryStorage, InventoryItem - -class SqliteInventory(InventoryStorage): # pylint: disable=too-many-ancestors - """Inventory using SQLite""" +class SqliteInventory(InventoryStorage): def __init__(self): - super(SqliteInventory, self).__init__() - # of objects (like msg payloads and pubkey payloads) - # Does not include protocol headers (the first 24 bytes of each packet). - self._inventory = {} - # cache for existing objects, used for quick lookups if we have an object. - # This is used for example whenever we receive an inv message from a peer - # to check to see what items are new to us. - # We don't delete things out of it; instead, - # the singleCleaner thread clears and refills it. - self._objects = {} - # Guarantees that two receiveDataThreads don't receive - # and process the same message concurrently - # (probably sent by a malicious individual) - self.lock = RLock() + super(self.__class__, self).__init__() + self._inventory = {} #of objects (like msg payloads and pubkey payloads) Does not include protocol headers (the first 24 bytes of each packet). + self._objects = {} # cache for existing objects, used for quick lookups if we have an object. This is used for example whenever we receive an inv message from a peer to check to see what items are new to us. We don't delete things out of it; instead, the singleCleaner thread clears and refills it. + self.lock = RLock() # Guarantees that two receiveDataThreads don't receive and process the same message concurrently (probably sent by a malicious individual) - def __contains__(self, hash_): + def __contains__(self, hash): with self.lock: - if hash_ in self._objects: + if hash in self._objects: return True - rows = sqlQuery( - 'SELECT streamnumber FROM inventory WHERE hash=?', - sqlite3.Binary(hash_)) + rows = sqlQuery('SELECT streamnumber FROM inventory WHERE hash=?', sqlite3.Binary(hash)) if not rows: return False - self._objects[hash_] = rows[0][0] + self._objects[hash] = rows[0][0] return True - def __getitem__(self, hash_): + def __getitem__(self, hash): with self.lock: - if hash_ in self._inventory: - return self._inventory[hash_] - rows = sqlQuery( - 'SELECT objecttype, streamnumber, payload, expirestime, tag' - ' FROM inventory WHERE hash=?', sqlite3.Binary(hash_)) + if hash in self._inventory: + return self._inventory[hash] + rows = sqlQuery('SELECT objecttype, streamnumber, payload, expirestime, tag FROM inventory WHERE hash=?', sqlite3.Binary(hash)) if not rows: - raise KeyError(hash_) + raise KeyError(hash) return InventoryItem(*rows[0]) - def __setitem__(self, hash_, value): + def __setitem__(self, hash, value): with self.lock: value = InventoryItem(*value) - self._inventory[hash_] = value - self._objects[hash_] = value.stream + self._inventory[hash] = value + self._objects[hash] = value.stream - def __delitem__(self, hash_): + def __delitem__(self, hash): raise NotImplementedError def __iter__(self): @@ -67,49 +50,32 @@ class SqliteInventory(InventoryStorage): # pylint: disable=too-many-ancestors def __len__(self): with self.lock: - return len(self._inventory) + sqlQuery( - 'SELECT count(*) FROM inventory')[0][0] + return len(self._inventory) + sqlQuery('SELECT count(*) FROM inventory')[0][0] def by_type_and_tag(self, objectType, tag): - """Return objects filtered by object type and tag""" with self.lock: - values = [value for value in self._inventory.values() - if value.type == objectType and value.tag == tag] - values += (InventoryItem(*value) for value in sqlQuery( - 'SELECT objecttype, streamnumber, payload, expirestime, tag' - ' FROM inventory WHERE objecttype=? AND tag=?', - objectType, sqlite3.Binary(tag))) + values = [value for value in self._inventory.values() if value.type == objectType and 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): - """Return unexpired inventory vectors filtered by stream""" with self.lock: t = int(time.time()) - hashes = [x for x, value in self._inventory.items() - if value.stream == stream and value.expires > t] - hashes += (str(payload) for payload, in sqlQuery( - 'SELECT hash FROM inventory WHERE streamnumber=?' - ' AND expirestime>?', stream, t)) + hashes = [x for x, value in self._inventory.items() if value.stream == stream and value.expires > t] + hashes += (str(payload) for payload, in sqlQuery('SELECT hash FROM inventory WHERE streamnumber=? AND expirestime>?', stream, t)) return hashes def flush(self): - """Flush cache""" - with self.lock: - # If you use both the inventoryLock and the sqlLock, - # always use the inventoryLock OUTSIDE of the sqlLock. + with self.lock: # If you use both the inventoryLock and the sqlLock, always use the inventoryLock OUTSIDE of the sqlLock. with SqlBulkExecute() as sql: for objectHash, value in self._inventory.items(): - sql.execute( - 'INSERT INTO inventory VALUES (?, ?, ?, ?, ?, ?)', - sqlite3.Binary(objectHash), *value) + sql.execute('INSERT INTO inventory VALUES (?, ?, ?, ?, ?, ?)', sqlite3.Binary(objectHash), *value) self._inventory.clear() def clean(self): - """Free memory / perform garbage collection""" with self.lock: - sqlExecute( - 'DELETE FROM inventory WHERE expirestime 2: - return - 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 defined in this module""" diff --git a/src/tests/test_api.py b/src/tests/test_api.py index 44505ffe..dfe1b273 100644 --- a/src/tests/test_api.py +++ b/src/tests/test_api.py @@ -31,7 +31,7 @@ class TestAPIShutdown(TestAPIProto, TestProcessShutdown): """Separate test case for API command 'shutdown'""" def test_shutdown(self): """Shutdown the pybitmessage""" - self.assertEqual(self.api.shutdown(), 'done') + self.assertEquals(self.api.shutdown(), 'done') for _ in range(5): if not self.process.is_running(): break diff --git a/src/tests/test_blindsig.py b/src/tests/test_blindsig.py deleted file mode 100644 index b8d0248a..00000000 --- a/src/tests/test_blindsig.py +++ /dev/null @@ -1,52 +0,0 @@ -""" -Test for ECC blind signatures -""" -import os -import unittest -from ctypes import cast, c_char_p - -from pybitmessage.pyelliptic.eccblind import ECCBlind -from pybitmessage.pyelliptic.openssl import OpenSSL - - -class TestBlindSig(unittest.TestCase): - """ - Test case for ECC blind signature - """ - def test_blind_sig(self): - """Test full sequence using a random certifier key and a random message""" - # See page 127 of the paper - # (1) Initialization - signer_obj = ECCBlind() - point_r = signer_obj.signer_init() - - # (2) Request - requester_obj = ECCBlind(pubkey=signer_obj.pubkey) - # only 64 byte messages are planned to be used in Bitmessage - msg = os.urandom(64) - msg_blinded = requester_obj.create_signing_request(point_r, msg) - - # check - msg_blinded_str = OpenSSL.malloc(0, OpenSSL.BN_num_bytes(msg_blinded)) - OpenSSL.BN_bn2bin(msg_blinded, msg_blinded_str) - self.assertNotEqual(msg, cast(msg_blinded_str, c_char_p).value) - - # (3) Signature Generation - signature_blinded = signer_obj.blind_sign(msg_blinded) - - # (4) Extraction - signature = requester_obj.unblind(signature_blinded) - - # check - signature_blinded_str = OpenSSL.malloc(0, - OpenSSL.BN_num_bytes( - signature_blinded)) - signature_str = OpenSSL.malloc(0, OpenSSL.BN_num_bytes(signature[0])) - OpenSSL.BN_bn2bin(signature_blinded, signature_blinded_str) - OpenSSL.BN_bn2bin(signature[0], signature_str) - self.assertNotEqual(cast(signature_str, c_char_p).value, - cast(signature_blinded_str, c_char_p).value) - - # (5) Verification - verifier_obj = ECCBlind(pubkey=signer_obj.pubkey) - self.assertTrue(verifier_obj.verify(msg, signature)) diff --git a/src/tests/test_config.py b/src/tests/test_config.py index 35ddd3fa..a0a727e5 100644 --- a/src/tests/test_config.py +++ b/src/tests/test_config.py @@ -26,7 +26,6 @@ class TestConfig(unittest.TestCase): False ) # no arg for default - # pylint: disable=too-many-function-args with self.assertRaises(TypeError): BMConfigParser().safeGetBoolean( 'nonexistent', 'nonexistent', True) @@ -48,9 +47,9 @@ class TestProcessConfig(TestProcessProto): config = BMConfigParser() config.read(os.path.join(self.home, 'keys.dat')) - self.assertEqual(config.safeGetInt( + self.assertEquals(config.safeGetInt( 'bitmessagesettings', 'settingsversion'), 10) - self.assertEqual(config.safeGetInt( + self.assertEquals(config.safeGetInt( 'bitmessagesettings', 'port'), 8444) # don't connect self.assertTrue(config.safeGetBoolean( @@ -60,7 +59,7 @@ class TestProcessConfig(TestProcessProto): 'bitmessagesettings', 'apienabled')) # extralowdifficulty is false - self.assertEqual(config.safeGetInt( + self.assertEquals(config.safeGetInt( 'bitmessagesettings', 'defaultnoncetrialsperbyte'), 1000) - self.assertEqual(config.safeGetInt( + self.assertEquals(config.safeGetInt( 'bitmessagesettings', 'defaultpayloadlengthextrabytes'), 1000) diff --git a/src/tests/test_logger.py b/src/tests/test_logger.py deleted file mode 100644 index 57448911..00000000 --- a/src/tests/test_logger.py +++ /dev/null @@ -1,69 +0,0 @@ -""" -Testing the logger configuration -""" - -import logging -import os -import tempfile -import unittest - - -class TestLogger(unittest.TestCase): - """A test case for bmconfigparser""" - - conf_template = ''' -[loggers] -keys=root - -[handlers] -keys=default - -[formatters] -keys=default - -[formatter_default] -format=%(asctime)s {1} %(message)s - -[handler_default] -class=FileHandler -level=NOTSET -formatter=default -args=('{0}', 'w') - -[logger_root] -level=DEBUG -handlers=default -''' - - def test_fileConfig(self): - """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') - - 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_networkgroup.py b/src/tests/test_networkgroup.py deleted file mode 100644 index 76cfb033..00000000 --- a/src/tests/test_networkgroup.py +++ /dev/null @@ -1,39 +0,0 @@ -""" -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/threads.py b/src/threads.py deleted file mode 100644 index b7471508..00000000 --- a/src/threads.py +++ /dev/null @@ -1,46 +0,0 @@ -""" -PyBitmessage does various tasks in separate threads. Most of them inherit -from `.network.StoppableThread`. There are `addressGenerator` for -addresses generation, `objectProcessor` for processing the network objects -passed minimal validation, `singleCleaner` to periodically clean various -internal storages (like inventory and knownnodes) and do forced garbage -collection, `singleWorker` for doing PoW, `sqlThread` for querying sqlite -database. - -There are also other threads in the `.network` package. - -:func:`set_thread_name` is defined here for the threads that don't inherit from -:class:`.network.StoppableThread` -""" - -import threading - -from class_addressGenerator import addressGenerator -from class_objectProcessor import objectProcessor -from class_singleCleaner import singleCleaner -from class_singleWorker import singleWorker -from class_sqlThread import sqlThread - -try: - import prctl -except ImportError: - def set_thread_name(name): - """Set a name for the thread for python internal use.""" - threading.current_thread().name = name -else: - def set_thread_name(name): - """Set the thread name for external use (visible from the OS).""" - prctl.set_name(name) - - def _thread_name_hack(self): - set_thread_name(self.name) - threading.Thread.__bootstrap_original__(self) - # pylint: disable=protected-access - threading.Thread.__bootstrap_original__ = threading.Thread._Thread__bootstrap - threading.Thread._Thread__bootstrap = _thread_name_hack - - -__all__ = [ - "addressGenerator", "objectProcessor", "singleCleaner", "singleWorker", - "sqlThread" -] diff --git a/src/tr.py b/src/tr.py index 9f531a99..8b41167f 100644 --- a/src/tr.py +++ b/src/tr.py @@ -1,56 +1,39 @@ -""" -Translating text -""" import os import state - +# This is used so that the translateText function can be used when we are in daemon mode and not using any QT functions. class translateClass: - """ - This is used so that the translateText function can be used - when we are in daemon mode and not using any QT functions. - """ - # pylint: disable=old-style-class,too-few-public-methods def __init__(self, context, text): self.context = context self.text = text - - def arg(self, argument): # pylint: disable=unused-argument - """Replace argument placeholders""" + def arg(self,argument): if '%' in self.text: - return translateClass(self.context, self.text.replace('%', '', 1)) - # This doesn't actually do anything with the arguments - # because we don't have a UI in which to display this information anyway. - return self.text + return translateClass(self.context, self.text.replace('%','',1)) # This doesn't actually do anything with the arguments because we don't have a UI in which to display this information anyway. + else: + return self.text - -def _translate(context, text, disambiguation=None, encoding=None, n=None): - # pylint: disable=unused-argument +def _translate(context, text, disambiguation = None, encoding = None, n = None): return translateText(context, text, n) - -def translateText(context, text, n=None): - """Translate text in context""" +def translateText(context, text, n = None): try: enableGUI = state.enableGUI except AttributeError: # inside the plugin enableGUI = True - if not state.kivy and enableGUI: + if enableGUI: try: from PyQt4 import QtCore, QtGui except Exception as err: - print 'PyBitmessage requires PyQt unless you want to run it as a daemon'\ - ' and interact with it using the API.'\ - ' You can download PyQt from http://www.riverbankcomputing.com/software/pyqt/download'\ - ' or by searching Google for \'PyQt Download\'.'\ - ' If you want to run in daemon mode, see https://bitmessage.org/wiki/Daemon' + print '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 + os._exit(0) if n is None: return QtGui.QApplication.translate(context, text) - return QtGui.QApplication.translate(context, text, None, QtCore.QCoreApplication.CodecForTr, n) + else: + return QtGui.QApplication.translate(context, text, None, QtCore.QCoreApplication.CodecForTr, n) else: if '%' in text: - return translateClass(context, text.replace('%', '', 1)) - return text + return translateClass(context, text.replace('%','',1)) + else: + return text diff --git a/src/upnp.py b/src/upnp.py index 99000413..da075c65 100644 --- a/src/upnp.py +++ b/src/upnp.py @@ -1,25 +1,28 @@ # pylint: disable=too-many-statements,too-many-branches,protected-access,no-self-use """ -Complete UPnP port forwarding implementation in separate thread. +src/upnp.py +=========== + +A simple upnp module to forward port for BitMessage Reference: http://mattscodecave.com/posts/using-python-and-upnp-to-forward-a-port """ import httplib import socket +import threading import time import urllib2 from random import randint from urlparse import urlparse from xml.dom.minidom import Document, parseString -import knownnodes import queues import state import tr from bmconfigparser import BMConfigParser from debug import logger -from network import BMConnectionPool, StoppableThread -from network.node import Peer +from helper_threading import StoppableThread +from network.connectionpool import BMConnectionPool def createRequestXML(service, action, arguments=None): @@ -163,11 +166,9 @@ class Router: # pylint: disable=old-style-class def GetExternalIPAddress(self): """Get the external address""" - resp = self.soapRequest( - self.upnp_schema + ':1', 'GetExternalIPAddress') - dom = parseString(resp.read()) - return dom.getElementsByTagName( - 'NewExternalIPAddress')[0].childNodes[0].data + resp = self.soapRequest(self.upnp_schema + ':1', 'GetExternalIPAddress') + dom = parseString(resp) + return dom.getElementsByTagName('NewExternalIPAddress')[0].childNodes[0].data def soapRequest(self, service, action, arguments=None): """Make a request to a router""" @@ -197,7 +198,7 @@ class Router: # pylint: disable=old-style-class return resp -class uPnPThread(StoppableThread): +class uPnPThread(threading.Thread, StoppableThread): """Start a thread to handle UPnP activity""" SSDP_ADDR = "239.255.255.250" @@ -207,7 +208,7 @@ class uPnPThread(StoppableThread): SSDP_ST = "urn:schemas-upnp-org:device:InternetGatewayDevice:1" def __init__(self): - super(uPnPThread, self).__init__(name="uPnPThread") + threading.Thread.__init__(self, name="uPnPThread") try: self.extPort = BMConfigParser().getint('bitmessagesettings', 'extport') except: @@ -219,6 +220,7 @@ class uPnPThread(StoppableThread): self.sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2) self.sock.settimeout(5) self.sendSleep = 60 + self.initStop() def run(self): """Start the thread to manage UPnP activity""" @@ -259,17 +261,6 @@ class uPnPThread(StoppableThread): logger.debug("Found UPnP router at %s", ip) self.routers.append(newRouter) self.createPortMapping(newRouter) - try: - self_peer = Peer( - newRouter.GetExternalIPAddress(), - self.extPort - ) - except: - logger.debug('Failed to get external IP') - else: - with knownnodes.knownNodesLock: - knownnodes.addKnownNode( - 1, self_peer, is_self=True) queues.UISignalQueue.put(('updateStatusBar', tr._translate( "MainWindow", 'UPnP port mapping established on port %1' ).arg(str(self.extPort))))