From 4e77ddc2de19242d128243924548a4ee3c94e55a Mon Sep 17 00:00:00 2001 From: emdee Date: Sun, 10 Dec 2023 02:39:58 +0000 Subject: [PATCH] simple updates --- .gitignore | 3 +- .rsync | 0 _Bugs/segv.err | 130 ++ _Bugs/tox.abilinski.com.ping | 11 + toxygen/.pylint.sh | 5 + toxygen/app.py | 23 +- toxygen/av/call.py | 4 - toxygen/av/calls.py | 8 - toxygen/av/calls_manager.py | 6 - toxygen/bootstrap/bootstrap.py | 4 +- toxygen/contacts/basecontact.py | 33 +- toxygen/contacts/common.py | 6 +- toxygen/contacts/contact.py | 24 - toxygen/contacts/contact_menu.py | 8 - toxygen/contacts/contact_provider.py | 12 - toxygen/contacts/contacts_manager.py | 24 +- toxygen/contacts/friend.py | 8 - toxygen/contacts/friend_factory.py | 4 +- toxygen/contacts/group_chat.py | 6 - toxygen/contacts/group_factory.py | 4 +- toxygen/contacts/profile.py | 4 - toxygen/file_transfers/file_transfers.py | 8 +- .../file_transfers/file_transfers_handler.py | 6 - .../file_transfers_messages_service.py | 2 - toxygen/groups/group_peer.py | 4 - toxygen/groups/groups_service.py | 16 - toxygen/groups/peers_list.py | 4 - toxygen/history/database.py | 6 - toxygen/history/history.py | 4 - toxygen/main.py | 4 +- toxygen/messenger/messenger.py | 14 - toxygen/middleware/callbacks.py | 16 - toxygen/middleware/threads.py | 8 - toxygen/plugins/README.md | 27 + toxygen/plugins/ae.py | 83 + toxygen/plugins/awayl.py | 111 + toxygen/plugins/awayw.py.windows | 115 + toxygen/plugins/bday.pro | 2 + toxygen/plugins/bday.py | 95 + toxygen/plugins/bot.py | 81 + toxygen/plugins/chess.py | 1695 +++++++++++++++ toxygen/plugins/en_GB.ts | 31 + toxygen/plugins/en_US.ts | 31 + toxygen/plugins/garland.py | 75 + toxygen/plugins/mrq.py | 86 + toxygen/plugins/plugin_super_class.py | 12 - toxygen/plugins/ru_RU.qm | Bin 0 -> 525 bytes toxygen/plugins/ru_RU.ts | 32 + toxygen/plugins/srch.pro | 2 + toxygen/plugins/srch.py | 54 + toxygen/plugins/toxid.pro | 2 + toxygen/plugins/toxid.py | 136 ++ toxygen/tests/README.md | 87 + toxygen/tests/__init__.py | 0 toxygen/tests/conference_tests.py | 151 ++ toxygen/tests/logging_toxygen_echo.py | 439 ++++ toxygen/tests/socks.py | 391 ++++ toxygen/tests/support_testing.py | 914 ++++++++ toxygen/tests/test_gdb.py | 936 ++++++++ toxygen/tests/test_gdb.urls | 1 + toxygen/tests/tests_socks.py | 1885 +++++++++++++++++ toxygen/tests/tests_wrapper.py | 1885 +++++++++++++++++ toxygen/tests/toxygen_tests.py | 17 + toxygen/third_party/qweechat/qweechat.py | 4 +- toxygen/ui/items_factories.py | 2 - toxygen/ui/main_screen.py | 10 +- toxygen/ui/messages_widgets.py | 2 +- toxygen/ui/views/add_bootstrap_screen.ui | 99 + toxygen/ui/views/add_contact_screen.ui | 99 + toxygen/ui/views/audio_settings_screen.ui | 87 + toxygen/ui/views/bans_list_screen.ui | 29 + toxygen/ui/views/create_group_screen.ui | 127 ++ toxygen/ui/views/create_profile_screen.ui | 128 ++ toxygen/ui/views/gc_ban_item.ui | 58 + toxygen/ui/views/gc_invite_item.ui | 71 + toxygen/ui/views/gc_settings_screen.ui | 83 + toxygen/ui/views/group_invites_screen.ui | 113 + toxygen/ui/views/group_management_screen.ui | 123 ++ toxygen/ui/views/interface_settings_screen.ui | 255 +++ toxygen/ui/views/join_group_screen.ui | 139 ++ toxygen/ui/views/login_screen.ui | 135 ++ toxygen/ui/views/ms_left_column.ui | 94 + toxygen/ui/views/network_settings_screen.ui | 196 ++ .../ui/views/notifications_settings_screen.ui | 71 + toxygen/ui/views/peer_screen.ui | 202 ++ toxygen/ui/views/profile_settings_screen.ui | 280 +++ toxygen/ui/views/self_peer_screen.ui | 119 ++ toxygen/ui/views/update_settings_screen.ui | 67 + toxygen/ui/views/video_settings_screen.ui | 77 + toxygen/user_data/profile_manager.py | 4 - toxygen/user_data/settings.py | 8 - toxygen/wrapper_tests/__init__.py | 0 toxygen/wrapper_tests/socks.py | 391 ++++ toxygen/wrapper_tests/support_http.py | 164 ++ toxygen/wrapper_tests/support_onions.py | 572 +++++ toxygen/wrapper_tests/support_testing.py | 914 ++++++++ toxygen/wrapper_tests/tests_wrapper.py | 1885 +++++++++++++++++ 97 files changed, 16121 insertions(+), 282 deletions(-) create mode 100644 .rsync create mode 100644 _Bugs/segv.err create mode 100644 _Bugs/tox.abilinski.com.ping create mode 100755 toxygen/.pylint.sh create mode 100644 toxygen/plugins/README.md create mode 100644 toxygen/plugins/ae.py create mode 100644 toxygen/plugins/awayl.py create mode 100644 toxygen/plugins/awayw.py.windows create mode 100644 toxygen/plugins/bday.pro create mode 100644 toxygen/plugins/bday.py create mode 100644 toxygen/plugins/bot.py create mode 100644 toxygen/plugins/chess.py create mode 100644 toxygen/plugins/en_GB.ts create mode 100644 toxygen/plugins/en_US.ts create mode 100644 toxygen/plugins/garland.py create mode 100644 toxygen/plugins/mrq.py create mode 100644 toxygen/plugins/ru_RU.qm create mode 100644 toxygen/plugins/ru_RU.ts create mode 100644 toxygen/plugins/srch.pro create mode 100644 toxygen/plugins/srch.py create mode 100644 toxygen/plugins/toxid.pro create mode 100644 toxygen/plugins/toxid.py create mode 100644 toxygen/tests/README.md create mode 100644 toxygen/tests/__init__.py create mode 100644 toxygen/tests/conference_tests.py create mode 100644 toxygen/tests/logging_toxygen_echo.py create mode 100644 toxygen/tests/socks.py create mode 100644 toxygen/tests/support_testing.py create mode 100644 toxygen/tests/test_gdb.py create mode 100644 toxygen/tests/test_gdb.urls create mode 100644 toxygen/tests/tests_socks.py create mode 100644 toxygen/tests/tests_wrapper.py create mode 100644 toxygen/tests/toxygen_tests.py create mode 100644 toxygen/ui/views/add_bootstrap_screen.ui create mode 100644 toxygen/ui/views/add_contact_screen.ui create mode 100644 toxygen/ui/views/audio_settings_screen.ui create mode 100644 toxygen/ui/views/bans_list_screen.ui create mode 100644 toxygen/ui/views/create_group_screen.ui create mode 100644 toxygen/ui/views/create_profile_screen.ui create mode 100644 toxygen/ui/views/gc_ban_item.ui create mode 100644 toxygen/ui/views/gc_invite_item.ui create mode 100644 toxygen/ui/views/gc_settings_screen.ui create mode 100644 toxygen/ui/views/group_invites_screen.ui create mode 100644 toxygen/ui/views/group_management_screen.ui create mode 100644 toxygen/ui/views/interface_settings_screen.ui create mode 100644 toxygen/ui/views/join_group_screen.ui create mode 100644 toxygen/ui/views/login_screen.ui create mode 100644 toxygen/ui/views/ms_left_column.ui create mode 100644 toxygen/ui/views/network_settings_screen.ui create mode 100644 toxygen/ui/views/notifications_settings_screen.ui create mode 100644 toxygen/ui/views/peer_screen.ui create mode 100644 toxygen/ui/views/profile_settings_screen.ui create mode 100644 toxygen/ui/views/self_peer_screen.ui create mode 100644 toxygen/ui/views/update_settings_screen.ui create mode 100644 toxygen/ui/views/video_settings_screen.ui create mode 100644 toxygen/wrapper_tests/__init__.py create mode 100644 toxygen/wrapper_tests/socks.py create mode 100644 toxygen/wrapper_tests/support_http.py create mode 100644 toxygen/wrapper_tests/support_onions.py create mode 100644 toxygen/wrapper_tests/support_testing.py create mode 100644 toxygen/wrapper_tests/tests_wrapper.py diff --git a/.gitignore b/.gitignore index 0a8182a..3a6d1e9 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,5 @@ Toxygen.egg-info *.tox .cache *.db - +*~ +Makefile diff --git a/.rsync b/.rsync new file mode 100644 index 0000000..e69de29 diff --git a/_Bugs/segv.err b/_Bugs/segv.err new file mode 100644 index 0000000..6f1294e --- /dev/null +++ b/_Bugs/segv.err @@ -0,0 +1,130 @@ +0 +TRAC> network.c#1748:net_connect connecting socket 58 to 127.0.0.1:9050 +TRAC> Messenger.c#2709:do_messenger Friend num in DHT 2 != friend num in msger 14 +TRAC> Messenger.c#2723:do_messenger F[--: 0] D3385007C28852C5398393E3338E6AABE5F86EF249BF724E7404233207D4D927 +TRAC> Messenger.c#2723:do_messenger F[--: 1] 98984E104B8A97CC43AF03A27BE159AC1F4CF35FADCC03D6CD5F8D67B5942A56 +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> network.c#789:loglogdata [05 = ] =>T 2= 185.87.49.189:3389 (0: OK) | 0000000000000000...00 +TRAC> network.c#789:loglogdata [05 = ] T=> 10= 185.87.49.189:3389 (0: OK) | 010001b95731bd0d...3d +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> network.c#789:loglogdata [05 = ] =>T 2= 37.221.66.161:443 (0: OK) | 0000000000000000...00 +TRAC> network.c#789:loglogdata [05 = ] T=> 10= 37.221.66.161:443 (0: OK) | 01000125dd42a101...bb +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> network.c#789:loglogdata [05 = ] T=> 3= 172.93.52.70:33445 (0: OK) | 0000000000000000...00 +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes +TRAC> network.c#789:loglogdata [05 = ] T=> 3= 139.162.110.188:33445 (0: OK) | 0000000000000000...00 +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes +TRAC> network.c#789:loglogdata [05 = ] T=> 3= 37.59.63.150:33445 (0: OK) | 0000000000000000...00 +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes +TRAC> network.c#789:loglogdata [05 = ] T=> 3= 130.133.110.14:33445 (0: OK) | 0000000000000000...00 +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes +TRAC> network.c#789:loglogdata [05 = ] T=> 3= 37.97.185.116:33445 (0: OK) | 0000000000000000...00 +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes +TRAC> network.c#789:loglogdata [05 = ] T=> 3= 85.143.221.42:33445 (0: OK) | 0000000000000000...00 +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes +TRAC> network.c#789:loglogdata [05 = ] T=> 3= 104.244.74.69:38445 (0: OK) | 0000000000000000...00 +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes +TRAC> network.c#789:loglogdata [05 = ] T=> 3= 49.12.229.145:3389 (0: OK) | 0000000000000000...00 +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes +TRAC> network.c#789:loglogdata [05 = ] T=> 3= 168.119.209.10:33445 (0: OK) | 0000000000000000...00 +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes +TRAC> network.c#789:loglogdata [05 = ] T=> 3= 81.169.136.229:33445 (0: OK) | 0000000000000000...00 +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes +TRAC> network.c#789:loglogdata [05 = ] T=> 3= 91.219.59.156:33445 (0: OK) | 0000000000000000...00 +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes +TRAC> network.c#789:loglogdata [05 = ] T=> 3= 46.101.197.175:3389 (0: OK) | 0000000000000000...00 +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes +TRAC> network.c#789:loglogdata [05 = ] T=> 3= 198.199.98.108:33445 (0: OK) | 0000000000000000...00 +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes +TRAC> network.c#789:loglogdata [05 = ] T=> 3= 130.133.110.14:33445 (0: OK) | 0000000000000000...00 +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes +TRAC> network.c#789:loglogdata [05 = ] T=> 3= 49.12.229.145:3389 (0: OK) | 0000000000000000...00 +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes +TRAC> network.c#789:loglogdata [05 = ] T=> 3= 188.225.9.167:33445 (0: OK) | 0000000000000000...00 +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes +TRAC> network.c#789:loglogdata [05 = ] T=> 3= 5.19.249.240:38296 (0: OK) | 0000000000000000...00 +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes +TRAC> network.c#789:loglogdata [05 = ] T=> 3= 94.156.35.247:3389 (0: OK) | 0000000000000000...00 +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 2 bytes +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> network.c#789:loglogdata [05 = ] =>T 2= 172.93.52.70:33445 (0: OK) | 0000000000000000...00 +TRAC> network.c#789:loglogdata [05 = ] T=> 10= 172.93.52.70:33445 (0: OK) | 010001ac5d344682...a5 +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> network.c#789:loglogdata [05 = ] =>T 2= 139.162.110.188:33445 (0: OK) | 0000000000000000...00 +TRAC> network.c#789:loglogdata [05 = ] T=> 10= 139.162.110.188:33445 (0: OK) | 0100018ba26ebc82...a5 +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> network.c#789:loglogdata [05 = ] =>T 2= 37.59.63.150:33445 (0: OK) | 0000000000000000...00 +TRAC> network.c#789:loglogdata [05 = ] T=> 10= 37.59.63.150:33445 (0: OK) | 010001253b3f9682...a5 +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> network.c#789:loglogdata [05 = ] =>T 2= 130.133.110.14:33445 (0: OK) | 0000000000000000...00 +TRAC> network.c#789:loglogdata [05 = ] T=> 10= 130.133.110.14:33445 (0: OK) | 01000182856e0e82...a5 +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> network.c#789:loglogdata [05 = ] =>T 2= 37.97.185.116:33445 (0: OK) | 0000000000000000...00 +TRAC> network.c#789:loglogdata [05 = ] T=> 10= 37.97.185.116:33445 (0: OK) | 0100012561b97482...a5 +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> network.c#789:loglogdata [05 = ] =>T 2= 85.143.221.42:33445 (0: OK) | 0000000000000000...00 +TRAC> network.c#789:loglogdata [05 = ] T=> 10= 85.143.221.42:33445 (0: OK) | 010001558fdd2a82...a5 +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> network.c#789:loglogdata [05 = ] =>T 2= 104.244.74.69:38445 (0: OK) | 0000000000000000...00 +TRAC> network.c#789:loglogdata [05 = ] T=> 10= 104.244.74.69:38445 (0: OK) | 01000168f44a4596...2d +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> network.c#789:loglogdata [05 = ] =>T 2= 49.12.229.145:3389 (0: OK) | 0000000000000000...00 +TRAC> network.c#789:loglogdata [05 = ] T=> 10= 49.12.229.145:3389 (0: OK) | 010001310ce5910d...3d +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> network.c#789:loglogdata [05 = ] =>T 2= 168.119.209.10:33445 (0: OK) | 0000000000000000...00 +TRAC> network.c#789:loglogdata [05 = ] T=> 10= 168.119.209.10:33445 (0: OK) | 010001a877d10a82...a5 +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> network.c#789:loglogdata [05 = ] =>T 2= 81.169.136.229:33445 (0: OK) | 0000000000000000...00 +TRAC> network.c#789:loglogdata [05 = ] T=> 10= 81.169.136.229:33445 (0: OK) | 01000151a988e582...a5 +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> network.c#789:loglogdata [05 = ] =>T 2= 91.219.59.156:33445 (0: OK) | 0000000000000000...00 +TRAC> network.c#789:loglogdata [05 = ] T=> 10= 91.219.59.156:33445 (0: OK) | 0100015bdb3b9c82...a5 +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> network.c#789:loglogdata [05 = ] =>T 2= 46.101.197.175:3389 (0: OK) | 0000000000000000...00 +TRAC> network.c#789:loglogdata [05 = ] T=> 10= 46.101.197.175:3389 (0: OK) | 0100012e65c5af0d...3d +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> network.c#789:loglogdata [05 = ] =>T 2= 198.199.98.108:33445 (0: OK) | 0000000000000000...00 +TRAC> network.c#789:loglogdata [05 = ] T=> 10= 198.199.98.108:33445 (0: OK) | 010001c6c7626c82...a5 +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> network.c#789:loglogdata [05 = ] =>T 2= 130.133.110.14:33445 (0: OK) | 0000000000000000...00 +TRAC> network.c#789:loglogdata [05 = ] T=> 10= 130.133.110.14:33445 (0: OK) | 01000182856e0e82...a5 +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> network.c#789:loglogdata [05 = ] =>T 2= 49.12.229.145:3389 (0: OK) | 0000000000000000...00 +TRAC> network.c#789:loglogdata [05 = ] T=> 10= 49.12.229.145:3389 (0: OK) | 010001310ce5910d...3d +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> network.c#789:loglogdata [05 = ] =>T 2= 188.225.9.167:33445 (0: OK) | 0000000000000000...00 +TRAC> network.c#789:loglogdata [05 = ] T=> 10= 188.225.9.167:33445 (0: OK) | 010001bce109a782...a5 +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> network.c#789:loglogdata [05 = ] =>T 2= 5.19.249.240:38296 (0: OK) | 0000000000000000...00 +TRAC> network.c#789:loglogdata [05 = ] T=> 10= 5.19.249.240:38296 (0: OK) | 0100010513f9f095...98 +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> network.c#789:loglogdata [05 = ] =>T 2= 94.156.35.247:3389 (0: OK) | 0000000000000000...00 +TRAC> network.c#789:loglogdata [05 = ] T=> 10= 94.156.35.247:3389 (0: OK) | 0100015e9c23f70d...3d +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes +app.contacts.contacts_manager INFO update_groups_numbers len(groups)={len(groups)} + +Thread 76 "ToxIterateThrea" received signal SIGSEGV, Segmentation fault. +[Switching to Thread 0x7ffedcb6b640 (LWP 2950427)] diff --git a/_Bugs/tox.abilinski.com.ping b/_Bugs/tox.abilinski.com.ping new file mode 100644 index 0000000..920f606 --- /dev/null +++ b/_Bugs/tox.abilinski.com.ping @@ -0,0 +1,11 @@ + ping tox.abilinski.com +ping: socket: Address family not supported by protocol +PING tox.abilinski.com (172.103.226.229) 56(84) bytes of data. +64 bytes from 172.103.226.229.cable.tpia.cipherkey.com (172.103.226.229): icmp_seq=1 ttl=48 time=86.6 ms +64 bytes from 172.103.226.229.cable.tpia.cipherkey.com (172.103.226.229): icmp_seq=2 ttl=48 time=83.1 ms +64 bytes from 172.103.226.229.cable.tpia.cipherkey.com (172.103.226.229): icmp_seq=3 ttl=48 time=82.9 ms +64 bytes from 172.103.226.229.cable.tpia.cipherkey.com (172.103.226.229): icmp_seq=4 ttl=48 time=83.4 ms +64 bytes from 172.103.226.229.cable.tpia.cipherkey.com (172.103.226.229): icmp_seq=5 ttl=48 time=102 ms +64 bytes from 172.103.226.229.cable.tpia.cipherkey.com (172.103.226.229): icmp_seq=6 ttl=48 time=87.4 ms +64 bytes from 172.103.226.229.cable.tpia.cipherkey.com (172.103.226.229): icmp_seq=7 ttl=48 time=84.9 ms +^C diff --git a/toxygen/.pylint.sh b/toxygen/.pylint.sh new file mode 100755 index 0000000..4fc3a3f --- /dev/null +++ b/toxygen/.pylint.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +ROLE=logging +/var/local/bin/pydev_pylint.bash -E -f text *py [a-nr-z]*/*py >.pylint.err +/var/local/bin/pydev_pylint.bash *py [a-nr-z]*/*py >.pylint.out diff --git a/toxygen/app.py b/toxygen/app.py index 2439201..4e6d20c 100644 --- a/toxygen/app.py +++ b/toxygen/app.py @@ -179,9 +179,7 @@ class App: self._uri = uri[4:] self._history = None - # ----------------------------------------------------------------------------------------------------------------- # Public methods - # ----------------------------------------------------------------------------------------------------------------- def set_trace(self): """unused""" @@ -255,9 +253,7 @@ class App: return retval - # ----------------------------------------------------------------------------------------------------------------- # App executing - # ----------------------------------------------------------------------------------------------------------------- def _execute_app(self): LOG.debug("_execute_app") @@ -321,9 +317,7 @@ class App: oArgs.log_oFd.close() delattr(oArgs, 'log_oFd') - # ----------------------------------------------------------------------------------------------------------------- # App loading - # ----------------------------------------------------------------------------------------------------------------- def _load_base_style(self): if self._args.theme in ['', 'default']: return @@ -470,9 +464,7 @@ class App: return True - # ----------------------------------------------------------------------------------------------------------------- # Threads - # ----------------------------------------------------------------------------------------------------------------- def _start_threads(self, initial_start=True): LOG.debug(f"_start_threads before: {threading.enumerate()!r}") @@ -513,9 +505,7 @@ class App: self._tox.iterate() gevent.sleep(interval / 1000.0) - # ----------------------------------------------------------------------------------------------------------------- # Profiles - # ----------------------------------------------------------------------------------------------------------------- def _select_profile(self): LOG.debug("_select_profile") @@ -600,9 +590,7 @@ class App: data = data or self._tox.get_savedata() self._profile_manager.save_profile(data) - # ----------------------------------------------------------------------------------------------------------------- # Other private methods - # ----------------------------------------------------------------------------------------------------------------- def _enter_password(self, data): """ @@ -967,17 +955,18 @@ class App: reply = util_ui.question(text, title) if not reply: return + if self._args.proxy_type == 0: + sProt = "udp4" + else: + sProt = "tcp4" if lElts is None: if self._args.proxy_type == 0: - sProt = "udp4" - lElts = self._settings['current_nodes_tcp'] + lElts = self._settings['current_nodes_udp'] else: - sProt = "tcp4" lElts = self._settings['current_nodes_tcp'] shuffle(lElts) try: ts.bootstrap_iNmapInfo(lElts, self._args, sProt) - self._ms.log_console() except Exception as e: LOG.error(f"test_nmap ' +' : {e}") LOG.error('_test_nmap(): ' \ @@ -990,7 +979,7 @@ class App: self._ms.log_console() def _test_main(self): - from tests.tests_socks import main as tests_main + from toxygen_wrapper.tests_wrapper import main as tests_main LOG.debug("_test_main") if not self._tox: return title = 'Extended Test Suite' diff --git a/toxygen/av/call.py b/toxygen/av/call.py index d3e023b..6098670 100644 --- a/toxygen/av/call.py +++ b/toxygen/av/call.py @@ -17,9 +17,7 @@ class Call: is_active = property(get_is_active, set_is_active) - # ----------------------------------------------------------------------------------------------------------------- # Audio - # ----------------------------------------------------------------------------------------------------------------- def get_in_audio(self): return self._in_audio @@ -37,9 +35,7 @@ class Call: out_audio = property(get_out_audio, set_out_audio) - # ----------------------------------------------------------------------------------------------------------------- # Video - # ----------------------------------------------------------------------------------------------------------------- def get_in_video(self): return self._in_video diff --git a/toxygen/av/calls.py b/toxygen/av/calls.py index ba34744..b365273 100644 --- a/toxygen/av/calls.py +++ b/toxygen/av/calls.py @@ -91,9 +91,7 @@ class AV(common.tox_save.ToxAvSave): def __contains__(self, friend_number): return friend_number in self._calls - # ----------------------------------------------------------------------------------------------------------------- # Calls - # ----------------------------------------------------------------------------------------------------------------- def __call__(self, friend_number, audio, video): """Call friend with specified number""" @@ -189,9 +187,7 @@ class AV(common.tox_save.ToxAvSave): def is_video_call(self, number): return number in self and self._calls[number].in_video - # ----------------------------------------------------------------------------------------------------------------- # Threads - # ----------------------------------------------------------------------------------------------------------------- def start_audio_thread(self): """ @@ -350,9 +346,7 @@ class AV(common.tox_save.ToxAvSave): self._video_thread = None self._video = None - # ----------------------------------------------------------------------------------------------------------------- # Incoming chunks - # ----------------------------------------------------------------------------------------------------------------- def audio_chunk(self, samples, channels_count, rate): """ @@ -388,9 +382,7 @@ class AV(common.tox_save.ToxAvSave): LOG.debug(f"audio_chunk output_device_index={iOutput} rate={rate} channels={channels_count}") self._out_stream.write(samples) - # ----------------------------------------------------------------------------------------------------------------- # AV sending - # ----------------------------------------------------------------------------------------------------------------- def send_audio_data(self, data, count, *largs, **kwargs): pcm = data diff --git a/toxygen/av/calls_manager.py b/toxygen/av/calls_manager.py index 12923a2..b919c90 100644 --- a/toxygen/av/calls_manager.py +++ b/toxygen/av/calls_manager.py @@ -30,9 +30,7 @@ class CallsManager: def set_toxav(self, toxav): self._callav.set_toxav(toxav) - # ----------------------------------------------------------------------------------------------------------------- # Events - # ----------------------------------------------------------------------------------------------------------------- def get_call_started_event(self): return self._call_started_event @@ -44,9 +42,7 @@ class CallsManager: call_finished_event = property(get_call_finished_event) - # ----------------------------------------------------------------------------------------------------------------- # AV support - # ----------------------------------------------------------------------------------------------------------------- def call_click(self, audio=True, video=False): """User clicked audio button in main window""" @@ -159,9 +155,7 @@ class CallsManager: if friend_number in self._callav: self._callav.finish_call(friend_number, True) - # ----------------------------------------------------------------------------------------------------------------- # Private methods - # ----------------------------------------------------------------------------------------------------------------- def _get_incoming_call_widget(self, friend_number, text, friend_name): return av_widgets.IncomingCallWidget(self._settings, self, friend_number, text, friend_name) diff --git a/toxygen/bootstrap/bootstrap.py b/toxygen/bootstrap/bootstrap.py index a700e69..3bfcf07 100644 --- a/toxygen/bootstrap/bootstrap.py +++ b/toxygen/bootstrap/bootstrap.py @@ -43,6 +43,6 @@ def download_nodes_list(settings, oArgs): def _save_nodes(nodes, app): if not nodes: return - with open(_get_nodes_path(oArgs=app._args), 'wb') as fl: - LOG.info("Saving nodes to " +_get_nodes_path()) + with open(_get_nodes_path(app._args), 'wb') as fl: + LOG.info("Saving nodes to " +_get_nodes_path(app._args)) fl.write(nodes) diff --git a/toxygen/contacts/basecontact.py b/toxygen/contacts/basecontact.py index ba7daa2..fb4da44 100644 --- a/toxygen/contacts/basecontact.py +++ b/toxygen/contacts/basecontact.py @@ -35,9 +35,7 @@ class BaseContact: self._avatar_changed_event = event.Event() self.init_widget() - # ----------------------------------------------------------------------------------------------------------------- # Name - current name or alias of user - # ----------------------------------------------------------------------------------------------------------------- def get_name(self): return self._name @@ -57,9 +55,7 @@ class BaseContact: name_changed_event = property(get_name_changed_event) - # ----------------------------------------------------------------------------------------------------------------- # Status message - # ----------------------------------------------------------------------------------------------------------------- def get_status_message(self): return self._status_message @@ -79,9 +75,7 @@ class BaseContact: status_message_changed_event = property(get_status_message_changed_event) - # ----------------------------------------------------------------------------------------------------------------- # Status - # ----------------------------------------------------------------------------------------------------------------- def get_status(self): return self._status @@ -100,31 +94,30 @@ class BaseContact: status_changed_event = property(get_status_changed_event) - # ----------------------------------------------------------------------------------------------------------------- # TOX ID. WARNING: for friend it will return public key, for profile - full address - # ----------------------------------------------------------------------------------------------------------------- def get_tox_id(self): return self._tox_id tox_id = property(get_tox_id) - # ----------------------------------------------------------------------------------------------------------------- # Avatars - # ----------------------------------------------------------------------------------------------------------------- def load_avatar(self): """ Tries to load avatar of contact or uses default avatar """ - avatar_path = self.get_avatar_path() - width = self._widget.avatar_label.width() - pixmap = QtGui.QPixmap(avatar_path) - self._widget.avatar_label.setPixmap(pixmap.scaled(width, width, QtCore.Qt.KeepAspectRatio, - QtCore.Qt.SmoothTransformation)) - self._widget.avatar_label.repaint() - self._avatar_changed_event(avatar_path) - + try: + avatar_path = self.get_avatar_path() + width = self._widget.avatar_label.width() + pixmap = QtGui.QPixmap(avatar_path) + self._widget.avatar_label.setPixmap(pixmap.scaled(width, width, QtCore.Qt.KeepAspectRatio, + QtCore.Qt.SmoothTransformation)) + self._widget.avatar_label.repaint() + self._avatar_changed_event(avatar_path) + except Exception as e: + pass + def reset_avatar(self, generate_new): avatar_path = self.get_avatar_path() if os.path.isfile(avatar_path) and not avatar_path == self._get_default_avatar_path(): @@ -165,9 +158,7 @@ class BaseContact: avatar_changed_event = property(get_avatar_changed_event) - # ----------------------------------------------------------------------------------------------------------------- # Widgets - # ----------------------------------------------------------------------------------------------------------------- def init_widget(self): self._widget.name.setText(self._name) @@ -177,9 +168,7 @@ class BaseContact: self._widget.connection_status.update(self._status) self.load_avatar() - # ----------------------------------------------------------------------------------------------------------------- # Private methods - # ----------------------------------------------------------------------------------------------------------------- @staticmethod def _get_default_avatar_path(): diff --git a/toxygen/contacts/common.py b/toxygen/contacts/common.py index 1fb7772..a8f2587 100644 --- a/toxygen/contacts/common.py +++ b/toxygen/contacts/common.py @@ -1,10 +1,8 @@ +from pydenticon import Generator import hashlib -from pydenticon import Generator -# ----------------------------------------------------------------------------------------------------------------- # Typing notifications -# ----------------------------------------------------------------------------------------------------------------- class BaseTypingNotificationHandler: @@ -30,9 +28,7 @@ class FriendTypingNotificationHandler(BaseTypingNotificationHandler): BaseTypingNotificationHandler.DEFAULT_HANDLER = BaseTypingNotificationHandler() -# ----------------------------------------------------------------------------------------------------------------- # Identicons support -# ----------------------------------------------------------------------------------------------------------------- def generate_avatar(public_key): diff --git a/toxygen/contacts/contact.py b/toxygen/contacts/contact.py index 1871b3d..63e25ca 100644 --- a/toxygen/contacts/contact.py +++ b/toxygen/contacts/contact.py @@ -42,9 +42,7 @@ class Contact(basecontact.BaseContact): if hasattr(self, '_message_getter'): del self._message_getter - # ----------------------------------------------------------------------------------------------------------------- # History support - # ----------------------------------------------------------------------------------------------------------------- def load_corr(self, first_time=True): """ @@ -121,9 +119,7 @@ class Contact(basecontact.BaseContact): return TextMessage(message, author, unix_time, message_type, unique_id) - # ----------------------------------------------------------------------------------------------------------------- # Unsent messages - # ----------------------------------------------------------------------------------------------------------------- def get_unsent_messages(self): """ @@ -151,9 +147,7 @@ class Contact(basecontact.BaseContact): # wrapped C/C++ object of type QLabel has been deleted LOG.error(f"Mark as sent: {ex!s}") - # ----------------------------------------------------------------------------------------------------------------- # Message deletion - # ----------------------------------------------------------------------------------------------------------------- def delete_message(self, message_id): elem = list(filter(lambda m: m.message_id == message_id, self._corr))[0] @@ -199,9 +193,7 @@ class Contact(basecontact.BaseContact): self._corr)) self._unsaved_messages = len(self.get_unsent_messages()) - # ----------------------------------------------------------------------------------------------------------------- # Chat history search - # ----------------------------------------------------------------------------------------------------------------- def search_string(self, search_string): self._search_string, self._search_index = search_string, 0 @@ -234,9 +226,7 @@ class Contact(basecontact.BaseContact): return i return None # not found - # ----------------------------------------------------------------------------------------------------------------- # Current text - text from message area - # ----------------------------------------------------------------------------------------------------------------- def get_curr_text(self): return self._curr_text @@ -246,9 +236,7 @@ class Contact(basecontact.BaseContact): curr_text = property(get_curr_text, set_curr_text) - # ----------------------------------------------------------------------------------------------------------------- # Alias support - # ----------------------------------------------------------------------------------------------------------------- def set_name(self, value): """ @@ -264,9 +252,7 @@ class Contact(basecontact.BaseContact): def has_alias(self): return self._alias - # ----------------------------------------------------------------------------------------------------------------- # Visibility in friends' list - # ----------------------------------------------------------------------------------------------------------------- def get_visibility(self): return self._visible @@ -276,9 +262,7 @@ class Contact(basecontact.BaseContact): visibility = property(get_visibility, set_visibility) - # ----------------------------------------------------------------------------------------------------------------- # Unread messages and other actions from friend - # ----------------------------------------------------------------------------------------------------------------- def get_actions(self): return self._new_actions @@ -306,9 +290,7 @@ class Contact(basecontact.BaseContact): messages = property(get_messages) - # ----------------------------------------------------------------------------------------------------------------- # Friend's or group's number (can be used in toxcore) - # ----------------------------------------------------------------------------------------------------------------- def get_number(self): return self._number @@ -318,25 +300,19 @@ class Contact(basecontact.BaseContact): number = property(get_number, set_number) - # ----------------------------------------------------------------------------------------------------------------- # Typing notifications - # ----------------------------------------------------------------------------------------------------------------- def get_typing_notification_handler(self): return common.BaseTypingNotificationHandler.DEFAULT_HANDLER typing_notification_handler = property(get_typing_notification_handler) - # ----------------------------------------------------------------------------------------------------------------- # Context menu support - # ----------------------------------------------------------------------------------------------------------------- def get_context_menu_generator(self): return BaseContactMenuGenerator(self) - # ----------------------------------------------------------------------------------------------------------------- # Filtration support - # ----------------------------------------------------------------------------------------------------------------- def set_widget(self, widget): self._widget = widget diff --git a/toxygen/contacts/contact_menu.py b/toxygen/contacts/contact_menu.py index 0e4922e..695a721 100644 --- a/toxygen/contacts/contact_menu.py +++ b/toxygen/contacts/contact_menu.py @@ -8,9 +8,7 @@ global LOG import logging LOG = logging.getLogger('app') -# ----------------------------------------------------------------------------------------------------------------- # Builder -# ----------------------------------------------------------------------------------------------------------------- def _create_menu(menu_name, parent): menu_name = menu_name or '' @@ -83,9 +81,7 @@ class ContactMenuBuilder: self._actions[self._index] = (text, handler) self._index += 1 -# ----------------------------------------------------------------------------------------------------------------- # Generators -# ----------------------------------------------------------------------------------------------------------------- class BaseContactMenuGenerator: @@ -96,9 +92,7 @@ class BaseContactMenuGenerator: def generate(self, plugin_loader, contacts_manager, main_screen, settings, number, groups_service, history_loader): return ContactMenuBuilder().build() - # ----------------------------------------------------------------------------------------------------------------- # Private methods - # ----------------------------------------------------------------------------------------------------------------- def _generate_copy_menu_builder(self, main_screen): copy_menu_builder = ContactMenuBuilder() @@ -150,9 +144,7 @@ class FriendMenuGenerator(BaseContactMenuGenerator): return menu - # ----------------------------------------------------------------------------------------------------------------- # Private methods - # ----------------------------------------------------------------------------------------------------------------- @staticmethod def _generate_plugins_menu_builder(plugin_loader, number): diff --git a/toxygen/contacts/contact_provider.py b/toxygen/contacts/contact_provider.py index ff7b93f..079a82c 100644 --- a/toxygen/contacts/contact_provider.py +++ b/toxygen/contacts/contact_provider.py @@ -28,9 +28,7 @@ class ContactProvider(tox_save.ToxSave): self._group_peer_factory = group_peer_factory self._cache = {} # key - contact's public key, value - contact instance - # ----------------------------------------------------------------------------------------------------------------- # Friends - # ----------------------------------------------------------------------------------------------------------------- def get_friend_by_number(self, friend_number): try: @@ -60,9 +58,7 @@ class ContactProvider(tox_save.ToxSave): return list(friends) - # ----------------------------------------------------------------------------------------------------------------- # Groups - # ----------------------------------------------------------------------------------------------------------------- def get_all_groups(self): """from callbacks""" @@ -131,9 +127,7 @@ class ContactProvider(tox_save.ToxSave): return group - # ----------------------------------------------------------------------------------------------------------------- # Group peers - # ----------------------------------------------------------------------------------------------------------------- def get_all_group_peers(self): return list() @@ -148,16 +142,12 @@ class ContactProvider(tox_save.ToxSave): return self._get_group_peer(group, peer) - # ----------------------------------------------------------------------------------------------------------------- # All contacts - # ----------------------------------------------------------------------------------------------------------------- def get_all(self): return self.get_all_friends() + self.get_all_groups() + self.get_all_group_peers() - # ----------------------------------------------------------------------------------------------------------------- # Caching - # ----------------------------------------------------------------------------------------------------------------- def clear_cache(self): self._cache.clear() @@ -166,9 +156,7 @@ class ContactProvider(tox_save.ToxSave): if contact_public_key in self._cache: del self._cache[contact_public_key] - # ----------------------------------------------------------------------------------------------------------------- # Private methods - # ----------------------------------------------------------------------------------------------------------------- def _get_contact_from_cache(self, public_key): return self._cache[public_key] if public_key in self._cache else None diff --git a/toxygen/contacts/contacts_manager.py b/toxygen/contacts/contacts_manager.py index 87797aa..ddcdca8 100644 --- a/toxygen/contacts/contacts_manager.py +++ b/toxygen/contacts/contacts_manager.py @@ -105,17 +105,13 @@ class ContactsManager(ToxSave): return self._contacts[self._active_contact].tox_id == contact.tox_id - # ----------------------------------------------------------------------------------------------------------------- # Reconnection support - # ----------------------------------------------------------------------------------------------------------------- def reset_contacts_statuses(self): for contact in self._contacts: contact.status = None - # ----------------------------------------------------------------------------------------------------------------- # Work with active friend - # ----------------------------------------------------------------------------------------------------------------- def get_active(self): return self._active_contact @@ -204,9 +200,7 @@ class ContactsManager(ToxSave): def is_active_a_group_chat_peer(self): return type(self.get_curr_contact()) is GroupPeerContact - # ----------------------------------------------------------------------------------------------------------------- # Filtration - # ----------------------------------------------------------------------------------------------------------------- def filtration_and_sorting(self, sorting=0, filter_str=''): """ @@ -286,9 +280,7 @@ class ContactsManager(ToxSave): """ self.filtration_and_sorting(self._sorting, self._filter_string) - # ----------------------------------------------------------------------------------------------------------------- # Contact getters - # ----------------------------------------------------------------------------------------------------------------- def get_friend_by_number(self, number): return list(filter(lambda c: c.number == number and type(c) is Friend, self._contacts))[0] @@ -324,9 +316,7 @@ class ContactsManager(ToxSave): def is_active_online(self): return self._active_contact + 1 and self.get_curr_contact().status is not None - # ----------------------------------------------------------------------------------------------------------------- # Work with friends (remove, block, set alias, get public key) - # ----------------------------------------------------------------------------------------------------------------- def set_alias(self, num): """ @@ -411,9 +401,7 @@ class ContactsManager(ToxSave): self.add_friend(tox_id) self.save_profile() - # ----------------------------------------------------------------------------------------------------------------- # Groups support - # ----------------------------------------------------------------------------------------------------------------- def get_group_chats(self): return list(filter(lambda c: type(c) is GroupChat, self._contacts)) @@ -423,7 +411,7 @@ class ContactsManager(ToxSave): group = self._contact_provider.get_group_by_number(group_number) if group is None: LOG.warn(f"CM.add_group: NULL group from group_number={group_number}") - elif group < 0: + elif type(group) == int and group < 0: LOG.warn(f"CM.add_group: NO group from group={group} group_number={group_number}") else: LOG.info(f"CM.add_group: Adding group {group._name}") @@ -441,9 +429,7 @@ class ContactsManager(ToxSave): num = self._contacts.index(group) self._delete_contact(num) - # ----------------------------------------------------------------------------------------------------------------- # Groups private messaging - # ----------------------------------------------------------------------------------------------------------------- def add_group_peer(self, group, peer): contact = self._contact_provider.get_group_peer_by_id(group, peer.id) @@ -485,9 +471,7 @@ class ContactsManager(ToxSave): return suggested_names[0] - # ----------------------------------------------------------------------------------------------------------------- # Friend requests - # ----------------------------------------------------------------------------------------------------------------- def send_friend_request(self, sToxPkOrId, message): """ @@ -551,9 +535,7 @@ class ContactsManager(ToxSave): def can_send_typing_notification(self): return self._settings['typing_notifications'] and not self.is_active_a_group_chat_peer() - # ----------------------------------------------------------------------------------------------------------------- # Contacts numbers update - # ----------------------------------------------------------------------------------------------------------------- def update_friends_numbers(self): for friend in self._contact_provider.get_all_friends(): @@ -581,9 +563,7 @@ class ContactsManager(ToxSave): for group in groups: group.remove_all_peers_except_self() - # ----------------------------------------------------------------------------------------------------------------- # Private methods - # ----------------------------------------------------------------------------------------------------------------- def _load_contacts(self): self._load_friends() @@ -608,9 +588,7 @@ class ContactsManager(ToxSave): def _load_groups(self): self._contacts.extend(self._contact_provider.get_all_groups()) - # ----------------------------------------------------------------------------------------------------------------- # Current contact subscriptions - # ----------------------------------------------------------------------------------------------------------------- def _subscribe_to_events(self, contact): contact.name_changed_event.add_callback(self._current_contact_name_changed) diff --git a/toxygen/contacts/friend.py b/toxygen/contacts/friend.py index 5c8eabb..549b480 100644 --- a/toxygen/contacts/friend.py +++ b/toxygen/contacts/friend.py @@ -14,9 +14,7 @@ class Friend(contact.Contact): self._receipts = 0 self._typing_notification_handler = common.FriendTypingNotificationHandler(number) - # ----------------------------------------------------------------------------------------------------------------- # File transfers support - # ----------------------------------------------------------------------------------------------------------------- def insert_inline(self, before_message_id, inline): """ @@ -52,23 +50,17 @@ class Friend(contact.Contact): self._corr = list(filter(lambda m: not (type(m) is UnsentFileMessage and m.message_id == message_id), self._corr)) - # ----------------------------------------------------------------------------------------------------------------- # Full status - # ----------------------------------------------------------------------------------------------------------------- def get_full_status(self): return self._status_message - # ----------------------------------------------------------------------------------------------------------------- # Typing notifications - # ----------------------------------------------------------------------------------------------------------------- def get_typing_notification_handler(self): return self._typing_notification_handler - # ----------------------------------------------------------------------------------------------------------------- # Context menu support - # ----------------------------------------------------------------------------------------------------------------- def get_context_menu_generator(self): return FriendMenuGenerator(self) diff --git a/toxygen/contacts/friend_factory.py b/toxygen/contacts/friend_factory.py index 9d661cd..c3c3646 100644 --- a/toxygen/contacts/friend_factory.py +++ b/toxygen/contacts/friend_factory.py @@ -1,5 +1,5 @@ -from common.tox_save import ToxSave from contacts.friend import Friend +from common.tox_save import ToxSave class FriendFactory(ToxSave): @@ -32,9 +32,7 @@ class FriendFactory(ToxSave): return friend - # ----------------------------------------------------------------------------------------------------------------- # Private methods - # ----------------------------------------------------------------------------------------------------------------- def _create_friend_item(self): """ diff --git a/toxygen/contacts/group_chat.py b/toxygen/contacts/group_chat.py index 74ac0e4..f954894 100644 --- a/toxygen/contacts/group_chat.py +++ b/toxygen/contacts/group_chat.py @@ -35,9 +35,7 @@ class GroupChat(contact.Contact, ToxSave): def get_context_menu_generator(self): return GroupMenuGenerator(self) - # ----------------------------------------------------------------------------------------------------------------- # Properties - # ----------------------------------------------------------------------------------------------------------------- def get_is_private(self): return self._is_private @@ -63,9 +61,7 @@ class GroupChat(contact.Contact, ToxSave): peers_limit = property(get_peers_limit, set_peers_limit) - # ----------------------------------------------------------------------------------------------------------------- # Peers methods - # ----------------------------------------------------------------------------------------------------------------- def get_self_peer(self): return self._peers[0] @@ -156,9 +152,7 @@ class GroupChat(contact.Contact, ToxSave): # bans = property(get_bans) - # ----------------------------------------------------------------------------------------------------------------- # Private methods - # ----------------------------------------------------------------------------------------------------------------- @staticmethod def _get_default_avatar_path(): diff --git a/toxygen/contacts/group_factory.py b/toxygen/contacts/group_factory.py index ecbe6b5..2763471 100644 --- a/toxygen/contacts/group_factory.py +++ b/toxygen/contacts/group_factory.py @@ -43,9 +43,7 @@ class GroupFactory(ToxSave): return group - # ----------------------------------------------------------------------------------------------------------------- # Private methods - # ----------------------------------------------------------------------------------------------------------------- def _create_group_item(self): """ @@ -55,7 +53,7 @@ class GroupFactory(ToxSave): return self._items_factory.create_contact_item() def _get_group_number_by_chat_id(self, chat_id): - for i in range(self._tox.group_get_number_groups()): + for i in range(self._tox.group_get_number_groups()+100): if self._tox.group_get_chat_id(i) == chat_id: return i return -1 diff --git a/toxygen/contacts/profile.py b/toxygen/contacts/profile.py index ed816d9..2eb4553 100644 --- a/toxygen/contacts/profile.py +++ b/toxygen/contacts/profile.py @@ -35,9 +35,7 @@ class Profile(basecontact.BaseContact, tox_save.ToxSave): self._waiting_for_reconnection = False self._timer = None - # ----------------------------------------------------------------------------------------------------------------- # Edit current user's data - # ----------------------------------------------------------------------------------------------------------------- def change_status(self): """ @@ -72,9 +70,7 @@ class Profile(basecontact.BaseContact, tox_save.ToxSave): self._sToxId = self._tox.self_get_address() return self._sToxId - # ----------------------------------------------------------------------------------------------------------------- # Reset - # ----------------------------------------------------------------------------------------------------------------- def restart(self): """ diff --git a/toxygen/file_transfers/file_transfers.py b/toxygen/file_transfers/file_transfers.py index b980dcb..4f0b547 100644 --- a/toxygen/file_transfers/file_transfers.py +++ b/toxygen/file_transfers/file_transfers.py @@ -1,11 +1,11 @@ from os import chdir, remove, rename -from os.path import basename, dirname, exists, getsize +from os.path import basename, getsize, exists, dirname from time import time from common.event import Event from middleware.threads import invoke_in_main_thread from wrapper.tox import Tox -from wrapper.toxcore_enums_and_consts import TOX_FILE_CONTROL, TOX_FILE_KIND +from wrapper.toxcore_enums_and_consts import TOX_FILE_KIND, TOX_FILE_CONTROL FILE_TRANSFER_STATE = { 'RUNNING': 0, @@ -126,9 +126,7 @@ class FileTransfer: def _finished(self): self._finished_event(self._friend_number, self._file_number) -# ----------------------------------------------------------------------------------------------------------------- # Send file -# ----------------------------------------------------------------------------------------------------------------- class SendTransfer(FileTransfer): @@ -223,9 +221,7 @@ class SendFromFileBuffer(SendTransfer): chdir(dirname(self._path)) remove(self._path) -# ----------------------------------------------------------------------------------------------------------------- # Receive file -# ----------------------------------------------------------------------------------------------------------------- class ReceiveTransfer(FileTransfer): diff --git a/toxygen/file_transfers/file_transfers_handler.py b/toxygen/file_transfers/file_transfers_handler.py index 673b1fe..ba1fea7 100644 --- a/toxygen/file_transfers/file_transfers_handler.py +++ b/toxygen/file_transfers/file_transfers_handler.py @@ -33,9 +33,7 @@ class FileTransfersHandler(ToxSave): self._settings['paused_file_transfers'] = self._paused_file_transfers if self._settings['resend_files'] else {} self._settings.save() - # ----------------------------------------------------------------------------------------------------------------- # File transfers support - # ----------------------------------------------------------------------------------------------------------------- def incoming_file_transfer(self, friend_number, file_number, size, file_name): """ @@ -255,9 +253,7 @@ class FileTransfersHandler(ToxSave): self._paused_file_transfers[ft.file_id] = [ft.path, friend_num, True, ft.total_size()] self.cancel_transfer(friend_num, file_num, True) - # ----------------------------------------------------------------------------------------------------------------- # Avatars support - # ----------------------------------------------------------------------------------------------------------------- def send_avatar(self, friend_number, avatar_path=None): """ @@ -297,9 +293,7 @@ class FileTransfersHandler(ToxSave): for friend in filter(self._is_friend_online, friends): self.send_avatar(friend.number) - # ----------------------------------------------------------------------------------------------------------------- # Private methods - # ----------------------------------------------------------------------------------------------------------------- def _is_friend_online(self, friend_number): friend = self._get_friend_by_number(friend_number) diff --git a/toxygen/file_transfers/file_transfers_messages_service.py b/toxygen/file_transfers/file_transfers_messages_service.py index d51f748..9462b2e 100644 --- a/toxygen/file_transfers/file_transfers_messages_service.py +++ b/toxygen/file_transfers/file_transfers_messages_service.py @@ -75,9 +75,7 @@ class FileTransfersMessagesService: return tm - # ----------------------------------------------------------------------------------------------------------------- # Private methods - # ----------------------------------------------------------------------------------------------------------------- def _is_friend_active(self, friend_number): if not self._contacts_manager.is_active_a_friend(): diff --git a/toxygen/groups/group_peer.py b/toxygen/groups/group_peer.py index 3cd2fa7..65d1726 100644 --- a/toxygen/groups/group_peer.py +++ b/toxygen/groups/group_peer.py @@ -15,9 +15,7 @@ class GroupChatPeer: self._is_muted = is_muted # unused? self._kind = 'grouppeer' - # ----------------------------------------------------------------------------------------------------------------- # Readonly properties - # ----------------------------------------------------------------------------------------------------------------- def get_id(self): return self._peer_id @@ -34,9 +32,7 @@ class GroupChatPeer: is_current_user = property(get_is_current_user) - # ----------------------------------------------------------------------------------------------------------------- # Read-write properties - # ----------------------------------------------------------------------------------------------------------------- def get_name(self): return self._name diff --git a/toxygen/groups/groups_service.py b/toxygen/groups/groups_service.py index 3a0e4cb..844751d 100644 --- a/toxygen/groups/groups_service.py +++ b/toxygen/groups/groups_service.py @@ -31,9 +31,7 @@ class GroupsService(tox_save.ToxSave): for group in self._get_all_groups(): group.set_tox(tox) - # ----------------------------------------------------------------------------------------------------------------- # Groups creation - # ----------------------------------------------------------------------------------------------------------------- def create_new_gc(self, name, privacy_state, nick, status): try: @@ -74,9 +72,7 @@ class GroupsService(tox_save.ToxSave): group.status = constants.TOX_USER_STATUS['NONE'] self._contacts_manager.update_filtration() - # ----------------------------------------------------------------------------------------------------------------- # Groups reconnect and leaving - # ----------------------------------------------------------------------------------------------------------------- def leave_group(self, group_number): if type(group_number) == int: @@ -95,9 +91,7 @@ class GroupsService(tox_save.ToxSave): group.status = constants.TOX_USER_STATUS['NONE'] self._clear_peers_list(group) - # ----------------------------------------------------------------------------------------------------------------- # Group invites - # ----------------------------------------------------------------------------------------------------------------- def invite_friend(self, friend_number, group_number): if self._tox.friend_get_connection_status(friend_number) == TOX_CONNECTION['NONE']: @@ -142,9 +136,7 @@ class GroupsService(tox_save.ToxSave): group_invites_count = property(get_group_invites_count) - # ----------------------------------------------------------------------------------------------------------------- # Group info methods - # ----------------------------------------------------------------------------------------------------------------- def update_group_info(self, group): group.name = self._tox.group_get_name(group.number) @@ -190,9 +182,7 @@ class GroupsService(tox_save.ToxSave): self._tox.group_founder_set_privacy_state(group.number, privacy_state) group.is_private = is_private - # ----------------------------------------------------------------------------------------------------------------- # Peers list - # ----------------------------------------------------------------------------------------------------------------- def generate_peers_list(self): if not self._contacts_manager.is_active_a_group(): @@ -210,9 +200,7 @@ class GroupsService(tox_save.ToxSave): self._screen = widgets_factory.create_self_peer_screen_window(group) self._screen.show() - # ----------------------------------------------------------------------------------------------------------------- # Peers actions - # ----------------------------------------------------------------------------------------------------------------- def set_new_peer_role(self, group, peer, role): self._tox.group_mod_set_role(group.number, peer.id, role) @@ -231,9 +219,7 @@ class GroupsService(tox_save.ToxSave): self_peer.status = status self.generate_peers_list() - # ----------------------------------------------------------------------------------------------------------------- # Bans support - # ----------------------------------------------------------------------------------------------------------------- def show_bans_list(self, group): return @@ -250,9 +236,7 @@ class GroupsService(tox_save.ToxSave): def cancel_ban(self, group_number, ban_id): self._tox.group_mod_remove_ban(group_number, ban_id) - # ----------------------------------------------------------------------------------------------------------------- # Private methods - # ----------------------------------------------------------------------------------------------------------------- def _add_new_group_by_number(self, group_number): LOG.debug(f"_add_new_group_by_number group_number={group_number}") diff --git a/toxygen/groups/peers_list.py b/toxygen/groups/peers_list.py index 17495f5..4661e93 100644 --- a/toxygen/groups/peers_list.py +++ b/toxygen/groups/peers_list.py @@ -3,9 +3,7 @@ from wrapper.toxcore_enums_and_consts import * from ui.widgets import * -# ----------------------------------------------------------------------------------------------------------------- # Builder -# ----------------------------------------------------------------------------------------------------------------- class PeerListBuilder: @@ -63,9 +61,7 @@ class PeerListBuilder: self._peers[self._index] = peer self._index += 1 -# ----------------------------------------------------------------------------------------------------------------- # Generators -# ----------------------------------------------------------------------------------------------------------------- class PeersListGenerator: diff --git a/toxygen/history/database.py b/toxygen/history/database.py index f91e4cc..130572e 100644 --- a/toxygen/history/database.py +++ b/toxygen/history/database.py @@ -51,9 +51,7 @@ class Database: os.remove(path) LOG.info('Db opened: ' +path) - # ----------------------------------------------------------------------------------------------------------------- # Public methods - # ----------------------------------------------------------------------------------------------------------------- def save(self): if self._toxes.has_password(): @@ -178,9 +176,7 @@ class Database: return Database.MessageGetter(self._path, tox_id) - # ----------------------------------------------------------------------------------------------------------------- # Messages loading - # ----------------------------------------------------------------------------------------------------------------- class MessageGetter: @@ -225,9 +221,7 @@ class Database: def _disconnect(self): self._db.close() - # ----------------------------------------------------------------------------------------------------------------- # Private methods - # ----------------------------------------------------------------------------------------------------------------- def _connect(self): return connect(self._path, timeout=TIMEOUT) diff --git a/toxygen/history/history.py b/toxygen/history/history.py index c141ba7..971fa29 100644 --- a/toxygen/history/history.py +++ b/toxygen/history/history.py @@ -22,9 +22,7 @@ class History: def set_contacts_manager(self, contacts_manager): self._contacts_manager = contacts_manager - # ----------------------------------------------------------------------------------------------------------------- # History support - # ----------------------------------------------------------------------------------------------------------------- def save_history(self): """ @@ -128,9 +126,7 @@ class History: return generator.generate() - # ----------------------------------------------------------------------------------------------------------------- # Items creation - # ----------------------------------------------------------------------------------------------------------------- def _create_message_item(self, message): return self._messages_items_factory.create_message_item(message, False) diff --git a/toxygen/main.py b/toxygen/main.py index 972f378..d71ed7b 100644 --- a/toxygen/main.py +++ b/toxygen/main.py @@ -192,8 +192,8 @@ def main_parser(_=None, iMode=2): parser.add_argument('--auto_accept_path', '--auto-accept-path', type=str, default=os.path.join(os.environ['HOME'], 'Downloads'), help="auto_accept_path") - parser.add_argument('--mode', type=int, default=iMode, - help='Mode: 0=chat 1=chat+audio 2=chat+audio+video default: 0') +# parser.add_argument('--mode', type=int, default=iMode, +# help='Mode: 0=chat 1=chat+audio 2=chat+audio+video default: 0') parser.add_argument('--font', type=str, default="Courier", help='Message font') parser.add_argument('--message_font_size', type=int, default=15, diff --git a/toxygen/messenger/messenger.py b/toxygen/messenger/messenger.py index 9e6b807..48f9244 100644 --- a/toxygen/messenger/messenger.py +++ b/toxygen/messenger/messenger.py @@ -38,9 +38,7 @@ class Messenger(tox_save.ToxSave): return contact.get_last_message_text() - # ----------------------------------------------------------------------------------------------------------------- # Messaging - friends - # ----------------------------------------------------------------------------------------------------------------- def new_message(self, friend_number, message_type, message): """ @@ -140,9 +138,7 @@ class Messenger(tox_save.ToxSave): except Exception as ex: LOG.warn('Sending pending messages failed with ' + str(ex)) - # ----------------------------------------------------------------------------------------------------------------- # Messaging - groups - # ----------------------------------------------------------------------------------------------------------------- def send_message_to_group(self, text, message_type, group_number=None): if group_number is None: @@ -183,9 +179,7 @@ class Messenger(tox_save.ToxSave): text_message = TextMessage(message, MessageAuthor(peer.name, MESSAGE_AUTHOR['GC_PEER']), t, message_type) self._add_message(text_message, group) - # ----------------------------------------------------------------------------------------------------------------- # Messaging - group peers - # ----------------------------------------------------------------------------------------------------------------- def send_message_to_group_peer(self, text, message_type, group_number=None, peer_id=None): if group_number is None or peer_id is None: @@ -238,17 +232,13 @@ class Messenger(tox_save.ToxSave): return self._add_message(text_message, group_peer_contact) - # ----------------------------------------------------------------------------------------------------------------- # Message receipts - # ----------------------------------------------------------------------------------------------------------------- def receipt(self, friend_number, message_id): friend = self._get_friend_by_number(friend_number) friend.mark_as_sent(message_id) - # ----------------------------------------------------------------------------------------------------------------- # Typing notifications - # ----------------------------------------------------------------------------------------------------------------- def send_typing(self, typing): """ @@ -266,9 +256,7 @@ class Messenger(tox_save.ToxSave): if self._contacts_manager.is_friend_active(friend_number): self._screen.typing.setVisible(typing) - # ----------------------------------------------------------------------------------------------------------------- # Contact info updated - # ----------------------------------------------------------------------------------------------------------------- def new_friend_name(self, friend, old_name, new_name): if old_name == new_name or friend.has_alias(): @@ -279,9 +267,7 @@ class Messenger(tox_save.ToxSave): friend.actions = True self._add_info_message(friend.number, message) - # ----------------------------------------------------------------------------------------------------------------- # Private methods - # ----------------------------------------------------------------------------------------------------------------- @staticmethod def _split_message(message): diff --git a/toxygen/middleware/callbacks.py b/toxygen/middleware/callbacks.py index 2aed257..5d76362 100644 --- a/toxygen/middleware/callbacks.py +++ b/toxygen/middleware/callbacks.py @@ -46,9 +46,7 @@ def bTooSoon(key, sSlot, fSec=10.0): # TODO: refactoring. Use contact provider instead of manager -# ----------------------------------------------------------------------------------------------------------------- # Callbacks - current user -# ----------------------------------------------------------------------------------------------------------------- global iBYTES iBYTES=0 @@ -95,9 +93,7 @@ def self_connection_status(tox, profile): return wrapped -# ----------------------------------------------------------------------------------------------------------------- # Callbacks - friends -# ----------------------------------------------------------------------------------------------------------------- def friend_status(contacts_manager, file_transfer_handler, profile, settings): @@ -235,9 +231,7 @@ def friend_read_receipt(messenger): return wrapped -# ----------------------------------------------------------------------------------------------------------------- # Callbacks - file transfers -# ----------------------------------------------------------------------------------------------------------------- def tox_file_recv(window, tray, profile, file_transfer_handler, contacts_manager, settings): @@ -312,9 +306,7 @@ def file_recv_control(file_transfer_handler): return wrapped -# ----------------------------------------------------------------------------------------------------------------- # Callbacks - custom packets -# ----------------------------------------------------------------------------------------------------------------- def lossless_packet(plugin_loader): @@ -339,9 +331,7 @@ def lossy_packet(plugin_loader): return wrapped -# ----------------------------------------------------------------------------------------------------------------- # Callbacks - audio -# ----------------------------------------------------------------------------------------------------------------- def call_state(calls_manager): def wrapped(iToxav, friend_number, mask, user_data): @@ -384,9 +374,7 @@ def callback_audio(calls_manager): return wrapped -# ----------------------------------------------------------------------------------------------------------------- # Callbacks - video -# ----------------------------------------------------------------------------------------------------------------- def video_receive_frame(toxav, friend_number, width, height, y, u, v, ystride, ustride, vstride, user_data): @@ -444,9 +432,7 @@ def video_receive_frame(toxav, friend_number, width, height, y, u, v, ystride, u LOG_ERROR(f"video_receive_frame {ex} #{friend_number}") pass -# ----------------------------------------------------------------------------------------------------------------- # Callbacks - groups -# ----------------------------------------------------------------------------------------------------------------- def group_message(window, tray, tox, messenger, settings, profile): @@ -714,9 +700,7 @@ def group_privacy_state(contacts_provider): return wrapped -# ----------------------------------------------------------------------------------------------------------------- # Callbacks - initialization -# ----------------------------------------------------------------------------------------------------------------- def init_callbacks(tox, profile, settings, plugin_loader, contacts_manager, diff --git a/toxygen/middleware/threads.py b/toxygen/middleware/threads.py index 7f1232e..b98cf06 100644 --- a/toxygen/middleware/threads.py +++ b/toxygen/middleware/threads.py @@ -28,9 +28,7 @@ def LOG_TRACE(l): pass # print('TRACE+ '+l) iLAST_CONN = 0 iLAST_DELTA = 60 -# ----------------------------------------------------------------------------------------------------------------- # Base threads -# ----------------------------------------------------------------------------------------------------------------- class BaseThread(threading.Thread): @@ -74,9 +72,7 @@ class BaseQThread(QtCore.QThread): else: LOG_WARN(f"BaseQThread {self.name} BLOCKED") -# ----------------------------------------------------------------------------------------------------------------- # Toxcore threads -# ----------------------------------------------------------------------------------------------------------------- class InitThread(BaseThread): @@ -163,9 +159,7 @@ class ToxAVIterateThread(BaseQThread): sleep(self._toxav.iteration_interval() / 1000) -# ----------------------------------------------------------------------------------------------------------------- # File transfers thread -# ----------------------------------------------------------------------------------------------------------------- class FileTransfersThread(BaseQThread): @@ -203,9 +197,7 @@ def execute(func, *args, **kwargs): _thread.execute(func, *args, **kwargs) -# ----------------------------------------------------------------------------------------------------------------- # Invoking in main thread -# ----------------------------------------------------------------------------------------------------------------- class InvokeEvent(QtCore.QEvent): EVENT_TYPE = QtCore.QEvent.Type(QtCore.QEvent.registerEventType()) diff --git a/toxygen/plugins/README.md b/toxygen/plugins/README.md new file mode 100644 index 0000000..12ed7b0 --- /dev/null +++ b/toxygen/plugins/README.md @@ -0,0 +1,27 @@ +# Plugins + +Repo with plugins for [Toxygen](https://macaw.me/emdee/toxygen/) + +For more info visit [plugins.md](https://macaw.me/emdee/toxygen/blob/master/docs/plugins.md) and [plugin_api.md](https://github.com/toxygen-project[/toxygen/blob/master/docs/plugin-api.md) + +# Plugins list: + +- ToxId - share your Tox ID and copy friend's Tox ID easily. +- MarqueeStatus - create ticker from your status message. +- BirthDay - get notifications on your friends' birthdays. +- Bot - bot which can communicate with your friends when you are away. +- SearchPlugin - select text in message and find it in search engine. +- AutoAwayStatusLinux - sets "Away" status when user is inactive (Linux only). +- AutoAwayStatusWindows - sets "Away" status when user is inactive (Windows only). +- Chess - play chess with your friends using Tox. +- Garland - changes your status like it's garland. +- AutoAnswer - calls auto answering. +- uToxInlineSending - send inlines with the same name as uTox does. +- AvatarEncryption - encrypt all avatars using profile password + +## Hard fork + +Not all of these are working... + +Work on this project is suspended until the +[MultiDevice](https://git.plastiras.org/emdee/tox_profile/wiki/MultiDevice-Announcements-POC) problem is solved. Fork me! diff --git a/toxygen/plugins/ae.py b/toxygen/plugins/ae.py new file mode 100644 index 0000000..f0146cb --- /dev/null +++ b/toxygen/plugins/ae.py @@ -0,0 +1,83 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +import plugin_super_class +import json +from user_data import settings +import os +from bootstrap.bootstrap import get_user_config_path + +class AvatarEncryption(plugin_super_class.PluginSuperClass): + + def __init__(self, *args): + super(AvatarEncryption, self).__init__('AvatarEncryption', 'ae', *args) + self._path = os.path.join(get_user_config_path(), 'avatars') + self._app = args[0] + self._profile = self._app._ms._profile + self._window = None + #was self._contacts = self._profile._contacts[:] + self._contacts = self._profile._contacts_provider.get_all_friends() + + def get_description(self): + return QApplication.translate("AvatarEncryption", 'Encrypt all avatars using profile password.') + + def close(self): + if not self._encrypt_save.has_password(): + return + i, data = 1, {} + + self.save_contact_avatar(data, self._profile, 0) + for friend in self._contacts: + self.save_contact_avatar(data, friend, i) + i += 1 + self.save_settings(json.dumps(data)) + + def start(self): + if not self._encrypt_save.has_password(): + return + data = json.loads(self.load_settings()) + + self.load_contact_avatar(data, self._profile) + for friend in self._contacts: + self.load_contact_avatar(data, friend) + self._profile.update() + + def save_contact_avatar(self, data, contact, i): + tox_id = contact.tox_id[:64] + data[str(tox_id)] = str(i) + path = os.path.join(self._path, tox_id + '.png') + if os.path.isfile(path): + with open(path, 'rb') as fl: + avatar = fl.read() + encr_avatar = self._encrypt_save.pass_encrypt(avatar) + with open(os.path.join(self._path, self._settings.name + '_' + str(i) + '.png'), 'wb') as fl: + fl.write(encr_avatar) + os.remove(path) + + def load_contact_avatar(self, data, contact): + tox_id = str(contact.tox_id[:64]) + if tox_id not in data: + return + path = os.path.join(self._path, self._settings.name + '_' + data[tox_id] + '.png') + if os.path.isfile(path): + with open(path, 'rb') as fl: + avatar = fl.read() + decr_avatar = self._encrypt_save.pass_decrypt(avatar) + with open(os.path.join(self._path, str(tox_id) + '.png'), 'wb') as fl: + fl.write(decr_avatar) + os.remove(path) + contact.load_avatar() + + def load_settings(self): + try: + with open(plugin_super_class.path_to_data(self._short_name) + self._settings.name + '.json', 'rb') as fl: + data = fl.read() + return str(self._encrypt_save.pass_decrypt(data), 'utf-8') if data else '{}' + except: + return '{}' + + def save_settings(self, data): + try: + data = self._encrypt_save.pass_encrypt(bytes(data, 'utf-8')) + with open(plugin_super_class.path_to_data(self._short_name) + self._settings.name + '.json', 'wb') as fl: + fl.write(data) + except: + pass diff --git a/toxygen/plugins/awayl.py b/toxygen/plugins/awayl.py new file mode 100644 index 0000000..9c12743 --- /dev/null +++ b/toxygen/plugins/awayl.py @@ -0,0 +1,111 @@ +import plugin_super_class +import threading +import time +from PyQt5 import QtCore, QtWidgets +from subprocess import check_output +import json + + +class InvokeEvent(QtCore.QEvent): + EVENT_TYPE = QtCore.QEvent.Type(QtCore.QEvent.registerEventType()) + + def __init__(self, fn, *args, **kwargs): + QtCore.QEvent.__init__(self, InvokeEvent.EVENT_TYPE) + self.fn = fn + self.args = args + self.kwargs = kwargs + + +class Invoker(QtCore.QObject): + + def event(self, event): + event.fn(*event.args, **event.kwargs) + return True + +_invoker = Invoker() + + +def invoke_in_main_thread(fn, *args, **kwargs): + QtCore.QCoreApplication.postEvent(_invoker, InvokeEvent(fn, *args, **kwargs)) + + +class AutoAwayStatusLinux(plugin_super_class.PluginSuperClass): + + def __init__(self, *args): + super().__init__('AutoAwayStatusLinux', 'awayl', *args) + self._thread = None + self._exec = None + self._active = False + self._time = json.loads(self.load_settings())['time'] + self._prev_status = 0 + self._app = args[0] + self._profile=self._app._ms._profile + self._window = None + + def get_description(self): + return QApplication.translate("AutoAwayStatusLinux", 'sets "Away" status when user is inactive (Linux only).') + + def close(self): + self.stop() + + def stop(self): + self._exec = False + if self._active: + self._thread.join() + + def start(self): + self._exec = True + self._thread = threading.Thread(target=self.loop) + self._thread.start() + + def save(self): + self.save_settings('{"time": ' + str(self._time) + '}') + + def change_status(self, status=1): + if self._profile.status in (0, 2): + self._prev_status = self._profile.status + if status is not None: + invoke_in_main_thread(self._profile.set_status, status) + + def get_window(self): + inst = self + + class Window(QtWidgets.QWidget): + def __init__(self): + super(Window, self).__init__() + self.setGeometry(QtCore.QRect(450, 300, 350, 100)) + self.label = QtWidgets.QLabel(self) + self.label.setGeometry(QtCore.QRect(20, 0, 310, 35)) + self.label.setText(QtWidgets.QApplication.translate("AutoAwayStatusLinux", "Auto away time in minutes\n(0 - to disable)")) + self.time = QtWidgets.QLineEdit(self) + self.time.setGeometry(QtCore.QRect(20, 40, 310, 25)) + self.time.setText(str(inst._time)) + self.setWindowTitle("AutoAwayStatusLinux") + self.ok = QtWidgets.QPushButton(self) + self.ok.setGeometry(QtCore.QRect(20, 70, 310, 25)) + self.ok.setText( + QtWidgets.QApplication.translate("AutoAwayStatusLinux", "Save")) + self.ok.clicked.connect(self.update) + + def update(self): + try: + t = int(self.time.text()) + except: + t = 0 + inst._time = t + inst.save() + self.close() + + return Window() + + def loop(self): + self._active = True + while self._exec: + time.sleep(5) + d = check_output(['xprintidle']) + d = int(d) // 1000 + if self._time: + if d > 60 * self._time: + self.change_status() + elif self._profile.status == 1: + self.change_status(self._prev_status) diff --git a/toxygen/plugins/awayw.py.windows b/toxygen/plugins/awayw.py.windows new file mode 100644 index 0000000..5c4b768 --- /dev/null +++ b/toxygen/plugins/awayw.py.windows @@ -0,0 +1,115 @@ +import plugin_super_class +import threading +import time +from PyQt5 import QtCore, QtWidgets +from ctypes import Structure, windll, c_uint, sizeof, byref +import json + + +class LASTINPUTINFO(Structure): + _fields_ = [('cbSize', c_uint), ('dwTime', c_uint)] + + +def get_idle_duration(): + lastInputInfo = LASTINPUTINFO() + lastInputInfo.cbSize = sizeof(lastInputInfo) + windll.user32.GetLastInputInfo(byref(lastInputInfo)) + millis = windll.kernel32.GetTickCount() - lastInputInfo.dwTime + return millis / 1000.0 + + +class InvokeEvent(QtCore.QEvent): + EVENT_TYPE = QtCore.QEvent.Type(QtCore.QEvent.registerEventType()) + + def __init__(self, fn, *args, **kwargs): + QtCore.QEvent.__init__(self, InvokeEvent.EVENT_TYPE) + self.fn = fn + self.args = args + self.kwargs = kwargs + + +class Invoker(QtCore.QObject): + + def event(self, event): + event.fn(*event.args, **event.kwargs) + return True + +_invoker = Invoker() + + +def invoke_in_main_thread(fn, *args, **kwargs): + QtCore.QCoreApplication.postEvent(_invoker, InvokeEvent(fn, *args, **kwargs)) + + +class AutoAwayStatusWindows(plugin_super_class.PluginSuperClass): + + def __init__(self, *args): + super().__init__('AutoAwayStatusWindows', 'awayw', *args) + self._thread = None + self._exec = None + self._active = False + self._time = json.loads(self.load_settings())['time'] + self._prev_status = 0 + + def close(self): + self.stop() + + def stop(self): + self._exec = False + if self._active: + self._thread.join() + + def start(self): + self._exec = True + self._thread = threading.Thread(target=self.loop) + self._thread.start() + + def save(self): + self.save_settings('{"time": ' + str(self._time) + '}') + + def change_status(self, status=1): + if self._profile.status != 1: + self._prev_status = self._profile.status + invoke_in_main_thread(self._profile.set_status, status) + + def get_window(self): + inst = self + + class Window(QtWidgets.QWidget): + def __init__(self): + super(Window, self).__init__() + self.setGeometry(QtCore.QRect(450, 300, 350, 100)) + self.label = QtWidgets.QLabel(self) + self.label.setGeometry(QtCore.QRect(20, 0, 310, 35)) + self.label.setText(QtWidgets.QApplication.translate("AutoAwayStatusWindows", "Auto away time in minutes\n(0 - to disable)")) + self.time = QtWidgets.QLineEdit(self) + self.time.setGeometry(QtCore.QRect(20, 40, 310, 25)) + self.time.setText(str(inst._time)) + self.setWindowTitle("AutoAwayStatusWindows") + self.ok = QtWidgets.QPushButton(self) + self.ok.setGeometry(QtCore.QRect(20, 70, 310, 25)) + self.ok.setText( + QtWidgets.QApplication.translate("AutoAwayStatusWindows", "Save")) + self.ok.clicked.connect(self.update) + + def update(self): + try: + t = int(self.time.text()) + except: + t = 0 + inst._time = t + inst.save() + self.close() + + return Window() + + def loop(self): + self._active = True + while self._exec: + time.sleep(5) + d = get_idle_duration() + if self._time: + if d > 60 * self._time: + self.change_status() + elif self._profile.status == 1: + self.change_status(self._prev_status) diff --git a/toxygen/plugins/bday.pro b/toxygen/plugins/bday.pro new file mode 100644 index 0000000..7393e95 --- /dev/null +++ b/toxygen/plugins/bday.pro @@ -0,0 +1,2 @@ +SOURCES = bday.py +TRANSLATIONS = bday/en_GB.ts bday/en_US.ts bday/ru_RU.ts diff --git a/toxygen/plugins/bday.py b/toxygen/plugins/bday.py new file mode 100644 index 0000000..976256d --- /dev/null +++ b/toxygen/plugins/bday.py @@ -0,0 +1,95 @@ +import plugin_super_class +from PyQt5 import QtWidgets, QtCore +import json +import importlib + + +class BirthDay(plugin_super_class.PluginSuperClass): + + def __init__(self, *args): + # Constructor. In plugin __init__ should take only 1 last argument + super(BirthDay, self).__init__('BirthDay', 'bday', *args) + self._data = json.loads(self.load_settings()) + self._datetime = importlib.import_module('datetime') + self._timers = [] + self._app = args[0] + self._profile=self._app._ms._profile + self._window = None + + def start(self): + now = self._datetime.datetime.now() + today = {} + x = self._profile.tox_id[:64] + for key in self._data: + if key != x and key != 'send_date': + arr = self._data[key].split('.') + if int(arr[0]) == now.day and int(arr[1]) == now.month: + today[key] = now.year - int(arr[2]) + if len(today): + msgbox = QtWidgets.QMessageBox() + title = QtWidgets.QApplication.translate('BirthDay', "Birthday!") + msgbox.setWindowTitle(title) + text = ', '.join(self._profile.get_friend_by_number(self._tox.friend_by_public_key(x)).name + ' ({})'.format(today[x]) for x in today) + msgbox.setText('Birthdays: ' + text) + msgbox.exec_() + + def get_description(self): + return QApplication.translate("BirthDay", "Send and get notifications on your friends' birthdays.") + + def get_window(self): + inst = self + x = self._profile.tox_id[:64] + + class Window(QtWidgets.QWidget): + + def __init__(self): + super(Window, self).__init__() + self.setGeometry(QtCore.QRect(450, 300, 350, 150)) + self.send = QtWidgets.QCheckBox(self) + self.send.setGeometry(QtCore.QRect(20, 10, 310, 25)) + self.send.setText(QtWidgets.QApplication.translate('BirthDay', "Send my birthday date to contacts")) + self.setWindowTitle(QtWidgets.QApplication.translate('BirthDay', "Birthday")) + self.send.clicked.connect(self.update) + self.send.setChecked(inst._data['send_date']) + self.date = QtWidgets.QLineEdit(self) + self.date.setGeometry(QtCore.QRect(20, 50, 310, 25)) + self.date.setPlaceholderText(QtWidgets.QApplication.translate('BirthDay', "Date in format dd.mm.yyyy")) + self.set_date = QtWidgets.QPushButton(self) + self.set_date.setGeometry(QtCore.QRect(20, 90, 310, 25)) + self.set_date.setText(QtWidgets.QApplication.translate('BirthDay', "Save date")) + self.set_date.clicked.connect(self.save_curr_date) + self.date.setText(inst._data[x] if x in inst._data else '') + + def save_curr_date(self): + inst._data[x] = self.date.text() + inst.save_settings(json.dumps(inst._data)) + self.close() + + def update(self): + inst._data['send_date'] = self.send.isChecked() + inst.save_settings(json.dumps(inst._data)) + + if not hasattr(self, '_window') or not self._window: + self._window = Window() + return self._window + + def lossless_packet(self, data, friend_number): + if len(data): + friend = self._profile.get_friend_by_number(friend_number) + self._data[friend.tox_id] = data + self.save_settings(json.dumps(self._data)) + elif self._data['send_date'] and self._profile.tox_id[:64] in self._data: + self.send_lossless(self._data[self._profile.tox_id[:64]], friend_number) + + def friend_connected(self, friend_number): + timer = QtCore.QTimer() + timer.timeout.connect(lambda: self.timer(friend_number)) + timer.start(10000) + self._timers.append(timer) + + def timer(self, friend_number): + timer = self._timers.pop() + timer.stop() + if self._profile.get_friend_by_number(friend_number).tox_id not in self._data: + self.send_lossless('', friend_number) + diff --git a/toxygen/plugins/bot.py b/toxygen/plugins/bot.py new file mode 100644 index 0000000..4f6057a --- /dev/null +++ b/toxygen/plugins/bot.py @@ -0,0 +1,81 @@ +import plugin_super_class +from PyQt5 import QtCore +import time + + +class InvokeEvent(QtCore.QEvent): + EVENT_TYPE = QtCore.QEvent.Type(QtCore.QEvent.registerEventType()) + + def __init__(self, fn, *args, **kwargs): + QtCore.QEvent.__init__(self, InvokeEvent.EVENT_TYPE) + self.fn = fn + self.args = args + self.kwargs = kwargs + + +class Invoker(QtCore.QObject): + + def event(self, event): + event.fn(*event.args, **event.kwargs) + return True + +_invoker = Invoker() + + +def invoke_in_main_thread(fn, *args, **kwargs): + QtCore.QCoreApplication.postEvent(_invoker, InvokeEvent(fn, *args, **kwargs)) + + +class Bot(plugin_super_class.PluginSuperClass): + + def __init__(self, *args): + super(Bot, self).__init__('Bot', 'bot', *args) + self._callback = None + self._mode = 0 + self._message = "I'm away, will back soon" + self._timer = QtCore.QTimer() + self._timer.timeout.connect(self.initialize) + self._app = args[0] + self._profile=self._app._ms._profile + self._window = None + + def get_description(self): + return QApplication.translate("Bot", 'Plugin to answer bot to your friends.') + + def start(self): + self._timer.start(10000) + + def command(self, command): + if command.startswith('mode '): + self._mode = int(command.split(' ')[-1]) + elif command.startswith('message '): + self._message = command[8:] + else: + super().command(command) + + def initialize(self): + self._timer.stop() + self._callback = self._tox.friend_message_cb + + def incoming_message(tox, friend_number, message_type, message, size, user_data): + self._callback(tox, friend_number, message_type, message, size, user_data) + if self._profile.status == 1: # TOX_USER_STATUS['AWAY'] + self.answer(friend_number, str(message, 'utf-8')) + + self._tox.callback_friend_message(incoming_message) # , None + + def stop(self): + if not self._callback: return + try: + # TypeError: argument must be callable or integer function address + self._tox.callback_friend_message(self._callback) # , None + except: pass + + def close(self): + self.stop() + + def answer(self, friend_number, message): + if not self._mode: + message = self._message + invoke_in_main_thread(self._profile.send_message, message, friend_number) + diff --git a/toxygen/plugins/chess.py b/toxygen/plugins/chess.py new file mode 100644 index 0000000..92fe439 --- /dev/null +++ b/toxygen/plugins/chess.py @@ -0,0 +1,1695 @@ +# -*- coding: utf-8 -*- + +import collections +import re +import math +import plugin_super_class + +from PyQt5.QtCore import * +from PyQt5.QtWidgets import * +from PyQt5.QtGui import * +from PyQt5.QtSvg import * + + +START_FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" + + +def opposite_color(color): + """:return: The opposite color. + + :param color: + "w", "white, "b" or "black". + """ + if color == "w": + return "b" + elif color == "white": + return "black" + elif color == "b": + return "w" + elif color == "black": + return "white" + else: + raise ValueError("Expected w, b, white or black, got: %s." % color) + + +class Piece(object): + + __cache = dict() + + def __init__(self, symbol): + self.__symbol = symbol + + self.__color = "w" if symbol != symbol.lower() else "b" + self.__full_color = "white" if self.__color == "w" else "black" + + self.__type = symbol.lower() + if self.__type == "p": + self.__full_type = "pawn" + elif self.__type == "n": + self.__full_type = "knight" + elif self.__type == "b": + self.__full_type = "bishop" + elif self.__type == "r": + self.__full_type = "rook" + elif self.__type == "q": + self.__full_type = "queen" + elif self.__type == "k": + self.__full_type = "king" + else: + raise ValueError("Expected valid piece symbol, got: %s." % symbol) + + self.__hash = ord(self.__symbol) + + @classmethod + def from_color_and_type(cls, color, type): + """Creates a piece object from color and type. + """ + if type == "p" or type == "pawn": + symbol = "p" + elif type == "n" or type == "knight": + symbol = "n" + elif type == "b" or type == "bishop": + symbol = "b" + elif type == "r" or type == "rook": + symbol = "r" + elif type == "q" or type == "queen": + symbol = "q" + elif type == "k" or type == "king": + symbol = "k" + else: + raise ValueError("Expected piece type, got: %s." % type) + + if color == "w" or color == "white": + return cls(symbol.upper()) + elif color == "b" or color == "black": + return cls(symbol) + else: + raise ValueError("Expected w, b, white or black, got: %s." % color) + + @property + def symbol(self): + return self.__symbol + + @property + def color(self): + """The color of the piece as `"b"` or `"w"`.""" + return self.__color + + @property + def full_color(self): + """The full color of the piece as `"black"` or `"white`.""" + return self.__full_color + + @property + def type(self): + """The type of the piece as `"p"`, `"b"`, `"n"`, `"r"`, `"k"`, + or `"q"` for pawn, bishop, knight, rook, king or queen. + """ + return self.__type + + @property + def full_type(self): + """The full type of the piece as `"pawn"`, `"bishop"`, + `"knight"`, `"rook"`, `"king"` or `"queen"`. + """ + return self.__full_type + + def __str__(self): + return self.__symbol + + def __repr__(self): + return "Piece('%s')" % self.__symbol + + def __eq__(self, other): + return isinstance(other, Piece) and self.__symbol == other.symbol + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return self.__hash + + +class Square(object): + """Represents a square on the chess board. + + :param name: The name of the square in algebraic notation. + + Square objects that represent the same square compare as equal. + """ + + __cache = dict() + + def __init__(self, name): + if not len(name) == 2: + raise ValueError("Expected square name, got: %s." % repr(name)) + self.__name = name + + if not name[0] in ["a", "b", "c", "d", "e", "f", "g", "h"]: + raise ValueError("Expected file, got: %s." % repr(name[0])) + self.__file = name[0] + self.__x = ord(self.__name[0]) - ord("a") + + if not name[1] in ["1", "2", "3", "4", "5", "6", "7", "8"]: + raise ValueError("Expected rank, got: %s." % repr(name[1])) + self.__rank = int(name[1]) + self.__y = ord(self.__name[1]) - ord("1") + + self.__x88 = self.__x + 16 * (7 - self.__y) + + @classmethod + def from_x88(cls, x88): + """Creates a square object from an `x88 `_ + index. + + :param x88: + The x88 index as integer between 0 and 128. + """ + if x88 < 0 or x88 > 128: + raise ValueError("x88 index is out of range: %s." % repr(x88)) + + if x88 & 0x88: + raise ValueError("x88 is not on the board: %s." % repr(x88)) + + return cls("abcdefgh"[x88 & 7] + "87654321"[x88 >> 4]) + + @classmethod + def from_rank_and_file(cls, rank, file): + """Creates a square object from rank and file. + + :param rank: + An integer between 1 and 8. + :param file: + The rank as a letter between `"a"` and `"h"`. + """ + if rank < 1 or rank > 8: + raise ValueError("Expected rank to be between 1 and 8: %s." % repr(rank)) + + if not file in ["a", "b", "c", "d", "e", "f", "g", "h"]: + raise ValueError("Expected the file to be a letter between 'a' and 'h': %s." % repr(file)) + + return cls(file + str(rank)) + + @classmethod + def from_x_and_y(cls, x, y): + """Creates a square object from x and y coordinates. + + :param x: + An integer between 0 and 7 where 0 is the a-file. + :param y: + An integer between 0 and 7 where 0 is the first rank. + """ + return cls("abcdefgh"[x] + "12345678"[y]) + + @property + def name(self): + """The algebraic name of the square.""" + return self.__name + + @property + def file(self): + """The file as a letter between `"a"` and `"h"`.""" + return self.__file + + @property + def x(self): + """The x-coordinate, starting with 0 for the a-file.""" + return self.__x + + @property + def rank(self): + """The rank as an integer between 1 and 8.""" + return self.__rank + + @property + def y(self): + """The y-coordinate, starting with 0 for the first rank.""" + return self.__y + + @property + def x88(self): + """The `x88 `_ + index of the square.""" + return self.__x88 + + def is_dark(self): + """:return: Whether it is a dark square.""" + return (self.__x - self.__y % 2) == 0 + + def is_light(self): + """:return: Whether it is a light square.""" + return not self.is_dark() + + def is_backrank(self): + """:return: Whether the square is on either sides backrank.""" + return self.__y == 0 or self.__y == 7 + + def __str__(self): + return self.__name + + def __repr__(self): + return "Square('%s')" % self.__name + + def __eq__(self, other): + return isinstance(other, Square) and self.__name == other.name + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return self.__x88 + + +class Move(object): + """Represents a move. + """ + + __uci_move_regex = re.compile(r"^([a-h][1-8])([a-h][1-8])([rnbq]?)$") + + def __init__(self, source, target, promotion=None): + if not isinstance(source, Square): + raise TypeError("Expected source to be a Square.") + self.__source = source + + if not isinstance(target, Square): + raise TypeError("Expected target to be a Square.") + self.__target = target + + if not promotion: + self.__promotion = None + self.__full_promotion = None + else: + promotion = promotion.lower() + if promotion == "n" or promotion == "knight": + self.__promotion = "n" + self.__full_promotion = "knight" + elif promotion == "b" or promotion == "bishop": + self.__promotion = "b" + self.__full_promotion = "bishop" + elif promotion == "r" or promotion == "rook": + self.__promotion = "r" + self.__full_promotion = "rook" + elif promotion == "q" or promotion == "queen": + self.__promotion = "q" + self.__full_promotion = "queen" + else: + raise ValueError("Expected promotion type, got: %s." % repr(promotion)) + + @classmethod + def from_uci(cls, uci): + """The UCI move string like `"a1a2"` or `"b7b8q"`.""" + if uci == "0000": + return cls.get_null() + + match = cls.__uci_move_regex.match(uci) + + return cls( + source=Square(match.group(1)), + target=Square(match.group(2)), + promotion=match.group(3) or None) + + @classmethod + def get_null(cls): + """:return: A null move.""" + return cls(Square("a1"), Square("a1")) + + @property + def source(self): + """The source square.""" + return self.__source + + @property + def target(self): + """The target square.""" + return self.__target + + @property + def promotion(self): + """The promotion type as `None`, `"r"`, `"n"`, `"b"` or `"q"`.""" + return self.__promotion + + @property + def full_promotion(self): + """Like `promotion`, but with full piece type names.""" + return self.__full_promotion + + @property + def uci(self): + """The UCI move string like `"a1a2"` or `"b7b8q"`.""" + if self.is_null(): + return "0000" + else: + if self.__promotion: + return self.__source.name + self.__target.name + self.__promotion + else: + return self.__source.name + self.__target.name + + def is_null(self): + """:return: Whether the move is a null move.""" + return self.__source == self.__target + + def __nonzero__(self): + return not self.is_null() + + def __str__(self): + return self.uci + + def __repr__(self): + return "Move.from_uci(%s)" % repr(self.uci) + + def __eq__(self, other): + return isinstance(other, Move) and self.uci == other.uci + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash(self.uci) + + +MoveInfo = collections.namedtuple("MoveInfo", [ + "move", + "piece", + "captured", + "san", + "is_enpassant", + "is_king_side_castle", + "is_queen_side_castle", + "is_castle", + "is_check", + "is_checkmate"]) + + +class Position(object): + """Represents a chess position. + + :param fen: + Optional. The FEN of the position. Defaults to the standard + chess start position. + """ + + __san_regex = re.compile('^([NBKRQ])?([a-h])?([1-8])?x?([a-h][1-8])(=[NBRQ])?(\+|#)?$') + + def __init__(self, fen=START_FEN): + self.__castling = "KQkq" + self.fen = fen + + def copy(self): + """Gets a copy of the position. The copy will not change when the + original instance is changed. + + :return: + An exact copy of the positon. + """ + return Position(self.fen) + + def __get_square_index(self, square_or_int): + if type(square_or_int) is int: + # Validate the index by passing it through the constructor. + return Square.from_x88(square_or_int).x88 + elif isinstance(square_or_int, str): + return Square(square_or_int).x88 + elif type(square_or_int) is Square: + return square_or_int.x88 + else: + raise TypeError( + "Expected integer or Square, got: %s." % repr(square_or_int)) + + def __getitem__(self, key): + return self.__board[self.__get_square_index(key)] + + def __setitem__(self, key, value): + if value is None or type(value) is Piece: + self.__board[self.__get_square_index(key)] = value + else: + raise TypeError("Expected Piece or None, got: %s." % repr(value)) + + def __delitem__(self, key): + self.__board[self.__get_square_index(key)] = None + + def clear_board(self): + """Removes all pieces from the board.""" + self.__board = [None] * 128 + + def reset(self): + """Resets to the standard chess start position.""" + self.set_fen(START_FEN) + + def __get_disambiguator(self, move): + same_rank = False + same_file = False + piece = self[move.source] + + for m in self.get_legal_moves(): + ambig_piece = self[m.source] + if (piece == ambig_piece and move.source != m.source and + move.target == m.target): + if move.source.rank == m.source.rank: + same_rank = True + + if move.source.file == m.source.file: + same_file = True + + if same_rank and same_file: + break + + if same_rank and same_file: + return move.source.name + elif same_file: + return str(move.source.rank) + elif same_rank: + return move.source.file + else: + return "" + + def get_move_from_san(self, san): + """Gets a move from standard algebraic notation. + + :param san: + A move string in standard algebraic notation. + + :return: + A Move object. + + :raise Exception: + If not exactly one legal move matches. + """ + # Castling moves. + if san == "O-O" or san == "O-O-O": + rank = 1 if self.turn == "w" else 8 + if san == "O-O": + return Move( + source=Square.from_rank_and_file(rank, 'e'), + target=Square.from_rank_and_file(rank, 'g')) + else: + return Move( + source=Square.from_rank_and_file(rank, 'e'), + target=Square.from_rank_and_file(rank, 'c')) + # Regular moves. + else: + matches = Position.__san_regex.match(san) + if not matches: + raise ValueError("Invalid SAN: %s." % repr(san)) + + piece = Piece.from_color_and_type( + color=self.turn, + type=matches.group(1).lower() if matches.group(1) else 'p') + target = Square(matches.group(4)) + + source = None + for m in self.get_legal_moves(): + if self[m.source] != piece or m.target != target: + continue + + if matches.group(2) and matches.group(2) != m.source.file: + continue + if matches.group(3) and matches.group(3) != str(m.source.rank): + continue + + # Move matches. Assert it is not ambiguous. + if source: + raise Exception( + "Move is ambiguous: %s matches %s and %s." + % san, source, m) + source = m.source + + if not source: + raise Exception("No legal move matches %s." % san) + + return Move(source, target, matches.group(5) or None) + + def get_move_info(self, move): + """Gets information about a move. + + :param move: + The move to get information about. + + :return: + A named tuple with these properties: + + `move`: + The move object. + `piece`: + The piece that has been moved. + `san`: + The standard algebraic notation of the move. + `captured`: + The piece that has been captured or `None`. + `is_enpassant`: + A boolean indicating if the move is an en-passant + capture. + `is_king_side_castle`: + Whether it is a king-side castling move. + `is_queen_side_castle`: + Whether it is a queen-side castling move. + `is_castle`: + Whether it is a castling move. + `is_check`: + Whether the move gives check. + `is_checkmate`: + Whether the move gives checkmate. + + :raise Exception: + If the move is not legal in the position. + """ + resulting_position = self.copy().make_move(move) + + capture = self[move.target] + piece = self[move.source] + + # Pawn moves. + enpassant = False + if piece.type == "p": + # En-passant. + if move.target.file != move.source.file and not capture: + enpassant = True + capture = Piece.from_color_and_type( + color=resulting_position.turn, type='p') + + # Castling. + if piece.type == "k": + is_king_side_castle = move.target.x - move.source.x == 2 + is_queen_side_castle = move.target.x - move.source.x == -2 + else: + is_king_side_castle = is_queen_side_castle = False + + # Checks. + is_check = resulting_position.is_check() + is_checkmate = resulting_position.is_checkmate() + + # Generate the SAN. + san = "" + if is_king_side_castle: + san += "o-o" + elif is_queen_side_castle: + san += "o-o-o" + else: + if piece.type != 'p': + san += piece.type.upper() + + san += self.__get_disambiguator(move) + + if capture: + if piece.type == 'p': + san += move.source.file + san += "x" + san += move.target.name + + if move.promotion: + san += "=" + san += move.promotion.upper() + + if is_checkmate: + san += "#" + elif is_check: + san += "+" + + if enpassant: + san += " (e.p.)" + + # Return the named tuple. + return MoveInfo( + move=move, + piece=piece, + captured=capture, + san=san, + is_enpassant=enpassant, + is_king_side_castle=is_king_side_castle, + is_queen_side_castle=is_queen_side_castle, + is_castle=is_king_side_castle or is_queen_side_castle, + is_check=is_check, + is_checkmate=is_checkmate) + + def make_move(self, move, validate=True): + """Makes a move. + + :param move: + The move to make. + :param validate: + Defaults to `True`. Whether the move should be validated. + + :return: + Making a move changes the position object. The same + (changed) object is returned for chainability. + + :raise Exception: + If the validate parameter is `True` and the move is not + legal in the position. + """ + if validate and move not in self.get_legal_moves(): + raise Exception( + "%s is not a legal move in the position %s." % (move, self.fen)) + piece = self[move.source] + capture = self[move.target] + + # Move the piece. + self[move.target] = self[move.source] + del self[move.source] + + # It is the next players turn. + self.toggle_turn() + + # Pawn moves. + self.ep_file = None + if piece.type == "p": + # En-passant. + if move.target.file != move.source.file and not capture: + if self.turn == "w": + self[move.target.x88 - 16] = None + else: + self[move.target.x88 + 16] = None + capture = True + # If big pawn move, set the en-passant file. + if abs(move.target.rank - move.source.rank) == 2: + if self.get_theoretical_ep_right(move.target.file): + self.ep_file = move.target.file + + # Promotion. + if move.promotion: + self[move.target] = Piece.from_color_and_type( + color=piece.color, type=move.promotion) + + # Potential castling. + if piece.type == "k": + steps = move.target.x - move.source.x + if abs(steps) == 2: + # Queen-side castling. + if steps == -2: + rook_target = move.target.x88 + 1 + rook_source = move.target.x88 - 2 + # King-side castling. + else: + rook_target = move.target.x88 - 1 + rook_source = move.target.x88 + 1 + self[rook_target] = self[rook_source] + del self[rook_source] + + # Increment the half move counter. + if piece.type == "p" or capture: + self.half_moves = 0 + else: + self.half_moves += 1 + + # Increment the move number. + if self.turn == "w": + self.ply += 1 + + # Update castling rights. + for type in ["K", "Q", "k", "q"]: + if not self.get_theoretical_castling_right(type): + self.set_castling_right(type, False) + + return self + + @property + def turn(self): + """Whos turn it is as `"w"` or `"b"`.""" + return self.__turn + + @turn.setter + def turn(self, value): + if value not in ["w", "b"]: + raise ValueError( + "Expected 'w' or 'b' for turn, got: %s." % repr(value)) + self.__turn = value + + def toggle_turn(self): + """Toggles whos turn it is.""" + self.turn = opposite_color(self.turn) + + def get_castling_right(self, type): + """Checks the castling rights. + + :param type: + The castling move to check. "K" for king-side castling of + the white player, "Q" for queen-side castling of the white + player. "k" and "q" for the corresponding castling moves of + the black player. + + :return: + A boolean indicating whether the player has that castling + right. + """ + if not type in ["K", "Q", "k", "q"]: + raise KeyError( + "Expected 'K', 'Q', 'k' or 'q' as a castling type, got: %s." % repr(type)) + return type in self.__castling + + def get_theoretical_castling_right(self, type): + """Checks if a player could have a castling right in theory from + looking just at the piece positions. + + :param type: + The castling move to check. See + `Position.get_castling_right(type)` for values. + + :return: + A boolean indicating whether the player could theoretically + have that castling right. + """ + if not type in ["K", "Q", "k", "q"]: + raise KeyError( + "Expected 'K', 'Q', 'k' or 'q' as a castling type, got: %s." + % repr(type)) + if type == "K" or type == "Q": + if self["e1"] != Piece("K"): + return False + if type == "K": + return self["h1"] == Piece("R") + elif type == "Q": + return self["a1"] == Piece("R") + elif type == "k" or type == "q": + if self["e8"] != Piece("k"): + return False + if type == "k": + return self["h8"] == Piece("r") + elif type == "q": + return self["a8"] == Piece("r") + + def get_theoretical_ep_right(self, file): + """Checks if a player could have an ep-move in theory from + looking just at the piece positions. + + :param file: + The file to check as a letter between `"a"` and `"h"`. + + :return: + A boolean indicating whether the player could theoretically + have that en-passant move. + """ + if not file in ["a", "b", "c", "d", "e", "f", "g", "h"]: + raise KeyError( + "Expected a letter between 'a' and 'h' for the file, got: %s." + % repr(file)) + + # Check there is a pawn. + pawn_square = Square.from_rank_and_file( + rank=4 if self.turn == "b" else 5, file=file) + opposite_color_pawn = Piece.from_color_and_type( + color=opposite_color(self.turn), type="p") + if self[pawn_square] != opposite_color_pawn: + return False + + # Check the square below is empty. + square_below = Square.from_rank_and_file( + rank=3 if self.turn == "b" else 6, file=file) + if self[square_below]: + return False + + # Check there is a pawn of the other color on a neighbor file. + f = ord(file) - ord("a") + p = Piece("p") + P = Piece("P") + if self.turn == "b": + if f > 0 and self[Square.from_x_and_y(f - 1, 3)] == p: + return True + elif f < 7 and self[Square.from_x_and_y(f + 1, 3)] == p: + return True + else: + if f > 0 and self[Square.from_x_and_y(f - 1, 4)] == P: + return True + elif f < 7 and self[Square.from_x_and_y(f + 1, 4)] == P: + return True + return False + + def set_castling_right(self, type, status): + """Sets a castling right. + + :param type: + `"K"`, `"Q"`, `"k"`, or `"q"` as used by + `Position.get_castling_right(type)`. + :param status: + A boolean indicating whether that castling right should be + granted or denied. + """ + if not type in ["K", "Q", "k", "q"]: + raise KeyError( + "Expected 'K', 'Q', 'k' or 'q' as a castling type, got: %s." + % repr(type)) + + castling = "" + for t in ["K", "Q", "k", "q"]: + if type == t: + if status: + castling += t + elif self.get_castling_right(t): + castling += t + self.__castling = castling + + @property + def ep_file(self): + """The en-passant file as a lowercase letter between `"a"` and + `"h"` or `None`.""" + return self.__ep_file + + @ep_file.setter + def ep_file(self, value): + if not value in ["a", "b", "c", "d", "e", "f", "g", "h", None]: + raise ValueError( + "Expected None or a letter between 'a' and 'h' for the " + "en-passant file, got: %s." % repr(value)) + + self.__ep_file = value + + @property + def half_moves(self): + """The number of half-moves since the last capture or pawn move.""" + return self.__half_moves + + @half_moves.setter + def half_moves(self, value): + if type(value) is not int: + raise TypeError( + "Expected integer for half move count, got: %s." % repr(value)) + if value < 0: + raise ValueError("Half move count must be >= 0.") + + self.__half_moves = value + + @property + def ply(self): + """The number of this move. The game starts at 1 and the counter + is incremented every time white moves. + """ + return self.__ply + + @ply.setter + def ply(self, value): + if type(value) is not int: + raise TypeError( + "Expected integer for ply count, got: %s." % repr(value)) + if value < 1: + raise ValueError("Ply count must be >= 1.") + self.__ply = value + + def get_piece_counts(self, color = "wb"): + """Counts the pieces on the board. + + :param color: + Defaults to `"wb"`. A color to filter the pieces by. Valid + values are "w", "b", "wb" and "bw". + + :return: + A dictionary of piece counts, keyed by lowercase piece type + letters. + """ + if not color in ["w", "b", "wb", "bw"]: + raise KeyError( + "Expected color filter to be one of 'w', 'b', 'wb', 'bw', " + "got: %s." % repr(color)) + + counts = { + "p": 0, + "b": 0, + "n": 0, + "r": 0, + "k": 0, + "q": 0, + } + for piece in self.__board: + if piece and piece.color in color: + counts[piece.type] += 1 + return counts + + def get_king(self, color): + """Gets the square of the king. + + :param color: + `"w"` for the white players king. `"b"` for the black + players king. + + :return: + The first square with a matching king or `None` if that + player has no king. + """ + if not color in ["w", "b"]: + raise KeyError("Invalid color: %s." % repr(color)) + + for x88, piece in enumerate(self.__board): + if piece and piece.color == color and piece.type == "k": + return Square.from_x88(x88) + + @property + def fen(self): + """The FEN string representing the position.""" + # Board setup. + empty = 0 + fen = "" + for y in range(7, -1, -1): + for x in range(0, 8): + square = Square.from_x_and_y(x, y) + + # Add pieces. + if not self[square]: + empty += 1 + else: + if empty > 0: + fen += str(empty) + empty = 0 + fen += self[square].symbol + + # Boarder of the board. + if empty > 0: + fen += str(empty) + if not (x == 7 and y == 0): + fen += "/" + empty = 0 + + if self.ep_file and self.get_theoretical_ep_right(self.ep_file): + ep_square = self.ep_file + ("3" if self.turn == "b" else "6") + else: + ep_square = "-" + + # Join the parts together. + return " ".join([ + fen, + self.turn, + self.__castling if self.__castling else "-", + ep_square, + str(self.half_moves), + str(self.__ply)]) + + @fen.setter + def fen(self, fen): + # Split into 6 parts. + tokens = fen.split() + if len(tokens) != 6: + raise Exception("A FEN does not consist of 6 parts.") + + # Check that the position part is valid. + rows = tokens[0].split("/") + assert len(rows) == 8 + for row in rows: + field_sum = 0 + previous_was_number = False + for char in row: + if char in "12345678": + if previous_was_number: + raise Exception( + "Position part of the FEN is invalid: " + "Multiple numbers immediately after each other.") + field_sum += int(char) + previous_was_number = True + elif char in "pnbrkqPNBRKQ": + field_sum += 1 + previous_was_number = False + else: + raise Exception( + "Position part of the FEN is invalid: " + "Invalid character in the position part of the FEN.") + + if field_sum != 8: + Exception( + "Position part of the FEN is invalid: " + "Row with invalid length.") + + # Check that the other parts are valid. + if not tokens[1] in ["w", "b"]: + raise Exception( + "Turn part of the FEN is invalid: Expected b or w.") + if not re.compile(r"^(KQ?k?q?|Qk?q?|kq?|q|-)$").match(tokens[2]): + raise Exception("Castling part of the FEN is invalid.") + if not re.compile(r"^(-|[a-h][36])$").match(tokens[3]): + raise Exception("En-passant part of the FEN is invalid.") + if not re.compile(r"^(0|[1-9][0-9]*)$").match(tokens[4]): + raise Exception("Half move part of the FEN is invalid.") + if not re.compile(r"^[1-9][0-9]*$").match(tokens[5]): + raise Exception("Ply part of the FEN is invalid.") + + # Set pieces on the board. + self.__board = [None] * 128 + i = 0 + for symbol in tokens[0]: + if symbol == "/": + i += 8 + elif symbol in "12345678": + i += int(symbol) + else: + self.__board[i] = Piece(symbol) + i += 1 + + # Set the turn. + self.__turn = tokens[1] + + # Set the castling rights. + for type in ["K", "Q", "k", "q"]: + self.set_castling_right(type, type in tokens[2]) + + # Set the en-passant file. + if tokens[3] == "-": + self.__ep_file = None + else: + self.__ep_file = tokens[3][0] + + # Set the move counters. + self.__half_moves = int(tokens[4]) + self.__ply = int(tokens[5]) + + def is_king_attacked(self, color): + """:return: Whether the king of the given color is attacked. + + :param color: `"w"` or `"b"`. + """ + square = self.get_king(color) + if square: + return self.is_attacked(opposite_color(color), square) + else: + return False + + def get_pseudo_legal_moves(self): + """:yield: Pseudo legal moves in the current position.""" + PAWN_OFFSETS = { + "b": [16, 32, 17, 15], + "w": [-16, -32, -17, -15] + } + + PIECE_OFFSETS = { + "n": [-18, -33, -31, -14, 18, 33, 31, 14], + "b": [-17, -15, 17, 15], + "r": [-16, 1, 16, -1], + "q": [-17, -16, -15, 1, 17, 16, 15, -1], + "k": [-17, -16, -15, 1, 17, 16, 15, -1] + } + + for x88, piece in enumerate(self.__board): + # Skip pieces of the opponent. + if not piece or piece.color != self.turn: + continue + + square = Square.from_x88(x88) + + # Pawn moves. + if piece.type == "p": + # Single square ahead. Do not capture. + target = Square.from_x88(x88 + PAWN_OFFSETS[self.turn][0]) + if not self[target]: + # Promotion. + if target.is_backrank(): + for promote_to in "bnrq": + yield Move(square, target, promote_to) + else: + yield Move(square, target) + + # Two squares ahead. Do not capture. + if (self.turn == "w" and square.rank == 2) or (self.turn == "b" and square.rank == 7): + target = Square.from_x88(square.x88 + PAWN_OFFSETS[self.turn][1]) + if not self[target]: + yield Move(square, target) + + # Pawn captures. + for j in [2, 3]: + target_index = square.x88 + PAWN_OFFSETS[self.turn][j] + if target_index & 0x88: + continue + target = Square.from_x88(target_index) + if self[target] and self[target].color != self.turn: + # Promotion. + if target.is_backrank(): + for promote_to in "bnrq": + yield Move(square, target, promote_to) + else: + yield Move(square, target) + # En-passant. + elif not self[target] and target.file == self.ep_file: + yield Move(square, target) + # Other pieces. + else: + for offset in PIECE_OFFSETS[piece.type]: + target_index = square.x88 + while True: + target_index += offset + if target_index & 0x88: + break + target = Square.from_x88(target_index) + if not self[target]: + yield Move(square, target) + else: + if self[target].color == self.turn: + break + yield Move(square, target) + break + + # Knight and king do not go multiple times in their + # direction. + if piece.type in ["n", "k"]: + break + + opponent = opposite_color(self.turn) + + # King-side castling. + k = "k" if self.turn == "b" else "K" + if self.get_castling_right(k): + of = self.get_king(self.turn).x88 + to = of + 2 + if not self[of + 1] and not self[to] and not self.is_check() and not self.is_attacked(opponent, Square.from_x88(of + 1)) and not self.is_attacked(opponent, Square.from_x88(to)): + yield Move(Square.from_x88(of), Square.from_x88(to)) + + # Queen-side castling + q = "q" if self.turn == "b" else "Q" + if self.get_castling_right(q): + of = self.get_king(self.turn).x88 + to = of - 2 + + if not self[of - 1] and not self[of - 2] and not self[of - 3] and not self.is_check() and not self.is_attacked(opponent, Square.from_x88(of - 1)) and not self.is_attacked(opponent, Square.from_x88(to)): + yield Move(Square.from_x88(of), Square.from_x88(to)) + + def get_legal_moves(self): + """:yield: All legal moves in the current position.""" + for move in self.get_pseudo_legal_moves(): + potential_position = self.copy() + potential_position.make_move(move, False) + if not potential_position.is_king_attacked(self.turn): + yield move + + def get_attackers(self, color, square): + """Gets the attackers of a specific square. + + :param color: + Filter attackers by this piece color. + :param square: + The square to check for. + + :yield: + Source squares of the attack. + """ + if color not in ["b", "w"]: + raise KeyError("Invalid color: %s." % repr(color)) + + ATTACKS = [ + 20, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 20, 0, + 0, 20, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 20, 0, 0, + 0, 0, 20, 0, 0, 0, 0, 24, 0, 0, 0, 0, 20, 0, 0, 0, + 0, 0, 0, 20, 0, 0, 0, 24, 0, 0, 0, 20, 0, 0, 0, 0, + 0, 0, 0, 0, 20, 0, 0, 24, 0, 0, 20, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 20, 2, 24, 2, 20, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 2, 53, 56, 53, 2, 0, 0, 0, 0, 0, 0, + 24, 24, 24, 24, 24, 24, 56, 0, 56, 24, 24, 24, 24, 24, 24, 0, + 0, 0, 0, 0, 0, 2, 53, 56, 53, 2, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 20, 2, 24, 2, 20, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 20, 0, 0, 24, 0, 0, 20, 0, 0, 0, 0, 0, + 0, 0, 0, 20, 0, 0, 0, 24, 0, 0, 0, 20, 0, 0, 0, 0, + 0, 0, 20, 0, 0, 0, 0, 24, 0, 0, 0, 0, 20, 0, 0, 0, + 0, 20, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 20, 0, 0, + 20, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 20 + ] + + RAYS = [ + 17, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 15, 0, + 0, 17, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 15, 0, 0, + 0, 0, 17, 0, 0, 0, 0, 16, 0, 0, 0, 0, 15, 0, 0, 0, + 0, 0, 0, 17, 0, 0, 0, 16, 0, 0, 0, 15, 0, 0, 0, 0, + 0, 0, 0, 0, 17, 0, 0, 16, 0, 0, 15, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 17, 0, 16, 0, 15, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 17, 16, 15, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 0, -1, -1, -1, -1, -1, -1, -1, 0, + 0, 0, 0, 0, 0, 0, -15, -16, -17, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, -15, 0, -16, 0, -17, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -15, 0, 0, -16, 0, 0, -17, 0, 0, 0, 0, 0, + 0, 0, 0, -15, 0, 0, 0, -16, 0, 0, 0, -17, 0, 0, 0, 0, + 0, 0, -15, 0, 0, 0, 0, -16, 0, 0, 0, 0, -17, 0, 0, 0, + 0, -15, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, -17, 0, 0, + -15, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, -17 + ] + + SHIFTS = { + "p": 0, + "n": 1, + "b": 2, + "r": 3, + "q": 4, + "k": 5 + } + + for x88, piece in enumerate(self.__board): + if not piece or piece.color != color: + continue + source = Square.from_x88(x88) + + difference = source.x88 - square.x88 + index = difference + 119 + + if ATTACKS[index] & (1 << SHIFTS[piece.type]): + # Handle pawns. + if piece.type == "p": + if difference > 0: + if piece.color == "w": + yield source + else: + if piece.color == "b": + yield source + continue + + # Handle knights and king. + if piece.type in ["n", "k"]: + yield source + + # Handle the others. + offset = RAYS[index] + j = source.x88 + offset + blocked = False + while j != square.x88: + if self[j]: + blocked = True + break + j += offset + if not blocked: + yield source + + def is_attacked(self, color, square): + """Checks whether a square is attacked. + + :param color: + Check if this player is attacking. + :param square: + The square the player might be attacking. + + :return: + A boolean indicating whether the given square is attacked + by the player of the given color. + """ + x = list(self.get_attackers(color, square)) + return len(x) > 0 + + def is_check(self): + """:return: Whether the current player is in check.""" + return self.is_king_attacked(self.turn) + + def is_checkmate(self): + """:return: Whether the current player has been checkmated.""" + if not self.is_check(): + return False + else: + arr = list(self.get_legal_moves()) + return len(arr) == 0 + + def is_stalemate(self): + """:return: Whether the current player is in stalemate.""" + if self.is_check(): + return False + else: + arr = list(self.get_legal_moves()) + return len(arr) == 0 + + def is_insufficient_material(self): + """Checks if there is sufficient material to mate. + + Mating is impossible in: + + * A king versus king endgame. + * A king with bishop versus king endgame. + * A king with knight versus king endgame. + * A king with bishop versus king with bishop endgame, where both + bishops are on the same color. Same goes for additional + bishops on the same color. + + Assumes that the position is valid and each player has exactly + one king. + + :return: + Whether there is insufficient material to mate. + """ + piece_counts = self.get_piece_counts() + if sum(piece_counts.values()) == 2: + # King versus king. + return True + elif sum(piece_counts.values()) == 3: + # King and knight or bishop versus king. + if piece_counts["b"] == 1 or piece_counts["n"] == 1: + return True + elif sum(piece_counts.values()) == 2 + piece_counts["b"]: + # Each player with only king and any number of bishops, where all + # bishops are on the same color. + white_has_bishop = self.get_piece_counts("w")["b"] != 0 + black_has_bishop = self.get_piece_counts("b")["b"] != 0 + if white_has_bishop and black_has_bishop: + color = None + for x88, piece in enumerate(self.__board): + if piece and piece.type == "b": + square = Square.from_x88(x88) + if color is not None and color != square.is_light(): + return False + color = square.is_light() + return True + return False + + def is_game_over(self): + """Checks if the game is over. + + :return: + Whether the game is over by the rules of chess, + disregarding that players can agree on a draw, claim a draw + or resign. + """ + return (self.is_checkmate() or self.is_stalemate() or + self.is_insufficient_material()) + + def __str__(self): + return self.fen + + def __repr__(self): + return "Position.from_fen(%s)" % repr(self.fen) + + def __eq__(self, other): + return self.fen == other.fen + + def __ne__(self, other): + return self.fen != other.fen + + +class Board(QWidget): + + def __init__(self, parent): + super(Board, self).__init__() + self.margin = 0.1 + self.padding = 0.06 + self.showCoordinates = True + self.lightSquareColor = QColor(255, 255, 255) + self.darkSquareColor = QColor(100, 100, 255) + self.borderColor = QColor(100, 100, 200) + self.shadowWidth = 2 + self.rotation = 0 + self.ply = 1 + self.setWindowTitle('Chess') + self.backgroundPixmap = QPixmap(plugin_super_class.path_to_data('chess') + "background.png") + + self.draggedSquare = None + self.dragPosition = None + + self.position = Position() + + self.parent = parent + + # Load piece set. + self.pieceRenderers = dict() + for symbol in "PNBRQKpnbrqk": + piece = Piece(symbol) + self.pieceRenderers[piece] = QSvgRenderer(plugin_super_class.path_to_data('chess') + "classic-pieces/%s-%s.svg" % (piece.full_color, piece.full_type)) + + def update_title(self, my_move=False): + if self.position.is_checkmate(): + self.setWindowTitle('Checkmate') + elif self.position.is_stalemate(): + self.setWindowTitle('Stalemate') + else: + self.setWindowTitle('Chess' + (' [Your move]' if my_move else '')) + + def mousePressEvent(self, e): + self.dragPosition = e.pos() + square = self.squareAt(e.pos()) + if self.canDragSquare(square): + self.draggedSquare = square + + def mouseMoveEvent(self, e): + if self.draggedSquare: + self.dragPosition = e.pos() + self.repaint() + + def mouseReleaseEvent(self, e): + if self.draggedSquare: + dropSquare = self.squareAt(e.pos()) + if dropSquare == self.draggedSquare: + self.onSquareClicked(self.draggedSquare) + elif dropSquare: + move = self.moveFromDragDrop(self.draggedSquare, dropSquare) + if move: + self.position.make_move(move) + self.parent.move(move) + self.ply += 1 + self.draggedSquare = None + self.repaint() + + def closeEvent(self, *args): + self.parent.stop_game() + + def paintEvent(self, event): + painter = QPainter() + painter.begin(self) + + # Light shines from upper left. + if math.cos(math.radians(self.rotation)) >= 0: + lightBorderColor = self.borderColor.lighter() + darkBorderColor = self.borderColor.darker() + else: + lightBorderColor = self.borderColor.darker() + darkBorderColor = self.borderColor.lighter() + + # Draw the background. + backgroundBrush = QBrush(Qt.red, self.backgroundPixmap) + backgroundBrush.setStyle(Qt.TexturePattern) + painter.fillRect(QRect(QPoint(0, 0), self.size()), backgroundBrush) + + # Do the rotation. + painter.save() + painter.translate(self.width() / 2, self.height() / 2) + painter.rotate(self.rotation) + + # Draw the border. + frameSize = min(self.width(), self.height()) * (1 - self.margin * 2) + borderSize = min(self.width(), self.height()) * self.padding + painter.translate(-frameSize / 2, -frameSize / 2) + painter.fillRect(QRect(0, 0, frameSize, frameSize), self.borderColor) + painter.setPen(QPen(QBrush(lightBorderColor), self.shadowWidth)) + painter.drawLine(0, 0, 0, frameSize) + painter.drawLine(0, 0, frameSize, 0) + painter.setPen(QPen(QBrush(darkBorderColor), self.shadowWidth)) + painter.drawLine(frameSize, 0, frameSize, frameSize) + painter.drawLine(0, frameSize, frameSize, frameSize) + + # Draw the squares. + painter.translate(borderSize, borderSize) + squareSize = (frameSize - 2 * borderSize) / 8.0 + for x in range(0, 8): + for y in range(0, 8): + rect = QRect(x * squareSize, y * squareSize, squareSize, squareSize) + if (x - y) % 2 == 0: + painter.fillRect(rect, QBrush(self.lightSquareColor)) + else: + painter.fillRect(rect, QBrush(self.darkSquareColor)) + + # Draw the inset. + painter.setPen(QPen(QBrush(darkBorderColor), self.shadowWidth)) + painter.drawLine(0, 0, 0, squareSize * 8) + painter.drawLine(0, 0, squareSize * 8, 0) + painter.setPen(QPen(QBrush(lightBorderColor), self.shadowWidth)) + painter.drawLine(squareSize * 8, 0, squareSize * 8, squareSize * 8) + painter.drawLine(0, squareSize * 8, squareSize * 8, squareSize * 8) + + # Display coordinates. + if self.showCoordinates: + painter.setPen(QPen(QBrush(self.borderColor.lighter()), self.shadowWidth)) + coordinateSize = min(borderSize, squareSize) + font = QFont() + font.setPixelSize(coordinateSize * 0.6) + painter.setFont(font) + for i, rank in enumerate(["8", "7", "6", "5", "4", "3", "2", "1"]): + pos = QRect(-borderSize, squareSize * i, borderSize, squareSize).center() + painter.save() + painter.translate(pos.x(), pos.y()) + painter.rotate(-self.rotation) + painter.drawText(QRect(-coordinateSize / 2, -coordinateSize / 2, coordinateSize, coordinateSize), Qt.AlignCenter, rank) + painter.restore() + for i, file in enumerate(["a", "b", "c", "d", "e", "f", "g", "h"]): + pos = QRect(squareSize * i, squareSize * 8, squareSize, borderSize).center() + painter.save() + painter.translate(pos.x(), pos.y()) + painter.rotate(-self.rotation) + painter.drawText(QRect(-coordinateSize / 2, -coordinateSize / 2, coordinateSize, coordinateSize), Qt.AlignCenter, file) + painter.restore() + + # Draw pieces. + for x in range(0, 8): + for y in range(0, 8): + square = Square.from_x_and_y(x, 7 - y) + piece = self.position[square] + if piece and square != self.draggedSquare: + painter.save() + painter.translate((x + 0.5) * squareSize, (y + 0.5) * squareSize) + painter.rotate(-self.rotation) + self.pieceRenderers[piece].render(painter, QRectF(-squareSize / 2, -squareSize / 2, squareSize, squareSize)) + painter.restore() + + # Draw a floating piece. + painter.restore() + if self.draggedSquare: + piece = self.position[self.draggedSquare] + if piece: + painter.save() + painter.translate(self.dragPosition.x(), self.dragPosition.y()) + painter.rotate(-self.rotation) + self.pieceRenderers[piece].render(painter, QRect(-squareSize / 2, -squareSize / 2, squareSize, squareSize)) + painter.restore() + + painter.end() + + def squareAt(self, point): + # Undo the rotation. + transform = QTransform() + transform.translate(self.width() / 2, self.height() / 2) + transform.rotate(self.rotation) + logicalPoint = transform.inverted()[0].map(point) + + frameSize = min(self.width(), self.height()) * (1 - self.margin * 2) + borderSize = min(self.width(), self.height()) * self.padding + squareSize = (frameSize - 2 * borderSize) / 8.0 + x = int(logicalPoint.x() / squareSize + 4) + y = 7 - int(logicalPoint.y() / squareSize + 4) + try: + return Square.from_x_and_y(x, y) + except IndexError: + return None + + def canDragSquare(self, square): + if (self.ply % 2 == 0 and self.parent.white) or (self.ply % 2 == 1 and not self.parent.white): + return False + for move in self.position.get_legal_moves(): + if move.source == square: + return True + return False + + def onSquareClicked(self, square): + pass + + def moveFromDragDrop(self, source, target): + for move in self.position.get_legal_moves(): + if move.source == source and move.target == target: + if move.promotion: + dialog = PromotionDialog(self.position[move.source].color, self) + if dialog.exec_(): + return Move(source, target, dialog.selectedType()) + else: + return move + return move + + +class PromotionDialog(QDialog): + + def __init__(self, color, parent=None): + super(PromotionDialog, self).__init__(parent) + + self.promotionTypes = ["q", "n", "r", "b"] + + grid = QGridLayout() + hbox = QHBoxLayout() + grid.addLayout(hbox, 0, 0) + + # Add the piece buttons. + self.buttonGroup = QButtonGroup(self) + for i, promotionType in enumerate(self.promotionTypes): + # Create an icon for the piece. + piece = Piece.from_color_and_type(color, promotionType) + renderer = QSvgRenderer(plugin_super_class.path_to_data('chess') + "classic-pieces/%s-%s.svg" % (piece.full_color, piece.full_type)) + pixmap = QPixmap(32, 32) + pixmap.fill(Qt.transparent) + painter = QPainter() + painter.begin(pixmap) + renderer.render(painter, QRect(0, 0, 32, 32)) + painter.end() + + # Add the button. + button = QPushButton(QIcon(pixmap), '', self) + button.setCheckable(True) + self.buttonGroup.addButton(button, i) + hbox.addWidget(button) + + self.buttonGroup.button(0).setChecked(True) + + # Add the ok and cancel buttons. + buttons = QDialogButtonBox(QDialogButtonBox.Cancel | QDialogButtonBox.Ok) + buttons.rejected.connect(self.reject) + buttons.accepted.connect(self.accept) + grid.addWidget(buttons, 1, 0) + + self.setLayout(grid) + self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) + + def selectedType(self): + return self.promotionTypes[self.buttonGroup.checkedId()] + + +class Chess(plugin_super_class.PluginSuperClass): + + def __init__(self, *args): + super(Chess, self).__init__('Chess', 'chess', *args) + self.game = -1 + self.board = None + self.white = True + self.pre = None + self.last_move = None + self.is_my_move = False + self._app = args[0] + self._profile=self._app._ms._profile + self._window = None + + def get_description(self): + return QApplication.translate("Chess", 'Plugin which allows you to play chess with your friends.') + + def get_window(self): + inst = self + if not self.board: + self.board = Board(self) + if not hasattr(self, '_window') or not self._window: + self._window = self.board + return self.board + + def lossless_packet(self, data, friend_number): + if data == 'new': + self.pre = None + friend = self._profile.get_friend_by_number(friend_number) + reply = QMessageBox.question(None, + 'New chess game', + 'Friend {} wants to play chess game against you. Start?'.format(friend.name), + QMessageBox.Yes, + QMessageBox.No) + if reply != QMessageBox.Yes: + self.send_lossless('no', friend_number) + else: + self.send_lossless('yes', friend_number) + self.board = Board(self) + self.board.show() + self.game = friend_number + self.white = False + self.is_my_move = False + elif data == 'yes' and friend_number == self.game: + self.board = Board(self) + self.board.show() + self.board.update_title(True) + self.is_my_move = True + self.last_move = None + elif data == 'no': + self.game = -1 + elif data != self.pre: # move + self.pre = data + self.is_my_move = True + self.last_move = None + a = Square.from_x_and_y(ord(data[0]) - ord('a'), ord(data[1]) - ord('1')) + b = Square.from_x_and_y(ord(data[2]) - ord('a'), ord(data[3]) - ord('1')) + self.board.position.make_move(Move(a, b, data[4] if len(data) == 5 else None)) + self.board.repaint() + self.board.update_title(True) + self.board.ply += 1 + + def start_game(self, num): + self.white = True + self.send_lossless('new', num) + self.game = num + + def resend_move(self): + if self.is_my_move or self.last_move is None: + return + self.send_lossless(str(self.last_move), self.game) + QTimer.singleShot(1000, self.resend_move) + + def stop_game(self): + self.last_move = None + + def move(self, move): + self.is_my_move = False + self.last_move = move + self.send_lossless(str(move), self.game) + self.board.update_title() + QTimer.singleShot(1000, self.resend_move) + + def get_menu(self, menu, num): + act = QAction(QApplication.translate("Chess", "Start chess game"), menu) + act.triggered.connect(lambda: self.start_game(num)) + return [act] diff --git a/toxygen/plugins/en_GB.ts b/toxygen/plugins/en_GB.ts new file mode 100644 index 0000000..b7be07c --- /dev/null +++ b/toxygen/plugins/en_GB.ts @@ -0,0 +1,31 @@ + + + + BirthDay + + + Birthday! + + + + + Send my birthday date to contacts + + + + + Birthday + + + + + Date in format dd.mm.yyyy + + + + + Save date + + + + diff --git a/toxygen/plugins/en_US.ts b/toxygen/plugins/en_US.ts new file mode 100644 index 0000000..b7be07c --- /dev/null +++ b/toxygen/plugins/en_US.ts @@ -0,0 +1,31 @@ + + + + BirthDay + + + Birthday! + + + + + Send my birthday date to contacts + + + + + Birthday + + + + + Date in format dd.mm.yyyy + + + + + Save date + + + + diff --git a/toxygen/plugins/garland.py b/toxygen/plugins/garland.py new file mode 100644 index 0000000..c37f81e --- /dev/null +++ b/toxygen/plugins/garland.py @@ -0,0 +1,75 @@ +import plugin_super_class +import threading +import time +from PyQt5 import QtCore + + +class InvokeEvent(QtCore.QEvent): + EVENT_TYPE = QtCore.QEvent.Type(QtCore.QEvent.registerEventType()) + + def __init__(self, fn, *args, **kwargs): + QtCore.QEvent.__init__(self, InvokeEvent.EVENT_TYPE) + self.fn = fn + self.args = args + self.kwargs = kwargs + + +class Invoker(QtCore.QObject): + + def event(self, event): + event.fn(*event.args, **event.kwargs) + return True + +_invoker = Invoker() + + +def invoke_in_main_thread(fn, *args, **kwargs): + QtCore.QCoreApplication.postEvent(_invoker, InvokeEvent(fn, *args, **kwargs)) + + +class Garland(plugin_super_class.PluginSuperClass): + + def __init__(self, *args): + super(Garland, self).__init__('Garland', 'grlnd', *args) + self._thread = None + self._exec = None + self._time = 3 + self._app = args[0] + self._profile=self._app._ms._profile + self._window = None + + def get_description(self): + return QApplication.translate("Garland", "Changes your status like it's garland.") + + def close(self): + self.stop() + + def stop(self): + self._exec = False + self._thread.join() + + def start(self): + self._exec = True + self._thread = threading.Thread(target=self.change_status) + self._thread.start() + + def command(self, command): + if command.startswith('time'): + self._time = max(int(command.split(' ')[1]), 300) / 1000 + else: + super().command(command) + + def update(self): + if hasattr(self, '_profile'): + if not hasattr(self._profile, 'status') or not self._profile.status: + retval = 0 + else: + retval = (self._profile.status + 1) % 3 + self._profile.set_status(retval) + + def change_status(self): + time.sleep(5) + while self._exec: + invoke_in_main_thread(self.update) + time.sleep(self._time) + diff --git a/toxygen/plugins/mrq.py b/toxygen/plugins/mrq.py new file mode 100644 index 0000000..f91e2cb --- /dev/null +++ b/toxygen/plugins/mrq.py @@ -0,0 +1,86 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +import plugin_super_class +import threading +import time +from PyQt5 import QtCore + + +class InvokeEvent(QtCore.QEvent): + EVENT_TYPE = QtCore.QEvent.Type(QtCore.QEvent.registerEventType()) + + def __init__(self, fn, *args, **kwargs): + QtCore.QEvent.__init__(self, InvokeEvent.EVENT_TYPE) + self.fn = fn + self.args = args + self.kwargs = kwargs + + +class Invoker(QtCore.QObject): + + def event(self, event): + event.fn(*event.args, **event.kwargs) + return True + +_invoker = Invoker() + +def invoke_in_main_thread(fn, *args, **kwargs): + QtCore.QCoreApplication.postEvent(_invoker, InvokeEvent(fn, *args, **kwargs)) + + +class MarqueeStatus(plugin_super_class.PluginSuperClass): + + def __init__(self, *args): + super(MarqueeStatus, self).__init__('MarqueeStatus', 'mrq', *args) + self._thread = None + self._exec = None + self.active = False + self.left = True + self._app = args[0] + self._profile=self._app._ms._profile + self._window = None + + def get_description(self): + return QApplication.translate("MarqueeStatus", 'Create ticker from your status message.') + + def close(self): + self.stop() + + def stop(self): + self._exec = False + if self.active: + self._thread.join() + + def start(self): + self._exec = True + self._thread = threading.Thread(target=self.change_status) + self._thread.start() + + def command(self, command): + if command == 'rev': + self.left = not self.left + else: + super(MarqueeStatus, self).command(command) + + def set_status_message(self): + message = str(self._profile.status_message) + if self.left: + self._profile.set_status_message(bytes(message[1:] + message[0], 'utf-8')) + else: + self._profile.set_status_message(bytes(message[-1] + message[:-1], 'utf-8')) + + def init_status(self): + self._profile.status_message = bytes(self._profile.status_message.strip() + ' ', 'utf-8') + + def change_status(self): + self.active = True + if hasattr(self, '_profile'): + tmp = self._profile.status_message + time.sleep(10) + invoke_in_main_thread(self.init_status) + while self._exec: + time.sleep(1) + if self._profile.status is not None: + invoke_in_main_thread(self.set_status_message) + invoke_in_main_thread(self._profile.set_status_message, bytes(tmp, 'utf-8')) + self.active = False + diff --git a/toxygen/plugins/plugin_super_class.py b/toxygen/plugins/plugin_super_class.py index a3ae5a5..af0b792 100644 --- a/toxygen/plugins/plugin_super_class.py +++ b/toxygen/plugins/plugin_super_class.py @@ -53,9 +53,7 @@ class PluginSuperClass(tox_save.ToxSave): self._short_name = short_name[:MAX_SHORT_NAME_LENGTH] self._translator = None # translator for plugin's GUI - # ----------------------------------------------------------------------------------------------------------------- # Get methods - # ----------------------------------------------------------------------------------------------------------------- def get_name(self): """ @@ -98,9 +96,7 @@ class PluginSuperClass(tox_save.ToxSave): """ return None - # ----------------------------------------------------------------------------------------------------------------- # Plugin was stopped, started or new command received - # ----------------------------------------------------------------------------------------------------------------- def start(self): """ @@ -130,9 +126,7 @@ class PluginSuperClass(tox_save.ToxSave): title = util_ui.tr('List of commands for plugin {}').format(self._name) util_ui.message_box(text, title) - # ----------------------------------------------------------------------------------------------------------------- # Translations support - # ----------------------------------------------------------------------------------------------------------------- def load_translator(self): """ @@ -149,9 +143,7 @@ class PluginSuperClass(tox_save.ToxSave): self._translator.load(path_to_data(self._short_name) + lang_path) app.installTranslator(self._translator) - # ----------------------------------------------------------------------------------------------------------------- # Settings loading and saving - # ----------------------------------------------------------------------------------------------------------------- def load_settings(self): """ @@ -170,9 +162,7 @@ class PluginSuperClass(tox_save.ToxSave): with open(path_to_data(self._short_name) + 'settings.json', 'wb') as fl: fl.write(bytes(data, 'utf-8')) - # ----------------------------------------------------------------------------------------------------------------- # Callbacks - # ----------------------------------------------------------------------------------------------------------------- def lossless_packet(self, data, friend_number): """ @@ -196,9 +186,7 @@ class PluginSuperClass(tox_save.ToxSave): """ pass - # ----------------------------------------------------------------------------------------------------------------- # Custom packets sending - # ----------------------------------------------------------------------------------------------------------------- def send_lossless(self, data, friend_number): """ diff --git a/toxygen/plugins/ru_RU.qm b/toxygen/plugins/ru_RU.qm new file mode 100644 index 0000000000000000000000000000000000000000..6ba937ce2f72e4a01cbb56d75e064410107c4d1d GIT binary patch literal 525 zcmcE7ks@*G{hX<16=n7(EZlq7iGhJZgJIS?Ng&O@*n0XTkZR=kS@9Xj)8<_Fa3hd! z&r|kM6-d9#1PbnC2C}4BL|9B&Y*~C56j&Ts>{!fLOu&2#7Jm+)Gy@wDb2w!dm1LwO zRz7HbxNR5O64*#K=Y1-j7=$VLlHpokSv z))wSbAPYp=z-?6wPR&bE$gNaJf(A6$1qvnk3d#9-C5g!;#i*|70ERrTAmd+6?*Af8 JEPr*lgaIdwV+8;J literal 0 HcmV?d00001 diff --git a/toxygen/plugins/ru_RU.ts b/toxygen/plugins/ru_RU.ts new file mode 100644 index 0000000..d5b0374 --- /dev/null +++ b/toxygen/plugins/ru_RU.ts @@ -0,0 +1,32 @@ + + + + + BirthDay + + + Birthday! + День рождения! + + + + Send my birthday date to contacts + Отправлять дату моего рождения контактам + + + + Birthday + День рождения + + + + Date in format dd.mm.yyyy + Дата в формате дд.мм.гггг + + + + Save date + Сохранить дату + + + diff --git a/toxygen/plugins/srch.pro b/toxygen/plugins/srch.pro new file mode 100644 index 0000000..d071285 --- /dev/null +++ b/toxygen/plugins/srch.pro @@ -0,0 +1,2 @@ +SOURCES = srch.py +TRANSLATIONS = srch/en_GB.ts srch/en_US.ts srch/ru_RU.ts diff --git a/toxygen/plugins/srch.py b/toxygen/plugins/srch.py new file mode 100644 index 0000000..08d5de8 --- /dev/null +++ b/toxygen/plugins/srch.py @@ -0,0 +1,54 @@ +import plugin_super_class +from PyQt5 import QtGui, QtCore, QtWidgets + + +class SearchPlugin(plugin_super_class.PluginSuperClass): + + def __init__(self, *args): + super(SearchPlugin, self).__init__('SearchPlugin', 'srch', *args) + + def get_description(self): + return QApplication.translate("SearchPlugin", 'Plugin search with search engines.') + + def get_message_menu(self, menu, text): + google = QtWidgets.QAction( + QtWidgets.QApplication.translate("srch", "Find in Google"), + menu) + google.triggered.connect(lambda: self.google(text)) + + duck = QtWidgets.QAction( + QtWidgets.QApplication.translate("srch", "Find in DuckDuckGo"), + menu) + duck.triggered.connect(lambda: self.duck(text)) + + yandex = QtWidgets.QAction( + QtWidgets.QApplication.translate("srch", "Find in Yandex"), + menu) + yandex.triggered.connect(lambda: self.yandex(text)) + + bing = QtWidgets.QAction( + QtWidgets.QApplication.translate("srch", "Find in Bing"), + menu) + bing.triggered.connect(lambda: self.bing(text)) + + return [duck, google, yandex, bing] + + def google(self, text): + url = QtCore.QUrl('https://www.google.com/search?q=' + text) + self.open_url(url) + + def duck(self, text): + url = QtCore.QUrl('https://duckduckgo.com/?q=' + text) + self.open_url(url) + + def yandex(self, text): + url = QtCore.QUrl('https://yandex.com/search/?text=' + text) + self.open_url(url) + + def bing(self, text): + url = QtCore.QUrl('https://www.bing.com/search?q=' + text) + self.open_url(url) + + def open_url(self, url): + QtGui.QDesktopServices.openUrl(url) + diff --git a/toxygen/plugins/toxid.pro b/toxygen/plugins/toxid.pro new file mode 100644 index 0000000..3b1cc64 --- /dev/null +++ b/toxygen/plugins/toxid.pro @@ -0,0 +1,2 @@ +SOURCES = toxid.py +TRANSLATIONS = toxid/en_GB.ts toxid/en_US.ts toxid/ru_RU.ts diff --git a/toxygen/plugins/toxid.py b/toxygen/plugins/toxid.py new file mode 100644 index 0000000..e403e5d --- /dev/null +++ b/toxygen/plugins/toxid.py @@ -0,0 +1,136 @@ +import plugin_super_class +from PyQt5 import QtCore, QtWidgets +import json + + +class CopyableToxId(plugin_super_class.PluginSuperClass): + + def __init__(self, *args): + super(CopyableToxId, self).__init__('CopyableToxId', 'toxid', *args) + self._data = json.loads(self.load_settings()) + self._copy = False + self._curr = -1 + self._timer = QtCore.QTimer() + self._timer.timeout.connect(lambda: self.timer()) + self.load_translator() + self._app = args[0] + self._profile=self._app._ms._profile + self._window = None + + def get_description(self): + return QtWidgets.QApplication.translate("TOXID", 'Plugin which allows you to copy TOX ID of your friends easily.') + + def get_window(self): + inst = self + + class Window(QtWidgets.QWidget): + + def __init__(self): + super(Window, self).__init__() + self.setGeometry(QtCore.QRect(450, 300, 350, 100)) + self.send = QtWidgets.QCheckBox(self) + self.send.setGeometry(QtCore.QRect(20, 10, 310, 25)) + self.send.setText(QtWidgets.QApplication.translate("TOXID", "Send my TOX ID to contacts")) + self.setWindowTitle(QtWidgets.QApplication.translate("TOXID", "CopyableToxID")) + self.send.clicked.connect(self.update) + self.send.setChecked(inst._data['send_id']) + self.help = QtWidgets.QPushButton(self) + self.help.setGeometry(QtCore.QRect(20, 40, 200, 25)) + self.help.setText(QtWidgets.QApplication.translate("TOXID", "List of commands")) + self.help.clicked.connect(lambda: inst.command('help')) + + def update(self): + inst._data['send_id'] = self.send.isChecked() + inst.save_settings(json.dumps(inst._data)) + + if not hasattr(self, '_window') or not self._window: + self._window = Window() + return self._window + + def lossless_packet(self, data, friend_number): + if len(data): + self._data['id'] = list(filter(lambda x: not x.startswith(data[:64]), self._data['id'])) + self._data['id'].append(data) + if self._copy: + self._timer.stop() + self._copy = False + clipboard = QtWidgets.QApplication.clipboard() + clipboard.setText(data) + self.save_settings(json.dumps(self._data)) + elif self._data['send_id']: + self.send_lossless(self._tox.self_get_address(), friend_number) + + def error(self): + msgbox = QtWidgets.QMessageBox() + title = QtWidgets.QApplication.translate("TOXID", "Error") + msgbox.setWindowTitle(title.format(self._name)) + text = QtWidgets.QApplication.translate("TOXID", "Tox ID cannot be copied") + msgbox.setText(text) + msgbox.exec_() + + def timer(self): + self._copy = False + if self._curr + 1: + public_key = self._tox.friend_get_public_key(self._curr) + self._curr = -1 + arr = list(filter(lambda x: x.startswith(public_key), self._data['id'])) + if len(arr): + clipboard = QtWidgets.QApplication.clipboard() + clipboard.setText(arr[0]) + else: + self.error() + else: + self.error() + self._timer.stop() + + def friend_connected(self, friend_number): + self.send_lossless('', friend_number) + + def command(self, text): + if text == 'copy': + num = self._profile.get_active_number() + if num == -1: + return + elif text.startswith('copy '): + num = int(text[5:]) + if num < 0: + return + elif text == 'enable': + self._copy = True + return + elif text == 'disable': + self._copy = False + return + elif text == 'help': + msgbox = QtWidgets.QMessageBox() + title = QtWidgets.QApplication.translate("TOXID", "List of commands for plugin CopyableToxID") + msgbox.setWindowTitle(title) + text = QtWidgets.QApplication.translate("TOXID", """Commands: +copy: copy TOX ID of current friend +copy : copy TOX ID of friend with specified number +enable: allow send your TOX ID to friends +disable: disallow send your TOX ID to friends +help: show this help""") + msgbox.setText(text) + msgbox.exec_() + return + else: + return + public_key = self._tox.friend_get_public_key(num) + arr = list(filter(lambda x: x.startswith(public_key), self._data['id'])) + if self._profile.get_friend_by_number(num).status is None and len(arr): + clipboard = QtWidgets.QApplication.clipboard() + clipboard.setText(arr[0]) + elif self._profile.get_friend_by_number(num).status is not None: + self._copy = True + self._curr = num + self.send_lossless('', num) + self._timer.start(2000) + else: + self.error() + + def get_menu(self, menu, num): + act = QtWidgets.QAction(QtWidgets.QApplication.translate("TOXID", "Copy TOX ID"), menu) + friend = self._profile.get_friend(num) + act.connect(act, QtCore.SIGNAL("triggered()"), lambda: self.command('copy ' + str(friend.number))) + return [act] diff --git a/toxygen/tests/README.md b/toxygen/tests/README.md new file mode 100644 index 0000000..0ea7aa7 --- /dev/null +++ b/toxygen/tests/README.md @@ -0,0 +1,87 @@ +# toxygen_wrapper + +[ctypes](https://docs.python.org/3/library/ctypes.html) +wrapping of [Tox](https://tox.chat/) +[```libtoxcore```](https://github.com/TokTok/c-toxcore) into Python. +Taken from the ```wrapper``` directory of the now abandoned + ```next_gen``` branch +by Ingvar. + +The basics of NGC groups are supported, as well as AV and toxencryptsave. +There is no coverage of conferences as they are not used in ```toxygen``` +and the list of still unwrapped calls as of Sept. 2022 can be found in +```tox.c-toxcore.missing```. The code still needs double-checking +that every call in ```tox.py``` has the right signature, but it runs +```toxygen``` with no apparent issues. + +It has been tested with UDP and TCP proxy (Tor). It has ***not*** been +tested on Windows, and there may be some minor breakage, which should be +easy to fix. There is a good coverage integration testsuite in ```wrapper_tests```. +Change to that directory and run ```tests_wrapper.py --help```; the test +suite gives a good set of examples of usage. + +## Install + +Put the parent of the wrapper directory on your PYTHONPATH and +touch a file called `__init__.py` in its parent directory. + +Then you need a ```libs``` directory beside the `wrapper` directory +and you need to link your ```libtoxcore.so``` and ```libtoxav.so``` +and ```libtoxencryptsave.so``` into it. Link all 3 filenames +to ```libtoxcore.so``` if you have only ```libtoxcore.so``` +(which is usually the case if you built ```c-toxcore``` with ```cmake``` +rather than ```autogen/configure```). If you want to be different, +the environment variable TOXCORE_LIBS overrides the location of ```libs```. + +As is, the code in ```tox.py``` is very verbose. Edit the file to change +``` +def LOG_ERROR(a): print('EROR> '+a) +def LOG_WARN(a): print('WARN> '+a) +def LOG_INFO(a): print('INFO> '+a) +def LOG_DEBUG(a): print('DBUG> '+a) +def LOG_TRACE(a): pass # print('TRAC> '+a) +``` +to all ```pass #``` or use ```logging.logger``` to suite your tastes. +```logging.logger``` can be dangerous in callbacks in ```Qt``` applications, +so we use simple print statements as default. The same applies to +```wrapper/tests_wrapper.py```. + +## Prerequisites + +No prerequisites in Python3. + +## Other wrappers + +There are a number of other wrappings into Python of Tox core. +This one uses [ctypes](https://docs.python.org/3/library/ctypes.html) +which has its merits - there is no need to recompile anything as with +Cython - change the Python file and it's done. And you can follow things +in a Python debugger, or with the utterly stupendous Python feature of +```gdb``` (```gdb -ex r --args /usr/bin/python3.9 ```). + +CTYPES code can be brittle, segfaulting if you've got things wrong, +but if your wrapping is right, it is very efficient and easy to work on. +The [faulthandler](https://docs.python.org/3/library/faulthandler.html) +module can be helpful in debugging crashes +(e.g. from segmentation faults produced by erroneous C library wrapping). + +Others include: + +* Cython bindings. + Incomplete and not really actively supported. Maybe it will get + worked on in the future, but TokTok seems to be working on + java, rust, scalla, go, etc. bindings instead. + No support for NGC groups or toxencryptsave. + +* + forked from https://github.com/aitjcize/PyTox + by Wei-Ning Huang . + Hardcore C wrapping which is not easy to keep up to date. + No support for NGC or toxencryptsave. Abandonned. + This was the basis for the TokTok/py-toxcore-c code until recently. + +To our point of view, the ability of CTYPEs to follow code in the +debugger is a crucial advantage. + +Work on this project is suspended until the +[MultiDevice](https://git.plastiras.org/emdee/tox_profile/wiki/MultiDevice-Announcements-POC) problem is solved. Fork me! diff --git a/toxygen/tests/__init__.py b/toxygen/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/tests/conference_tests.py b/toxygen/tests/conference_tests.py new file mode 100644 index 0000000..8da5912 --- /dev/null +++ b/toxygen/tests/conference_tests.py @@ -0,0 +1,151 @@ +if False: + @unittest.skip # to yet + def test_conference(self): + """ + t:group_new + t:conference_delete + t:conference_get_chatlist_size + t:conference_get_chatlist + t:conference_send_message + """ + bob_addr = self.bob.self_get_address() + alice_addr = self.alice.self_get_address() + + self.abid = self.alice.friend_by_public_key(bob_addr) + self.baid = self.bob.friend_by_public_key(alice_addr) + + assert self.bob_just_add_alice_as_friend() + + #: Test group add + privacy_state = enums.TOX_GROUP_PRIVACY_STATE['PUBLIC'] + group_name = 'test_group' + nick = 'test_nick' + status = None # dunno + self.group_id = self.bob.group_new(privacy_state, group_name, nick, status) + # :return group number on success, UINT32_MAX on failure. + assert self.group_id >= 0 + + self.loop(50) + + BID = self.abid + + def alices_on_conference_invite(self, fid, type_, data): + assert fid == BID + assert type_ == 0 + gn = self.conference_join(fid, data) + assert type_ == self.conference_get_type(gn) + self.gi = True + + def alices_on_conference_peer_list_changed(self, gid): + logging.debug("alices_on_conference_peer_list_changed") + assert gid == self.group_id + self.gn = True + + try: + AliceTox.on_conference_invite = alices_on_conference_invite + AliceTox.on_conference_peer_list_changed = alices_on_conference_peer_list_changed + + self.alice.gi = False + self.alice.gn = False + + self.wait_ensure_exec(self.bob.conference_invite, (self.aid, self.group_id)) + + assert self.wait_callback_trues(self.alice, ['gi', 'gn']) + except AssertionError as e: + raise + finally: + AliceTox.on_conference_invite = Tox.on_conference_invite + AliceTox.on_conference_peer_list_change = Tox.on_conference_peer_list_changed + + #: Test group number of peers + self.loop(50) + assert self.bob.conference_peer_count(self.group_id) == 2 + + #: Test group peername + self.alice.self_set_name('Alice') + self.bob.self_set_name('Bob') + + def alices_on_conference_peer_list_changed(self, gid): + logging.debug("alices_on_conference_peer_list_changed") + self.gn = True + try: + AliceTox.on_conference_peer_list_changed = alices_on_conference_peer_list_changed + self.alice.gn = False + + assert self.wait_callback_true(self.alice, 'gn') + except AssertionError as e: + raise + finally: + AliceTox.on_conference_peer_list_changed = Tox.on_conference_peer_list_changed + + peernames = [self.bob.conference_peer_get_name(self.group_id, i) for i in + range(self.bob.conference_peer_count(self.group_id))] + assert 'Alice' in peernames + assert 'Bob' in peernames + + #: Test title change + self.bob.conference_set_title(self.group_id, 'My special title') + assert self.bob.conference_get_title(self.group_id) == 'My special title' + + #: Test group message + AID = self.aid + BID = self.bid + MSG = 'Group message test' + + def alices_on_conference_message(self, gid, fgid, msg_type, message): + logging.debug("alices_on_conference_message" +repr(message)) + if fgid == AID: + assert gid == self.group_id + assert str(message, 'UTF-8') == MSG + self.alice.gm = True + + try: + AliceTox.on_conference_message = alices_on_conference_message + self.alice.gm = False + + self.wait_ensure_exec(self.bob.conference_send_message, ( + self.group_id, TOX_MESSAGE_TYPE['NORMAL'], MSG)) + assert self.wait_callback_true(self.alice, 'gm') + except AssertionError as e: + raise + finally: + AliceTox.on_conference_message = Tox.on_conference_message + + #: Test group action + AID = self.aid + BID = self.bid + MSG = 'Group action test' + + def on_conference_action(self, gid, fgid, msg_type, action): + if fgid == AID: + assert gid == self.group_id + assert msg_type == TOX_MESSAGE_TYPE['ACTION'] + assert str(action, 'UTF-8') == MSG + self.ga = True + + try: + AliceTox.on_conference_message = on_conference_action + self.alice.ga = False + + self.wait_ensure_exec(self.bob.conference_send_message, + (self.group_id, TOX_MESSAGE_TYPE['ACTION'], MSG)) + + assert self.wait_callback_true(self.alice, 'ga') + + #: Test chatlist + assert len(self.bob.conference_get_chatlist()) == self.bob.conference_get_chatlist_size(), \ + print(len(self.bob.conference_get_chatlist()), '!=', self.bob.conference_get_chatlist_size()) + assert len(self.alice.conference_get_chatlist()) == self.bob.conference_get_chatlist_size(), \ + print(len(self.alice.conference_get_chatlist()), '!=', self.bob.conference_get_chatlist_size()) + assert self.bob.conference_get_chatlist_size() == 1, \ + self.bob.conference_get_chatlist_size() + self.bob.conference_delete(self.group_id) + assert self.bob.conference_get_chatlist_size() == 0, \ + self.bob.conference_get_chatlist_size() + + except AssertionError as e: + raise + finally: + AliceTox.on_conference_message = Tox.on_conference_message + + diff --git a/toxygen/tests/logging_toxygen_echo.py b/toxygen/tests/logging_toxygen_echo.py new file mode 100644 index 0000000..c475424 --- /dev/null +++ b/toxygen/tests/logging_toxygen_echo.py @@ -0,0 +1,439 @@ +#!/var/local/bin/python3.bash +# +""" echo.py features + - accept friend request + - echo back friend message + - accept and answer friend call request + - send back friend audio/video data + - send back files friend sent +""" + +from __future__ import print_function + +import sys +import os +import traceback +import random +from ctypes import * +import argparse + +import time +from os.path import exists + +# LOG=util.log +global LOG +import logging +# log = lambda x: LOG.info(x) +LOG = logging.getLogger('app') +def LOG_error(a): print('EROR_ '+a) +def LOG_warn(a): print('WARN_ '+a) +def LOG_info(a): print('INFO_ '+a) +def LOG_debug(a): print('DBUG_ '+a) +def LOG_trace(a): pass # print('TRAC_ '+a) + +from middleware.tox_factory import tox_factory +import wrapper +import wrapper.toxcore_enums_and_consts as enums +from wrapper.toxcore_enums_and_consts import TOX_CONNECTION, TOX_USER_STATUS, \ + TOX_MESSAGE_TYPE, TOX_PUBLIC_KEY_SIZE, TOX_FILE_CONTROL +import user_data +from wrapper.libtox import LibToxCore +import wrapper_tests.support_testing as ts +from wrapper_tests.support_testing import oMainArgparser +from wrapper_tests.support_testing import logging_toxygen_echo + +def sleep(fSec): + if 'QtCore' in globals(): + if fSec > .000001: globals['QtCore'].QThread.msleep(fSec) + globals['QtCore'].QCoreApplication.processEvents() + else: + time.sleep(fSec) + +try: + import coloredlogs + if 'COLOREDLOGS_LEVEL_STYLES' not in os.environ: + os.environ['COLOREDLOGS_LEVEL_STYLES'] = 'spam=22;debug=28;verbose=34;notice=220;warning=202;success=118,bold;error=124;critical=background=red' +except ImportError as e: + # logging.log(logging.DEBUG, f"coloredlogs not available: {e}") + coloredlogs = None + +import wrapper_tests.support_testing as ts +if 'USER' in os.environ: + sDATA_FILE = '/tmp/logging_toxygen_' +os.environ['USER'] +'.tox' +elif 'USERNAME' in os.environ: + sDATA_FILE = '/tmp/logging_toxygen_' +os.environ['USERNAME'] +'.tox' +else: + sDATA_FILE = '/tmp/logging_toxygen_' +'data' +'.tox' + +bHAVE_AV = True +iDHT_TRIES = 100 +iDHT_TRY = 0 + +#?SERVER = lLOCAL[-1] + +class AV(wrapper.tox.ToxAV): + def __init__(self, core): + super(AV, self).__init__(core) + self.core = self.get_tox() + + def on_call(self, fid, audio_enabled, video_enabled): + LOG.info("Incoming %s call from %d:%s ..." % ( + "video" if video_enabled else "audio", fid, + self.core.friend_get_name(fid))) + bret = self.answer(fid, 48, 64) + LOG.info(f"Answered, in call... {bret!s}") + + def on_call_state(self, fid, state): + LOG.info('call state:fn=%d, state=%d' % (fid, state)) + + def on_audio_bit_rate(self, fid, audio_bit_rate): + LOG.info('audio bit rate status: fn=%d, abr=%d' % + (fid, audio_bit_rate)) + + def on_video_bit_rate(self, fid, video_bit_rate): + LOG.info('video bit rate status: fn=%d, vbr=%d' % + (fid, video_bit_rate)) + + def on_audio_receive_frame(self, fid, pcm, sample_count, + channels, sampling_rate): + # LOG.info('audio frame: %d, %d, %d, %d' % + # (fid, sample_count, channels, sampling_rate)) + # LOG.info('pcm len:%d, %s' % (len(pcm), str(type(pcm)))) + sys.stdout.write('.') + sys.stdout.flush() + bret = self.audio_send_frame(fid, pcm, sample_count, + channels, sampling_rate) + if bret is False: + LOG.error('on_audio_receive_frame error.') + + def on_video_receive_frame(self, fid, width, height, frame, u, v): + LOG.info('video frame: %d, %d, %d, ' % (fid, width, height)) + sys.stdout.write('*') + sys.stdout.flush() + bret = self.video_send_frame(fid, width, height, frame, u, v) + if bret is False: + LOG.error('on_video_receive_frame error.') + + def witerate(self): + self.iterate() + + +def save_to_file(tox, fname): + data = tox.get_savedata() + with open(fname, 'wb') as f: + f.write(data) + +def load_from_file(fname): + assert os.path.exists(fname) + return open(fname, 'rb').read() + +class EchoBot(): + def __init__(self, oTox): + self._tox = oTox + self._tox.self_set_name("EchoBot") + LOG.info('ID: %s' % self._tox.self_get_address()) + + self.files = {} + self.av = None + self.on_connection_status = None + + def start(self): + self.connect() + if bHAVE_AV: + # RuntimeError: Attempted to create a second session for the same Tox instance. + + self.av = True # AV(self._tox_pointer) + def bobs_on_friend_request(iTox, + public_key, + message_data, + message_data_size, + *largs): + key = ''.join(chr(x) for x in public_key[:TOX_PUBLIC_KEY_SIZE]) + sPk = wrapper.tox.bin_to_string(key, TOX_PUBLIC_KEY_SIZE) + sMd = str(message_data, 'UTF-8') + LOG.debug('on_friend_request ' +sPk +' ' +sMd) + self.on_friend_request(sPk, sMd) + LOG.info('setting bobs_on_friend_request') + self._tox.callback_friend_request(bobs_on_friend_request) + + def bobs_on_friend_message(iTox, + iFriendNum, + iMessageType, + message_data, + message_data_size, + *largs): + sMd = str(message_data, 'UTF-8') + LOG_debug(f"on_friend_message {iFriendNum}" +' ' +sMd) + self.on_friend_message(iFriendNum, iMessageType, sMd) + LOG.info('setting bobs_on_friend_message') + self._tox.callback_friend_message(bobs_on_friend_message) + + def bobs_on_file_chunk_request(iTox, fid, filenumber, position, length, *largs): + if length == 0: + return + + data = self.files[(fid, filenumber)]['f'][position:(position + length)] + self._tox.file_send_chunk(fid, filenumber, position, data) + self._tox.callback_file_chunk_request(bobs_on_file_chunk_request) + + def bobs_on_file_recv(iTox, fid, filenumber, kind, size, filename, *largs): + LOG_info(f"on_file_recv {fid!s} {filenumber!s} {kind!s} {size!s} {filename}") + if size == 0: + return + self.files[(fid, filenumber)] = { + 'f': bytes(), + 'filename': filename, + 'size': size + } + self._tox.file_control(fid, filenumber, TOX_FILE_CONTROL['RESUME']) + + + def connect(self): + if not self.on_connection_status: + def on_connection_status(iTox, iCon, *largs): + LOG_info('ON_CONNECTION_STATUS - CONNECTED ' + repr(iCon)) + self._tox.callback_self_connection_status(on_connection_status) + LOG.info('setting on_connection_status callback ') + self.on_connection_status = on_connection_status + if self._oargs.network in ['newlocal', 'local']: + LOG.info('connecting on the new network ') + sNet = 'newlocal' + elif self._oargs.network == 'new': + LOG.info('connecting on the new network ') + sNet = 'new' + else: # main old + LOG.info('connecting on the old network ') + sNet = 'old' + sFile = self._oargs.nodes_json + lNodes = generate_nodes_from_file(sFile) + lElts = lNodes + random.shuffle(lElts) + for lElt in lElts[:10]: + status = self._tox.self_get_connection_status() + try: + if self._tox.bootstrap(*lElt): + LOG.info('connected to ' + lElt[0]+' '+repr(status)) + else: + LOG.warn('failed connecting to ' + lElt[0]) + except Exception as e: + LOG.warn('error connecting to ' + lElt[0]) + + if self._oargs.proxy_type > 0: + random.shuffle(ts.lRELAYS) + for lElt in ts.lRELAYS[:10]: + status = self._tox.self_get_connection_status() + try: + if self._tox.add_tcp_relay(*lElt): + LOG.info('relayed to ' + lElt[0] +' '+repr(status)) + else: + LOG.warn('failed relay to ' + lElt[0]) + except Exception as e: + LOG.warn('error relay to ' + lElt[0]) + + def loop(self): + if not self.av: + self.start() + checked = False + save_to_file(self._tox, sDATA_FILE) + + LOG.info('Starting loop.') + while True: + + status = self._tox.self_get_connection_status() + if not checked and status: + LOG.info('Connected to DHT.') + checked = True + if not checked and not status: + global iDHT_TRY + iDHT_TRY += 10 + self.connect() + self.iterate(100) + if iDHT_TRY >= iDHT_TRIES: + raise RuntimeError("Failed to connect to the DHT.") + LOG.warn(f"NOT Connected to DHT. {iDHT_TRY}") + checked = True + if checked and not status: + LOG.info('Disconnected from DHT.') + self.connect() + checked = False + + if bHAVE_AV: + True # self.av.witerate() + self.iterate(100) + + LOG.info('Ending loop.') + + def iterate(self, n=100): + interval = self._tox.iteration_interval() + for i in range(n): + self._tox.iterate() + sleep(interval / 1000.0) + self._tox.iterate() + + def on_friend_request(self, pk, message): + LOG.debug('Friend request from %s: %s' % (pk, message)) + self._tox.friend_add_norequest(pk) + LOG.info('on_friend_request Accepted.') + save_to_file(self._tox, sDATA_FILE) + + def on_friend_message(self, friendId, type, message): + name = self._tox.friend_get_name(friendId) + LOG.debug('%s: %s' % (name, message)) + yMessage = bytes(message, 'UTF-8') + self._tox.friend_send_message(friendId, TOX_MESSAGE_TYPE['NORMAL'], yMessage) + LOG.info('EchoBot sent: %s' % message) + + def on_file_recv_chunk(self, fid, filenumber, position, data): + filename = self.files[(fid, filenumber)]['filename'] + size = self.files[(fid, filenumber)]['size'] + LOG.debug(f"on_file_recv_chunk {fid!s} {filenumber!s} {filename} {position/float(size)*100!s}") + + if data is None: + msg = "I got '{}', sending it back right away!".format(filename) + self._tox.friend_send_message(fid, TOX_MESSAGE_TYPE['NORMAL'], msg) + + self.files[(fid, 0)] = self.files[(fid, filenumber)] + + length = self.files[(fid, filenumber)]['size'] + self.file_send(fid, 0, length, filename, filename) + + del self.files[(fid, filenumber)] + return + + self.files[(fid, filenumber)]['f'] += data + +def iMain(oArgs): + global sDATA_FILE + # oTOX_OPTIONS = ToxOptions() + global oTOX_OPTIONS + oTOX_OPTIONS = oToxygenToxOptions(oArgs) + opts = oTOX_OPTIONS + if coloredlogs: + coloredlogs.install( + level=oArgs.loglevel, + logger=LOG, + # %(asctime)s,%(msecs)03d %(hostname)s [%(process)d] + fmt='%(name)s %(levelname)s %(message)s' + ) + else: + if 'logfile' in oArgs: + logging.basicConfig(filename=oArgs.logfile, + level=oArgs.loglevel, + format='%(levelname)-8s %(message)s') + else: + logging.basicConfig(level=oArgs.loglevel, + format='%(levelname)-8s %(message)s') + + iRet = 0 + if hasattr(oArgs,'profile') and oArgs.profile and os.path.isfile(oArgs.profile): + sDATA_FILE = oArgs.profile + LOG.info(f"loading from {sDATA_FILE}") + opts.savedata_data = load_from_file(sDATA_FILE) + opts.savedata_length = len(opts.savedata_data) + opts.savedata_type = enums.TOX_SAVEDATA_TYPE['TOX_SAVE'] + else: + opts.savedata_data = None + + try: + if False: + oTox = tox_factory(data=opts.savedata_data, + settings=opts, args=oArgs, app=None) + else: + oTox = wrapper.tox.Tox(opts) + t = EchoBot(oTox) + t._oargs = oArgs + t.start() + t.loop() + save_to_file(t._tox, sDATA_FILE) + except KeyboardInterrupt: + save_to_file(t._tox, sDATA_FILE) + except RuntimeError as e: + LOG.error(f"exiting with {e}") + iRet = 1 + except Exception as e: + LOG.error(f"exiting with {e}") + LOG.warn(' iMain(): ' \ + +'\n' + traceback.format_exc()) + iRet = 1 + return iRet + +def oToxygenToxOptions(oArgs, data=None): + tox_options = wrapper.tox.Tox.options_new() + + tox_options.contents.local_discovery_enabled = False + tox_options.contents.dht_announcements_enabled = False + tox_options.contents.hole_punching_enabled = False + tox_options.contents.experimental_thread_safety = False + tox_options.contents.ipv6_enabled = False + tox_options.contents.tcp_port = 3390 + + if oArgs.proxy_type > 0: + tox_options.contents.proxy_type = int(oArgs.proxy_type) + tox_options.contents.proxy_host = bytes(oArgs.proxy_host, 'UTF-8') + tox_options.contents.proxy_port = int(oArgs.proxy_port) + tox_options.contents.udp_enabled = False + LOG.debug('setting oArgs.proxy_host = ' +oArgs.proxy_host) + else: + tox_options.contents.udp_enabled = True + + if data: # load existing profile + tox_options.contents.savedata_type = enums.TOX_SAVEDATA_TYPE['TOX_SAVE'] + tox_options.contents.savedata_data = c_char_p(data) + tox_options.contents.savedata_length = len(data) + else: # create new profile + tox_options.contents.savedata_type = enums.TOX_SAVEDATA_TYPE['NONE'] + tox_options.contents.savedata_data = None + tox_options.contents.savedata_length = 0 + + if tox_options._options_pointer: + ts.vAddLoggerCallback(tox_options, ts.on_log) + else: + logging.warn("No tox_options._options_pointer " +repr(tox_options._options_pointer)) + + return tox_options + +def oArgparse(lArgv): + parser = ts.oMainArgparser() + parser.add_argument('profile', type=str, nargs='?', + default=sDATA_FILE, + help='Path to Tox profile to save') + oArgs = parser.parse_args(lArgv) + if hasattr(oArgs, 'sleep') and oArgs.sleep == 'qt': + pass # broken + else: + oArgs.sleep = 'time' + for key in ts.lBOOLEANS: + if key not in oArgs: continue + val = getattr(oArgs, key) + if val in ['False', 'false', 0]: + setattr(oArgs, key, False) + else: + setattr(oArgs, key, True) + if not os.path.exists('/proc/sys/net/ipv6') and oArgs.ipv6_enabled: + LOG.warn('setting oArgs.ipv6_enabled = False') + oArgs.ipv6_enabled = False + return oArgs + +def main(largs=None): + if largs is None: largs = [] + oArgs = oArgparse(largs) + global oTOX_OARGS + oTOX_OARGS = oArgs + print(oArgs) + + if coloredlogs: + logger = logging.getLogger() + # https://pypi.org/project/coloredlogs/ + coloredlogs.install(level=oArgs.loglevel, + logger=logger, + # %(asctime)s,%(msecs)03d %(hostname)s [%(process)d] + fmt='%(name)s %(levelname)s %(message)s' + ) + else: + logging.basicConfig(level=oArgs.loglevel) # logging.INFO + + return iMain(oArgs) + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) diff --git a/toxygen/tests/socks.py b/toxygen/tests/socks.py new file mode 100644 index 0000000..748fa8e --- /dev/null +++ b/toxygen/tests/socks.py @@ -0,0 +1,391 @@ +"""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 + +Minor modifications made by Eugene Dementiev (http://www.dementiev.eu/) + +""" + +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") + +_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. + """ + data = self.recv(count) + 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, destaddr, destport): + """__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])) + # 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 + if type(destaddr) != type(b''): # python3 + destaddr_bytes = destaddr.encode(encoding='idna') + else: + destaddr_bytes = destaddr + req = req + chr(0x03).encode() + chr(len(destaddr_bytes)).encode() + destaddr_bytes + 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 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 __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: + recv = self.recv(1) + if not recv: + raise GeneralProxyError((1, _generalerrors[1])) + resp = resp + recv + # 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 = int(self.__proxy[2]) + else: + portnum = 1080 + _orgsocket.connect(self, (self.__proxy[1], portnum)) + self.__negotiatesocks5(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 + _orgsocket.connect(self,(self.__proxy[1], portnum)) + self.__negotiatehttp(destpair[0], destpair[1]) + elif self.__proxy[0] == None: + _orgsocket.connect(self, (destpair[0], destpair[1])) + else: + raise GeneralProxyError((4, _generalerrors[4])) diff --git a/toxygen/tests/support_testing.py b/toxygen/tests/support_testing.py new file mode 100644 index 0000000..8e1ea48 --- /dev/null +++ b/toxygen/tests/support_testing.py @@ -0,0 +1,914 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +import argparse +import contextlib +import inspect +import json +import logging +import os +import re +import select +import shutil +import socket +import sys +import time +import traceback +import unittest +from ctypes import * +from random import Random +import functools + +random = Random() + +try: + import coloredlogs + if 'COLOREDLOGS_LEVEL_STYLES' not in os.environ: + os.environ['COLOREDLOGS_LEVEL_STYLES'] = 'spam=22;debug=28;verbose=34;notice=220;warning=202;success=118,bold;error=124;critical=background=red' + # https://pypi.org/project/coloredlogs/ +except ImportError as e: + coloredlogs = False +try: + import stem +except ImportError as e: + stem = False +try: + import nmap +except ImportError as e: + nmap = False + +import wrapper +from wrapper.toxcore_enums_and_consts import TOX_CONNECTION, TOX_USER_STATUS + +from wrapper_tests.support_http import bAreWeConnected +from wrapper_tests.support_onions import (is_valid_fingerprint, + lIntroductionPoints, + oGetStemController, + sMapaddressResolv, sTorResolve) + +try: + from user_data.settings import get_user_config_path +except ImportError: + get_user_config_path = None + +# LOG=util.log +global LOG +LOG = logging.getLogger() + +def LOG_ERROR(l): print('ERRORc: '+l) +def LOG_WARN(l): print('WARNc: ' +l) +def LOG_INFO(l): print('INFOc: ' +l) +def LOG_DEBUG(l): print('DEBUGc: '+l) +def LOG_TRACE(l): pass # print('TRACE+ '+l) + +try: + from trepan.api import debug + from trepan.interfaces import server as Mserver +except: +# print('trepan3 TCP server NOT available.') + pass +else: +# print('trepan3 TCP server available.') + def trepan_handler(num=None, f=None): + connection_opts={'IO': 'TCP', 'PORT': 6666} + intf = Mserver.ServerInterface(connection_opts=connection_opts) + dbg_opts = { 'interface': intf } + print(f'Starting TCP server listening on port 6666.') + debug(dbg_opts=dbg_opts) + return + +# self._audio_thread.isAlive +iTHREAD_TIMEOUT = 1 +iTHREAD_SLEEP = 1 +iTHREAD_JOINS = 8 +iNODES = 6 + +lToxSamplerates = [8000, 12000, 16000, 24000, 48000] +lToxSampleratesK = [8, 12, 16, 24, 48] +lBOOLEANS = [ + 'local_discovery_enabled', + 'udp_enabled', + 'ipv6_enabled', + 'trace_enabled', + 'compact_mode', + 'allow_inline', + 'notifications', + 'sound_notifications', + 'calls_sound', + 'hole_punching_enabled', + 'dht_announcements_enabled', + 'save_history', + 'download_nodes_list' + 'core_logging', + ] + +sDIR = os.environ.get('TMPDIR', '/tmp') +sTOX_VERSION = "1000002018" +bHAVE_NMAP = shutil.which('nmap') +bHAVE_JQ = shutil.which('jq') +bHAVE_BASH = shutil.which('bash') +bHAVE_TORR = shutil.which('tor-resolve') + +lDEAD_BS = [ + # Failed to resolve "tox3.plastiras.org" + "tox3.plastiras.org", + 'tox.kolka.tech', + # IPs that do not reverse resolve + '49.12.229.145', + "46.101.197.175", + '114.35.245.150', + '172.93.52.70', + '195.123.208.139', + '205.185.115.131', + # IPs that do not rreverse resolve + 'yggnode.cf', '188.225.9.167', + '85-143-221-42.simplecloud.ru', '85.143.221.42', + # IPs that do not ping + '104.244.74.69', 'tox.plastiras.org', + '195.123.208.139', + 'gt.sot-te.ch', '32.226.5.82', + # suspicious IPs + 'tox.abilinski.com', '172.103.164.250', '172.103.164.250.tpia.cipherkey.com', + ] + + +def assert_main_thread(): + from PyQt5 import QtCore, QtWidgets + from qtpy.QtWidgets import QApplication + + # this "instance" method is very useful! + app_thread = QtWidgets.QApplication.instance().thread() + curr_thread = QtCore.QThread.currentThread() + if app_thread != curr_thread: + raise RuntimeError('attempt to call MainWindow.append_message from non-app thread') + +@contextlib.contextmanager +def ignoreStdout(): + devnull = os.open(os.devnull, os.O_WRONLY) + old_stdout = os.dup(1) + sys.stdout.flush() + os.dup2(devnull, 1) + os.close(devnull) + try: + yield + finally: + os.dup2(old_stdout, 1) + os.close(old_stdout) + +@contextlib.contextmanager +def ignoreStderr(): + devnull = os.open(os.devnull, os.O_WRONLY) + old_stderr = os.dup(2) + sys.stderr.flush() + os.dup2(devnull, 2) + os.close(devnull) + try: + yield + finally: + os.dup2(old_stderr, 2) + os.close(old_stderr) + +def clean_booleans(oArgs): + for key in lBOOLEANS: + if not hasattr(oArgs, key): continue + val = getattr(oArgs, key) + if type(val) == bool: continue + if val in ['False', 'false', '0']: + setattr(oArgs, key, False) + else: + setattr(oArgs, key, True) + +def on_log(iTox, level, filename, line, func, message, *data): + # LOG.debug(repr((level, filename, line, func, message,))) + tox_log_cb(level, filename, line, func, message) + +def tox_log_cb(level, filename, line, func, message, *args): + """ + * @param level The severity of the log message. + * @param filename The source file from which the message originated. + * @param line The source line from which the message originated. + * @param func The function from which the message originated. + * @param message The log message. + * @param user_data The user data pointer passed to tox_new in options. + """ + if type(func) == bytes: + func = str(func, 'utf-8') + message = str(message, 'UTF-8') + filename = str(filename, 'UTF-8') + + if filename == 'network.c': + if line == 660: return + # root WARNING 3network.c#944:b'send_packet'attempted to send message with network family 10 (probably IPv6) on IPv4 socket + if line == 944: return + i = message.find('07 = GET_NODES') + if i > 0: + return + if filename == 'TCP_common.c': return + + i = message.find(' | ') + if i > 0: + message = message[:i] + # message = filename +'#' +str(line) +':'+func +' '+message + + name = 'core' + # old level is meaningless + level = 10 # LOG.level + + # LOG._log(LOG.level, f"{level}: {message}", list()) + + i = message.find('(0: OK)') + if i > 0: + level = 10 # LOG.debug + else: + i = message.find('(1: ') + if i > 0: + level = 30 # LOG.warn + else: + level = 20 # LOG.info + + o = LOG.makeRecord(filename, level, func, line, message, list(), None) + # LOG.handle(o) + LOG_TRACE(f"{level}: {func}{line} {message}") + return + + elif level == 1: + LOG.critical(f"{level}: {message}") + elif level == 2: + LOG.error(f"{level}: {message}") + elif level == 3: + LOG.warn(f"{level}: {message}") + elif level == 4: + LOG.info(f"{level}: {message}") + elif level == 5: + LOG.debug(f"{level}: {message}") + else: + LOG_TRACE(f"{level}: {message}") + +def vAddLoggerCallback(tox_options, callback=None): + if callback is None: + wrapper.tox.Tox.libtoxcore.tox_options_set_log_callback( + tox_options._options_pointer, + POINTER(None)()) + tox_options.self_logger_cb = None + return + + c_callback = CFUNCTYPE(None, c_void_p, c_int, c_char_p, c_int, c_char_p, c_char_p, c_void_p) + tox_options.self_logger_cb = c_callback(callback) + wrapper.tox.Tox.libtoxcore.tox_options_set_log_callback( + tox_options._options_pointer, + tox_options.self_logger_cb) + +def get_video_indexes(): + # Linux + return [str(l[5:]) for l in os.listdir('/dev/') if l.startswith('video')] + +def get_audio(): + with ignoreStderr(): + import pyaudio + oPyA = pyaudio.PyAudio() + + input_devices = output_devices = 0 + for i in range(oPyA.get_device_count()): + device = oPyA.get_device_info_by_index(i) + if device["maxInputChannels"]: + input_devices += 1 + if device["maxOutputChannels"]: + output_devices += 1 + # {'index': 21, 'structVersion': 2, 'name': 'default', 'hostApi': 0, 'maxInputChannels': 64, 'maxOutputChannels': 64, 'defaultLowInputLatency': 0.008707482993197279, 'defaultLowOutputLatency': 0.008707482993197279, 'defaultHighInputLatency': 0.034829931972789115, 'defaultHighOutputLatency': 0.034829931972789115, 'defaultSampleRate': 44100.0} + audio = {'input': oPyA.get_default_input_device_info()['index'] if input_devices else -1, + 'output': oPyA.get_default_output_device_info()['index'] if output_devices else -1, + 'enabled': input_devices and output_devices} + return audio + +def oMainArgparser(_=None, iMode=0): + # 'Mode: 0=chat 1=chat+audio 2=chat+audio+video default: 0' + if not os.path.exists('/proc/sys/net/ipv6'): + bIpV6 = 'False' + else: + bIpV6 = 'True' + lIpV6Choices=[bIpV6, 'False'] + + sNodesJson = os.path.join(os.environ['HOME'], '.config', 'tox', 'DHTnodes.json') + if not os.path.exists(sNodesJson): sNodesJson = '' + + logfile = os.path.join(os.environ.get('TMPDIR', '/tmp'), 'toxygen.log') + if not os.path.exists(sNodesJson): logfile = '' + + parser = argparse.ArgumentParser(add_help=True) + parser.add_argument('--proxy_host', '--proxy-host', type=str, + # oddball - we want to use '' as a setting + default='0.0.0.0', + help='proxy host') + parser.add_argument('--proxy_port', '--proxy-port', default=0, type=int, + help='proxy port') + parser.add_argument('--proxy_type', '--proxy-type', default=0, type=int, + choices=[0,1,2], + help='proxy type 1=http, 2=socks') + parser.add_argument('--tcp_port', '--tcp-port', default=0, type=int, + help='tcp port') + parser.add_argument('--udp_enabled', type=str, default='True', + choices=['True', 'False'], + help='En/Disable udp') + parser.add_argument('--ipv6_enabled', type=str, default=bIpV6, + choices=lIpV6Choices, + help=f"En/Disable ipv6 - default {bIpV6}") + parser.add_argument('--trace_enabled',type=str, + default='True' if os.environ.get('DEBUG') else 'False', + choices=['True','False'], + help='Debugging from toxcore logger_trace or env DEBUG=1') + parser.add_argument('--download_nodes_list', type=str, default='False', + choices=['True', 'False'], + help='Download nodes list') + parser.add_argument('--nodes_json', type=str, + default=sNodesJson) + parser.add_argument('--network', type=str, + choices=['main', 'local'], + default='main') + parser.add_argument('--download_nodes_url', type=str, + default='https://nodes.tox.chat/json') + parser.add_argument('--logfile', default=logfile, + help='Filename for logging - start with + for stdout too') + parser.add_argument('--loglevel', default=logging.INFO, type=int, + # choices=[logging.info,logging.trace,logging.debug,logging.error] + help='Threshold for logging (lower is more) default: 20') + parser.add_argument('--mode', type=int, default=iMode, + choices=[0,1,2], + help='Mode: 0=chat 1=chat+audio 2=chat+audio+video default: 0') + parser.add_argument('--hole_punching_enabled',type=str, + default='False', choices=['True','False'], + help='En/Enable hole punching') + parser.add_argument('--dht_announcements_enabled',type=str, + default='True', choices=['True','False'], + help='En/Disable DHT announcements') + return parser + +def vSetupLogging(oArgs): + global LOG + logging._defaultFormatter = logging.Formatter(datefmt='%m-%d %H:%M:%S') + logging._defaultFormatter.default_time_format = '%m-%d %H:%M:%S' + logging._defaultFormatter.default_msec_format = '' + + add = None + kwargs = dict(level=oArgs.loglevel, + format='%(levelname)-8s %(message)s') + if oArgs.logfile: + add = oArgs.logfile.startswith('+') + sub = oArgs.logfile.startswith('-') + if add or sub: + oArgs.logfile = oArgs.logfile[1:] + kwargs['filename'] = oArgs.logfile + + if coloredlogs: + # https://pypi.org/project/coloredlogs/ + aKw = dict(level=oArgs.loglevel, + logger=LOG, + stream=sys.stdout, + fmt='%(name)s %(levelname)s %(message)s' + ) + coloredlogs.install(**aKw) + if oArgs.logfile: + oHandler = logging.FileHandler(oArgs.logfile) + LOG.addHandler(oHandler) + else: + logging.basicConfig(**kwargs) + if add: + oHandler = logging.StreamHandler(sys.stdout) + LOG.addHandler(oHandler) + + LOG.info(f"Setting loglevel to {oArgs.loglevel!s}") + + +def setup_logging(oArgs): + global LOG + if coloredlogs: + aKw = dict(level=oArgs.loglevel, + logger=LOG, + fmt='%(name)s %(levelname)s %(message)s') + if oArgs.logfile: + oFd = open(oArgs.logfile, 'wt') + setattr(oArgs, 'log_oFd', oFd) + aKw['stream'] = oFd + coloredlogs.install(**aKw) + if oArgs.logfile: + oHandler = logging.StreamHandler(stream=sys.stdout) + LOG.addHandler(oHandler) + else: + aKw = dict(level=oArgs.loglevel, + format='%(name)s %(levelname)-4s %(message)s') + if oArgs.logfile: + aKw['filename'] = oArgs.logfile + logging.basicConfig(**aKw) + + logging._defaultFormatter = logging.Formatter(datefmt='%m-%d %H:%M:%S') + logging._defaultFormatter.default_time_format = '%m-%d %H:%M:%S' + logging._defaultFormatter.default_msec_format = '' + + LOG.setLevel(oArgs.loglevel) +# LOG.trace = lambda l: LOG.log(0, repr(l)) + LOG.info(f"Setting loglevel to {oArgs.loglevel!s}") + +def signal_handler(num, f): + from trepan.api import debug + from trepan.interfaces import server as Mserver + connection_opts={'IO': 'TCP', 'PORT': 6666} + intf = Mserver.ServerInterface(connection_opts=connection_opts) + dbg_opts = {'interface': intf} + LOG.info('Starting TCP server listening on port 6666.') + debug(dbg_opts=dbg_opts) + return + +def merge_args_into_settings(args, settings): + if args: + if not hasattr(args, 'audio'): + LOG.warn('No audio ' +repr(args)) + settings['audio'] = getattr(args, 'audio') + if not hasattr(args, 'video'): + LOG.warn('No video ' +repr(args)) + settings['video'] = getattr(args, 'video') + for key in settings.keys(): + # proxy_type proxy_port proxy_host + not_key = 'not_' +key + if hasattr(args, key): + val = getattr(args, key) + if type(val) == bytes: + # proxy_host - ascii? + # filenames - ascii? + val = str(val, 'UTF-8') + settings[key] = val + elif hasattr(args, not_key): + val = not getattr(args, not_key) + settings[key] = val + clean_settings(settings) + return + +def clean_settings(self): + # failsafe to ensure C tox is bytes and Py settings is str + + # overrides + self['mirror_mode'] = False + # REQUIRED!! + if not os.path.exists('/proc/sys/net/ipv6'): + LOG.warn('Disabling IPV6 because /proc/sys/net/ipv6 does not exist') + self['ipv6_enabled'] = False + + if 'proxy_type' in self and self['proxy_type'] == 0: + self['proxy_host'] = '' + self['proxy_port'] = 0 + + if 'proxy_type' in self and self['proxy_type'] != 0 and \ + 'proxy_host' in self and self['proxy_host'] != '' and \ + 'proxy_port' in self and self['proxy_port'] != 0: + if 'udp_enabled' in self and self['udp_enabled']: + # We don't currently support UDP over proxy. + LOG.info("UDP enabled and proxy set: disabling UDP") + self['udp_enabled'] = False + if 'local_discovery_enabled' in self and self['local_discovery_enabled']: + LOG.info("local_discovery_enabled enabled and proxy set: disabling local_discovery_enabled") + self['local_discovery_enabled'] = False + if 'dht_announcements_enabled' in self and self['dht_announcements_enabled']: + LOG.info("dht_announcements_enabled enabled and proxy set: disabling dht_announcements_enabled") + self['dht_announcements_enabled'] = False + + if 'auto_accept_path' in self and \ + type(self['auto_accept_path']) == bytes: + self['auto_accept_path'] = str(self['auto_accept_path'], 'UTF-8') + + LOG.debug("Cleaned settings") + +def lSdSamplerates(iDev): + try: + import sounddevice as sd + except ImportError: + return [] + samplerates = (32000, 44100, 48000, 96000, ) + device = iDev + supported_samplerates = [] + for fs in samplerates: + try: + sd.check_output_settings(device=device, samplerate=fs) + except Exception as e: + # LOG.debug(f"Sample rate not supported {fs}" +' '+str(e)) + pass + else: + supported_samplerates.append(fs) + return supported_samplerates + +def _get_nodes_path(oArgs=None): + if oArgs and oArgs.nodes_json and os.path.isfile(oArgs.nodes_json): + LOG.debug("_get_nodes_path: " +oArgs.nodes_json) + default = oArgs.nodes_json + elif get_user_config_path: + default = os.path.join(get_user_config_path(), 'toxygen_nodes.json') + else: + # Windwoes + default = os.path.join(os.getenv('HOME'), '.config', 'tox', 'toxygen_nodes.json') + LOG.debug("_get_nodes_path: " +default) + return default + +DEFAULT_NODES_COUNT = 8 + +global aNODES +aNODES = {} + + +# @functools.lru_cache(maxsize=12) TypeError: unhashable type: 'Namespace' +def generate_nodes(oArgs=None, + nodes_count=DEFAULT_NODES_COUNT, + ipv='ipv4', + udp_not_tcp=True): + global aNODES + sKey = ipv + sKey += ',0' if udp_not_tcp else ',1' + if sKey in aNODES and aNODES[sKey]: + return aNODES[sKey] + sFile = _get_nodes_path(oArgs=oArgs) + assert os.path.exists(sFile), sFile + lNodes = generate_nodes_from_file(sFile, + nodes_count=nodes_count, + ipv=ipv, udp_not_tcp=udp_not_tcp) + assert lNodes + aNODES[sKey] = lNodes + return aNODES[sKey] + +aNODES_CACHE = {} +def generate_nodes_from_file(sFile, + nodes_count=DEFAULT_NODES_COUNT, + ipv='ipv4', + udp_not_tcp=True, + ): + """https://github.com/TokTok/c-toxcore/issues/469 +I had a conversation with @irungentoo on IRC about whether we really need to call tox_bootstrap() when having UDP disabled and why. The answer is yes, because in addition to TCP relays (tox_add_tcp_relay()), toxcore also needs to know addresses of UDP onion nodes in order to work correctly. The DHT, however, is not used when UDP is disabled. tox_bootstrap() function resolves the address passed to it as argument and calls onion_add_bs_node_path() and DHT_bootstrap() functions. Although calling DHT_bootstrap() is not really necessary as DHT is not used, we still need to resolve the address of the DHT node in order to populate the onion routes with onion_add_bs_node_path() call. +""" + global aNODES_CACHE + + key = ipv + key += ',0' if udp_not_tcp else ',1' + if key in aNODES_CACHE: + sorted_nodes = aNODES_CACHE[key] + else: + if not os.path.exists(sFile): + LOG.error("generate_nodes_from_file file not found " +sFile) + return [] + try: + with open(sFile, 'rt') as fl: + json_nodes = json.loads(fl.read())['nodes'] + except Exception as e: + LOG.error(f"generate_nodes_from_file error {sFile}\n{e}") + return [] + else: + LOG.debug("generate_nodes_from_file " +sFile) + + if udp_not_tcp: + nodes = [(node[ipv], node['port'], node['public_key'],) for + node in json_nodes if node[ipv] != 'NONE' \ + and node["status_udp"] in [True, "true"] + ] + else: + nodes = [] + elts = [(node[ipv], node['tcp_ports'], node['public_key'],) \ + for node in json_nodes if node[ipv] != 'NONE' \ + and node["status_tcp"] in [True, "true"] + ] + for (ipv, ports, public_key,) in elts: + for port in ports: + nodes += [(ipv, port, public_key)] + if not nodes: + LOG.warn(f'empty generate_nodes from {sFile} {json_nodes!r}') + return [] + sorted_nodes = nodes + aNODES_CACHE[key] = sorted_nodes + + random.shuffle(sorted_nodes) + if nodes_count is not None and len(sorted_nodes) > nodes_count: + sorted_nodes = sorted_nodes[-nodes_count:] + LOG.debug(f"generate_nodes_from_file {sFile} len={len(sorted_nodes)}") + return sorted_nodes + +def tox_bootstrapd_port(): + port = 33446 + sFile = '/etc/tox-bootstrapd.conf' + if os.path.exists(sFile): + with open(sFile, 'rt') as oFd: + for line in oFd.readlines(): + if line.startswith('port = '): + port = int(line[7:]) + return port + +def bootstrap_local(elts, lToxes, oArgs=None): + if os.path.exists('/run/tox-bootstrapd/tox-bootstrapd.pid'): + LOG.debug('/run/tox-bootstrapd/tox-bootstrapd.pid') + iRet = True + else: + iRet = os.system("netstat -nle4|grep -q :33") + if iRet > 0: + LOG.warn(f'bootstraping local No local DHT running') + LOG.info(f'bootstraping local') + return bootstrap_udp(elts, lToxes, oArgs) + +def lDNSClean(l): + global lDEAD_BS + # list(set(l).difference(set(lDEAD_BS))) + return [elt for elt in l if elt not in lDEAD_BS] + +def lExitExcluder(oArgs, iPort=9051): + """ + https://raw.githubusercontent.com/nusenu/noContactInfo_Exit_Excluder/main/exclude_noContactInfo_Exits.py + """ + if not stem: + LOG.warn('please install the stem Python package') + return '' + LOG.debug('lExcludeExitNodes') + + try: + controller = oGetStemController(log_level=10) + # generator + relays = controller.get_server_descriptors() + except Exception as e: + LOG.error(f'Failed to get relay descriptors {e}') + return None + + if controller.is_set('ExcludeExitNodes'): + LOG.info('ExcludeExitNodes is in use already.') + return None + + exit_excludelist=[] + LOG.debug("Excluded exit relays:") + for relay in relays: + if relay.exit_policy.is_exiting_allowed() and not relay.contact: + if is_valid_fingerprint(relay.fingerprint): + exit_excludelist.append(relay.fingerprint) + LOG.debug("https://metrics.torproject.org/rs.html#details/%s" % relay.fingerprint) + else: + LOG.warn('Invalid Fingerprint: %s' % relay.fingerprint) + + try: + controller.set_conf('ExcludeExitNodes', exit_excludelist) + LOG.info('Excluded a total of %s exit relays without ContactInfo from the exit position.' % len(exit_excludelist)) + except Exception as e: + LOG.exception('ExcludeExitNodes ' +str(e)) + return exit_excludelist + +aHOSTS = {} +@functools.lru_cache(maxsize=20) +def sDNSLookup(host): + global aHOSTS + ipv = 0 + if host in lDEAD_BS: +# LOG.warn(f"address skipped because in lDEAD_BS {host}") + return '' + if host in aHOSTS: + return aHOSTS[host] + + try: + s = host.replace('.','') + int(s) + ipv = 4 + except: + try: + s = host.replace(':','') + int(s) + ipv = 6 + except: pass + + if ipv > 0: +# LOG.debug(f"v={ipv} IP address {host}") + return host + + LOG.debug(f"sDNSLookup {host}") + ip = '' + if host.endswith('.tox') or host.endswith('.onion'): + if False and stem: + ip = sMapaddressResolv(host) + if ip: return ip + + ip = sTorResolve(host) + if ip: return ip + + if not bHAVE_TORR: + LOG.warn(f"onion address skipped because no tor-resolve {host}") + return '' + try: + sout = f"/tmp/TR{os.getpid()}.log" + i = os.system(f"tor-resolve -4 {host} > {sout}") + if not i: + LOG.warn(f"onion address skipped because tor-resolve on {host}") + return '' + ip = open(sout, 'rt').read() + if ip.endswith('failed.'): + LOG.warn(f"onion address skipped because tor-resolve failed on {host}") + return '' + LOG.debug(f"onion address tor-resolve {ip} on {host}") + return ip + except: + pass + else: + try: + ip = socket.gethostbyname(host) + LOG.debug(f"host={host} gethostbyname IP address {ip}") + if ip: + aHOSTS[host] = ip + return ip + # drop through + except: + # drop through + pass + + if ip == '': + try: + sout = f"/tmp/TR{os.getpid()}.log" + i = os.system(f"dig {host} +timeout=15|grep ^{host}|sed -e 's/.* //'> {sout}") + if not i: + LOG.warn(f"address skipped because dig failed on {host}") + return '' + ip = open(sout, 'rt').read().strip() + LOG.debug(f"address dig {ip} on {host}") + aHOSTS[host] = ip + return ip + except: + ip = host + LOG.debug(f'sDNSLookup {host} -> {ip}') + if ip and ip != host: + aHOSTS[host] = ip + return ip + +def bootstrap_udp(lelts, lToxes, oArgs=None): + lelts = lDNSClean(lelts) + socket.setdefaulttimeout(15.0) + for oTox in lToxes: + random.shuffle(lelts) + if hasattr(oTox, 'oArgs'): + oArgs = oTox.oArgs + if hasattr(oArgs, 'contents') and oArgs.contents.proxy_type != 0: + lelts = lelts[:1] + +# LOG.debug(f'bootstrap_udp DHT bootstraping {oTox.name} {len(lelts)}') + for largs in lelts: + assert len(largs) == 3 + host, port, key = largs + assert host; assert port; assert key + if host in lDEAD_BS: continue + ip = sDNSLookup(host) + if not ip: + LOG.warn(f'bootstrap_udp to host={host} port={port} did not resolve ip={ip}') + continue + + if type(port) == str: + port = int(port) + try: + assert len(key) == 64, key + # NOT ip + oRet = oTox.bootstrap(host, + port, + key) + except Exception as e: + if oArgs is None or ( + hasattr(oArgs, 'contents') and oArgs.contents.proxy_type == 0): + pass + # LOG.error(f'bootstrap_udp failed to host={host} port={port} {e}') + continue + if not oRet: + LOG.warn(f'bootstrap_udp failed to {host} : {oRet}') + elif oTox.self_get_connection_status() != TOX_CONNECTION['NONE']: + LOG.info(f'bootstrap_udp to {host} connected') + break + else: +# LOG.debug(f'bootstrap_udp to {host} not connected') + pass + +def bootstrap_tcp(lelts, lToxes, oArgs=None): + lelts = lDNSClean(lelts) + for oTox in lToxes: + if hasattr(oTox, 'oArgs'): oArgs = oTox.oArgs + random.shuffle(lelts) +# LOG.debug(f'bootstrap_tcp bootstapping {oTox.name} {len(lelts)}') + for (host, port, key,) in lelts: + assert host; assert port;assert key + if host in lDEAD_BS: continue + ip = sDNSLookup(host) + if not ip: + LOG.warn(f'bootstrap_tcp to {host} did not resolve ip={ip}') +# continue + ip = host + if host.endswith('.onion') and stem: + l = lIntroductionPoints(host) + if not l: + LOG.warn(f'bootstrap_tcp to {host} has no introduction points') + continue + if type(port) == str: + port = int(port) + try: + assert len(key) == 64, key + oRet = oTox.add_tcp_relay(ip, + port, + key) + except Exception as e: + LOG.error(f'bootstrap_tcp to {host} : ' +str(e)) + continue + if not oRet: + LOG.warn(f'bootstrap_tcp failed to {host} : {oRet}') + elif oTox.mycon_time == 1: + LOG.info(f'bootstrap_tcp to {host} not yet connected last=1') + elif oTox.mycon_status is False: + LOG.info(f'bootstrap_tcp to {host} not True' \ + +f" last={int(oTox.mycon_time)}" ) + elif oTox.self_get_connection_status() != TOX_CONNECTION['NONE']: + LOG.info(f'bootstrap_tcp to {host} connected' \ + +f" last={int(oTox.mycon_time)}" ) + break + else: + LOG.debug(f'bootstrap_tcp to {host} but not connected' \ + +f" last={int(oTox.mycon_time)}" ) + pass + +def iNmapInfoNmap(sProt, sHost, sPort, key=None, environ=None, cmd=''): + if sHost in ['-', 'NONE']: return 0 + if not nmap: return 0 + nmps = nmap.PortScanner + if sProt in ['socks', 'socks5', 'tcp4']: + prot = 'tcp' + cmd = f" -Pn -n -sT -p T:{sPort}" + else: + prot = 'udp' + cmd = f" -Pn -n -sU -p U:{sPort}" + LOG.debug(f"iNmapInfoNmap cmd={cmd}") + sys.stdout.flush() + o = nmps().scan(hosts=sHost, arguments=cmd) + aScan = o['scan'] + ip = list(aScan.keys())[0] + state = aScan[ip][prot][sPort]['state'] + LOG.info(f"iNmapInfoNmap: to {sHost} {state}") + return 0 + +def iNmapInfo(sProt, sHost, sPort, key=None, environ=None, cmd='nmap'): + if sHost in ['-', 'NONE']: return 0 + sFile = os.path.join("/tmp", f"{sHost}.{os.getpid()}.nmap") + if sProt in ['socks', 'socks5', 'tcp4']: + cmd += f" -Pn -n -sT -p T:{sPort} {sHost} | grep /tcp " + else: + cmd += f" -Pn -n -sU -p U:{sPort} {sHost} | grep /udp " + LOG.debug(f"iNmapInfo cmd={cmd}") + sys.stdout.flush() + iRet = os.system('sudo ' +cmd +f" >{sFile} 2>&1 ") + LOG.debug(f"iNmapInfo cmd={cmd} iRet={iRet}") + if iRet != 0: + return iRet + assert os.path.exists(sFile), sFile + with open(sFile, 'rt') as oFd: + l = oFd.readlines() + assert len(l) + l = [line for line in l if line and not line.startswith('WARNING:')] + s = '\n'.join([s.strip() for s in l]) + LOG.info(f"iNmapInfo: to {sHost}\n{s}") + return 0 + +def bootstrap_iNmapInfo(lElts, oArgs, protocol="tcp4", bIS_LOCAL=False, iNODES=iNODES, cmd='nmap'): + if not bIS_LOCAL and not bAreWeConnected(): + LOG.warn(f"bootstrap_iNmapInfo not local and NOT CONNECTED") + return True + if os.environ['USER'] != 'root': + LOG.warn(f"bootstrap_iNmapInfo not ROOT") + return True + + lRetval = [] + for elts in lElts[:iNODES]: + host, port, key = elts + ip = sDNSLookup(host) + if not ip: + LOG.info('bootstrap_iNmapInfo to {host} did not resolve ip={ip}') + continue + if type(port) == str: + port = int(port) + iRet = -1 + try: + if not nmap: + iRet = iNmapInfo(protocol, ip, port, key, cmd=cmd) + else: + iRet = iNmapInfoNmap(protocol, ip, port, key) + if iRet != 0: + LOG.warn('iNmapInfo to ' +repr(host) +' retval=' +str(iRet)) + lRetval += [False] + else: + LOG.debug('iNmapInfo to ' +repr(host) +' retval=' +str(iRet)) + lRetval += [True] + except Exception as e: + LOG.exception('iNmapInfo to {host} : ' +str(e) + ) + lRetval += [False] + return any(lRetval) + +def caseFactory(cases): + """We want the tests run in order.""" + if len(cases) > 1: + ordered_cases = sorted(cases, key=lambda f: inspect.findsource(f)[1]) + else: + ordered_cases = cases + return ordered_cases + +def suiteFactory(*testcases): + """We want the tests run in order.""" + linen = lambda f: getattr(tc, f).__code__.co_firstlineno + lncmp = lambda a, b: linen(a) - linen(b) + + test_suite = unittest.TestSuite() + for tc in testcases: + test_suite.addTest(unittest.makeSuite(tc, sortUsing=lncmp)) + return test_suite diff --git a/toxygen/tests/test_gdb.py b/toxygen/tests/test_gdb.py new file mode 100644 index 0000000..46736f6 --- /dev/null +++ b/toxygen/tests/test_gdb.py @@ -0,0 +1,936 @@ +# Verify that gdb can pretty-print the various PyObject* types +# +# The code for testing gdb was adapted from similar work in Unladen Swallow's +# Lib/test/test_jit_gdb.py + +import locale +import os +import re +import subprocess +import sys +import sysconfig +import textwrap +import unittest + +# Is this Python configured to support threads? +try: + import _thread +except ImportError: + _thread = None + +from test import support +from test.support import run_unittest, findfile, python_is_optimized + +def get_gdb_version(): + try: + proc = subprocess.Popen(["gdb", "-nx", "--version"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True) + with proc: + version = proc.communicate()[0] + except OSError: + # This is what "no gdb" looks like. There may, however, be other + # errors that manifest this way too. + raise unittest.SkipTest("Couldn't find gdb on the path") + + # Regex to parse: + # 'GNU gdb (GDB; SUSE Linux Enterprise 12) 7.7\n' -> 7.7 + # 'GNU gdb (GDB) Fedora 7.9.1-17.fc22\n' -> 7.9 + # 'GNU gdb 6.1.1 [FreeBSD]\n' -> 6.1 + # 'GNU gdb (GDB) Fedora (7.5.1-37.fc18)\n' -> 7.5 + match = re.search(r"^GNU gdb.*?\b(\d+)\.(\d+)", version) + if match is None: + raise Exception("unable to parse GDB version: %r" % version) + return (version, int(match.group(1)), int(match.group(2))) + +gdb_version, gdb_major_version, gdb_minor_version = get_gdb_version() +if gdb_major_version < 7: + raise unittest.SkipTest("gdb versions before 7.0 didn't support python " + "embedding. Saw %s.%s:\n%s" + % (gdb_major_version, gdb_minor_version, + gdb_version)) + +if not sysconfig.is_python_build(): + raise unittest.SkipTest("test_gdb only works on source builds at the moment.") + +# Location of custom hooks file in a repository checkout. +checkout_hook_path = os.path.join(os.path.dirname(sys.executable), + 'python-gdb.py') + +PYTHONHASHSEED = '123' + +def run_gdb(*args, **env_vars): + """Runs gdb in --batch mode with the additional arguments given by *args. + + Returns its (stdout, stderr) decoded from utf-8 using the replace handler. + """ + if env_vars: + env = os.environ.copy() + env.update(env_vars) + else: + env = None + # -nx: Do not execute commands from any .gdbinit initialization files + # (issue #22188) + base_cmd = ('gdb', '--batch', '-nx') + if (gdb_major_version, gdb_minor_version) >= (7, 4): + base_cmd += ('-iex', 'add-auto-load-safe-path ' + checkout_hook_path) + proc = subprocess.Popen(base_cmd + args, + # Redirect stdin to prevent GDB from messing with + # the terminal settings + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=env) + with proc: + out, err = proc.communicate() + return out.decode('utf-8', 'replace'), err.decode('utf-8', 'replace') + +# Verify that "gdb" was built with the embedded python support enabled: +gdbpy_version, _ = run_gdb("--eval-command=python import sys; print(sys.version_info)") +if not gdbpy_version: + raise unittest.SkipTest("gdb not built with embedded python support") + +# Verify that "gdb" can load our custom hooks, as OS security settings may +# disallow this without a customized .gdbinit. +_, gdbpy_errors = run_gdb('--args', sys.executable) +if "auto-loading has been declined" in gdbpy_errors: + msg = "gdb security settings prevent use of custom hooks: " + raise unittest.SkipTest(msg + gdbpy_errors.rstrip()) + +def gdb_has_frame_select(): + # Does this build of gdb have gdb.Frame.select ? + stdout, _ = run_gdb("--eval-command=python print(dir(gdb.Frame))") + m = re.match(r'.*\[(.*)\].*', stdout) + if not m: + raise unittest.SkipTest("Unable to parse output from gdb.Frame.select test") + gdb_frame_dir = m.group(1).split(', ') + return "'select'" in gdb_frame_dir + +HAS_PYUP_PYDOWN = gdb_has_frame_select() + +BREAKPOINT_FN='builtin_id' + +@unittest.skipIf(support.PGO, "not useful for PGO") +class DebuggerTests(unittest.TestCase): + + """Test that the debugger can debug Python.""" + + def get_stack_trace(self, source=None, script=None, + breakpoint=BREAKPOINT_FN, + cmds_after_breakpoint=None, + import_site=False): + ''' + Run 'python -c SOURCE' under gdb with a breakpoint. + + Support injecting commands after the breakpoint is reached + + Returns the stdout from gdb + + cmds_after_breakpoint: if provided, a list of strings: gdb commands + ''' + # We use "set breakpoint pending yes" to avoid blocking with a: + # Function "foo" not defined. + # Make breakpoint pending on future shared library load? (y or [n]) + # error, which typically happens python is dynamically linked (the + # breakpoints of interest are to be found in the shared library) + # When this happens, we still get: + # Function "textiowrapper_write" not defined. + # emitted to stderr each time, alas. + + # Initially I had "--eval-command=continue" here, but removed it to + # avoid repeated print breakpoints when traversing hierarchical data + # structures + + # Generate a list of commands in gdb's language: + commands = ['set breakpoint pending yes', + 'break %s' % breakpoint, + + # The tests assume that the first frame of printed + # backtrace will not contain program counter, + # that is however not guaranteed by gdb + # therefore we need to use 'set print address off' to + # make sure the counter is not there. For example: + # #0 in PyObject_Print ... + # is assumed, but sometimes this can be e.g. + # #0 0x00003fffb7dd1798 in PyObject_Print ... + 'set print address off', + + 'run'] + + # GDB as of 7.4 onwards can distinguish between the + # value of a variable at entry vs current value: + # http://sourceware.org/gdb/onlinedocs/gdb/Variables.html + # which leads to the selftests failing with errors like this: + # AssertionError: 'v@entry=()' != '()' + # Disable this: + if (gdb_major_version, gdb_minor_version) >= (7, 4): + commands += ['set print entry-values no'] + + if cmds_after_breakpoint: + commands += cmds_after_breakpoint + else: + commands += ['backtrace'] + + # print commands + + # Use "commands" to generate the arguments with which to invoke "gdb": + args = ['--eval-command=%s' % cmd for cmd in commands] + args += ["--args", + sys.executable] + args.extend(subprocess._args_from_interpreter_flags()) + + if not import_site: + # -S suppresses the default 'import site' + args += ["-S"] + + if source: + args += ["-c", source] + elif script: + args += [script] + + # print args + # print (' '.join(args)) + + # Use "args" to invoke gdb, capturing stdout, stderr: + out, err = run_gdb(*args, PYTHONHASHSEED=PYTHONHASHSEED) + + errlines = err.splitlines() + unexpected_errlines = [] + + # Ignore some benign messages on stderr. + ignore_patterns = ( + 'Function "%s" not defined.' % breakpoint, + 'Do you need "set solib-search-path" or ' + '"set sysroot"?', + # BFD: /usr/lib/debug/(...): unable to initialize decompress + # status for section .debug_aranges + 'BFD: ', + # ignore all warnings + 'warning: ', + ) + for line in errlines: + if not line: + continue + if not line.startswith(ignore_patterns): + unexpected_errlines.append(line) + + # Ensure no unexpected error messages: + self.assertEqual(unexpected_errlines, []) + return out + + def get_gdb_repr(self, source, + cmds_after_breakpoint=None, + import_site=False): + # Given an input python source representation of data, + # run "python -c'id(DATA)'" under gdb with a breakpoint on + # builtin_id and scrape out gdb's representation of the "op" + # parameter, and verify that the gdb displays the same string + # + # Verify that the gdb displays the expected string + # + # For a nested structure, the first time we hit the breakpoint will + # give us the top-level structure + + # NOTE: avoid decoding too much of the traceback as some + # undecodable characters may lurk there in optimized mode + # (issue #19743). + cmds_after_breakpoint = cmds_after_breakpoint or ["backtrace 1"] + gdb_output = self.get_stack_trace(source, breakpoint=BREAKPOINT_FN, + cmds_after_breakpoint=cmds_after_breakpoint, + import_site=import_site) + # gdb can insert additional '\n' and space characters in various places + # in its output, depending on the width of the terminal it's connected + # to (using its "wrap_here" function) + m = re.match(r'.*#0\s+builtin_id\s+\(self\=.*,\s+v=\s*(.*?)\)\s+at\s+\S*Python/bltinmodule.c.*', + gdb_output, re.DOTALL) + if not m: + self.fail('Unexpected gdb output: %r\n%s' % (gdb_output, gdb_output)) + return m.group(1), gdb_output + + def assertEndsWith(self, actual, exp_end): + '''Ensure that the given "actual" string ends with "exp_end"''' + self.assertTrue(actual.endswith(exp_end), + msg='%r did not end with %r' % (actual, exp_end)) + + def assertMultilineMatches(self, actual, pattern): + m = re.match(pattern, actual, re.DOTALL) + if not m: + self.fail(msg='%r did not match %r' % (actual, pattern)) + + def get_sample_script(self): + return findfile('gdb_sample.py') + +class PrettyPrintTests(DebuggerTests): + def test_getting_backtrace(self): + gdb_output = self.get_stack_trace('id(42)') + self.assertTrue(BREAKPOINT_FN in gdb_output) + + def assertGdbRepr(self, val, exp_repr=None): + # Ensure that gdb's rendering of the value in a debugged process + # matches repr(value) in this process: + gdb_repr, gdb_output = self.get_gdb_repr('id(' + ascii(val) + ')') + if not exp_repr: + exp_repr = repr(val) + self.assertEqual(gdb_repr, exp_repr, + ('%r did not equal expected %r; full output was:\n%s' + % (gdb_repr, exp_repr, gdb_output))) + + def test_int(self): + 'Verify the pretty-printing of various int values' + self.assertGdbRepr(42) + self.assertGdbRepr(0) + self.assertGdbRepr(-7) + self.assertGdbRepr(1000000000000) + self.assertGdbRepr(-1000000000000000) + + def test_singletons(self): + 'Verify the pretty-printing of True, False and None' + self.assertGdbRepr(True) + self.assertGdbRepr(False) + self.assertGdbRepr(None) + + def test_dicts(self): + 'Verify the pretty-printing of dictionaries' + self.assertGdbRepr({}) + self.assertGdbRepr({'foo': 'bar'}, "{'foo': 'bar'}") + # Python preserves insertion order since 3.6 + self.assertGdbRepr({'foo': 'bar', 'douglas': 42}, "{'foo': 'bar', 'douglas': 42}") + + def test_lists(self): + 'Verify the pretty-printing of lists' + self.assertGdbRepr([]) + self.assertGdbRepr(list(range(5))) + + def test_bytes(self): + 'Verify the pretty-printing of bytes' + self.assertGdbRepr(b'') + self.assertGdbRepr(b'And now for something hopefully the same') + self.assertGdbRepr(b'string with embedded NUL here \0 and then some more text') + self.assertGdbRepr(b'this is a tab:\t' + b' this is a slash-N:\n' + b' this is a slash-R:\r' + ) + + self.assertGdbRepr(b'this is byte 255:\xff and byte 128:\x80') + + self.assertGdbRepr(bytes([b for b in range(255)])) + + def test_strings(self): + 'Verify the pretty-printing of unicode strings' + encoding = locale.getpreferredencoding() + def check_repr(text): + try: + text.encode(encoding) + printable = True + except UnicodeEncodeError: + self.assertGdbRepr(text, ascii(text)) + else: + self.assertGdbRepr(text) + + self.assertGdbRepr('') + self.assertGdbRepr('And now for something hopefully the same') + self.assertGdbRepr('string with embedded NUL here \0 and then some more text') + + # Test printing a single character: + # U+2620 SKULL AND CROSSBONES + check_repr('\u2620') + + # Test printing a Japanese unicode string + # (I believe this reads "mojibake", using 3 characters from the CJK + # Unified Ideographs area, followed by U+3051 HIRAGANA LETTER KE) + check_repr('\u6587\u5b57\u5316\u3051') + + # Test a character outside the BMP: + # U+1D121 MUSICAL SYMBOL C CLEF + # This is: + # UTF-8: 0xF0 0x9D 0x84 0xA1 + # UTF-16: 0xD834 0xDD21 + check_repr(chr(0x1D121)) + + def test_tuples(self): + 'Verify the pretty-printing of tuples' + self.assertGdbRepr(tuple(), '()') + self.assertGdbRepr((1,), '(1,)') + self.assertGdbRepr(('foo', 'bar', 'baz')) + + def test_sets(self): + 'Verify the pretty-printing of sets' + if (gdb_major_version, gdb_minor_version) < (7, 3): + self.skipTest("pretty-printing of sets needs gdb 7.3 or later") + self.assertGdbRepr(set(), "set()") + self.assertGdbRepr(set(['a']), "{'a'}") + # PYTHONHASHSEED is need to get the exact frozenset item order + if not sys.flags.ignore_environment: + self.assertGdbRepr(set(['a', 'b']), "{'a', 'b'}") + self.assertGdbRepr(set([4, 5, 6]), "{4, 5, 6}") + + # Ensure that we handle sets containing the "dummy" key value, + # which happens on deletion: + gdb_repr, gdb_output = self.get_gdb_repr('''s = set(['a','b']) +s.remove('a') +id(s)''') + self.assertEqual(gdb_repr, "{'b'}") + + def test_frozensets(self): + 'Verify the pretty-printing of frozensets' + if (gdb_major_version, gdb_minor_version) < (7, 3): + self.skipTest("pretty-printing of frozensets needs gdb 7.3 or later") + self.assertGdbRepr(frozenset(), "frozenset()") + self.assertGdbRepr(frozenset(['a']), "frozenset({'a'})") + # PYTHONHASHSEED is need to get the exact frozenset item order + if not sys.flags.ignore_environment: + self.assertGdbRepr(frozenset(['a', 'b']), "frozenset({'a', 'b'})") + self.assertGdbRepr(frozenset([4, 5, 6]), "frozenset({4, 5, 6})") + + def test_exceptions(self): + # Test a RuntimeError + gdb_repr, gdb_output = self.get_gdb_repr(''' +try: + raise RuntimeError("I am an error") +except RuntimeError as e: + id(e) +''') + self.assertEqual(gdb_repr, + "RuntimeError('I am an error',)") + + + # Test division by zero: + gdb_repr, gdb_output = self.get_gdb_repr(''' +try: + a = 1 / 0 +except ZeroDivisionError as e: + id(e) +''') + self.assertEqual(gdb_repr, + "ZeroDivisionError('division by zero',)") + + def test_modern_class(self): + 'Verify the pretty-printing of new-style class instances' + gdb_repr, gdb_output = self.get_gdb_repr(''' +class Foo: + pass +foo = Foo() +foo.an_int = 42 +id(foo)''') + m = re.match(r'', gdb_repr) + self.assertTrue(m, + msg='Unexpected new-style class rendering %r' % gdb_repr) + + def test_subclassing_list(self): + 'Verify the pretty-printing of an instance of a list subclass' + gdb_repr, gdb_output = self.get_gdb_repr(''' +class Foo(list): + pass +foo = Foo() +foo += [1, 2, 3] +foo.an_int = 42 +id(foo)''') + m = re.match(r'', gdb_repr) + + self.assertTrue(m, + msg='Unexpected new-style class rendering %r' % gdb_repr) + + def test_subclassing_tuple(self): + 'Verify the pretty-printing of an instance of a tuple subclass' + # This should exercise the negative tp_dictoffset code in the + # new-style class support + gdb_repr, gdb_output = self.get_gdb_repr(''' +class Foo(tuple): + pass +foo = Foo((1, 2, 3)) +foo.an_int = 42 +id(foo)''') + m = re.match(r'', gdb_repr) + + self.assertTrue(m, + msg='Unexpected new-style class rendering %r' % gdb_repr) + + def assertSane(self, source, corruption, exprepr=None): + '''Run Python under gdb, corrupting variables in the inferior process + immediately before taking a backtrace. + + Verify that the variable's representation is the expected failsafe + representation''' + if corruption: + cmds_after_breakpoint=[corruption, 'backtrace'] + else: + cmds_after_breakpoint=['backtrace'] + + gdb_repr, gdb_output = \ + self.get_gdb_repr(source, + cmds_after_breakpoint=cmds_after_breakpoint) + if exprepr: + if gdb_repr == exprepr: + # gdb managed to print the value in spite of the corruption; + # this is good (see http://bugs.python.org/issue8330) + return + + # Match anything for the type name; 0xDEADBEEF could point to + # something arbitrary (see http://bugs.python.org/issue8330) + pattern = '<.* at remote 0x-?[0-9a-f]+>' + + m = re.match(pattern, gdb_repr) + if not m: + self.fail('Unexpected gdb representation: %r\n%s' % \ + (gdb_repr, gdb_output)) + + def test_NULL_ptr(self): + 'Ensure that a NULL PyObject* is handled gracefully' + gdb_repr, gdb_output = ( + self.get_gdb_repr('id(42)', + cmds_after_breakpoint=['set variable v=0', + 'backtrace']) + ) + + self.assertEqual(gdb_repr, '0x0') + + def test_NULL_ob_type(self): + 'Ensure that a PyObject* with NULL ob_type is handled gracefully' + self.assertSane('id(42)', + 'set v->ob_type=0') + + def test_corrupt_ob_type(self): + 'Ensure that a PyObject* with a corrupt ob_type is handled gracefully' + self.assertSane('id(42)', + 'set v->ob_type=0xDEADBEEF', + exprepr='42') + + def test_corrupt_tp_flags(self): + 'Ensure that a PyObject* with a type with corrupt tp_flags is handled' + self.assertSane('id(42)', + 'set v->ob_type->tp_flags=0x0', + exprepr='42') + + def test_corrupt_tp_name(self): + 'Ensure that a PyObject* with a type with corrupt tp_name is handled' + self.assertSane('id(42)', + 'set v->ob_type->tp_name=0xDEADBEEF', + exprepr='42') + + def test_builtins_help(self): + 'Ensure that the new-style class _Helper in site.py can be handled' + + if sys.flags.no_site: + self.skipTest("need site module, but -S option was used") + + # (this was the issue causing tracebacks in + # http://bugs.python.org/issue8032#msg100537 ) + gdb_repr, gdb_output = self.get_gdb_repr('id(__builtins__.help)', import_site=True) + + m = re.match(r'<_Helper at remote 0x-?[0-9a-f]+>', gdb_repr) + self.assertTrue(m, + msg='Unexpected rendering %r' % gdb_repr) + + def test_selfreferential_list(self): + '''Ensure that a reference loop involving a list doesn't lead proxyval + into an infinite loop:''' + gdb_repr, gdb_output = \ + self.get_gdb_repr("a = [3, 4, 5] ; a.append(a) ; id(a)") + self.assertEqual(gdb_repr, '[3, 4, 5, [...]]') + + gdb_repr, gdb_output = \ + self.get_gdb_repr("a = [3, 4, 5] ; b = [a] ; a.append(b) ; id(a)") + self.assertEqual(gdb_repr, '[3, 4, 5, [[...]]]') + + def test_selfreferential_dict(self): + '''Ensure that a reference loop involving a dict doesn't lead proxyval + into an infinite loop:''' + gdb_repr, gdb_output = \ + self.get_gdb_repr("a = {} ; b = {'bar':a} ; a['foo'] = b ; id(a)") + + self.assertEqual(gdb_repr, "{'foo': {'bar': {...}}}") + + def test_selfreferential_old_style_instance(self): + gdb_repr, gdb_output = \ + self.get_gdb_repr(''' +class Foo: + pass +foo = Foo() +foo.an_attr = foo +id(foo)''') + self.assertTrue(re.match(r'\) at remote 0x-?[0-9a-f]+>', + gdb_repr), + 'Unexpected gdb representation: %r\n%s' % \ + (gdb_repr, gdb_output)) + + def test_selfreferential_new_style_instance(self): + gdb_repr, gdb_output = \ + self.get_gdb_repr(''' +class Foo(object): + pass +foo = Foo() +foo.an_attr = foo +id(foo)''') + self.assertTrue(re.match(r'\) at remote 0x-?[0-9a-f]+>', + gdb_repr), + 'Unexpected gdb representation: %r\n%s' % \ + (gdb_repr, gdb_output)) + + gdb_repr, gdb_output = \ + self.get_gdb_repr(''' +class Foo(object): + pass +a = Foo() +b = Foo() +a.an_attr = b +b.an_attr = a +id(a)''') + self.assertTrue(re.match(r'\) at remote 0x-?[0-9a-f]+>\) at remote 0x-?[0-9a-f]+>', + gdb_repr), + 'Unexpected gdb representation: %r\n%s' % \ + (gdb_repr, gdb_output)) + + def test_truncation(self): + 'Verify that very long output is truncated' + gdb_repr, gdb_output = self.get_gdb_repr('id(list(range(1000)))') + self.assertEqual(gdb_repr, + "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, " + "14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, " + "27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, " + "40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, " + "53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, " + "66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, " + "79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, " + "92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, " + "104, 105, 106, 107, 108, 109, 110, 111, 112, 113, " + "114, 115, 116, 117, 118, 119, 120, 121, 122, 123, " + "124, 125, 126, 127, 128, 129, 130, 131, 132, 133, " + "134, 135, 136, 137, 138, 139, 140, 141, 142, 143, " + "144, 145, 146, 147, 148, 149, 150, 151, 152, 153, " + "154, 155, 156, 157, 158, 159, 160, 161, 162, 163, " + "164, 165, 166, 167, 168, 169, 170, 171, 172, 173, " + "174, 175, 176, 177, 178, 179, 180, 181, 182, 183, " + "184, 185, 186, 187, 188, 189, 190, 191, 192, 193, " + "194, 195, 196, 197, 198, 199, 200, 201, 202, 203, " + "204, 205, 206, 207, 208, 209, 210, 211, 212, 213, " + "214, 215, 216, 217, 218, 219, 220, 221, 222, 223, " + "224, 225, 226...(truncated)") + self.assertEqual(len(gdb_repr), + 1024 + len('...(truncated)')) + + def test_builtin_method(self): + gdb_repr, gdb_output = self.get_gdb_repr('import sys; id(sys.stdout.readlines)') + self.assertTrue(re.match(r'', + gdb_repr), + 'Unexpected gdb representation: %r\n%s' % \ + (gdb_repr, gdb_output)) + + def test_frames(self): + gdb_output = self.get_stack_trace(''' +def foo(a, b, c): + pass + +foo(3, 4, 5) +id(foo.__code__)''', + breakpoint='builtin_id', + cmds_after_breakpoint=['print (PyFrameObject*)(((PyCodeObject*)v)->co_zombieframe)'] + ) + self.assertTrue(re.match(r'.*\s+\$1 =\s+Frame 0x-?[0-9a-f]+, for file , line 3, in foo \(\)\s+.*', + gdb_output, + re.DOTALL), + 'Unexpected gdb representation: %r\n%s' % (gdb_output, gdb_output)) + +@unittest.skipIf(python_is_optimized(), + "Python was compiled with optimizations") +class PyListTests(DebuggerTests): + def assertListing(self, expected, actual): + self.assertEndsWith(actual, expected) + + def test_basic_command(self): + 'Verify that the "py-list" command works' + bt = self.get_stack_trace(script=self.get_sample_script(), + cmds_after_breakpoint=['py-list']) + + self.assertListing(' 5 \n' + ' 6 def bar(a, b, c):\n' + ' 7 baz(a, b, c)\n' + ' 8 \n' + ' 9 def baz(*args):\n' + ' >10 id(42)\n' + ' 11 \n' + ' 12 foo(1, 2, 3)\n', + bt) + + def test_one_abs_arg(self): + 'Verify the "py-list" command with one absolute argument' + bt = self.get_stack_trace(script=self.get_sample_script(), + cmds_after_breakpoint=['py-list 9']) + + self.assertListing(' 9 def baz(*args):\n' + ' >10 id(42)\n' + ' 11 \n' + ' 12 foo(1, 2, 3)\n', + bt) + + def test_two_abs_args(self): + 'Verify the "py-list" command with two absolute arguments' + bt = self.get_stack_trace(script=self.get_sample_script(), + cmds_after_breakpoint=['py-list 1,3']) + + self.assertListing(' 1 # Sample script for use by test_gdb.py\n' + ' 2 \n' + ' 3 def foo(a, b, c):\n', + bt) + +class StackNavigationTests(DebuggerTests): + @unittest.skipUnless(HAS_PYUP_PYDOWN, "test requires py-up/py-down commands") + @unittest.skipIf(python_is_optimized(), + "Python was compiled with optimizations") + def test_pyup_command(self): + 'Verify that the "py-up" command works' + bt = self.get_stack_trace(script=self.get_sample_script(), + cmds_after_breakpoint=['py-up', 'py-up']) + self.assertMultilineMatches(bt, + r'''^.* +#[0-9]+ Frame 0x-?[0-9a-f]+, for file .*gdb_sample.py, line 7, in bar \(a=1, b=2, c=3\) + baz\(a, b, c\) +$''') + + @unittest.skipUnless(HAS_PYUP_PYDOWN, "test requires py-up/py-down commands") + def test_down_at_bottom(self): + 'Verify handling of "py-down" at the bottom of the stack' + bt = self.get_stack_trace(script=self.get_sample_script(), + cmds_after_breakpoint=['py-down']) + self.assertEndsWith(bt, + 'Unable to find a newer python frame\n') + + @unittest.skipUnless(HAS_PYUP_PYDOWN, "test requires py-up/py-down commands") + def test_up_at_top(self): + 'Verify handling of "py-up" at the top of the stack' + bt = self.get_stack_trace(script=self.get_sample_script(), + cmds_after_breakpoint=['py-up'] * 5) + self.assertEndsWith(bt, + 'Unable to find an older python frame\n') + + @unittest.skipUnless(HAS_PYUP_PYDOWN, "test requires py-up/py-down commands") + @unittest.skipIf(python_is_optimized(), + "Python was compiled with optimizations") + def test_up_then_down(self): + 'Verify "py-up" followed by "py-down"' + bt = self.get_stack_trace(script=self.get_sample_script(), + cmds_after_breakpoint=['py-up', 'py-up', 'py-down']) + self.assertMultilineMatches(bt, + r'''^.* +#[0-9]+ Frame 0x-?[0-9a-f]+, for file .*gdb_sample.py, line 7, in bar \(a=1, b=2, c=3\) + baz\(a, b, c\) +#[0-9]+ Frame 0x-?[0-9a-f]+, for file .*gdb_sample.py, line 10, in baz \(args=\(1, 2, 3\)\) + id\(42\) +$''') + +class PyBtTests(DebuggerTests): + @unittest.skipIf(python_is_optimized(), + "Python was compiled with optimizations") + def test_bt(self): + 'Verify that the "py-bt" command works' + bt = self.get_stack_trace(script=self.get_sample_script(), + cmds_after_breakpoint=['py-bt']) + self.assertMultilineMatches(bt, + r'''^.* +Traceback \(most recent call first\): + + File ".*gdb_sample.py", line 10, in baz + id\(42\) + File ".*gdb_sample.py", line 7, in bar + baz\(a, b, c\) + File ".*gdb_sample.py", line 4, in foo + bar\(a, b, c\) + File ".*gdb_sample.py", line 12, in + foo\(1, 2, 3\) +''') + + @unittest.skipIf(python_is_optimized(), + "Python was compiled with optimizations") + def test_bt_full(self): + 'Verify that the "py-bt-full" command works' + bt = self.get_stack_trace(script=self.get_sample_script(), + cmds_after_breakpoint=['py-bt-full']) + self.assertMultilineMatches(bt, + r'''^.* +#[0-9]+ Frame 0x-?[0-9a-f]+, for file .*gdb_sample.py, line 7, in bar \(a=1, b=2, c=3\) + baz\(a, b, c\) +#[0-9]+ Frame 0x-?[0-9a-f]+, for file .*gdb_sample.py, line 4, in foo \(a=1, b=2, c=3\) + bar\(a, b, c\) +#[0-9]+ Frame 0x-?[0-9a-f]+, for file .*gdb_sample.py, line 12, in \(\) + foo\(1, 2, 3\) +''') + + @unittest.skipUnless(_thread, + "Python was compiled without thread support") + def test_threads(self): + 'Verify that "py-bt" indicates threads that are waiting for the GIL' + cmd = ''' +from threading import Thread + +class TestThread(Thread): + # These threads would run forever, but we'll interrupt things with the + # debugger + def run(self): + i = 0 + while 1: + i += 1 + +t = {} +for i in range(4): + t[i] = TestThread() + t[i].start() + +# Trigger a breakpoint on the main thread +id(42) + +''' + # Verify with "py-bt": + gdb_output = self.get_stack_trace(cmd, + cmds_after_breakpoint=['thread apply all py-bt']) + self.assertIn('Waiting for the GIL', gdb_output) + + # Verify with "py-bt-full": + gdb_output = self.get_stack_trace(cmd, + cmds_after_breakpoint=['thread apply all py-bt-full']) + self.assertIn('Waiting for the GIL', gdb_output) + + @unittest.skipIf(python_is_optimized(), + "Python was compiled with optimizations") + # Some older versions of gdb will fail with + # "Cannot find new threads: generic error" + # unless we add LD_PRELOAD=PATH-TO-libpthread.so.1 as a workaround + @unittest.skipUnless(_thread, + "Python was compiled without thread support") + def test_gc(self): + 'Verify that "py-bt" indicates if a thread is garbage-collecting' + cmd = ('from gc import collect\n' + 'id(42)\n' + 'def foo():\n' + ' collect()\n' + 'def bar():\n' + ' foo()\n' + 'bar()\n') + # Verify with "py-bt": + gdb_output = self.get_stack_trace(cmd, + cmds_after_breakpoint=['break update_refs', 'continue', 'py-bt'], + ) + self.assertIn('Garbage-collecting', gdb_output) + + # Verify with "py-bt-full": + gdb_output = self.get_stack_trace(cmd, + cmds_after_breakpoint=['break update_refs', 'continue', 'py-bt-full'], + ) + self.assertIn('Garbage-collecting', gdb_output) + + @unittest.skipIf(python_is_optimized(), + "Python was compiled with optimizations") + # Some older versions of gdb will fail with + # "Cannot find new threads: generic error" + # unless we add LD_PRELOAD=PATH-TO-libpthread.so.1 as a workaround + @unittest.skipUnless(_thread, + "Python was compiled without thread support") + def test_pycfunction(self): + 'Verify that "py-bt" displays invocations of PyCFunction instances' + # Tested function must not be defined with METH_NOARGS or METH_O, + # otherwise call_function() doesn't call PyCFunction_Call() + cmd = ('from time import gmtime\n' + 'def foo():\n' + ' gmtime(1)\n' + 'def bar():\n' + ' foo()\n' + 'bar()\n') + # Verify with "py-bt": + gdb_output = self.get_stack_trace(cmd, + breakpoint='time_gmtime', + cmds_after_breakpoint=['bt', 'py-bt'], + ) + self.assertIn('\n.*") + +class PyLocalsTests(DebuggerTests): + @unittest.skipIf(python_is_optimized(), + "Python was compiled with optimizations") + def test_basic_command(self): + bt = self.get_stack_trace(script=self.get_sample_script(), + cmds_after_breakpoint=['py-up', 'py-locals']) + self.assertMultilineMatches(bt, + r".*\nargs = \(1, 2, 3\)\n.*") + + @unittest.skipUnless(HAS_PYUP_PYDOWN, "test requires py-up/py-down commands") + @unittest.skipIf(python_is_optimized(), + "Python was compiled with optimizations") + def test_locals_after_up(self): + bt = self.get_stack_trace(script=self.get_sample_script(), + cmds_after_breakpoint=['py-up', 'py-up', 'py-locals']) + self.assertMultilineMatches(bt, + r".*\na = 1\nb = 2\nc = 3\n.*") + +def test_main(): + if support.verbose: + print("GDB version %s.%s:" % (gdb_major_version, gdb_minor_version)) + for line in gdb_version.splitlines(): + print(" " * 4 + line) + run_unittest(PrettyPrintTests, + PyListTests, + StackNavigationTests, + PyBtTests, + PyPrintTests, + PyLocalsTests + ) + +if __name__ == "__main__": + test_main() diff --git a/toxygen/tests/test_gdb.urls b/toxygen/tests/test_gdb.urls new file mode 100644 index 0000000..5f2cb10 --- /dev/null +++ b/toxygen/tests/test_gdb.urls @@ -0,0 +1 @@ +https://github.com/akheron/cpython/raw/master/Lib/test/test_gdb.py diff --git a/toxygen/tests/tests_socks.py b/toxygen/tests/tests_socks.py new file mode 100644 index 0000000..640916d --- /dev/null +++ b/toxygen/tests/tests_socks.py @@ -0,0 +1,1885 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +# +# @file tests.py +# @author Wei-Ning Huang (AZ) +# +# Copyright (C) 2013 - 2014 Wei-Ning Huang (AZ) +# All Rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# + +"""Originaly from https://github.com/oxij/PyTox c-toxcore-02 branch +which itself was forked from https://github.com/aitjcize/PyTox/ + +Modified to work with +""" + +import ctypes +import faulthandler +import hashlib +import logging +import os +import random +import re +import sys +import threading +import traceback +import unittest +from ctypes import * + +faulthandler.enable() + +import warnings + +warnings.filterwarnings('ignore') + +try: + from io import BytesIO + + import certifi + import pycurl +except ImportError: + pycurl = None + +try: + import coloredlogs + os.environ['COLOREDLOGS_LEVEL_STYLES'] = 'spam=22;debug=28;verbose=34;notice=220;warning=202;success=118,bold;error=124;critical=background=red' +except ImportError as e: + logging.log(logging.DEBUG, f"coloredlogs not available: {e}") + coloredlogs = None + +try: + import color_runner +except ImportError as e: + logging.log(logging.DEBUG, f"color_runner not available: {e}") + color_runner = None + +import wrapper +import wrapper.toxcore_enums_and_consts as enums +from wrapper.tox import Tox +from wrapper.toxcore_enums_and_consts import (TOX_ADDRESS_SIZE, TOX_CONNECTION, + TOX_FILE_CONTROL, + TOX_MESSAGE_TYPE, + TOX_SECRET_KEY_SIZE, + TOX_USER_STATUS) + +try: + import support_testing as ts +except ImportError: + import wrapper_tests.support_testing as ts + +try: + from tests.toxygen_tests import test_sound_notification + bIS_NOT_TOXYGEN = False +except ImportError: + bIS_NOT_TOXYGEN = True + +# from PyQt5 import QtCore +import time + +sleep = time.sleep + +global LOG +LOG = logging.getLogger('TestS') +# just print to stdout so there is no complications from logging. +def LOG_ERROR(l): print('EROR+ '+l) +def LOG_WARN(l): print('WARN+ '+l) +def LOG_INFO(l): print('INFO+ '+l) +def LOG_DEBUG(l): print('DEBUG+ '+l) +def LOG_TRACE(l): pass # print('TRAC+ '+l) + +ADDR_SIZE = 38 * 2 +CLIENT_ID_SIZE = 32 * 2 +THRESHOLD = 25 + +global oTOX_OPTIONS +oTOX_OPTIONS = {} + +bIS_LOCAL = 'new' in sys.argv or 'main' in sys.argv or 'newlocal' in sys.argv + +# Patch unittest for Python version <= 2.6 +if not hasattr(unittest, 'skip'): + def unittest_skip(reason): + def _wrap1(func): + def _wrap2(self, *args, **kwargs): + pass + return _wrap2 + return _wrap1 + unittest.skip = unittest_skip + +if not hasattr(unittest, 'expectedFailureIf'): + def unittest_expectedFailureIf(condition, reason): + def _wrap1(test_item): + def _wrap2(self, *args, **kwargs): + if condition: + test_item.__unittest_expecting_failure__ = True + pass + return _wrap2 + return _wrap1 + + unittest.expectedFailureIf = unittest_expectedFailureIf + +def expectedFailure(test_item): + test_item.__unittest_expecting_failure__ = True + return test_item + +class ToxOptions(): + def __init__(self): + self.ipv6_enabled = True + self.udp_enabled = True + self.proxy_type = 0 + self.proxy_host = '' + self.proxy_port = 0 + self.start_port = 0 + self.end_port = 0 + self.tcp_port = 0 + self.savedata_type = 0 # 1=toxsave, 2=secretkey + self.savedata_data = b'' + self.savedata_length = 0 + self.local_discovery_enabled = False + self.dht_announcements_enabled = True + self.hole_punching_enabled = False + self.experimental_thread_safety = False + +class App(): + def __init__(self): + self.mode = 0 +oAPP = App() + +class AliceTox(Tox): + + def __init__(self, opts, app=None): + + super(AliceTox, self).__init__(opts, app=app) + self._address = self.self_get_address() + self.name = 'alice' + self._opts = opts + self._app = app + +class BobTox(Tox): + + def __init__(self, opts, app=None): + super(BobTox, self).__init__(opts, app=app) + self._address = self.self_get_address() + self.name = 'bob' + self._opts = opts + self._app = app + +class BaseThread(threading.Thread): + + def __init__(self, name=None, target=None): + if name: + super().__init__(name=name, target=target) + else: + super().__init__(target=target) + self._stop_thread = False + self.name = name + + def stop_thread(self, timeout=-1): + self._stop_thread = True + if timeout < 0: + timeout = ts.iTHREAD_TIMEOUT + i = 0 + while i < ts.iTHREAD_JOINS: + self.join(timeout) + if not self.is_alive(): break + i = i + 1 + else: + LOG.warning(f"{self.name} BLOCKED") + +class ToxIterateThread(BaseThread): + + def __init__(self, tox): + super().__init__(name='ToxIterateThread') + self._tox = tox + + def run(self): + while not self._stop_thread: + self._tox.iterate() + sleep(self._tox.iteration_interval() / 1000) + +global bob, alice +bob = alice = None + +def prepare(self): + global bob, alice + def bobs_on_self_connection_status(iTox, connection_state, *args): + status = connection_state + self.bob.dht_connected = status + self.bob.mycon_time = time.time() + try: + if status != TOX_CONNECTION['NONE']: + LOG_DEBUG(f"bobs_on_self_connection_status TRUE {status}" \ + +f" last={int(self.bob.mycon_time)}" ) + self.bob.mycon_status = True + else: + LOG_DEBUG(f"bobs_on_self_connection_status FALSE {status}" \ + +f" last={int(self.bob.mycon_time)}" ) + self.bob.mycon_status = False + except Exception as e: + LOG_ERROR(f"bobs_on_self_connection_status {e}") + else: + if self.bob.self_get_connection_status() != status: + LOG_WARN(f"bobs_on_self_connection_status DISAGREE {status}") + + def alices_on_self_connection_status(iTox, connection_state, *args): + #FixMe connection_num + status = connection_state + self.alice.dht_connected = status + self.alice.mycon_time = time.time() + try: + if status != TOX_CONNECTION['NONE']: + LOG_DEBUG(f"alices_on_self_connection_status TRUE {status}" \ + +f" last={int(self.alice.mycon_time)}" ) + self.alice.mycon_status = True + else: + LOG_WARN(f"alices_on_self_connection_status FALSE {status}" \ + +f" last={int(self.alice.mycon_time)}" ) + self.alice.mycon_status = False + except Exception as e: + LOG_ERROR(f"alices_on_self_connection_status error={e}") + else: + if self.alice.self_get_connection_status() != status: + LOG_WARN(f"alices_on_self_connection_status != {status}") + self.alice.dht_connected = status + + opts = oToxygenToxOptions(oTOX_OARGS) + alice = AliceTox(opts, app=oAPP) + alice.oArgs = opts + alice.dht_connected = -1 + alice.mycon_status = False + alice.mycon_time = 1 + alice.callback_self_connection_status(alices_on_self_connection_status) + + bob = BobTox(opts, app=oAPP) + bob.oArgs = opts + bob.dht_connected = -1 + bob.mycon_status = False + bob.mycon_time = 1 + bob.callback_self_connection_status(bobs_on_self_connection_status) + if not bIS_LOCAL and not ts.bAreWeConnected(): + LOG.warning(f"doOnce not local and NOT CONNECTED") + return [bob, alice] + +class ToxSuite(unittest.TestCase): + failureException = RuntimeError + + @classmethod + def setUpClass(cls): + global oTOX_OARGS + assert oTOX_OPTIONS + assert oTOX_OARGS + + if not hasattr(cls, 'alice') and not hasattr(cls, 'bob'): + l = prepare(cls) + assert l + cls.bob, cls.alice = l + if not hasattr(cls.bob, '_main_loop'): + cls.bob._main_loop = ToxIterateThread(cls.bob) + cls.bob._main_loop.start() + LOG.debug(f"cls.bob._main_loop: ") # {threading.enumerate()} + if not hasattr(cls.alice, '_main_loop'): + cls.alice._main_loop = ToxIterateThread(cls.alice) + cls.alice._main_loop.start() + LOG.debug(f"cls.alice._main_loop: ") # {threading.enumerate()} + + cls.lUdp = ts.generate_nodes( + oArgs=oTOX_OARGS, + nodes_count=2*ts.iNODES, + ipv='ipv4', + udp_not_tcp=True) + + cls.lTcp = ts.generate_nodes( + oArgs=oTOX_OARGS, + nodes_count=2*ts.iNODES, + ipv='ipv4', + udp_not_tcp=False) + + @classmethod + def tearDownClass(cls): + cls.bob._main_loop.stop_thread() + cls.alice._main_loop.stop_thread() + if False: + cls.alice.kill() + cls.bob.kill() + del cls.bob + del cls.alice + + def setUp(self): + """ + """ + if hasattr(self, 'baid') and self.baid >= 0 and \ + self.baid in self.bob.self_get_friend_list(): + LOG.warn(f"setUp ALICE IS ALREADY IN BOBS FRIEND LIST") + elif self.bob.self_get_friend_list_size() >= 1: + LOG.warn(f"setUp BOB STILL HAS A FRIEND LIST") + + if hasattr(self, 'abid') and self.abid >= 0 and \ + self.abid in self.alice.self_get_friend_list(): + LOG.warn(f"setUp BOB IS ALREADY IN ALICES FRIEND LIST") + elif self.alice.self_get_friend_list_size() >= 1: + LOG.warn(f"setUp ALICE STILL HAS A FRIEND LIST") + + def tearDown(self): + """ + """ + if hasattr(self, 'baid') and self.baid >= 0 and \ + self.baid in self.bob.self_get_friend_list(): + LOG.warn(f"tearDown ALICE IS STILL IN BOBS FRIEND LIST") + elif self.bob.self_get_friend_list_size() >= 1: + LOG.warn(f"tearDown BOBS STILL HAS A FRIEND LIST") + + if hasattr(self, 'abid') and self.abid >= 0 and \ + self.abid in self.alice.self_get_friend_list(): + LOG.warn(f"tearDown BOB IS STILL IN ALICES FRIEND LIST") + elif self.bob.self_get_friend_list_size() >= 1: + LOG.warn(f"tearDown ALICE STILL HAS A FRIEND LIST") + + def run(self, result=None): + """ Stop after first error """ + if not result.errors: + super(ToxSuite, self).run(result) + + def get_connection_status(self): + if self.bob.mycon_time == -1 or self.alice.mycon_time == -1: + pass + # drop through + elif self.bob.dht_connected == TOX_CONNECTION['NONE']: + return False + elif self.alice.dht_connected == TOX_CONNECTION['NONE']: + return False + + # if not self.connected + if self.bob.self_get_connection_status() == TOX_CONNECTION['NONE']: + return False + if self.alice.self_get_connection_status() == TOX_CONNECTION['NONE']: + return False + return True + + def loop(self, n): + """ + t:iterate + t:iteration_interval + """ + interval = self.bob.iteration_interval() + for i in range(n): + self.alice.iterate() + self.bob.iterate() + sleep(interval / 1000.0) + + def call_bootstrap(self, num=None, lToxes=None, i=0): + if num == None: num=ts.iNODES +# LOG.debug(f"call_bootstrap network={oTOX_OARGS.network}") + if oTOX_OARGS.network in ['new', 'newlocal', 'localnew']: + ts.bootstrap_local(self.lUdp, [self.alice, self.bob]) + elif not ts.bAreWeConnected(): + LOG.warning('we are NOT CONNECTED') + else: + random.shuffle(self.lUdp) + if oTOX_OARGS.proxy_port > 0: + lElts = self.lUdp[:1] + else: + lElts = self.lUdp[:num+i] + LOG.debug(f"call_bootstrap ts.bootstrap_udp {len(lElts)}") + if lToxes is None: lToxes = [self.alice, self.bob] + ts.bootstrap_udp(lElts, lToxes) + random.shuffle(self.lTcp) + lElts = self.lTcp[:num+i] + LOG.debug(f"call_bootstrap ts.bootstrap_tcp {len(lElts)}") + ts.bootstrap_tcp(lElts, lToxes) + + def loop_until_connected(self, num=None): + """ + t:on_self_connection_status + t:self_get_connection_status + """ + i = 0 + bRet = None + while i <= THRESHOLD : + if (self.alice.mycon_status and self.bob.mycon_status): + bRet = True + break + if i % 5 == 0: + j = i//5 + self.call_bootstrap(num, lToxes=None, i=j) + s = '' + if i == 0: s = '\n' + LOG.info(s+"loop_until_connected " \ + +" #" + str(i) \ + +" BOB=" +repr(self.bob.self_get_connection_status()) \ + +" ALICE=" +repr(self.alice.self_get_connection_status()) + +f" BOBS={self.bob.mycon_status}" \ + +f" ALICES={self.alice.mycon_status}" \ + +f" last={int(self.bob.mycon_time)}" ) + if (self.alice.mycon_status and self.bob.mycon_status): + bRet = True + break + if (self.alice.self_get_connection_status() and + self.bob.self_get_connection_status()): + LOG_WARN(f"loop_until_connected disagree status() DISAGREE" \ + +f' self.bob.mycon_status={self.bob.mycon_status}' \ + +f' alice.mycon_status={self.alice.mycon_status}' \ + +f" last={int(self.bob.mycon_time)}" ) + bRet = True + break + i += 1 + self.loop(100) + else: + bRet = False + + if bRet or \ + ( self.bob.self_get_connection_status() != TOX_CONNECTION['NONE'] and \ + self.alice.self_get_connection_status() != TOX_CONNECTION['NONE'] ): + LOG.info(f"loop_until_connected returning True {i}" \ + +f" BOB={self.bob.self_get_connection_status()}" \ + +f" ALICE={self.alice.self_get_connection_status()}" \ + +f" last={int(self.bob.mycon_time)}" ) + return True + else: + LOG.warning(f"loop_until_connected returning False {i}" \ + +f" BOB={self.bob.self_get_connection_status()}" \ + +f" ALICE={self.alice.self_get_connection_status()}" \ + +f" last={int(self.bob.mycon_time)}" ) + return False + + def wait_obj_attr(self, obj, attr): + return wait_otox_attrs(self, obj, [attr]) + + def wait_objs_attr(self, objs, attr): + i = 0 + while i <= THRESHOLD: + if i % 5 == 0: + num = None + j = i//5 + self.call_bootstrap(num, objs, i=j) + LOG.debug("wait_objs_attr " +repr(objs) \ + +" for " +repr(attr) \ + +" " +str(i)) + if all([getattr(obj, attr) for obj in objs]): + return True + self.loop(100) + i += 1 + else: + LOG.error(f"wait_obj_attr i >= {THRESHOLD}") + + return all([getattr(obj, attr) for obj in objs]) + + def wait_otox_attrs(self, obj, attrs): + i = 0 + while i <= THRESHOLD: + if i % 5 == 0: + num = None + j = 0 + if obj.mycon_time == 1: + num = 4 + j = i//5 + self.call_bootstrap(num, [obj], i=j) + LOG.debug(f"wait_otox_attrs {obj.name} for {attrs} {i}" \ + +f" last={int(obj.mycon_time)}") + if all([getattr(obj, attr) is not None for attr in attrs]): + return True + self.loop(100) + i += 1 + else: + LOG.warning(f"wait_otox_attrs i >= {THRESHOLD}") + + return all([getattr(obj, attr) for attr in attrs]) + + def wait_ensure_exec(self, method, args): + i = 0 + oRet = None + while i <= THRESHOLD: + if i % 5 == 0: + j = i//5 + self.call_bootstrap(num=None, lToxes=None, i=j) + LOG.debug("wait_ensure_exec " \ + +" " +str(method) + +" " +str(i)) + try: + oRet = method(*args) + if oRet: + LOG.info(f"wait_ensure_exec oRet {oRet!r}") + return True + except ArgumentError as e: + # ArgumentError('This client is currently NOT CONNECTED to the friend.') + # dunno + LOG.warning(f"wait_ensure_exec ArgumentError {e}") + return False + except Exception as e: + LOG.warning(f"wait_ensure_exec EXCEPTION {e}") + return False + sleep(3) + i += 1 + else: + LOG.error(f"wait_ensure_exec i >= {1*THRESHOLD}") + return False + + return oRet + + def bob_add_alice_as_friend_norequest(self): + if hasattr(self, 'baid') and self.baid >= 0 and \ + self.baid in self.bob.self_get_friend_list(): + LOG.warn('Alice is already in bobs friend list') + return True + if self.bob.self_get_friend_list_size() >= 1: + LOG.warn(f'Bob has a friend list {self.bob.self_get_friend_list()}') + return True + + MSG = 'Hi, this is Bob.' + iRet = self.bob.friend_add_norequest(self.alice._address) + self.baid = self.bob.friend_by_public_key(self.alice._address) + assert self.baid >= 0, self.baid + assert self.bob.friend_exists(self.baid), "bob.friend_exists" + assert not self.bob.friend_exists(self.baid + 1) + assert self.baid in self.bob.self_get_friend_list() + assert self.bob.self_get_friend_list_size() >= 1 + return iRet >= 0 + + def alice_add_bob_as_friend_norequest(self): + if hasattr(self, 'abid') and self.abid >= 0 and \ + self.abid in self.alice.self_get_friend_list(): + LOG.warn('Alice is already in Bobs friend list') + return True + if self.alice.self_get_friend_list_size() >= 1: + LOG.warn(f'Alice has a friend list {self.alice.self_get_friend_list()}') + + MSG = 'Hi Bob, this is Alice.' + iRet = self.alice.friend_add_norequest(self.bob._address) + self.abid = self.alice.friend_by_public_key(self.bob._address) + assert self.abid >= 0, self.abid + assert self.abid in self.alice.self_get_friend_list() + assert self.alice.friend_exists(self.abid), "alice.friend_exists" + assert not self.alice.friend_exists(self.abid + 1) + assert self.alice.self_get_friend_list_size() >= 1 + return iRet >= 0 + + def both_add_as_friend_norequest(self): + assert self.bob_add_alice_as_friend_norequest() + if not hasattr(self, 'baid') or self.baid < 0: + raise AssertionError("both_add_as_friend_norequest bob, 'baid'") + + assert self.alice_add_bob_as_friend_norequest() + if not hasattr(self, 'abid') or self.abid < 0: + raise AssertionError("both_add_as_friend_norequest alice, 'abid'") + + #: Test last online + assert self.alice.friend_get_last_online(self.abid) is not None + assert self.bob.friend_get_last_online(self.baid) is not None + return True + + def bob_add_alice_as_friend(self): + """ + t:friend_add + t:on_friend_request + t:friend_by_public_key + """ + MSG = 'Alice, this is Bob.' + sSlot = 'friend_request' + + def alices_on_friend_request(iTox, + public_key, + message_data, + message_data_size, + *largs): + LOG_DEBUG(f"alices_on_friend_request: " +repr(message_data)) + try: + assert str(message_data, 'UTF-8') == MSG + LOG_INFO(f"alices_on_friend_request: friend_added = True ") + except Exception as e: + LOG_WARN(f"alices_on_friend_request: Exception {e}") + # return + setattr(self.bob, sSlot, True) + + setattr(self.bob, sSlot, None) + inum = -1 + self.alice.callback_friend_request(alices_on_friend_request) + try: + inum = self.bob.friend_add(self.alice._address, bytes(MSG, 'UTF-8')) + if not inum >= 0: + LOG.warning('bob.friend_add !>= 0 ' +repr(inum)) + if not self.wait_otox_attrs(self.bob, [sSlot]): + return False + except Exception as e: + LOG.error(f"bob.friend_add EXCEPTION {e}") + return False + finally: + self.bob.callback_friend_message(None) + + self.baid = self.bob.friend_by_public_key(self.alice._address) + assert self.baid >= 0, self.baid + assert self.bob.friend_exists(self.baid) + assert not self.bob.friend_exists(self.baid + 1) + assert self.baid in self.bob.self_get_friend_list() + assert self.bob.self_get_friend_list_size() >= 1 + return True + + def alice_add_bob_as_friend(self): + """ + t:friend_add + t:on_friend_request + t:friend_by_public_key + """ + MSG = 'Bob, this is Alice.' + sSlot = 'friend_request' + + def bobs_on_friend_request(iTox, + public_key, + message_data, + message_data_size, + *largs): + LOG_DEBUG(f"bobs_on_friend_request: " +repr(message_data)) + try: + assert str(message_data, 'UTF-8') == MSG + LOG_INFO(f"bobs_on_friend_request: friend_added = True ") + except Exception as e: + LOG_WARN(f"bobs_on_friend_request: Exception {e}") + # return + else: + setattr(self.alice, sSlot, True) + + setattr(self.alice, sSlot, None) + inum = -1 + self.bob.callback_friend_request(bobs_on_friend_request) + try: + inum = self.alice.friend_add(self.bob._address, bytes(MSG, 'UTF-8')) + if not inum >= 0: + LOG.warning('alice.friend_add !>= 0 ' +repr(inum)) + if not self.wait_obj_attr(self.alice, sSlot): + return False + except Exception as e: + LOG.error(f"alice.friend_add EXCEPTION {e}") + return False + finally: + self.bob.callback_friend_message(None) + self.abid = self.alice.friend_by_public_key(self.bob._address) + assert self.abid >= 0, self.abid + assert self.alice.friend_exists(self.abid) + assert not self.alice.friend_exists(self.abid + 1) + assert self.abid in self.alice.self_get_friend_list() + assert self.alice.self_get_friend_list_size() >= 1 + return True + + def both_add_as_friend(self): + assert self.bob_add_alice_as_friend() + assert self.alice_add_bob_as_friend() + + #: Test last online + assert self.alice.friend_get_last_online(self.abid) is not None + assert self.bob.friend_get_last_online(self.baid) is not None + + def bob_add_alice_as_friend_and_status(self): + if oTOX_OARGS.bIS_LOCAL: + assert self.bob_add_alice_as_friend_norequest() + else: + assert self.bob_add_alice_as_friend() + + #: Wait until both are online + self.bob.friend_conn_status = False + def bobs_on_friend_connection_status(iTox, friend_id, iStatus, *largs): + LOG_INFO(f"bobs_on_friend_connection_status {friend_id} ?>=0" +repr(iStatus)) + if iStatus > 0: + self.bob.friend_conn_status = True + + self.bob.friend_status = None + def bobs_on_friend_status(iTox, friend_id, iStatus, *largs): + LOG_INFO(f"bobs_on_friend_status {friend_id} ?>=0" +repr(iStatus)) + if iStatus > 0: + self.bob.friend_status = True + + self.alice.friend_conn_status = None + def alices_on_friend_connection_status(iTox, friend_id, iStatus, *largs): + LOG_INFO(f"alices_on_friend_connection_status {friend_id} ?>=0 " +repr(iStatus)) + if iStatus > 0: + self.alice.friend_conn_status = True + + self.alice.friend_status = False + def alices_on_friend_status(iTox, friend_id, iStatus, *largs): + LOG_INFO(f"alices_on_friend_status {friend_id} ?>=0 " +repr(iStatus)) + if iStatus > 0: + self.alice.friend_status = True + + self.alice.callback_friend_connection_status(alices_on_friend_connection_status) + self.alice.callback_friend_status(alices_on_friend_status) + try: + LOG.info("bob_add_alice_as_friend_and_status waiting for alice connections") + if not self.wait_otox_attrs(self.alice, + ['friend_conn_status', + 'friend_status']): + return False + + self.bob.callback_friend_connection_status(bobs_on_friend_connection_status) + self.bob.callback_friend_status(bobs_on_friend_status) + + LOG.info("bob_add_alice_as_friend_and_status waiting for bob connections") + if not self.wait_otox_attrs(self.bob, + ['friend_conn_status', + 'friend_status']): + return False + except Exception as e: + LOG.error(f"bob_add_alice_as_friend_and_status ERROR {e}") + return False + finally: + self.alice.callback_friend_connection_status(None) + self.bob.callback_friend_connection_status(None) + self.alice.callback_friend_status(None) + self.bob.callback_friend_status(None) + return True + + def friend_delete(self, fname, baid): + #: Test delete friend + assert getattr(self, fname).friend_exists(baid) + getattr(self, fname).friend_delete(baid) + self.loop(50) + assert not self.bob.friend_exists(baid) + + def warn_if_no_cb(self, alice, sSlot): + if not hasattr(alice, sSlot+'_cb') or \ + not getattr(alice, sSlot+'_cb'): + LOG.warning(f"self.bob.{sSlot}_cb NOT EXIST") + + def warn_if_cb(self, alice, sSlot): + if hasattr(self.bob, sSlot+'_cb') and \ + getattr(self.bob, sSlot+'_cb'): + LOG.warning(f"self.bob.{sSlot}_cb EXIST") + + # tests are executed in order + def test_notice_log(self): # works + notice = '/var/lib/tor/.SelekTOR/3xx/cache/9050/notice.log' + if True or os.path.exists(notice): + iRet = os.system(f"sudo sed -e '1,/.notice. Bootstrapped 100%/d' {notice}" + \ + "| grep 'Tried for 120 seconds to get a connection to :0.'") + if iRet == 0: + raise SystemExit("seconds to get a connection to :0") + else: + LOG.debug(f"checked {notice}") + + def test_tests_logging(self): # works + with self.assertLogs('foo', level='INFO') as cm: + logging.getLogger('foo').info('first message') + logging.getLogger('foo.bar').error('second message') + logging.getLogger('foo.bar.baz').debug('third message') + self.assertEqual(cm.output, ['INFO:foo:first message', + 'ERROR:foo.bar:second message']) + + def test_tests_start(self): # works + LOG.info("test_tests_start " ) + port = ts.tox_bootstrapd_port() + + assert len(self.bob._address) == 2*TOX_ADDRESS_SIZE, len(self.bob._address) + assert len(self.alice._address) == 2*TOX_ADDRESS_SIZE, \ + len(self.alice._address) + + def test_bootstrap_local_netstat(self): # works + """ + t:bootstrap + """ + if oTOX_OARGS.network not in ['new', 'newlocal', 'local']: + return + + port = ts.tox_bootstrapd_port() + if not port: + return + iStatus = os.system(f"""netstat -nle4 | grep :{port}""") + if iStatus == 0: + LOG.info(f"bootstrap_local_netstat port {port} iStatus={iStatus}") + else: + LOG.warning(f"bootstrap_local_netstat NOT {port} iStatus={iStatus}") + + @unittest.skipIf(not bIS_LOCAL, "local test") + def test_bootstrap_local(self): # works + """ + t:bootstrap + """ + # get port from /etc/tox-bootstrapd.conf 33445 + self.call_bootstrap() + # ts.bootstrap_local(self, self.lUdp) + i = 0 + iStatus = -1 + while i < 10: + i = i + 1 + iStatus = self.bob.self_get_connection_status() + if iStatus != TOX_CONNECTION['NONE']: + break + sleep(3) + else: + pass + + o1 = self.alice.self_get_dht_id() + assert len(o1) == 64 + o2 = self.bob.self_get_dht_id() + assert len(o2) == 64 + +# if o1 != o2: LOG.warning(f"bootstrap_local DHT NOT same {o1} {o2} iStatus={iStatus}") + + iStatus = self.bob.self_get_connection_status() + if iStatus != TOX_CONNECTION['NONE']: + LOG.info(f"bootstrap_local connected iStatus={iStatus}") + return True + iStatus = self.alice.self_get_connection_status() + if iStatus != TOX_CONNECTION['NONE']: + LOG.info(f"bootstrap_local connected iStatus={iStatus}") + return True + LOG.warning(f"bootstrap_local NOT CONNECTED iStatus={iStatus}") + return False + + def test_bootstrap_iNmapInfo(self): # works + if os.environ['USER'] != 'root': + return + if oTOX_OARGS.network in ['new', 'newlocal', 'localnew']: + lElts = self.lUdp + elif oTOX_OARGS.proxy_port > 0: + lElts = self.lTcp + else: + lElts = self.lUdp + lRetval = [] + random.shuffle(lElts) + # assert + ts.bootstrap_iNmapInfo(lElts, oTOX_OARGS, bIS_LOCAL, iNODES=8) + + def test_self_get_secret_key(self): # works + """ + t:self_get_secret_key + """ + # test_self_get_secret_key + CRYPTO_SECRET_KEY_SIZE = 32 + secret_key = create_string_buffer(CRYPTO_SECRET_KEY_SIZE) + oRet0 = self.alice.self_get_secret_key(secret_key) + assert oRet0, repr(oRet0) + LOG.info('test_self_get_secret_key ' +repr(oRet0)) + assert len(str(oRet0)) + del secret_key + + def test_self_get_public_keys(self): # works + """ + t:self_get_secret_key + t:self_get_public_key + """ + + LOG.info('test_self_get_public_keys self.alice.self_get_secret_key') + oRet0 = self.alice.self_get_secret_key() + assert len(oRet0) + LOG.info('test_self_get_public_keys ' +repr(oRet0)) + oRet1 = self.alice.self_get_public_key() + assert len(oRet1) + LOG.info('test_self_get_public_keys ' +repr(oRet1)) + assert oRet0 != oRet1, repr(oRet0) +' != ' +repr(oRet1) + + def test_self_name(self): # works + """ + t:self_set_name + t:self_get_name + t:self_get_name_size + """ + self.alice.self_set_name('Alice') + assert self.alice.self_get_name() == 'Alice' + assert self.alice.self_get_name_size() == len('Alice') + self.bob.self_set_name('Bob') + assert self.bob.self_get_name() == 'Bob' + assert self.bob.self_get_name_size() == len('Bob') + + @unittest.skip('loud') + @unittest.skipIf(bIS_NOT_TOXYGEN or oTOX_OARGS.mode == 0, 'not testing in toxygen') + def test_sound_notification(self): # works + """ + Plays sound notification + :param type of notification + """ + from tests.toxygen_tests import test_sound_notification + test_sound_notification(self) + + def test_address(self): # works + """ + t:self_get_address + t:self_get_nospam + t:self_set_nospam + t:self_get_keys + """ + assert len(self.alice.self_get_address()) == ADDR_SIZE + assert len(self.bob.self_get_address()) == ADDR_SIZE + + self.alice.self_set_nospam(0x12345678) + assert self.alice.self_get_nospam() == 0x12345678 + self.loop(50) + + if hasattr(self.alice, 'self_get_keys'): + pk, sk = self.alice.self_get_keys() + assert pk == self.alice.self_get_address()[:CLIENT_ID_SIZE] + + def test_status_message(self): # works + MSG = 'Happy' + self.alice.self_set_status_message(MSG) + self.loop(100) + assert self.alice.self_get_status_message() == MSG, \ + self.alice.self_get_status_message() +' is not ' +MSG + assert self.alice.self_get_status_message_size() == len(MSG) + + def test_loop_until_connected(self): # works + assert self.loop_until_connected() + + def test_self_get_udp_port(self): # works + """ + t:self_get_udp_port + """ + if hasattr(oTOX_OPTIONS, 'udp_port') and oTOX_OPTIONS.udp_port: + o = self.alice.self_get_udp_port() + LOG.info('self_get_udp_port alice ' +repr(o)) + assert o > 0 + o = self.bob.self_get_udp_port() + LOG.info('self_get_udp_port bob ' +repr(o)) + assert o > 0 + + def test_self_get_tcp_port(self): # works + """ + t:self_get_tcp_port + """ + if hasattr(oTOX_OPTIONS, 'tcp_port') and oTOX_OPTIONS.tcp_port: + # errors if tcp_port <= 0 + o = self.alice.self_get_tcp_port() + LOG.info('self_get_tcp_port ' +repr(o)) + o = self.bob.self_get_tcp_port() + LOG.info('self_get_tcp_port ' +repr(o)) + + def test_get_dht_id(self): # works + """ + t:self_get_dht_id + """ + o1 = self.alice.self_get_dht_id() + assert len(o1) == 64 + o2 = self.bob.self_get_dht_id() + assert len(o2) == 64 + + def test_bob_assert_connection_status(self): # works + if self.bob.self_get_connection_status() == TOX_CONNECTION['NONE']: + RuntimeError("ERROR: NOT CONNECTED " \ + +repr(self.bob.self_get_connection_status())) + + def test_alice_assert_connection_status(self): # works + if self.alice.self_get_connection_status() == TOX_CONNECTION['NONE']: + RuntimeError("ERROR: NOT CONNECTED " \ + +repr(self.alice.self_get_connection_status())) + + def test_bob_assert_mycon_status(self): # works + if self.bob.mycon_status == False: + RuntimeError("ERROR: NOT CONNECTED " \ + +repr(self.bob.mycon_status)) + + def test_alice_assert_mycon_status(self): # works + if self.alice.mycon_status == False: + RuntimeError("ERROR: NOT CONNECTED " \ + +repr(self.alice.mycon_status)) + + def test_bob_add_alice_as_friend_norequest(self): # works + assert len(self.bob.self_get_friend_list()) == 0 + assert self.bob_add_alice_as_friend_norequest() + #: Test last online + assert self.bob.friend_get_last_online(self.baid) is not None + self.bob.friend_delete(self.baid) + + def test_alice_add_bob_as_friend_norequest(self): # works + assert len(self.alice.self_get_friend_list()) == 0 + assert self.alice_add_bob_as_friend_norequest() + assert len(self.alice.self_get_friend_list()) != 0 + #: Test last online + assert self.alice.friend_get_last_online(self.abid) is not None + self.alice.friend_delete(self.abid) + + def test_both_add_as_friend_norequest(self): # works + assert len(self.bob.self_get_friend_list()) == 0 + assert len(self.alice.self_get_friend_list()) == 0 + self.both_add_as_friend_norequest() + + self.bob.friend_delete(self.baid) + self.alice.friend_delete(self.abid) + assert len(self.bob.self_get_friend_list()) == 0 + assert len(self.alice.self_get_friend_list()) == 0 + + def test_bob_add_alice_as_friend_and_status(self): + self.bob_add_alice_as_friend_and_status() + self.bob.friend_delete(self.baid) + + @unittest.skip('malloc_consolidate(): invalid chunk size') +# @unittest.skipIf(bIS_LOCAL, "local test") +# @expectedFailure # (bIS_LOCAL, "local test") + def test_bob_add_alice_as_friend(self): # fails + assert len(self.bob.self_get_friend_list()) == 0 + try: + assert self.bob_add_alice_as_friend() + #: Test last online + assert self.bob.friend_get_last_online(self.baid) is not None + except AssertionError as e: + #WTF? + self.bob.friend_delete(self.baid) + raise RuntimeError(f"Failed test {e}") + finally: + self.bob.friend_delete(self.baid) + assert len(self.bob.self_get_friend_list()) == 0 + + @unittest.skip('malloc_consolidate(): invalid chunk size') +# @unittest.skipIf(bIS_LOCAL, "local test") +# @expectedFailure + def test_alice_add_bob_as_friend(self): # fails + assert len(self.bob.self_get_friend_list()) == 0 + try: + assert self.alice_add_bob_as_friend() + #: Test last online + assert self.alice.friend_get_last_online(self.abid) is not None + except AssertionError as e: + raise RuntimeError(f"Failed test {e}") + except Exception as e: + LOG.error(f"test_alice_add_bob_as_friend EXCEPTION {e}") + raise + finally: + self.alice.friend_delete(self.abid) + assert len(self.alice.self_get_friend_list()) == 0 + +# @unittest.skipIf(bIS_LOCAL, "local test") + @expectedFailure + def test_both_add_as_friend(self): # works + try: + self.both_add_as_friend() + except AssertionError as e: + raise RuntimeError(f"Failed test {e}") + except Exception as e: + LOG.error(f"test_both_add_as_friend EXCEPTION {e}") + raise + finally: + self.bob.friend_delete(self.baid) + self.alice.friend_delete(self.abid) + assert len(self.bob.self_get_friend_list()) == 0 + assert len(self.alice.self_get_friend_list()) == 0 + + @unittest.skip('unfinished') + def test_bob_add_alice_as_friend_and_status(self): + assert self.bob_add_alice_as_friend_and_status() + self.bob.friend_delete(self.baid) + +#? @unittest.skip('fails') + @expectedFailure + def test_on_friend_status_message(self): # fails + """ + t:self_set_status_message + t:self_get_status_message + t:self_get_status_message_size + t:friend_set_status_message + t:friend_get_status_message + t:friend_get_status_message_size + t:on_friend_status_message + """ + MSG = 'Happy' + sSlot = 'friend_status_message' + + def bob_on_friend_status_message(iTox, friend_id, new_status_message, new_status_size, *largs): + try: + assert str(new_status_message, 'UTF-8') == MSG + assert friend_id == self.baid + except Exception as e: + LOG_ERROR(f"BOB_ON_friend_status_message EXCEPTION {e}") + else: + LOG_INFO(f"BOB_ON_friend_status_message {friend_id}" \ + +repr(new_status_message)) + setattr(self.bob, sSlot, True) + + setattr(self.bob, sSlot, None) + try: + if oTOX_OARGS.bIS_LOCAL: + assert self.bob_add_alice_as_friend_norequest() + else: + assert self.bob_add_alice_as_friend() + + self.bob.callback_friend_status_message(bob_on_friend_status_message) + self.warn_if_no_cb(self.bob, sSlot) + self.alice.self_set_status_message(MSG) + assert self.wait_otox_attrs(self.bob, [sSlot]) + + assert self.bob.friend_get_status_message(self.baid) == MSG + assert self.bob.friend_get_status_message_size(self.baid) == len(MSG) + + except AssertionError as e: + raise RuntimeError(f"Failed test {e}") + except Exception as e: + LOG.error(f"test_on_friend_status_message EXCEPTION {e}") + raise + finally: + self.alice.callback_friend_status(None) + self.bob.friend_delete(self.baid) + + @expectedFailure + def test_friend(self): # works + """ + t:friend_delete + t:friend_exists + t:friend_get_public_key + t:self_get_friend_list + t:self_get_friend_list_size + t:self_set_name + t:friend_get_name + t:friend_get_name_size + t:on_friend_name + """ + + assert len(self.bob.self_get_friend_list()) == 0 + assert len(self.alice.self_get_friend_list()) == 0 + #: Test friend request + if oTOX_OARGS.bIS_LOCAL: + assert self.bob_add_alice_as_friend_norequest() + assert self.alice_add_bob_as_friend_norequest() + else: + # no not connected error + assert self.bob_add_alice_as_friend() + assert self.alice_add_bob_as_friend() + try: + assert self.bob.friend_get_public_key(self.baid) == \ + self.alice.self_get_address()[:CLIENT_ID_SIZE] + + #: Test friend_get_public_key + assert self.alice.friend_get_public_key(self.abid) == \ + self.bob.self_get_address()[:CLIENT_ID_SIZE] + except AssertionError as e: + raise RuntimeError(f"Failed test {e}") + except Exception as e: + LOG.error(f"test_friend EXCEPTION {e}") + raise + finally: + self.bob.friend_delete(self.baid) + self.alice.friend_delete(self.abid) + +# @unittest.skip('fails') +# @unittest.skipIf(not bIS_LOCAL and not ts.bAreWeConnected(), 'NOT CONNECTED') + @expectedFailure + def test_user_status(self): + """ + t:self_get_status + t:self_set_status + t:friend_get_status + t:friend_get_status + t:on_friend_status + """ + sSlot = 'friend_status' + if oTOX_OARGS.bIS_LOCAL: + assert self.bob_add_alice_as_friend_norequest() + else: + assert self.bob_add_alice_as_friend() + + sSTATUS = TOX_USER_STATUS['NONE'] + setattr(self.bob, sSlot, None) + def bobs_on_friend_set_status(iTox, friend_id, new_status, *largs): + LOG_INFO(f"bobs_on_friend_set_status {friend_id} {new_status}") + try: + assert friend_id == self.baid + assert new_status in [TOX_USER_STATUS['BUSY'], TOX_USER_STATUS['AWAY']] + except Exception as e: + LOG_WARN(f"bobs_on_friend_set_status EXCEPTION {e}") + setattr(self.bob, sSlot, True) + + try: + if not self.get_connection_status(): + LOG.warning(f"test_user_status NOT CONNECTED self.get_connection_status") + self.loop_until_connected() + + self.bob.callback_friend_status(bobs_on_friend_set_status) + self.warn_if_no_cb(self.bob, sSlot) + sSTATUS = TOX_USER_STATUS['BUSY'] + self.alice.self_set_status(sSTATUS) + sSTATUS = TOX_USER_STATUS['AWAY'] + self.alice.self_set_status(sSTATUS) + assert self.wait_otox_attrs(self.bob, [sSlot]) + # wait_obj_attr count >= 15 for friend_status + + self.alice.self_set_status(TOX_USER_STATUS['NONE']) + assert self.alice.self_get_status() == TOX_USER_STATUS['NONE'] + assert self.bob.friend_get_status(self.baid) == TOX_USER_STATUS['NONE'] + + except AssertionError as e: + raise RuntimeError(f"Failed test {e}") + + except Exception as e: + LOG.error(f"test_user_status EXCEPTION {e}") + raise + finally: + self.bob.callback_friend_status(None) + self.warn_if_cb(self.bob, sSlot) + self.bob.friend_delete(self.baid) + + @unittest.skip('crashes') + def test_connection_status(self): + """ + t:friend_get_connection_status + t:on_friend_connection_status + """ + LOG.info("test_connection_status ") + if oTOX_OARGS.bIS_LOCAL: + assert self.bob_add_alice_as_friend_norequest() + else: + assert self.bob_add_alice_as_friend() + + sSlot = 'friend_connection_status' + setattr(self.bob, sSlot, None) + def bobs_on_friend_connection_status(iTox, friend_id, iStatus, *largs): + setattr(self.bob, sSlot, True) + LOG_INFO(f"bobs_on_friend_connection_status " +repr(iStatus)) + try: + assert friend_id == self.baid + except Exception as e: + LOG.error(f"bobs_on_friend_connection_status ERROR {e}") + + opts = oToxygenToxOptions(oTOX_OARGS) + try: + setattr(self.bob, sSlot, True) + self.bob.callback_friend_connection_status(bobs_on_friend_connection_status) + + LOG.info("test_connection_status killing alice") + self.alice.kill() #! bang + LOG.info("test_connection_status making alice") + self.alice = Tox(opts, app=oAPP) + LOG.info("test_connection_status maked alice") + + assert self.wait_otox_attrs(self.bob, [sSlot]) + except AssertionError as e: + raise + except Exception as e: + LOG.error(f"bobs_on_friend_connection_status {e}") + raise + finally: + self.bob.callback_friend_connection_status(None) + + #? assert self.bob.friend_get_connection_status(self.aid) is False + self.bob.friend_delete(self.baid) + +#? @unittest.skip('fails') + def test_friend_name(self): # fails + """ + t:self_set_name + t:friend_get_name + t:friend_get_name_size + t:on_friend_name + """ + + sSlot= 'friend_name' + #: Test friend request + + LOG.info("test_friend_name") + if oTOX_OARGS.bIS_LOCAL: + assert self.bob_add_alice_as_friend_norequest() + else: + assert self.bob_add_alice_as_friend() + + if not self.get_connection_status(): + LOG.warning(f"test_friend_message NOT CONNECTED") + self.loop_until_connected() + + #: Test friend name + NEWNAME = 'Jenny' + + def bobs_on_friend_name(iTox, fid, newname, iNameSize, *largs): + LOG_INFO(f"bobs_on_friend_name {sSlot} {fid}") + try: + assert fid == self.baid + assert str(newname, 'UTF-8') == NEWNAME + except Exception as e: + LOG.error(f"bobs_on_friend_name EXCEPTION {e}") + setattr(self.bob, sSlot, True) + + setattr(self.bob, sSlot, None) + self.bob.callback_friend_name(bobs_on_friend_name) + self.warn_if_no_cb(self.bob, sSlot) + try: + self.alice.self_set_name(NEWNAME) + assert self.wait_otox_attrs(self.bob, [sSlot]) + + assert self.bob.friend_get_name(self.baid) == NEWNAME + assert self.bob.friend_get_name_size(self.baid) == len(NEWNAME) + + except AssertionError as e: + raise RuntimeError(f"test_friend Failed test {e}") + + except Exception as e: + LOG.error(f"test_friend EXCEPTION {e}") + raise + + finally: + self.bob.callback_friend_name(None) + if hasattr(self.bob, sSlot + '_cb') and \ + getattr(self.bob, sSlot + '_cb'): + LOG.warning(sSlot + ' EXISTS') + + self.bob.friend_delete(self.baid) + + # wait_ensure_exec ArgumentError This client is currently not connected to the friend. + def test_friend_message(self): # fails + """ + t:on_friend_action + t:on_friend_message + t:friend_send_message + """ + + #: Test message + MSG = 'Hi, Bob!' + sSlot = 'friend_message' + if oTOX_OARGS.bIS_LOCAL: + assert self.both_add_as_friend_norequest() + else: + assert self.both_add_as_friend() + + if not self.get_connection_status(): + LOG.warning(f"test_friend_message NOT CONNECTED") + self.loop_until_connected() + + iRet = self.bob.friend_get_connection_status(self.baid) + if iRet == TOX_CONNECTION['NONE']: + LOG.error("bob.friend_get_connection_status") + raise RuntimeError("bob.friend_get_connection_status") + iRet = self.alice.friend_get_connection_status(self.abid) + if iRet == TOX_CONNECTION['NONE']: + LOG.error("alice.friend_get_connection_status") + raise RuntimeError("alice.friend_get_connection_status") + + def alices_on_friend_message(iTox, fid, msg_type, message, iSize, *largs): + LOG_DEBUG(f"alices_on_friend_message {fid} {message}") + try: + assert fid == self.alice.abid + assert msg_type == TOX_MESSAGE_TYPE['NORMAL'] + assert str(message, 'UTF-8') == MSG + except Exception as e: + LOG_ERROR(f"alices_on_friend_message EXCEPTION {e}") + else: + LOG_INFO(f"alices_on_friend_message {message}") + setattr(self.alice, sSlot, True) + + setattr(self.alice, sSlot, None) + try: + self.alice.callback_friend_message(alices_on_friend_message) + self.warn_if_no_cb(self.alice, sSlot) + + # dunno - both This client is currently NOT CONNECTED to the friend. + if True: + iMesId = self.bob.friend_send_message( + self.baid, + TOX_MESSAGE_TYPE['NORMAL'], + bytes(MSG, 'UTF-8')) + # ArgumentError('This client is currently NOT CONNECTED to the friend.') + else: + iMesId = self.wait_ensure_exec(self.bob.friend_send_message, + [self.baid, + TOX_MESSAGE_TYPE['NORMAL'], + bytes(MSG, 'UTF-8')]) + assert iMesId >= 0 + assert self.wait_otox_attrs(self.alice, [sSlot]) + except ArgumentError as e: + # ArgumentError('This client is currently NOT CONNECTED to the friend.') + # dunno + LOG.error(f"test_friend_message {e}") + raise + except AssertionError as e: + LOG.warning(f"test_friend_message {e}") + raise RuntimeError(f"Failed test test_friend_message {e}") + except Exception as e: + LOG.error(f"test_friend_message {e}") + raise + finally: + self.alice.callback_friend_message(None) + self.warn_if_cb(self.alice, sSlot) + self.bob.friend_delete(self.baid) + self.alice.friend_delete(self.abid) + +#? @unittest.skip('fails') + def test_friend_action(self): + """ + t:on_friend_action + t:on_friend_message + t:friend_send_message + """ + + if oTOX_OARGS.bIS_LOCAL: + assert self.both_add_as_friend_norequest() + else: + assert self.both_add_as_friend() + + if not self.get_connection_status(): + LOG.warning(f"test_friend_message NOT CONNECTED") + self.loop_until_connected() + + iRet = self.bob.friend_get_connection_status(self.baid) + if iRet == TOX_CONNECTION['NONE']: + LOG.error("bob.friend_get_connection_status") + raise RuntimeError("bob.friend_get_connection_status") + iRet = self.alice.friend_get_connection_status(self.abid) + if iRet == TOX_CONNECTION['NONE']: + LOG.error("alice.friend_get_connection_status") + raise RuntimeError("alice.friend_get_connection_status") + + BID = self.baid + #: Test action + ACTION = 'Kick' + sSlot = 'friend_read_action' + setattr(self.bob, sSlot, None) + sSlot = 'friend_read_receipt' + setattr(self.bob, sSlot, None) + def alices_on_friend_action(iTox, fid, msg_type, action, *largs): + sSlot = 'friend_read_action' + LOG_DEBUG(f"alices_on_friend_action") + try: + assert fid == self.bob.baid + assert msg_type == TOX_MESSAGE_TYPE['ACTION'] + assert action == ACTION + except Exception as e: + LOG_ERROR(f"alices_on_friend_action EXCEPTION {e}") + else: + LOG_INFO(f"alices_on_friend_action {message}") + setattr(self.bob, sSlot, True) + + sSlot = 'friend_read_action' + setattr(self.alice, sSlot, None) + sSlot = 'friend_read_receipt' + setattr(self.alice, sSlot, None) + def alices_on_read_reciept(iTox, fid, msg_id, *largs): + LOG_DEBUG(f"alices_on_read_reciept") + sSlot = 'friend_read_receipt' + try: + assert fid == BID + except Exception as e: + LOG_ERROR(f"alices_on_read_reciept {e}") + else: + LOG_INFO(f"alices_on_read_reciept {fid}") + setattr(self.alice, sSlot, True) + + sSlot = 'friend_read_receipt' + try: + sSlot = 'friend_read_action' + setattr(self.bob, sSlot, False) + sSlot = 'friend_read_receipt' + setattr(self.alice, sSlot, False) + + self.alice.callback_friend_read_receipt(alices_on_read_reciept) #was alices_on_friend_action + self.warn_if_no_cb(self.alice, sSlot) + assert self.wait_ensure_exec(self.bob.friend_send_message, + [self.baid, + TOX_MESSAGE_TYPE['ACTION'], + bytes(ACTION, 'UTF-8')]) + assert self.wait_otox_attrs(self.alice, [sSlot]) + except AssertionError as e: + raise RuntimeError(f"Failed test {e}") + except ArgumentError as e: + # ArgumentError('This client is currently NOT CONNECTED to the friend.') + # dunno + LOG.warning(f"test_friend_action {e}") + except Exception as e: + LOG.error(f"test_friend_action {e}") + raise + finally: + self.alice.callback_friend_read_receipt(None) + self.bob.friend_delete(self.baid) + self.alice.friend_delete(self.abid) + + @unittest.skip('fails') + def test_alice_typing_status(self): + """ + t:on_friend_read_receipt + t:on_friend_typing + t:self_set_typing + t:friend_get_typing + t:friend_get_last_online + """ + + sSlot = 'friend_typing' + # works + LOG.info("test_typing_status bob adding alice") + if oTOX_OARGS.bIS_LOCAL: + assert self.both_add_as_friend_norequest() + else: + assert self.both_add_as_friend() + + BID = self.baid + + #: Test typing status + def bob_on_friend_typing(iTox, fid, is_typing, *largs): + try: + assert fid == BID + assert is_typing is True + assert self.bob.friend_get_typing(fid) is True + except Exception as e: + LOG.error(f"BOB_ON_friend_typing {e}") + raise + else: + LOG_INFO(f"BOB_ON_friend_typing" + str(fid)) + setattr(self.bob, sSlot, True) + + setattr(self.bob, sSlot, None) + try: + if not self.get_connection_status(): + LOG.warning(f"test_friend_message NOT CONNECTED") + self.loop_until_connected() + + self.bob.callback_friend_typing(bob_on_friend_typing) + self.alice.self_set_typing(self.abid, True) + assert self.wait_otox_attrs(self.bob, [sSlot]) + if not hasattr(self.bob, sSlot+'_cb') or \ + not getattr(self.bob, sSlot+'_cb'): + LOG.warning(f"self.bob.{sSlot}_cb NOT EXIST") + except AssertionError as e: + raise RuntimeError(f"Failed test {e}") + except Exception as e: + LOG.error(f"test_alice_typing_status error={e}") + raise + finally: + self.bob.callback_friend_typing(None) + self.bob.friend_delete(self.baid) + self.alice.friend_delete(self.abid) + + @unittest.skip('unfinished') + def test_file_transfer(self): # unfinished + """ + t:file_send + t:file_send_chunk + t:file_control + t:file_seek + t:file_get_file_id + t:on_file_recv + t:on_file_recv_control + t:on_file_recv_chunk + t:on_file_chunk_request + """ + + if oTOX_OARGS.bIS_LOCAL: + assert self.bob_add_alice_as_friend_norequest() + else: + assert self.bob_add_alice_as_friend() + + BID = self.baid + + FRIEND_NUMBER = self.baid + FILE_NUMBER = 1 + FILE = os.urandom(1024 * 1024) + FILE_NAME = b"/tmp/test.bin" + if not os.path.exists(FILE_NAME): + with open(FILE_NAME, 'wb') as oFd: + oFd.write(FILE) + FILE_SIZE = len(FILE) + OFFSET = 567 + + m = hashlib.md5() + m.update(FILE[OFFSET:]) + FILE_DIGEST = m.hexdigest() + + CONTEXT = { 'FILE': bytes(), 'RECEIVED': 0, 'START': False, 'SENT': 0 } + + def alice_on_file_recv(iTox, fid, file_number, kind, size, filename): + LOG_DEBUG(f"ALICE_ON_file_recv fid={fid} {file_number}") + try: + assert size == FILE_SIZE + assert filename == FILE_NAME + retv = self.alice.file_seek(fid, file_number, OFFSET) + assert retv is True + self.alice.file_control(fid, file_number, TOX_FILE_CONTROL['RESUME']) + except Exception as e: + LOG_ERROR(f"ALICE_ON_file_recv {e}") + else: + LOG_INFO(f"ALICE_ON_file_recv " + str(fid)) + + def alice_on_file_recv_control(iTox, fid, file_number, control, *largs): + # TOX_FILE_CONTROL = { 'RESUME': 0, 'PAUSE': 1, 'CANCEL': 2,} + LOG_DEBUG(f"ALICE_ON_file_recv_control fid={fid} {file_number} {control}") + try: + assert FILE_NUMBER == file_number + # FixMe _FINISHED? + if False and control == TOX_FILE_CONTROL['RESUME']: + # assert CONTEXT['RECEIVED'] == FILE_SIZE + # m = hashlib.md5() + # m.update(CONTEXT['FILE']) + # assert m.hexdigest() == FILE_DIGEST + self.alice.completed = True + except Exception as e: + LOG_ERROR(f"ALICE_ON_file_recv {e}") + else: + LOG_INFO(f"ALICE_ON_file_recv " + str(fid)) + + self.alice.completed = False + def alice_on_file_recv_chunk(iTox, fid, file_number, position, iNumBytes, *largs): + LOG_DEBUG(f"ALICE_ON_file_recv_chunk {fid} {file_number}") + # FixMe - use file_number and iNumBytes to get data? + data = '' + try: + if data is None: + assert CONTEXT['RECEIVED'] == (FILE_SIZE - OFFSET) + m = hashlib.md5() + m.update(CONTEXT['FILE']) + assert m.hexdigest() == FILE_DIGEST + self.alice.completed = True + self.alice.file_control(fid, file_number, TOX_FILE_CONTROL['CANCEL']) + return + + CONTEXT['FILE'] += data + CONTEXT['RECEIVED'] += len(data) + # if CONTEXT['RECEIVED'] < FILE_SIZE: + # assert self.file_data_remaining( + # fid, file_number, 1) == FILE_SIZE - CONTEXT['RECEIVED'] + except Exception as e: + LOG_ERROR(f"ALICE_ON_file_recv_chunk {e}") + else: + LOG_INFO(f"ALICE_ON_file_recv_chunk {fid}") + + # AliceTox.on_file_send_request = on_file_send_request + # AliceTox.on_file_control = on_file_control + # AliceTox.on_file_data = on_file_data + + LOG.info(f"test_file_transfer: baid={self.baid}") + try: + self.alice.callback_file_recv(alice_on_file_recv) + self.alice.callback_file_recv_control(alice_on_file_recv_control) + self.alice.callback_file_recv_chunk(alice_on_file_recv_chunk) + + self.bob.completed = False + def bob_on_file_recv_control2(iTox, fid, file_number, control): + LOG_DEBUG(f"BOB_ON_file_recv_control2 {fid} {file_number} control={control}") + if control == TOX_FILE_CONTROL['RESUME']: + CONTEXT['START'] = True + elif control == TOX_FILE_CONTROL['CANCEL']: + self.bob.completed = True + pass + + def bob_on_file_chunk_request(iTox, fid, file_number, position, length, *largs): + LOG_DEBUG(f"BOB_ON_file_chunk_request {fid} {file_number}") + if length == 0: + return + data = FILE[position:(position + length)] + self.bob.file_send_chunk(fid, file_number, position, data) + + sSlot = 'file_recv_control' + self.bob.callback_file_recv_control(bob_on_file_recv_control2) + self.bob.callback_file_chunk_request(bob_on_file_chunk_request) + + # was FILE_ID = FILE_NAME + FILE_ID = 32*'1' # + FILE_NAME = b'test.in' + + if not self.get_connection_status(): + LOG.warning(f"test_file_transfer NOT CONNECTED") + self.loop_until_connected() + + i = 0 + iKind = 0 + while i < 2: + i += 1 + try: + FN = self.bob.file_send(self.baid, iKind, FILE_SIZE, FILE_ID, FILE_NAME) + LOG.info(f"test_file_transfer bob.file_send {FN}") + except ArgumentError as e: + LOG.debug(f"test_file_transfer bob.file_send {e} {i}") + # ctypes.ArgumentError: This client is currently not connected to the friend. + raise + else: + break + self.loop(100) + sleep(1) + else: + LOG.error(f"test_file_transfer bob.file_send 2") + raise RuntimeError(f"test_file_transfer bob.file_send {THRESHOLD // 2}") + + # UINT32_MAX + FID = self.bob.file_get_file_id(self.baid, FN) + hexFID = "".join([hex(ord(c))[2:].zfill(2) for c in FILE_NAME]) + assert FID.startswith(hexFID.upper()) + + if not self.wait_obj_attrs(self.bob, ['completed']): + LOG.warning(f"test_file_transfer Bob not completed") + return False + if not self.wait_obj_attrs(self.alice, ['completed']): + LOG.warning(f"test_file_transfer Alice not completed") + return False + return True + + except (ArgumentError, ValueError,) as e: + # ValueError: non-hexadecimal number found in fromhex() arg at position 0 + LOG_ERROR(f"test_file_transfer: {e}") + raise + + except Exception as e: + LOG_ERROR(f"test_file_transfer:: {e}") + LOG_DEBUG('\n' + traceback.format_exc()) + raise + + finally: + self.bob.friend_delete(self.baid) + self.alice.callback_file_recv(None) + self.alice.callback_file_recv_control(None) + self.alice.callback_file_recv_chunk(None) + self.bob.callback_file_recv_control(None) + self.bob.callback_file_chunk_request(None) + + LOG_INFO(f"test_file_transfer:: self.wait_objs_attr completed") + + @unittest.skip('crashes') + def test_tox_savedata(self): # works sorta + # but "{addr} != {self.alice.self_get_address()}" + """ + t:get_savedata_size + t:get_savedata + """ + # Fatal Python error: Aborted + # "/var/local/src/toxygen_wrapper/wrapper/tox.py", line 180 in kill + return + + assert self.alice.get_savedata_size() > 0 + data = self.alice.get_savedata() + assert data is not None + addr = self.alice.self_get_address() + # self._address + + try: + LOG.info("test_tox_savedata alice.kill") + # crashes + self.alice.kill() + except: + pass + + oArgs = oTOX_OARGS + opts = oToxygenToxOptions(oArgs) + opts.savedata_data = data + opts.savedata_length = len(data) + + self.alice = Tox(tox_options=opts) + if addr != self.alice.self_get_address(): + LOG.warning("test_tox_savedata " + + f"{addr} != {self.alice.self_get_address()}") + else: + LOG.info("passed test_tox_savedata") + +def vOargsToxPreamble(oArgs, Tox, ToxTest): + + ts.vSetupLogging(oArgs) + + methods = set([x for x in dir(Tox) if not x[0].isupper() + and not x[0] == '_']) + docs = "".join([getattr(ToxTest, x).__doc__ for x in dir(ToxTest) + if getattr(ToxTest, x).__doc__ is not None]) + + tested = set(re.findall(r't:(.*?)\n', docs)) + not_tested = methods.difference(tested) + + logging.info('Test Coverage: %.2f%%' % (len(tested) * 100.0 / len(methods))) + if len(not_tested): + logging.info('Not tested:\n %s' % "\n ".join(sorted(list(not_tested)))) + +### + +def iMain(oArgs): + failfast=True + + vOargsToxPreamble(oArgs, Tox, ToxSuite) + # https://stackoverflow.com/questions/35930811/how-to-sort-unittest-testcases-properly/35930812#35930812 + cases = ts.suiteFactory(*ts.caseFactory([ToxSuite])) + if color_runner: + runner = color_runner.runner.TextTestRunner(verbosity=2, failfast=failfast) + else: + runner = unittest.TextTestRunner(verbosity=2, failfast=failfast, warnings='ignore') + runner.run(cases) + +def oToxygenToxOptions(oArgs): + data = None + tox_options = wrapper.tox.Tox.options_new() + if oArgs.proxy_type: + tox_options.contents.proxy_type = int(oArgs.proxy_type) + tox_options.contents.proxy_host = bytes(oArgs.proxy_host, 'UTF-8') + tox_options.contents.proxy_port = int(oArgs.proxy_port) + tox_options.contents.udp_enabled = False + else: + tox_options.contents.udp_enabled = oArgs.udp_enabled + if not os.path.exists('/proc/sys/net/ipv6'): + oArgs.ipv6_enabled = False + else: + tox_options.contents.ipv6_enabled = oArgs.ipv6_enabled + + tox_options.contents.tcp_port = int(oArgs.tcp_port) + tox_options.contents.dht_announcements_enabled = oArgs.dht_announcements_enabled + tox_options.contents.hole_punching_enabled = oArgs.hole_punching_enabled + + # overrides + tox_options.contents.local_discovery_enabled = False + tox_options.contents.experimental_thread_safety = False + # REQUIRED!! + if oArgs.ipv6_enabled and not os.path.exists('/proc/sys/net/ipv6'): + LOG.warning('Disabling IPV6 because /proc/sys/net/ipv6 does not exist' + repr(oArgs.ipv6_enabled)) + tox_options.contents.ipv6_enabled = False + else: + tox_options.contents.ipv6_enabled = bool(oArgs.ipv6_enabled) + + if data: # load existing profile + tox_options.contents.savedata_type = enums.TOX_SAVEDATA_TYPE['TOX_SAVE'] + tox_options.contents.savedata_data = c_char_p(data) + tox_options.contents.savedata_length = len(data) + else: # create new profile + tox_options.contents.savedata_type = enums.TOX_SAVEDATA_TYPE['NONE'] + tox_options.contents.savedata_data = None + tox_options.contents.savedata_length = 0 + + #? tox_options.contents.log_callback = LOG + if tox_options._options_pointer: + # LOG.debug("Adding logging to tox_options._options_pointer ") + ts.vAddLoggerCallback(tox_options, ts.on_log) + else: + LOG.warning("No tox_options._options_pointer " +repr(tox_options._options_pointer)) + + return tox_options + +def oArgparse(lArgv): + parser = ts.oMainArgparser() + parser.add_argument('profile', type=str, nargs='?', default=None, + help='Path to Tox profile') + oArgs = parser.parse_args(lArgv) + + for key in ts.lBOOLEANS: + if key not in oArgs: continue + val = getattr(oArgs, key) + setattr(oArgs, key, bool(val)) + + if hasattr(oArgs, 'sleep'): + if oArgs.sleep == 'qt': + pass # broken or gevent.sleep(idle_period) + elif oArgs.sleep == 'gevent': + pass # broken or gevent.sleep(idle_period) + else: + oArgs.sleep = 'time' + + return oArgs + +def main(lArgs=None): + global oTOX_OARGS + if lArgs is None: lArgs = [] + oArgs = oArgparse(lArgs) + global bIS_LOCAL + bIS_LOCAL = oArgs.network in ['newlocal', 'localnew', 'local'] + oTOX_OARGS = oArgs + setattr(oTOX_OARGS, 'bIS_LOCAL', bIS_LOCAL) + bIS_LOCAL = True + setattr(oTOX_OARGS, 'bIS_LOCAL', bIS_LOCAL) + # oTOX_OPTIONS = ToxOptions() + global oTOX_OPTIONS + oTOX_OPTIONS = oToxygenToxOptions(oArgs) + if coloredlogs: + # https://pypi.org/project/coloredlogs/ + coloredlogs.install(level=oArgs.loglevel, + logger=LOG, + # %(asctime)s,%(msecs)03d %(hostname)s [%(process)d] + fmt='%(name)s %(levelname)s %(message)s' + ) + else: + logging.basicConfig(level=oArgs.loglevel) # logging.INFO + + return iMain(oArgs) + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) + +# Ran 33 tests in 51.733s diff --git a/toxygen/tests/tests_wrapper.py b/toxygen/tests/tests_wrapper.py new file mode 100644 index 0000000..640916d --- /dev/null +++ b/toxygen/tests/tests_wrapper.py @@ -0,0 +1,1885 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +# +# @file tests.py +# @author Wei-Ning Huang (AZ) +# +# Copyright (C) 2013 - 2014 Wei-Ning Huang (AZ) +# All Rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# + +"""Originaly from https://github.com/oxij/PyTox c-toxcore-02 branch +which itself was forked from https://github.com/aitjcize/PyTox/ + +Modified to work with +""" + +import ctypes +import faulthandler +import hashlib +import logging +import os +import random +import re +import sys +import threading +import traceback +import unittest +from ctypes import * + +faulthandler.enable() + +import warnings + +warnings.filterwarnings('ignore') + +try: + from io import BytesIO + + import certifi + import pycurl +except ImportError: + pycurl = None + +try: + import coloredlogs + os.environ['COLOREDLOGS_LEVEL_STYLES'] = 'spam=22;debug=28;verbose=34;notice=220;warning=202;success=118,bold;error=124;critical=background=red' +except ImportError as e: + logging.log(logging.DEBUG, f"coloredlogs not available: {e}") + coloredlogs = None + +try: + import color_runner +except ImportError as e: + logging.log(logging.DEBUG, f"color_runner not available: {e}") + color_runner = None + +import wrapper +import wrapper.toxcore_enums_and_consts as enums +from wrapper.tox import Tox +from wrapper.toxcore_enums_and_consts import (TOX_ADDRESS_SIZE, TOX_CONNECTION, + TOX_FILE_CONTROL, + TOX_MESSAGE_TYPE, + TOX_SECRET_KEY_SIZE, + TOX_USER_STATUS) + +try: + import support_testing as ts +except ImportError: + import wrapper_tests.support_testing as ts + +try: + from tests.toxygen_tests import test_sound_notification + bIS_NOT_TOXYGEN = False +except ImportError: + bIS_NOT_TOXYGEN = True + +# from PyQt5 import QtCore +import time + +sleep = time.sleep + +global LOG +LOG = logging.getLogger('TestS') +# just print to stdout so there is no complications from logging. +def LOG_ERROR(l): print('EROR+ '+l) +def LOG_WARN(l): print('WARN+ '+l) +def LOG_INFO(l): print('INFO+ '+l) +def LOG_DEBUG(l): print('DEBUG+ '+l) +def LOG_TRACE(l): pass # print('TRAC+ '+l) + +ADDR_SIZE = 38 * 2 +CLIENT_ID_SIZE = 32 * 2 +THRESHOLD = 25 + +global oTOX_OPTIONS +oTOX_OPTIONS = {} + +bIS_LOCAL = 'new' in sys.argv or 'main' in sys.argv or 'newlocal' in sys.argv + +# Patch unittest for Python version <= 2.6 +if not hasattr(unittest, 'skip'): + def unittest_skip(reason): + def _wrap1(func): + def _wrap2(self, *args, **kwargs): + pass + return _wrap2 + return _wrap1 + unittest.skip = unittest_skip + +if not hasattr(unittest, 'expectedFailureIf'): + def unittest_expectedFailureIf(condition, reason): + def _wrap1(test_item): + def _wrap2(self, *args, **kwargs): + if condition: + test_item.__unittest_expecting_failure__ = True + pass + return _wrap2 + return _wrap1 + + unittest.expectedFailureIf = unittest_expectedFailureIf + +def expectedFailure(test_item): + test_item.__unittest_expecting_failure__ = True + return test_item + +class ToxOptions(): + def __init__(self): + self.ipv6_enabled = True + self.udp_enabled = True + self.proxy_type = 0 + self.proxy_host = '' + self.proxy_port = 0 + self.start_port = 0 + self.end_port = 0 + self.tcp_port = 0 + self.savedata_type = 0 # 1=toxsave, 2=secretkey + self.savedata_data = b'' + self.savedata_length = 0 + self.local_discovery_enabled = False + self.dht_announcements_enabled = True + self.hole_punching_enabled = False + self.experimental_thread_safety = False + +class App(): + def __init__(self): + self.mode = 0 +oAPP = App() + +class AliceTox(Tox): + + def __init__(self, opts, app=None): + + super(AliceTox, self).__init__(opts, app=app) + self._address = self.self_get_address() + self.name = 'alice' + self._opts = opts + self._app = app + +class BobTox(Tox): + + def __init__(self, opts, app=None): + super(BobTox, self).__init__(opts, app=app) + self._address = self.self_get_address() + self.name = 'bob' + self._opts = opts + self._app = app + +class BaseThread(threading.Thread): + + def __init__(self, name=None, target=None): + if name: + super().__init__(name=name, target=target) + else: + super().__init__(target=target) + self._stop_thread = False + self.name = name + + def stop_thread(self, timeout=-1): + self._stop_thread = True + if timeout < 0: + timeout = ts.iTHREAD_TIMEOUT + i = 0 + while i < ts.iTHREAD_JOINS: + self.join(timeout) + if not self.is_alive(): break + i = i + 1 + else: + LOG.warning(f"{self.name} BLOCKED") + +class ToxIterateThread(BaseThread): + + def __init__(self, tox): + super().__init__(name='ToxIterateThread') + self._tox = tox + + def run(self): + while not self._stop_thread: + self._tox.iterate() + sleep(self._tox.iteration_interval() / 1000) + +global bob, alice +bob = alice = None + +def prepare(self): + global bob, alice + def bobs_on_self_connection_status(iTox, connection_state, *args): + status = connection_state + self.bob.dht_connected = status + self.bob.mycon_time = time.time() + try: + if status != TOX_CONNECTION['NONE']: + LOG_DEBUG(f"bobs_on_self_connection_status TRUE {status}" \ + +f" last={int(self.bob.mycon_time)}" ) + self.bob.mycon_status = True + else: + LOG_DEBUG(f"bobs_on_self_connection_status FALSE {status}" \ + +f" last={int(self.bob.mycon_time)}" ) + self.bob.mycon_status = False + except Exception as e: + LOG_ERROR(f"bobs_on_self_connection_status {e}") + else: + if self.bob.self_get_connection_status() != status: + LOG_WARN(f"bobs_on_self_connection_status DISAGREE {status}") + + def alices_on_self_connection_status(iTox, connection_state, *args): + #FixMe connection_num + status = connection_state + self.alice.dht_connected = status + self.alice.mycon_time = time.time() + try: + if status != TOX_CONNECTION['NONE']: + LOG_DEBUG(f"alices_on_self_connection_status TRUE {status}" \ + +f" last={int(self.alice.mycon_time)}" ) + self.alice.mycon_status = True + else: + LOG_WARN(f"alices_on_self_connection_status FALSE {status}" \ + +f" last={int(self.alice.mycon_time)}" ) + self.alice.mycon_status = False + except Exception as e: + LOG_ERROR(f"alices_on_self_connection_status error={e}") + else: + if self.alice.self_get_connection_status() != status: + LOG_WARN(f"alices_on_self_connection_status != {status}") + self.alice.dht_connected = status + + opts = oToxygenToxOptions(oTOX_OARGS) + alice = AliceTox(opts, app=oAPP) + alice.oArgs = opts + alice.dht_connected = -1 + alice.mycon_status = False + alice.mycon_time = 1 + alice.callback_self_connection_status(alices_on_self_connection_status) + + bob = BobTox(opts, app=oAPP) + bob.oArgs = opts + bob.dht_connected = -1 + bob.mycon_status = False + bob.mycon_time = 1 + bob.callback_self_connection_status(bobs_on_self_connection_status) + if not bIS_LOCAL and not ts.bAreWeConnected(): + LOG.warning(f"doOnce not local and NOT CONNECTED") + return [bob, alice] + +class ToxSuite(unittest.TestCase): + failureException = RuntimeError + + @classmethod + def setUpClass(cls): + global oTOX_OARGS + assert oTOX_OPTIONS + assert oTOX_OARGS + + if not hasattr(cls, 'alice') and not hasattr(cls, 'bob'): + l = prepare(cls) + assert l + cls.bob, cls.alice = l + if not hasattr(cls.bob, '_main_loop'): + cls.bob._main_loop = ToxIterateThread(cls.bob) + cls.bob._main_loop.start() + LOG.debug(f"cls.bob._main_loop: ") # {threading.enumerate()} + if not hasattr(cls.alice, '_main_loop'): + cls.alice._main_loop = ToxIterateThread(cls.alice) + cls.alice._main_loop.start() + LOG.debug(f"cls.alice._main_loop: ") # {threading.enumerate()} + + cls.lUdp = ts.generate_nodes( + oArgs=oTOX_OARGS, + nodes_count=2*ts.iNODES, + ipv='ipv4', + udp_not_tcp=True) + + cls.lTcp = ts.generate_nodes( + oArgs=oTOX_OARGS, + nodes_count=2*ts.iNODES, + ipv='ipv4', + udp_not_tcp=False) + + @classmethod + def tearDownClass(cls): + cls.bob._main_loop.stop_thread() + cls.alice._main_loop.stop_thread() + if False: + cls.alice.kill() + cls.bob.kill() + del cls.bob + del cls.alice + + def setUp(self): + """ + """ + if hasattr(self, 'baid') and self.baid >= 0 and \ + self.baid in self.bob.self_get_friend_list(): + LOG.warn(f"setUp ALICE IS ALREADY IN BOBS FRIEND LIST") + elif self.bob.self_get_friend_list_size() >= 1: + LOG.warn(f"setUp BOB STILL HAS A FRIEND LIST") + + if hasattr(self, 'abid') and self.abid >= 0 and \ + self.abid in self.alice.self_get_friend_list(): + LOG.warn(f"setUp BOB IS ALREADY IN ALICES FRIEND LIST") + elif self.alice.self_get_friend_list_size() >= 1: + LOG.warn(f"setUp ALICE STILL HAS A FRIEND LIST") + + def tearDown(self): + """ + """ + if hasattr(self, 'baid') and self.baid >= 0 and \ + self.baid in self.bob.self_get_friend_list(): + LOG.warn(f"tearDown ALICE IS STILL IN BOBS FRIEND LIST") + elif self.bob.self_get_friend_list_size() >= 1: + LOG.warn(f"tearDown BOBS STILL HAS A FRIEND LIST") + + if hasattr(self, 'abid') and self.abid >= 0 and \ + self.abid in self.alice.self_get_friend_list(): + LOG.warn(f"tearDown BOB IS STILL IN ALICES FRIEND LIST") + elif self.bob.self_get_friend_list_size() >= 1: + LOG.warn(f"tearDown ALICE STILL HAS A FRIEND LIST") + + def run(self, result=None): + """ Stop after first error """ + if not result.errors: + super(ToxSuite, self).run(result) + + def get_connection_status(self): + if self.bob.mycon_time == -1 or self.alice.mycon_time == -1: + pass + # drop through + elif self.bob.dht_connected == TOX_CONNECTION['NONE']: + return False + elif self.alice.dht_connected == TOX_CONNECTION['NONE']: + return False + + # if not self.connected + if self.bob.self_get_connection_status() == TOX_CONNECTION['NONE']: + return False + if self.alice.self_get_connection_status() == TOX_CONNECTION['NONE']: + return False + return True + + def loop(self, n): + """ + t:iterate + t:iteration_interval + """ + interval = self.bob.iteration_interval() + for i in range(n): + self.alice.iterate() + self.bob.iterate() + sleep(interval / 1000.0) + + def call_bootstrap(self, num=None, lToxes=None, i=0): + if num == None: num=ts.iNODES +# LOG.debug(f"call_bootstrap network={oTOX_OARGS.network}") + if oTOX_OARGS.network in ['new', 'newlocal', 'localnew']: + ts.bootstrap_local(self.lUdp, [self.alice, self.bob]) + elif not ts.bAreWeConnected(): + LOG.warning('we are NOT CONNECTED') + else: + random.shuffle(self.lUdp) + if oTOX_OARGS.proxy_port > 0: + lElts = self.lUdp[:1] + else: + lElts = self.lUdp[:num+i] + LOG.debug(f"call_bootstrap ts.bootstrap_udp {len(lElts)}") + if lToxes is None: lToxes = [self.alice, self.bob] + ts.bootstrap_udp(lElts, lToxes) + random.shuffle(self.lTcp) + lElts = self.lTcp[:num+i] + LOG.debug(f"call_bootstrap ts.bootstrap_tcp {len(lElts)}") + ts.bootstrap_tcp(lElts, lToxes) + + def loop_until_connected(self, num=None): + """ + t:on_self_connection_status + t:self_get_connection_status + """ + i = 0 + bRet = None + while i <= THRESHOLD : + if (self.alice.mycon_status and self.bob.mycon_status): + bRet = True + break + if i % 5 == 0: + j = i//5 + self.call_bootstrap(num, lToxes=None, i=j) + s = '' + if i == 0: s = '\n' + LOG.info(s+"loop_until_connected " \ + +" #" + str(i) \ + +" BOB=" +repr(self.bob.self_get_connection_status()) \ + +" ALICE=" +repr(self.alice.self_get_connection_status()) + +f" BOBS={self.bob.mycon_status}" \ + +f" ALICES={self.alice.mycon_status}" \ + +f" last={int(self.bob.mycon_time)}" ) + if (self.alice.mycon_status and self.bob.mycon_status): + bRet = True + break + if (self.alice.self_get_connection_status() and + self.bob.self_get_connection_status()): + LOG_WARN(f"loop_until_connected disagree status() DISAGREE" \ + +f' self.bob.mycon_status={self.bob.mycon_status}' \ + +f' alice.mycon_status={self.alice.mycon_status}' \ + +f" last={int(self.bob.mycon_time)}" ) + bRet = True + break + i += 1 + self.loop(100) + else: + bRet = False + + if bRet or \ + ( self.bob.self_get_connection_status() != TOX_CONNECTION['NONE'] and \ + self.alice.self_get_connection_status() != TOX_CONNECTION['NONE'] ): + LOG.info(f"loop_until_connected returning True {i}" \ + +f" BOB={self.bob.self_get_connection_status()}" \ + +f" ALICE={self.alice.self_get_connection_status()}" \ + +f" last={int(self.bob.mycon_time)}" ) + return True + else: + LOG.warning(f"loop_until_connected returning False {i}" \ + +f" BOB={self.bob.self_get_connection_status()}" \ + +f" ALICE={self.alice.self_get_connection_status()}" \ + +f" last={int(self.bob.mycon_time)}" ) + return False + + def wait_obj_attr(self, obj, attr): + return wait_otox_attrs(self, obj, [attr]) + + def wait_objs_attr(self, objs, attr): + i = 0 + while i <= THRESHOLD: + if i % 5 == 0: + num = None + j = i//5 + self.call_bootstrap(num, objs, i=j) + LOG.debug("wait_objs_attr " +repr(objs) \ + +" for " +repr(attr) \ + +" " +str(i)) + if all([getattr(obj, attr) for obj in objs]): + return True + self.loop(100) + i += 1 + else: + LOG.error(f"wait_obj_attr i >= {THRESHOLD}") + + return all([getattr(obj, attr) for obj in objs]) + + def wait_otox_attrs(self, obj, attrs): + i = 0 + while i <= THRESHOLD: + if i % 5 == 0: + num = None + j = 0 + if obj.mycon_time == 1: + num = 4 + j = i//5 + self.call_bootstrap(num, [obj], i=j) + LOG.debug(f"wait_otox_attrs {obj.name} for {attrs} {i}" \ + +f" last={int(obj.mycon_time)}") + if all([getattr(obj, attr) is not None for attr in attrs]): + return True + self.loop(100) + i += 1 + else: + LOG.warning(f"wait_otox_attrs i >= {THRESHOLD}") + + return all([getattr(obj, attr) for attr in attrs]) + + def wait_ensure_exec(self, method, args): + i = 0 + oRet = None + while i <= THRESHOLD: + if i % 5 == 0: + j = i//5 + self.call_bootstrap(num=None, lToxes=None, i=j) + LOG.debug("wait_ensure_exec " \ + +" " +str(method) + +" " +str(i)) + try: + oRet = method(*args) + if oRet: + LOG.info(f"wait_ensure_exec oRet {oRet!r}") + return True + except ArgumentError as e: + # ArgumentError('This client is currently NOT CONNECTED to the friend.') + # dunno + LOG.warning(f"wait_ensure_exec ArgumentError {e}") + return False + except Exception as e: + LOG.warning(f"wait_ensure_exec EXCEPTION {e}") + return False + sleep(3) + i += 1 + else: + LOG.error(f"wait_ensure_exec i >= {1*THRESHOLD}") + return False + + return oRet + + def bob_add_alice_as_friend_norequest(self): + if hasattr(self, 'baid') and self.baid >= 0 and \ + self.baid in self.bob.self_get_friend_list(): + LOG.warn('Alice is already in bobs friend list') + return True + if self.bob.self_get_friend_list_size() >= 1: + LOG.warn(f'Bob has a friend list {self.bob.self_get_friend_list()}') + return True + + MSG = 'Hi, this is Bob.' + iRet = self.bob.friend_add_norequest(self.alice._address) + self.baid = self.bob.friend_by_public_key(self.alice._address) + assert self.baid >= 0, self.baid + assert self.bob.friend_exists(self.baid), "bob.friend_exists" + assert not self.bob.friend_exists(self.baid + 1) + assert self.baid in self.bob.self_get_friend_list() + assert self.bob.self_get_friend_list_size() >= 1 + return iRet >= 0 + + def alice_add_bob_as_friend_norequest(self): + if hasattr(self, 'abid') and self.abid >= 0 and \ + self.abid in self.alice.self_get_friend_list(): + LOG.warn('Alice is already in Bobs friend list') + return True + if self.alice.self_get_friend_list_size() >= 1: + LOG.warn(f'Alice has a friend list {self.alice.self_get_friend_list()}') + + MSG = 'Hi Bob, this is Alice.' + iRet = self.alice.friend_add_norequest(self.bob._address) + self.abid = self.alice.friend_by_public_key(self.bob._address) + assert self.abid >= 0, self.abid + assert self.abid in self.alice.self_get_friend_list() + assert self.alice.friend_exists(self.abid), "alice.friend_exists" + assert not self.alice.friend_exists(self.abid + 1) + assert self.alice.self_get_friend_list_size() >= 1 + return iRet >= 0 + + def both_add_as_friend_norequest(self): + assert self.bob_add_alice_as_friend_norequest() + if not hasattr(self, 'baid') or self.baid < 0: + raise AssertionError("both_add_as_friend_norequest bob, 'baid'") + + assert self.alice_add_bob_as_friend_norequest() + if not hasattr(self, 'abid') or self.abid < 0: + raise AssertionError("both_add_as_friend_norequest alice, 'abid'") + + #: Test last online + assert self.alice.friend_get_last_online(self.abid) is not None + assert self.bob.friend_get_last_online(self.baid) is not None + return True + + def bob_add_alice_as_friend(self): + """ + t:friend_add + t:on_friend_request + t:friend_by_public_key + """ + MSG = 'Alice, this is Bob.' + sSlot = 'friend_request' + + def alices_on_friend_request(iTox, + public_key, + message_data, + message_data_size, + *largs): + LOG_DEBUG(f"alices_on_friend_request: " +repr(message_data)) + try: + assert str(message_data, 'UTF-8') == MSG + LOG_INFO(f"alices_on_friend_request: friend_added = True ") + except Exception as e: + LOG_WARN(f"alices_on_friend_request: Exception {e}") + # return + setattr(self.bob, sSlot, True) + + setattr(self.bob, sSlot, None) + inum = -1 + self.alice.callback_friend_request(alices_on_friend_request) + try: + inum = self.bob.friend_add(self.alice._address, bytes(MSG, 'UTF-8')) + if not inum >= 0: + LOG.warning('bob.friend_add !>= 0 ' +repr(inum)) + if not self.wait_otox_attrs(self.bob, [sSlot]): + return False + except Exception as e: + LOG.error(f"bob.friend_add EXCEPTION {e}") + return False + finally: + self.bob.callback_friend_message(None) + + self.baid = self.bob.friend_by_public_key(self.alice._address) + assert self.baid >= 0, self.baid + assert self.bob.friend_exists(self.baid) + assert not self.bob.friend_exists(self.baid + 1) + assert self.baid in self.bob.self_get_friend_list() + assert self.bob.self_get_friend_list_size() >= 1 + return True + + def alice_add_bob_as_friend(self): + """ + t:friend_add + t:on_friend_request + t:friend_by_public_key + """ + MSG = 'Bob, this is Alice.' + sSlot = 'friend_request' + + def bobs_on_friend_request(iTox, + public_key, + message_data, + message_data_size, + *largs): + LOG_DEBUG(f"bobs_on_friend_request: " +repr(message_data)) + try: + assert str(message_data, 'UTF-8') == MSG + LOG_INFO(f"bobs_on_friend_request: friend_added = True ") + except Exception as e: + LOG_WARN(f"bobs_on_friend_request: Exception {e}") + # return + else: + setattr(self.alice, sSlot, True) + + setattr(self.alice, sSlot, None) + inum = -1 + self.bob.callback_friend_request(bobs_on_friend_request) + try: + inum = self.alice.friend_add(self.bob._address, bytes(MSG, 'UTF-8')) + if not inum >= 0: + LOG.warning('alice.friend_add !>= 0 ' +repr(inum)) + if not self.wait_obj_attr(self.alice, sSlot): + return False + except Exception as e: + LOG.error(f"alice.friend_add EXCEPTION {e}") + return False + finally: + self.bob.callback_friend_message(None) + self.abid = self.alice.friend_by_public_key(self.bob._address) + assert self.abid >= 0, self.abid + assert self.alice.friend_exists(self.abid) + assert not self.alice.friend_exists(self.abid + 1) + assert self.abid in self.alice.self_get_friend_list() + assert self.alice.self_get_friend_list_size() >= 1 + return True + + def both_add_as_friend(self): + assert self.bob_add_alice_as_friend() + assert self.alice_add_bob_as_friend() + + #: Test last online + assert self.alice.friend_get_last_online(self.abid) is not None + assert self.bob.friend_get_last_online(self.baid) is not None + + def bob_add_alice_as_friend_and_status(self): + if oTOX_OARGS.bIS_LOCAL: + assert self.bob_add_alice_as_friend_norequest() + else: + assert self.bob_add_alice_as_friend() + + #: Wait until both are online + self.bob.friend_conn_status = False + def bobs_on_friend_connection_status(iTox, friend_id, iStatus, *largs): + LOG_INFO(f"bobs_on_friend_connection_status {friend_id} ?>=0" +repr(iStatus)) + if iStatus > 0: + self.bob.friend_conn_status = True + + self.bob.friend_status = None + def bobs_on_friend_status(iTox, friend_id, iStatus, *largs): + LOG_INFO(f"bobs_on_friend_status {friend_id} ?>=0" +repr(iStatus)) + if iStatus > 0: + self.bob.friend_status = True + + self.alice.friend_conn_status = None + def alices_on_friend_connection_status(iTox, friend_id, iStatus, *largs): + LOG_INFO(f"alices_on_friend_connection_status {friend_id} ?>=0 " +repr(iStatus)) + if iStatus > 0: + self.alice.friend_conn_status = True + + self.alice.friend_status = False + def alices_on_friend_status(iTox, friend_id, iStatus, *largs): + LOG_INFO(f"alices_on_friend_status {friend_id} ?>=0 " +repr(iStatus)) + if iStatus > 0: + self.alice.friend_status = True + + self.alice.callback_friend_connection_status(alices_on_friend_connection_status) + self.alice.callback_friend_status(alices_on_friend_status) + try: + LOG.info("bob_add_alice_as_friend_and_status waiting for alice connections") + if not self.wait_otox_attrs(self.alice, + ['friend_conn_status', + 'friend_status']): + return False + + self.bob.callback_friend_connection_status(bobs_on_friend_connection_status) + self.bob.callback_friend_status(bobs_on_friend_status) + + LOG.info("bob_add_alice_as_friend_and_status waiting for bob connections") + if not self.wait_otox_attrs(self.bob, + ['friend_conn_status', + 'friend_status']): + return False + except Exception as e: + LOG.error(f"bob_add_alice_as_friend_and_status ERROR {e}") + return False + finally: + self.alice.callback_friend_connection_status(None) + self.bob.callback_friend_connection_status(None) + self.alice.callback_friend_status(None) + self.bob.callback_friend_status(None) + return True + + def friend_delete(self, fname, baid): + #: Test delete friend + assert getattr(self, fname).friend_exists(baid) + getattr(self, fname).friend_delete(baid) + self.loop(50) + assert not self.bob.friend_exists(baid) + + def warn_if_no_cb(self, alice, sSlot): + if not hasattr(alice, sSlot+'_cb') or \ + not getattr(alice, sSlot+'_cb'): + LOG.warning(f"self.bob.{sSlot}_cb NOT EXIST") + + def warn_if_cb(self, alice, sSlot): + if hasattr(self.bob, sSlot+'_cb') and \ + getattr(self.bob, sSlot+'_cb'): + LOG.warning(f"self.bob.{sSlot}_cb EXIST") + + # tests are executed in order + def test_notice_log(self): # works + notice = '/var/lib/tor/.SelekTOR/3xx/cache/9050/notice.log' + if True or os.path.exists(notice): + iRet = os.system(f"sudo sed -e '1,/.notice. Bootstrapped 100%/d' {notice}" + \ + "| grep 'Tried for 120 seconds to get a connection to :0.'") + if iRet == 0: + raise SystemExit("seconds to get a connection to :0") + else: + LOG.debug(f"checked {notice}") + + def test_tests_logging(self): # works + with self.assertLogs('foo', level='INFO') as cm: + logging.getLogger('foo').info('first message') + logging.getLogger('foo.bar').error('second message') + logging.getLogger('foo.bar.baz').debug('third message') + self.assertEqual(cm.output, ['INFO:foo:first message', + 'ERROR:foo.bar:second message']) + + def test_tests_start(self): # works + LOG.info("test_tests_start " ) + port = ts.tox_bootstrapd_port() + + assert len(self.bob._address) == 2*TOX_ADDRESS_SIZE, len(self.bob._address) + assert len(self.alice._address) == 2*TOX_ADDRESS_SIZE, \ + len(self.alice._address) + + def test_bootstrap_local_netstat(self): # works + """ + t:bootstrap + """ + if oTOX_OARGS.network not in ['new', 'newlocal', 'local']: + return + + port = ts.tox_bootstrapd_port() + if not port: + return + iStatus = os.system(f"""netstat -nle4 | grep :{port}""") + if iStatus == 0: + LOG.info(f"bootstrap_local_netstat port {port} iStatus={iStatus}") + else: + LOG.warning(f"bootstrap_local_netstat NOT {port} iStatus={iStatus}") + + @unittest.skipIf(not bIS_LOCAL, "local test") + def test_bootstrap_local(self): # works + """ + t:bootstrap + """ + # get port from /etc/tox-bootstrapd.conf 33445 + self.call_bootstrap() + # ts.bootstrap_local(self, self.lUdp) + i = 0 + iStatus = -1 + while i < 10: + i = i + 1 + iStatus = self.bob.self_get_connection_status() + if iStatus != TOX_CONNECTION['NONE']: + break + sleep(3) + else: + pass + + o1 = self.alice.self_get_dht_id() + assert len(o1) == 64 + o2 = self.bob.self_get_dht_id() + assert len(o2) == 64 + +# if o1 != o2: LOG.warning(f"bootstrap_local DHT NOT same {o1} {o2} iStatus={iStatus}") + + iStatus = self.bob.self_get_connection_status() + if iStatus != TOX_CONNECTION['NONE']: + LOG.info(f"bootstrap_local connected iStatus={iStatus}") + return True + iStatus = self.alice.self_get_connection_status() + if iStatus != TOX_CONNECTION['NONE']: + LOG.info(f"bootstrap_local connected iStatus={iStatus}") + return True + LOG.warning(f"bootstrap_local NOT CONNECTED iStatus={iStatus}") + return False + + def test_bootstrap_iNmapInfo(self): # works + if os.environ['USER'] != 'root': + return + if oTOX_OARGS.network in ['new', 'newlocal', 'localnew']: + lElts = self.lUdp + elif oTOX_OARGS.proxy_port > 0: + lElts = self.lTcp + else: + lElts = self.lUdp + lRetval = [] + random.shuffle(lElts) + # assert + ts.bootstrap_iNmapInfo(lElts, oTOX_OARGS, bIS_LOCAL, iNODES=8) + + def test_self_get_secret_key(self): # works + """ + t:self_get_secret_key + """ + # test_self_get_secret_key + CRYPTO_SECRET_KEY_SIZE = 32 + secret_key = create_string_buffer(CRYPTO_SECRET_KEY_SIZE) + oRet0 = self.alice.self_get_secret_key(secret_key) + assert oRet0, repr(oRet0) + LOG.info('test_self_get_secret_key ' +repr(oRet0)) + assert len(str(oRet0)) + del secret_key + + def test_self_get_public_keys(self): # works + """ + t:self_get_secret_key + t:self_get_public_key + """ + + LOG.info('test_self_get_public_keys self.alice.self_get_secret_key') + oRet0 = self.alice.self_get_secret_key() + assert len(oRet0) + LOG.info('test_self_get_public_keys ' +repr(oRet0)) + oRet1 = self.alice.self_get_public_key() + assert len(oRet1) + LOG.info('test_self_get_public_keys ' +repr(oRet1)) + assert oRet0 != oRet1, repr(oRet0) +' != ' +repr(oRet1) + + def test_self_name(self): # works + """ + t:self_set_name + t:self_get_name + t:self_get_name_size + """ + self.alice.self_set_name('Alice') + assert self.alice.self_get_name() == 'Alice' + assert self.alice.self_get_name_size() == len('Alice') + self.bob.self_set_name('Bob') + assert self.bob.self_get_name() == 'Bob' + assert self.bob.self_get_name_size() == len('Bob') + + @unittest.skip('loud') + @unittest.skipIf(bIS_NOT_TOXYGEN or oTOX_OARGS.mode == 0, 'not testing in toxygen') + def test_sound_notification(self): # works + """ + Plays sound notification + :param type of notification + """ + from tests.toxygen_tests import test_sound_notification + test_sound_notification(self) + + def test_address(self): # works + """ + t:self_get_address + t:self_get_nospam + t:self_set_nospam + t:self_get_keys + """ + assert len(self.alice.self_get_address()) == ADDR_SIZE + assert len(self.bob.self_get_address()) == ADDR_SIZE + + self.alice.self_set_nospam(0x12345678) + assert self.alice.self_get_nospam() == 0x12345678 + self.loop(50) + + if hasattr(self.alice, 'self_get_keys'): + pk, sk = self.alice.self_get_keys() + assert pk == self.alice.self_get_address()[:CLIENT_ID_SIZE] + + def test_status_message(self): # works + MSG = 'Happy' + self.alice.self_set_status_message(MSG) + self.loop(100) + assert self.alice.self_get_status_message() == MSG, \ + self.alice.self_get_status_message() +' is not ' +MSG + assert self.alice.self_get_status_message_size() == len(MSG) + + def test_loop_until_connected(self): # works + assert self.loop_until_connected() + + def test_self_get_udp_port(self): # works + """ + t:self_get_udp_port + """ + if hasattr(oTOX_OPTIONS, 'udp_port') and oTOX_OPTIONS.udp_port: + o = self.alice.self_get_udp_port() + LOG.info('self_get_udp_port alice ' +repr(o)) + assert o > 0 + o = self.bob.self_get_udp_port() + LOG.info('self_get_udp_port bob ' +repr(o)) + assert o > 0 + + def test_self_get_tcp_port(self): # works + """ + t:self_get_tcp_port + """ + if hasattr(oTOX_OPTIONS, 'tcp_port') and oTOX_OPTIONS.tcp_port: + # errors if tcp_port <= 0 + o = self.alice.self_get_tcp_port() + LOG.info('self_get_tcp_port ' +repr(o)) + o = self.bob.self_get_tcp_port() + LOG.info('self_get_tcp_port ' +repr(o)) + + def test_get_dht_id(self): # works + """ + t:self_get_dht_id + """ + o1 = self.alice.self_get_dht_id() + assert len(o1) == 64 + o2 = self.bob.self_get_dht_id() + assert len(o2) == 64 + + def test_bob_assert_connection_status(self): # works + if self.bob.self_get_connection_status() == TOX_CONNECTION['NONE']: + RuntimeError("ERROR: NOT CONNECTED " \ + +repr(self.bob.self_get_connection_status())) + + def test_alice_assert_connection_status(self): # works + if self.alice.self_get_connection_status() == TOX_CONNECTION['NONE']: + RuntimeError("ERROR: NOT CONNECTED " \ + +repr(self.alice.self_get_connection_status())) + + def test_bob_assert_mycon_status(self): # works + if self.bob.mycon_status == False: + RuntimeError("ERROR: NOT CONNECTED " \ + +repr(self.bob.mycon_status)) + + def test_alice_assert_mycon_status(self): # works + if self.alice.mycon_status == False: + RuntimeError("ERROR: NOT CONNECTED " \ + +repr(self.alice.mycon_status)) + + def test_bob_add_alice_as_friend_norequest(self): # works + assert len(self.bob.self_get_friend_list()) == 0 + assert self.bob_add_alice_as_friend_norequest() + #: Test last online + assert self.bob.friend_get_last_online(self.baid) is not None + self.bob.friend_delete(self.baid) + + def test_alice_add_bob_as_friend_norequest(self): # works + assert len(self.alice.self_get_friend_list()) == 0 + assert self.alice_add_bob_as_friend_norequest() + assert len(self.alice.self_get_friend_list()) != 0 + #: Test last online + assert self.alice.friend_get_last_online(self.abid) is not None + self.alice.friend_delete(self.abid) + + def test_both_add_as_friend_norequest(self): # works + assert len(self.bob.self_get_friend_list()) == 0 + assert len(self.alice.self_get_friend_list()) == 0 + self.both_add_as_friend_norequest() + + self.bob.friend_delete(self.baid) + self.alice.friend_delete(self.abid) + assert len(self.bob.self_get_friend_list()) == 0 + assert len(self.alice.self_get_friend_list()) == 0 + + def test_bob_add_alice_as_friend_and_status(self): + self.bob_add_alice_as_friend_and_status() + self.bob.friend_delete(self.baid) + + @unittest.skip('malloc_consolidate(): invalid chunk size') +# @unittest.skipIf(bIS_LOCAL, "local test") +# @expectedFailure # (bIS_LOCAL, "local test") + def test_bob_add_alice_as_friend(self): # fails + assert len(self.bob.self_get_friend_list()) == 0 + try: + assert self.bob_add_alice_as_friend() + #: Test last online + assert self.bob.friend_get_last_online(self.baid) is not None + except AssertionError as e: + #WTF? + self.bob.friend_delete(self.baid) + raise RuntimeError(f"Failed test {e}") + finally: + self.bob.friend_delete(self.baid) + assert len(self.bob.self_get_friend_list()) == 0 + + @unittest.skip('malloc_consolidate(): invalid chunk size') +# @unittest.skipIf(bIS_LOCAL, "local test") +# @expectedFailure + def test_alice_add_bob_as_friend(self): # fails + assert len(self.bob.self_get_friend_list()) == 0 + try: + assert self.alice_add_bob_as_friend() + #: Test last online + assert self.alice.friend_get_last_online(self.abid) is not None + except AssertionError as e: + raise RuntimeError(f"Failed test {e}") + except Exception as e: + LOG.error(f"test_alice_add_bob_as_friend EXCEPTION {e}") + raise + finally: + self.alice.friend_delete(self.abid) + assert len(self.alice.self_get_friend_list()) == 0 + +# @unittest.skipIf(bIS_LOCAL, "local test") + @expectedFailure + def test_both_add_as_friend(self): # works + try: + self.both_add_as_friend() + except AssertionError as e: + raise RuntimeError(f"Failed test {e}") + except Exception as e: + LOG.error(f"test_both_add_as_friend EXCEPTION {e}") + raise + finally: + self.bob.friend_delete(self.baid) + self.alice.friend_delete(self.abid) + assert len(self.bob.self_get_friend_list()) == 0 + assert len(self.alice.self_get_friend_list()) == 0 + + @unittest.skip('unfinished') + def test_bob_add_alice_as_friend_and_status(self): + assert self.bob_add_alice_as_friend_and_status() + self.bob.friend_delete(self.baid) + +#? @unittest.skip('fails') + @expectedFailure + def test_on_friend_status_message(self): # fails + """ + t:self_set_status_message + t:self_get_status_message + t:self_get_status_message_size + t:friend_set_status_message + t:friend_get_status_message + t:friend_get_status_message_size + t:on_friend_status_message + """ + MSG = 'Happy' + sSlot = 'friend_status_message' + + def bob_on_friend_status_message(iTox, friend_id, new_status_message, new_status_size, *largs): + try: + assert str(new_status_message, 'UTF-8') == MSG + assert friend_id == self.baid + except Exception as e: + LOG_ERROR(f"BOB_ON_friend_status_message EXCEPTION {e}") + else: + LOG_INFO(f"BOB_ON_friend_status_message {friend_id}" \ + +repr(new_status_message)) + setattr(self.bob, sSlot, True) + + setattr(self.bob, sSlot, None) + try: + if oTOX_OARGS.bIS_LOCAL: + assert self.bob_add_alice_as_friend_norequest() + else: + assert self.bob_add_alice_as_friend() + + self.bob.callback_friend_status_message(bob_on_friend_status_message) + self.warn_if_no_cb(self.bob, sSlot) + self.alice.self_set_status_message(MSG) + assert self.wait_otox_attrs(self.bob, [sSlot]) + + assert self.bob.friend_get_status_message(self.baid) == MSG + assert self.bob.friend_get_status_message_size(self.baid) == len(MSG) + + except AssertionError as e: + raise RuntimeError(f"Failed test {e}") + except Exception as e: + LOG.error(f"test_on_friend_status_message EXCEPTION {e}") + raise + finally: + self.alice.callback_friend_status(None) + self.bob.friend_delete(self.baid) + + @expectedFailure + def test_friend(self): # works + """ + t:friend_delete + t:friend_exists + t:friend_get_public_key + t:self_get_friend_list + t:self_get_friend_list_size + t:self_set_name + t:friend_get_name + t:friend_get_name_size + t:on_friend_name + """ + + assert len(self.bob.self_get_friend_list()) == 0 + assert len(self.alice.self_get_friend_list()) == 0 + #: Test friend request + if oTOX_OARGS.bIS_LOCAL: + assert self.bob_add_alice_as_friend_norequest() + assert self.alice_add_bob_as_friend_norequest() + else: + # no not connected error + assert self.bob_add_alice_as_friend() + assert self.alice_add_bob_as_friend() + try: + assert self.bob.friend_get_public_key(self.baid) == \ + self.alice.self_get_address()[:CLIENT_ID_SIZE] + + #: Test friend_get_public_key + assert self.alice.friend_get_public_key(self.abid) == \ + self.bob.self_get_address()[:CLIENT_ID_SIZE] + except AssertionError as e: + raise RuntimeError(f"Failed test {e}") + except Exception as e: + LOG.error(f"test_friend EXCEPTION {e}") + raise + finally: + self.bob.friend_delete(self.baid) + self.alice.friend_delete(self.abid) + +# @unittest.skip('fails') +# @unittest.skipIf(not bIS_LOCAL and not ts.bAreWeConnected(), 'NOT CONNECTED') + @expectedFailure + def test_user_status(self): + """ + t:self_get_status + t:self_set_status + t:friend_get_status + t:friend_get_status + t:on_friend_status + """ + sSlot = 'friend_status' + if oTOX_OARGS.bIS_LOCAL: + assert self.bob_add_alice_as_friend_norequest() + else: + assert self.bob_add_alice_as_friend() + + sSTATUS = TOX_USER_STATUS['NONE'] + setattr(self.bob, sSlot, None) + def bobs_on_friend_set_status(iTox, friend_id, new_status, *largs): + LOG_INFO(f"bobs_on_friend_set_status {friend_id} {new_status}") + try: + assert friend_id == self.baid + assert new_status in [TOX_USER_STATUS['BUSY'], TOX_USER_STATUS['AWAY']] + except Exception as e: + LOG_WARN(f"bobs_on_friend_set_status EXCEPTION {e}") + setattr(self.bob, sSlot, True) + + try: + if not self.get_connection_status(): + LOG.warning(f"test_user_status NOT CONNECTED self.get_connection_status") + self.loop_until_connected() + + self.bob.callback_friend_status(bobs_on_friend_set_status) + self.warn_if_no_cb(self.bob, sSlot) + sSTATUS = TOX_USER_STATUS['BUSY'] + self.alice.self_set_status(sSTATUS) + sSTATUS = TOX_USER_STATUS['AWAY'] + self.alice.self_set_status(sSTATUS) + assert self.wait_otox_attrs(self.bob, [sSlot]) + # wait_obj_attr count >= 15 for friend_status + + self.alice.self_set_status(TOX_USER_STATUS['NONE']) + assert self.alice.self_get_status() == TOX_USER_STATUS['NONE'] + assert self.bob.friend_get_status(self.baid) == TOX_USER_STATUS['NONE'] + + except AssertionError as e: + raise RuntimeError(f"Failed test {e}") + + except Exception as e: + LOG.error(f"test_user_status EXCEPTION {e}") + raise + finally: + self.bob.callback_friend_status(None) + self.warn_if_cb(self.bob, sSlot) + self.bob.friend_delete(self.baid) + + @unittest.skip('crashes') + def test_connection_status(self): + """ + t:friend_get_connection_status + t:on_friend_connection_status + """ + LOG.info("test_connection_status ") + if oTOX_OARGS.bIS_LOCAL: + assert self.bob_add_alice_as_friend_norequest() + else: + assert self.bob_add_alice_as_friend() + + sSlot = 'friend_connection_status' + setattr(self.bob, sSlot, None) + def bobs_on_friend_connection_status(iTox, friend_id, iStatus, *largs): + setattr(self.bob, sSlot, True) + LOG_INFO(f"bobs_on_friend_connection_status " +repr(iStatus)) + try: + assert friend_id == self.baid + except Exception as e: + LOG.error(f"bobs_on_friend_connection_status ERROR {e}") + + opts = oToxygenToxOptions(oTOX_OARGS) + try: + setattr(self.bob, sSlot, True) + self.bob.callback_friend_connection_status(bobs_on_friend_connection_status) + + LOG.info("test_connection_status killing alice") + self.alice.kill() #! bang + LOG.info("test_connection_status making alice") + self.alice = Tox(opts, app=oAPP) + LOG.info("test_connection_status maked alice") + + assert self.wait_otox_attrs(self.bob, [sSlot]) + except AssertionError as e: + raise + except Exception as e: + LOG.error(f"bobs_on_friend_connection_status {e}") + raise + finally: + self.bob.callback_friend_connection_status(None) + + #? assert self.bob.friend_get_connection_status(self.aid) is False + self.bob.friend_delete(self.baid) + +#? @unittest.skip('fails') + def test_friend_name(self): # fails + """ + t:self_set_name + t:friend_get_name + t:friend_get_name_size + t:on_friend_name + """ + + sSlot= 'friend_name' + #: Test friend request + + LOG.info("test_friend_name") + if oTOX_OARGS.bIS_LOCAL: + assert self.bob_add_alice_as_friend_norequest() + else: + assert self.bob_add_alice_as_friend() + + if not self.get_connection_status(): + LOG.warning(f"test_friend_message NOT CONNECTED") + self.loop_until_connected() + + #: Test friend name + NEWNAME = 'Jenny' + + def bobs_on_friend_name(iTox, fid, newname, iNameSize, *largs): + LOG_INFO(f"bobs_on_friend_name {sSlot} {fid}") + try: + assert fid == self.baid + assert str(newname, 'UTF-8') == NEWNAME + except Exception as e: + LOG.error(f"bobs_on_friend_name EXCEPTION {e}") + setattr(self.bob, sSlot, True) + + setattr(self.bob, sSlot, None) + self.bob.callback_friend_name(bobs_on_friend_name) + self.warn_if_no_cb(self.bob, sSlot) + try: + self.alice.self_set_name(NEWNAME) + assert self.wait_otox_attrs(self.bob, [sSlot]) + + assert self.bob.friend_get_name(self.baid) == NEWNAME + assert self.bob.friend_get_name_size(self.baid) == len(NEWNAME) + + except AssertionError as e: + raise RuntimeError(f"test_friend Failed test {e}") + + except Exception as e: + LOG.error(f"test_friend EXCEPTION {e}") + raise + + finally: + self.bob.callback_friend_name(None) + if hasattr(self.bob, sSlot + '_cb') and \ + getattr(self.bob, sSlot + '_cb'): + LOG.warning(sSlot + ' EXISTS') + + self.bob.friend_delete(self.baid) + + # wait_ensure_exec ArgumentError This client is currently not connected to the friend. + def test_friend_message(self): # fails + """ + t:on_friend_action + t:on_friend_message + t:friend_send_message + """ + + #: Test message + MSG = 'Hi, Bob!' + sSlot = 'friend_message' + if oTOX_OARGS.bIS_LOCAL: + assert self.both_add_as_friend_norequest() + else: + assert self.both_add_as_friend() + + if not self.get_connection_status(): + LOG.warning(f"test_friend_message NOT CONNECTED") + self.loop_until_connected() + + iRet = self.bob.friend_get_connection_status(self.baid) + if iRet == TOX_CONNECTION['NONE']: + LOG.error("bob.friend_get_connection_status") + raise RuntimeError("bob.friend_get_connection_status") + iRet = self.alice.friend_get_connection_status(self.abid) + if iRet == TOX_CONNECTION['NONE']: + LOG.error("alice.friend_get_connection_status") + raise RuntimeError("alice.friend_get_connection_status") + + def alices_on_friend_message(iTox, fid, msg_type, message, iSize, *largs): + LOG_DEBUG(f"alices_on_friend_message {fid} {message}") + try: + assert fid == self.alice.abid + assert msg_type == TOX_MESSAGE_TYPE['NORMAL'] + assert str(message, 'UTF-8') == MSG + except Exception as e: + LOG_ERROR(f"alices_on_friend_message EXCEPTION {e}") + else: + LOG_INFO(f"alices_on_friend_message {message}") + setattr(self.alice, sSlot, True) + + setattr(self.alice, sSlot, None) + try: + self.alice.callback_friend_message(alices_on_friend_message) + self.warn_if_no_cb(self.alice, sSlot) + + # dunno - both This client is currently NOT CONNECTED to the friend. + if True: + iMesId = self.bob.friend_send_message( + self.baid, + TOX_MESSAGE_TYPE['NORMAL'], + bytes(MSG, 'UTF-8')) + # ArgumentError('This client is currently NOT CONNECTED to the friend.') + else: + iMesId = self.wait_ensure_exec(self.bob.friend_send_message, + [self.baid, + TOX_MESSAGE_TYPE['NORMAL'], + bytes(MSG, 'UTF-8')]) + assert iMesId >= 0 + assert self.wait_otox_attrs(self.alice, [sSlot]) + except ArgumentError as e: + # ArgumentError('This client is currently NOT CONNECTED to the friend.') + # dunno + LOG.error(f"test_friend_message {e}") + raise + except AssertionError as e: + LOG.warning(f"test_friend_message {e}") + raise RuntimeError(f"Failed test test_friend_message {e}") + except Exception as e: + LOG.error(f"test_friend_message {e}") + raise + finally: + self.alice.callback_friend_message(None) + self.warn_if_cb(self.alice, sSlot) + self.bob.friend_delete(self.baid) + self.alice.friend_delete(self.abid) + +#? @unittest.skip('fails') + def test_friend_action(self): + """ + t:on_friend_action + t:on_friend_message + t:friend_send_message + """ + + if oTOX_OARGS.bIS_LOCAL: + assert self.both_add_as_friend_norequest() + else: + assert self.both_add_as_friend() + + if not self.get_connection_status(): + LOG.warning(f"test_friend_message NOT CONNECTED") + self.loop_until_connected() + + iRet = self.bob.friend_get_connection_status(self.baid) + if iRet == TOX_CONNECTION['NONE']: + LOG.error("bob.friend_get_connection_status") + raise RuntimeError("bob.friend_get_connection_status") + iRet = self.alice.friend_get_connection_status(self.abid) + if iRet == TOX_CONNECTION['NONE']: + LOG.error("alice.friend_get_connection_status") + raise RuntimeError("alice.friend_get_connection_status") + + BID = self.baid + #: Test action + ACTION = 'Kick' + sSlot = 'friend_read_action' + setattr(self.bob, sSlot, None) + sSlot = 'friend_read_receipt' + setattr(self.bob, sSlot, None) + def alices_on_friend_action(iTox, fid, msg_type, action, *largs): + sSlot = 'friend_read_action' + LOG_DEBUG(f"alices_on_friend_action") + try: + assert fid == self.bob.baid + assert msg_type == TOX_MESSAGE_TYPE['ACTION'] + assert action == ACTION + except Exception as e: + LOG_ERROR(f"alices_on_friend_action EXCEPTION {e}") + else: + LOG_INFO(f"alices_on_friend_action {message}") + setattr(self.bob, sSlot, True) + + sSlot = 'friend_read_action' + setattr(self.alice, sSlot, None) + sSlot = 'friend_read_receipt' + setattr(self.alice, sSlot, None) + def alices_on_read_reciept(iTox, fid, msg_id, *largs): + LOG_DEBUG(f"alices_on_read_reciept") + sSlot = 'friend_read_receipt' + try: + assert fid == BID + except Exception as e: + LOG_ERROR(f"alices_on_read_reciept {e}") + else: + LOG_INFO(f"alices_on_read_reciept {fid}") + setattr(self.alice, sSlot, True) + + sSlot = 'friend_read_receipt' + try: + sSlot = 'friend_read_action' + setattr(self.bob, sSlot, False) + sSlot = 'friend_read_receipt' + setattr(self.alice, sSlot, False) + + self.alice.callback_friend_read_receipt(alices_on_read_reciept) #was alices_on_friend_action + self.warn_if_no_cb(self.alice, sSlot) + assert self.wait_ensure_exec(self.bob.friend_send_message, + [self.baid, + TOX_MESSAGE_TYPE['ACTION'], + bytes(ACTION, 'UTF-8')]) + assert self.wait_otox_attrs(self.alice, [sSlot]) + except AssertionError as e: + raise RuntimeError(f"Failed test {e}") + except ArgumentError as e: + # ArgumentError('This client is currently NOT CONNECTED to the friend.') + # dunno + LOG.warning(f"test_friend_action {e}") + except Exception as e: + LOG.error(f"test_friend_action {e}") + raise + finally: + self.alice.callback_friend_read_receipt(None) + self.bob.friend_delete(self.baid) + self.alice.friend_delete(self.abid) + + @unittest.skip('fails') + def test_alice_typing_status(self): + """ + t:on_friend_read_receipt + t:on_friend_typing + t:self_set_typing + t:friend_get_typing + t:friend_get_last_online + """ + + sSlot = 'friend_typing' + # works + LOG.info("test_typing_status bob adding alice") + if oTOX_OARGS.bIS_LOCAL: + assert self.both_add_as_friend_norequest() + else: + assert self.both_add_as_friend() + + BID = self.baid + + #: Test typing status + def bob_on_friend_typing(iTox, fid, is_typing, *largs): + try: + assert fid == BID + assert is_typing is True + assert self.bob.friend_get_typing(fid) is True + except Exception as e: + LOG.error(f"BOB_ON_friend_typing {e}") + raise + else: + LOG_INFO(f"BOB_ON_friend_typing" + str(fid)) + setattr(self.bob, sSlot, True) + + setattr(self.bob, sSlot, None) + try: + if not self.get_connection_status(): + LOG.warning(f"test_friend_message NOT CONNECTED") + self.loop_until_connected() + + self.bob.callback_friend_typing(bob_on_friend_typing) + self.alice.self_set_typing(self.abid, True) + assert self.wait_otox_attrs(self.bob, [sSlot]) + if not hasattr(self.bob, sSlot+'_cb') or \ + not getattr(self.bob, sSlot+'_cb'): + LOG.warning(f"self.bob.{sSlot}_cb NOT EXIST") + except AssertionError as e: + raise RuntimeError(f"Failed test {e}") + except Exception as e: + LOG.error(f"test_alice_typing_status error={e}") + raise + finally: + self.bob.callback_friend_typing(None) + self.bob.friend_delete(self.baid) + self.alice.friend_delete(self.abid) + + @unittest.skip('unfinished') + def test_file_transfer(self): # unfinished + """ + t:file_send + t:file_send_chunk + t:file_control + t:file_seek + t:file_get_file_id + t:on_file_recv + t:on_file_recv_control + t:on_file_recv_chunk + t:on_file_chunk_request + """ + + if oTOX_OARGS.bIS_LOCAL: + assert self.bob_add_alice_as_friend_norequest() + else: + assert self.bob_add_alice_as_friend() + + BID = self.baid + + FRIEND_NUMBER = self.baid + FILE_NUMBER = 1 + FILE = os.urandom(1024 * 1024) + FILE_NAME = b"/tmp/test.bin" + if not os.path.exists(FILE_NAME): + with open(FILE_NAME, 'wb') as oFd: + oFd.write(FILE) + FILE_SIZE = len(FILE) + OFFSET = 567 + + m = hashlib.md5() + m.update(FILE[OFFSET:]) + FILE_DIGEST = m.hexdigest() + + CONTEXT = { 'FILE': bytes(), 'RECEIVED': 0, 'START': False, 'SENT': 0 } + + def alice_on_file_recv(iTox, fid, file_number, kind, size, filename): + LOG_DEBUG(f"ALICE_ON_file_recv fid={fid} {file_number}") + try: + assert size == FILE_SIZE + assert filename == FILE_NAME + retv = self.alice.file_seek(fid, file_number, OFFSET) + assert retv is True + self.alice.file_control(fid, file_number, TOX_FILE_CONTROL['RESUME']) + except Exception as e: + LOG_ERROR(f"ALICE_ON_file_recv {e}") + else: + LOG_INFO(f"ALICE_ON_file_recv " + str(fid)) + + def alice_on_file_recv_control(iTox, fid, file_number, control, *largs): + # TOX_FILE_CONTROL = { 'RESUME': 0, 'PAUSE': 1, 'CANCEL': 2,} + LOG_DEBUG(f"ALICE_ON_file_recv_control fid={fid} {file_number} {control}") + try: + assert FILE_NUMBER == file_number + # FixMe _FINISHED? + if False and control == TOX_FILE_CONTROL['RESUME']: + # assert CONTEXT['RECEIVED'] == FILE_SIZE + # m = hashlib.md5() + # m.update(CONTEXT['FILE']) + # assert m.hexdigest() == FILE_DIGEST + self.alice.completed = True + except Exception as e: + LOG_ERROR(f"ALICE_ON_file_recv {e}") + else: + LOG_INFO(f"ALICE_ON_file_recv " + str(fid)) + + self.alice.completed = False + def alice_on_file_recv_chunk(iTox, fid, file_number, position, iNumBytes, *largs): + LOG_DEBUG(f"ALICE_ON_file_recv_chunk {fid} {file_number}") + # FixMe - use file_number and iNumBytes to get data? + data = '' + try: + if data is None: + assert CONTEXT['RECEIVED'] == (FILE_SIZE - OFFSET) + m = hashlib.md5() + m.update(CONTEXT['FILE']) + assert m.hexdigest() == FILE_DIGEST + self.alice.completed = True + self.alice.file_control(fid, file_number, TOX_FILE_CONTROL['CANCEL']) + return + + CONTEXT['FILE'] += data + CONTEXT['RECEIVED'] += len(data) + # if CONTEXT['RECEIVED'] < FILE_SIZE: + # assert self.file_data_remaining( + # fid, file_number, 1) == FILE_SIZE - CONTEXT['RECEIVED'] + except Exception as e: + LOG_ERROR(f"ALICE_ON_file_recv_chunk {e}") + else: + LOG_INFO(f"ALICE_ON_file_recv_chunk {fid}") + + # AliceTox.on_file_send_request = on_file_send_request + # AliceTox.on_file_control = on_file_control + # AliceTox.on_file_data = on_file_data + + LOG.info(f"test_file_transfer: baid={self.baid}") + try: + self.alice.callback_file_recv(alice_on_file_recv) + self.alice.callback_file_recv_control(alice_on_file_recv_control) + self.alice.callback_file_recv_chunk(alice_on_file_recv_chunk) + + self.bob.completed = False + def bob_on_file_recv_control2(iTox, fid, file_number, control): + LOG_DEBUG(f"BOB_ON_file_recv_control2 {fid} {file_number} control={control}") + if control == TOX_FILE_CONTROL['RESUME']: + CONTEXT['START'] = True + elif control == TOX_FILE_CONTROL['CANCEL']: + self.bob.completed = True + pass + + def bob_on_file_chunk_request(iTox, fid, file_number, position, length, *largs): + LOG_DEBUG(f"BOB_ON_file_chunk_request {fid} {file_number}") + if length == 0: + return + data = FILE[position:(position + length)] + self.bob.file_send_chunk(fid, file_number, position, data) + + sSlot = 'file_recv_control' + self.bob.callback_file_recv_control(bob_on_file_recv_control2) + self.bob.callback_file_chunk_request(bob_on_file_chunk_request) + + # was FILE_ID = FILE_NAME + FILE_ID = 32*'1' # + FILE_NAME = b'test.in' + + if not self.get_connection_status(): + LOG.warning(f"test_file_transfer NOT CONNECTED") + self.loop_until_connected() + + i = 0 + iKind = 0 + while i < 2: + i += 1 + try: + FN = self.bob.file_send(self.baid, iKind, FILE_SIZE, FILE_ID, FILE_NAME) + LOG.info(f"test_file_transfer bob.file_send {FN}") + except ArgumentError as e: + LOG.debug(f"test_file_transfer bob.file_send {e} {i}") + # ctypes.ArgumentError: This client is currently not connected to the friend. + raise + else: + break + self.loop(100) + sleep(1) + else: + LOG.error(f"test_file_transfer bob.file_send 2") + raise RuntimeError(f"test_file_transfer bob.file_send {THRESHOLD // 2}") + + # UINT32_MAX + FID = self.bob.file_get_file_id(self.baid, FN) + hexFID = "".join([hex(ord(c))[2:].zfill(2) for c in FILE_NAME]) + assert FID.startswith(hexFID.upper()) + + if not self.wait_obj_attrs(self.bob, ['completed']): + LOG.warning(f"test_file_transfer Bob not completed") + return False + if not self.wait_obj_attrs(self.alice, ['completed']): + LOG.warning(f"test_file_transfer Alice not completed") + return False + return True + + except (ArgumentError, ValueError,) as e: + # ValueError: non-hexadecimal number found in fromhex() arg at position 0 + LOG_ERROR(f"test_file_transfer: {e}") + raise + + except Exception as e: + LOG_ERROR(f"test_file_transfer:: {e}") + LOG_DEBUG('\n' + traceback.format_exc()) + raise + + finally: + self.bob.friend_delete(self.baid) + self.alice.callback_file_recv(None) + self.alice.callback_file_recv_control(None) + self.alice.callback_file_recv_chunk(None) + self.bob.callback_file_recv_control(None) + self.bob.callback_file_chunk_request(None) + + LOG_INFO(f"test_file_transfer:: self.wait_objs_attr completed") + + @unittest.skip('crashes') + def test_tox_savedata(self): # works sorta + # but "{addr} != {self.alice.self_get_address()}" + """ + t:get_savedata_size + t:get_savedata + """ + # Fatal Python error: Aborted + # "/var/local/src/toxygen_wrapper/wrapper/tox.py", line 180 in kill + return + + assert self.alice.get_savedata_size() > 0 + data = self.alice.get_savedata() + assert data is not None + addr = self.alice.self_get_address() + # self._address + + try: + LOG.info("test_tox_savedata alice.kill") + # crashes + self.alice.kill() + except: + pass + + oArgs = oTOX_OARGS + opts = oToxygenToxOptions(oArgs) + opts.savedata_data = data + opts.savedata_length = len(data) + + self.alice = Tox(tox_options=opts) + if addr != self.alice.self_get_address(): + LOG.warning("test_tox_savedata " + + f"{addr} != {self.alice.self_get_address()}") + else: + LOG.info("passed test_tox_savedata") + +def vOargsToxPreamble(oArgs, Tox, ToxTest): + + ts.vSetupLogging(oArgs) + + methods = set([x for x in dir(Tox) if not x[0].isupper() + and not x[0] == '_']) + docs = "".join([getattr(ToxTest, x).__doc__ for x in dir(ToxTest) + if getattr(ToxTest, x).__doc__ is not None]) + + tested = set(re.findall(r't:(.*?)\n', docs)) + not_tested = methods.difference(tested) + + logging.info('Test Coverage: %.2f%%' % (len(tested) * 100.0 / len(methods))) + if len(not_tested): + logging.info('Not tested:\n %s' % "\n ".join(sorted(list(not_tested)))) + +### + +def iMain(oArgs): + failfast=True + + vOargsToxPreamble(oArgs, Tox, ToxSuite) + # https://stackoverflow.com/questions/35930811/how-to-sort-unittest-testcases-properly/35930812#35930812 + cases = ts.suiteFactory(*ts.caseFactory([ToxSuite])) + if color_runner: + runner = color_runner.runner.TextTestRunner(verbosity=2, failfast=failfast) + else: + runner = unittest.TextTestRunner(verbosity=2, failfast=failfast, warnings='ignore') + runner.run(cases) + +def oToxygenToxOptions(oArgs): + data = None + tox_options = wrapper.tox.Tox.options_new() + if oArgs.proxy_type: + tox_options.contents.proxy_type = int(oArgs.proxy_type) + tox_options.contents.proxy_host = bytes(oArgs.proxy_host, 'UTF-8') + tox_options.contents.proxy_port = int(oArgs.proxy_port) + tox_options.contents.udp_enabled = False + else: + tox_options.contents.udp_enabled = oArgs.udp_enabled + if not os.path.exists('/proc/sys/net/ipv6'): + oArgs.ipv6_enabled = False + else: + tox_options.contents.ipv6_enabled = oArgs.ipv6_enabled + + tox_options.contents.tcp_port = int(oArgs.tcp_port) + tox_options.contents.dht_announcements_enabled = oArgs.dht_announcements_enabled + tox_options.contents.hole_punching_enabled = oArgs.hole_punching_enabled + + # overrides + tox_options.contents.local_discovery_enabled = False + tox_options.contents.experimental_thread_safety = False + # REQUIRED!! + if oArgs.ipv6_enabled and not os.path.exists('/proc/sys/net/ipv6'): + LOG.warning('Disabling IPV6 because /proc/sys/net/ipv6 does not exist' + repr(oArgs.ipv6_enabled)) + tox_options.contents.ipv6_enabled = False + else: + tox_options.contents.ipv6_enabled = bool(oArgs.ipv6_enabled) + + if data: # load existing profile + tox_options.contents.savedata_type = enums.TOX_SAVEDATA_TYPE['TOX_SAVE'] + tox_options.contents.savedata_data = c_char_p(data) + tox_options.contents.savedata_length = len(data) + else: # create new profile + tox_options.contents.savedata_type = enums.TOX_SAVEDATA_TYPE['NONE'] + tox_options.contents.savedata_data = None + tox_options.contents.savedata_length = 0 + + #? tox_options.contents.log_callback = LOG + if tox_options._options_pointer: + # LOG.debug("Adding logging to tox_options._options_pointer ") + ts.vAddLoggerCallback(tox_options, ts.on_log) + else: + LOG.warning("No tox_options._options_pointer " +repr(tox_options._options_pointer)) + + return tox_options + +def oArgparse(lArgv): + parser = ts.oMainArgparser() + parser.add_argument('profile', type=str, nargs='?', default=None, + help='Path to Tox profile') + oArgs = parser.parse_args(lArgv) + + for key in ts.lBOOLEANS: + if key not in oArgs: continue + val = getattr(oArgs, key) + setattr(oArgs, key, bool(val)) + + if hasattr(oArgs, 'sleep'): + if oArgs.sleep == 'qt': + pass # broken or gevent.sleep(idle_period) + elif oArgs.sleep == 'gevent': + pass # broken or gevent.sleep(idle_period) + else: + oArgs.sleep = 'time' + + return oArgs + +def main(lArgs=None): + global oTOX_OARGS + if lArgs is None: lArgs = [] + oArgs = oArgparse(lArgs) + global bIS_LOCAL + bIS_LOCAL = oArgs.network in ['newlocal', 'localnew', 'local'] + oTOX_OARGS = oArgs + setattr(oTOX_OARGS, 'bIS_LOCAL', bIS_LOCAL) + bIS_LOCAL = True + setattr(oTOX_OARGS, 'bIS_LOCAL', bIS_LOCAL) + # oTOX_OPTIONS = ToxOptions() + global oTOX_OPTIONS + oTOX_OPTIONS = oToxygenToxOptions(oArgs) + if coloredlogs: + # https://pypi.org/project/coloredlogs/ + coloredlogs.install(level=oArgs.loglevel, + logger=LOG, + # %(asctime)s,%(msecs)03d %(hostname)s [%(process)d] + fmt='%(name)s %(levelname)s %(message)s' + ) + else: + logging.basicConfig(level=oArgs.loglevel) # logging.INFO + + return iMain(oArgs) + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) + +# Ran 33 tests in 51.733s diff --git a/toxygen/tests/toxygen_tests.py b/toxygen/tests/toxygen_tests.py new file mode 100644 index 0000000..f45bb52 --- /dev/null +++ b/toxygen/tests/toxygen_tests.py @@ -0,0 +1,17 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +from notifications import sound +from notifications.sound import SOUND_NOTIFICATION +from time import sleep + +if True: + def test_sound_notification(self): + """ + Plays sound notification + :param type of notification + """ + sound.sound_notification( SOUND_NOTIFICATION['MESSAGE'] ) + sleep(10) + sound.sound_notification( SOUND_NOTIFICATION['FILE_TRANSFER'] ) + sleep(10) + sound.sound_notification( None ) diff --git a/toxygen/third_party/qweechat/qweechat.py b/toxygen/third_party/qweechat/qweechat.py index 9b057bd..850cb95 100644 --- a/toxygen/third_party/qweechat/qweechat.py +++ b/toxygen/third_party/qweechat/qweechat.py @@ -90,7 +90,7 @@ class MainWindow(QtWidgets.QMainWindow): self.list_buffers.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) self.stacked_buffers.setSizePolicy(QtWidgets.QSizePolicy.Expanding, - QtWidgets.QSizePolicy.Expanding) + QtWidgets.QSizePolicy.Expanding) # MainWindow self.setCentralWidget(splitter) @@ -191,7 +191,7 @@ class MainWindow(QtWidgets.QMainWindow): self.actions['preferences'], self.actions['about'], self.actions['quit']]) - self.toolbar = toolbar + self.toolbar = toolbar self.buffers[0].widget.input.setFocus() # open debug dialog diff --git a/toxygen/ui/items_factories.py b/toxygen/ui/items_factories.py index 02ef31e..f6f38eb 100644 --- a/toxygen/ui/items_factories.py +++ b/toxygen/ui/items_factories.py @@ -98,9 +98,7 @@ class MessagesItemsFactory: return item - # ----------------------------------------------------------------------------------------------------------------- # Private methods - # ----------------------------------------------------------------------------------------------------------------- def _create_message_browser(self, text, width, message_type, parent=None): return MessageBrowser(self._settings, self._message_edit, self._smiley_loader, self._plugin_loader, diff --git a/toxygen/ui/main_screen.py b/toxygen/ui/main_screen.py index cfe677c..b873814 100644 --- a/toxygen/ui/main_screen.py +++ b/toxygen/ui/main_screen.py @@ -652,9 +652,7 @@ class MainWindow(QtWidgets.QMainWindow): else: super().keyPressEvent(event) - # ----------------------------------------------------------------------------------------------------------------- # Functions which called when user click in menu - # ----------------------------------------------------------------------------------------------------------------- def log_console(self): self._me.show() @@ -759,7 +757,7 @@ class MainWindow(QtWidgets.QMainWindow): self._we.list_buffers.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) self._we.stacked_buffers.setSizePolicy(QtWidgets.QSizePolicy.Expanding, - QtWidgets.QSizePolicy.Expanding) + QtWidgets.QSizePolicy.Expanding) LOG.info("Showing WeechatConsole") self._we.show() @@ -877,9 +875,7 @@ class MainWindow(QtWidgets.QMainWindow): 120)) self.menu.show() - # ----------------------------------------------------------------------------------------------------------------- # Messages, calls and file transfers - # ----------------------------------------------------------------------------------------------------------------- def send_message(self): self._messenger.send_message() @@ -942,9 +938,7 @@ class MainWindow(QtWidgets.QMainWindow): self.videocallButton.setIcon(icon) self.videocallButton.setIconSize(QtCore.QSize(35, 35)) - # ----------------------------------------------------------------------------------------------------------------- # Functions which called when user open context menu in friends list - # ----------------------------------------------------------------------------------------------------------------- def _friend_right_click(self, pos): item = self.friends_list.itemAt(pos) @@ -1001,9 +995,7 @@ class MainWindow(QtWidgets.QMainWindow): def select_contact_row(self, row_index): self.friends_list.setCurrentRow(row_index) - # ----------------------------------------------------------------------------------------------------------------- # Functions which called when user click somewhere else - # ----------------------------------------------------------------------------------------------------------------- def _selected_contact_changed(self): num = self.friends_list.currentRow() diff --git a/toxygen/ui/messages_widgets.py b/toxygen/ui/messages_widgets.py index 1bdf77c..40a5435 100644 --- a/toxygen/ui/messages_widgets.py +++ b/toxygen/ui/messages_widgets.py @@ -48,7 +48,7 @@ class MessageBrowser(QtWidgets.QTextBrowser): # resize(self, a0: QSize): argument 1 has unexpected type 'int' # resize(self, w: int, h: int): argument 2 has unexpected type 'float' pass - + self.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse | QtCore.Qt.LinksAccessibleByMouse) self.anchorClicked.connect(self.on_anchor_clicked) diff --git a/toxygen/ui/views/add_bootstrap_screen.ui b/toxygen/ui/views/add_bootstrap_screen.ui new file mode 100644 index 0000000..0549e90 --- /dev/null +++ b/toxygen/ui/views/add_bootstrap_screen.ui @@ -0,0 +1,99 @@ + + + Form + + + + 0 + 0 + 560 + 320 + + + + + 560 + 320 + + + + + 560 + 320 + + + + Form + + + + + 50 + 10 + 150 + 20 + + + + TextLabel + + + + + + 50 + 70 + 150 + 30 + + + + TextLabel + + + + + + 50 + 110 + 460 + 150 + + + + + + + 50 + 270 + 460 + 30 + + + + PushButton + + + + + true + + + + 220 + 10 + 321 + 31 + + + + Qt::NoContextMenu + + + + + + + + + diff --git a/toxygen/ui/views/add_contact_screen.ui b/toxygen/ui/views/add_contact_screen.ui new file mode 100644 index 0000000..0f26a25 --- /dev/null +++ b/toxygen/ui/views/add_contact_screen.ui @@ -0,0 +1,99 @@ + + + Form + + + + 0 + 0 + 560 + 320 + + + + + 560 + 320 + + + + + 560 + 320 + + + + Form + + + + + 50 + 10 + 150 + 20 + + + + TextLabel + + + + + + 50 + 70 + 150 + 30 + + + + TextLabel + + + + + + 50 + 110 + 460 + 150 + + + + + + + 50 + 270 + 460 + 30 + + + + PushButton + + + + + true + + + + 220 + 10 + 321 + 31 + + + + Qt::NoContextMenu + + + + + + + + + diff --git a/toxygen/ui/views/audio_settings_screen.ui b/toxygen/ui/views/audio_settings_screen.ui new file mode 100644 index 0000000..a404592 --- /dev/null +++ b/toxygen/ui/views/audio_settings_screen.ui @@ -0,0 +1,87 @@ + + + Form + + + + 0 + 0 + 315 + 218 + + + + + 315 + 218 + + + + + 315 + 218 + + + + Form + + + + + 30 + 10 + 261 + 30 + + + + + 16 + + + + TextLabel + + + + + + 30 + 100 + 261 + 30 + + + + + 16 + + + + TextLabel + + + + + + 30 + 50 + 255 + 41 + + + + + + + 30 + 140 + 255 + 41 + + + + + + + diff --git a/toxygen/ui/views/bans_list_screen.ui b/toxygen/ui/views/bans_list_screen.ui new file mode 100644 index 0000000..16339d8 --- /dev/null +++ b/toxygen/ui/views/bans_list_screen.ui @@ -0,0 +1,29 @@ + + + Form + + + + 0 + 0 + 500 + 375 + + + + Form + + + + + 0 + 0 + 500 + 375 + + + + + + + diff --git a/toxygen/ui/views/create_group_screen.ui b/toxygen/ui/views/create_group_screen.ui new file mode 100644 index 0000000..3a3358a --- /dev/null +++ b/toxygen/ui/views/create_group_screen.ui @@ -0,0 +1,127 @@ + + + Form + + + + 0 + 0 + 640 + 300 + + + + Form + + + + false + + + + 20 + 250 + 601 + 41 + + + + + + + + + + 150 + 20 + 470 + 35 + + + + + + + 150 + 80 + 470 + 35 + + + + + + + 20 + 20 + 121 + 31 + + + + TextLabel + + + + + + 20 + 80 + 121 + 31 + + + + TextLabel + + + + + + 20 + 200 + 111 + 17 + + + + TextLabel + + + + + + 20 + 150 + 111 + 17 + + + + TextLabel + + + + + + 150 + 140 + 470 + 35 + + + + + + + 150 + 190 + 470 + 35 + + + + + + + diff --git a/toxygen/ui/views/create_profile_screen.ui b/toxygen/ui/views/create_profile_screen.ui new file mode 100644 index 0000000..bfffee5 --- /dev/null +++ b/toxygen/ui/views/create_profile_screen.ui @@ -0,0 +1,128 @@ + + + Form + + + + 0 + 0 + 400 + 340 + + + + + 400 + 340 + + + + + 400 + 340 + + + + Form + + + + + 30 + 270 + 341 + 51 + + + + PushButton + + + + + + 30 + 170 + 341 + 41 + + + + QLineEdit::Password + + + + + + 30 + 120 + 341 + 41 + + + + QLineEdit::Password + + + + + + 30 + 80 + 330 + 20 + + + + TextLabel + + + + + + 30 + 10 + 330 + 23 + + + + RadioButton + + + true + + + + + + 30 + 40 + 330 + 23 + + + + RadioButton + + + + + + 30 + 220 + 341 + 30 + + + + + + + Qt::AlignCenter + + + + + + diff --git a/toxygen/ui/views/gc_ban_item.ui b/toxygen/ui/views/gc_ban_item.ui new file mode 100644 index 0000000..a57d0e1 --- /dev/null +++ b/toxygen/ui/views/gc_ban_item.ui @@ -0,0 +1,58 @@ + + + Form + + + + 0 + 0 + 500 + 100 + + + + Form + + + + + 330 + 30 + 161 + 41 + + + + PushButton + + + + + + 15 + 20 + 305 + 20 + + + + TextLabel + + + + + + 15 + 50 + 305 + 20 + + + + TextLabel + + + + + + diff --git a/toxygen/ui/views/gc_invite_item.ui b/toxygen/ui/views/gc_invite_item.ui new file mode 100644 index 0000000..6eddbeb --- /dev/null +++ b/toxygen/ui/views/gc_invite_item.ui @@ -0,0 +1,71 @@ + + + Form + + + + 0 + 0 + 600 + 150 + + + + Form + + + + + 250 + 30 + 300 + 21 + + + + TextLabel + + + + + + 250 + 70 + 300 + 21 + + + + TextLabel + + + + + + 140 + 30 + 60 + 60 + + + + TextLabel + + + + + + 40 + 50 + 20 + 23 + + + + + + + + + + diff --git a/toxygen/ui/views/gc_settings_screen.ui b/toxygen/ui/views/gc_settings_screen.ui new file mode 100644 index 0000000..526c156 --- /dev/null +++ b/toxygen/ui/views/gc_settings_screen.ui @@ -0,0 +1,83 @@ + + + Form + + + + 0 + 0 + 400 + 220 + + + + + 400 + 220 + + + + + 400 + 220 + + + + Form + + + + + 10 + 20 + 380 + 20 + + + + TextLabel + + + + + + 10 + 60 + 380 + 40 + + + + PushButton + + + + + + 10 + 120 + 380 + 20 + + + + TextLabel + + + + + + 10 + 160 + 380 + 20 + + + + TextLabel + + + + + + diff --git a/toxygen/ui/views/group_invites_screen.ui b/toxygen/ui/views/group_invites_screen.ui new file mode 100644 index 0000000..183f801 --- /dev/null +++ b/toxygen/ui/views/group_invites_screen.ui @@ -0,0 +1,113 @@ + + + Form + + + + 0 + 0 + 600 + 500 + + + + + 600 + 500 + + + + + 600 + 500 + + + + Form + + + + + 0 + 150 + 600 + 25 + + + + TextLabel + + + Qt::AlignCenter + + + + + + 0 + 0 + 600 + 341 + + + + + + + 10 + 360 + 350 + 35 + + + + + + + 10 + 410 + 350 + 35 + + + + + + + 390 + 390 + 200 + 35 + + + + + + + 40 + 460 + 201 + 31 + + + + PushButton + + + + + + 360 + 460 + 201 + 31 + + + + PushButton + + + + + + diff --git a/toxygen/ui/views/group_management_screen.ui b/toxygen/ui/views/group_management_screen.ui new file mode 100644 index 0000000..de7c21e --- /dev/null +++ b/toxygen/ui/views/group_management_screen.ui @@ -0,0 +1,123 @@ + + + Form + + + + 0 + 0 + 658 + 283 + + + + Form + + + + + 180 + 20 + 450 + 41 + + + + + + + 20 + 30 + 145 + 20 + + + + TextLabel + + + + + + 20 + 80 + 145 + 20 + + + + TextLabel + + + + + + 180 + 70 + 450 + 40 + + + + 2 + + + 9999 + + + 512 + + + + + + 20 + 130 + 145 + 20 + + + + TextLabel + + + + + + 180 + 120 + 450 + 40 + + + + + + + 20 + 180 + 300 + 41 + + + + PushButton + + + + + + 20 + 220 + 611 + 41 + + + + PushButton + + + + + + diff --git a/toxygen/ui/views/interface_settings_screen.ui b/toxygen/ui/views/interface_settings_screen.ui new file mode 100644 index 0000000..b762903 --- /dev/null +++ b/toxygen/ui/views/interface_settings_screen.ui @@ -0,0 +1,255 @@ + + + Form + + + + 0 + 0 + 552 + 847 + + + + Form + + + + + + Qt::ScrollBarAsNeeded + + + true + + + + + 0 + 0 + 532 + 827 + + + + + + 30 + 140 + 67 + 17 + + + + TextLabel + + + + + + 20 + 180 + 471 + 31 + + + + + + + 20 + 60 + 471 + 31 + + + + + + + 30 + 20 + 67 + 17 + + + + TextLabel + + + + + + + 30 + 280 + 461 + 221 + + + + GroupBox + + + + + 30 + 40 + 92 + 23 + + + + CheckBox + + + + + + 30 + 80 + 411 + 17 + + + + TextLabel + + + + + + 30 + 120 + 411 + 31 + + + + + + + + 30 + 250 + 461 + 23 + + + + CheckBox + + + + + + 30 + 750 + 471 + 40 + + + + PushButton + + + + + + 30 + 690 + 471 + 40 + + + + PushButton + + + + + + 30 + 520 + 461 + 23 + + + + CheckBox + + + + + + 30 + 550 + 471 + 131 + + + + GroupBox + + + + + 30 + 30 + 421 + 23 + + + + RadioButton + + + + + + 30 + 60 + 431 + 23 + + + + RadioButton + + + + + + 30 + 90 + 421 + 23 + + + + RadioButton + + + + + + + + + + + diff --git a/toxygen/ui/views/join_group_screen.ui b/toxygen/ui/views/join_group_screen.ui new file mode 100644 index 0000000..077a332 --- /dev/null +++ b/toxygen/ui/views/join_group_screen.ui @@ -0,0 +1,139 @@ + + + Form + + + + 0 + 0 + 740 + 320 + + + + + 740 + 320 + + + + + 740 + 320 + + + + Form + + + + + 30 + 30 + 67 + 17 + + + + TextLabel + + + + + + 30 + 90 + 67 + 17 + + + + TextLabel + + + + + false + + + + 30 + 260 + 680 + 51 + + + + + + + + + + 190 + 20 + 520 + 41 + + + + + + + 190 + 80 + 520 + 41 + + + + + + + 30 + 150 + 67 + 17 + + + + TextLabel + + + + + + 30 + 210 + 67 + 17 + + + + TextLabel + + + + + + 190 + 140 + 520 + 41 + + + + + + + 190 + 200 + 520 + 41 + + + + + + + diff --git a/toxygen/ui/views/login_screen.ui b/toxygen/ui/views/login_screen.ui new file mode 100644 index 0000000..d100803 --- /dev/null +++ b/toxygen/ui/views/login_screen.ui @@ -0,0 +1,135 @@ + + + loginScreen + + + + 0 + 0 + 400 + 200 + + + + + 400 + 200 + + + + + 400 + 200 + + + + Form + + + + + 0 + 5 + 401 + 30 + + + + + 16 + 75 + true + + + + Toxygen + + + Qt::AlignCenter + + + + + + 10 + 40 + 180 + 150 + + + + GroupBox + + + Qt::AlignCenter + + + + + 10 + 110 + 160 + 27 + + + + PushButton + + + + + + + 210 + 40 + 180 + 150 + + + + GroupBox + + + Qt::AlignCenter + + + + + 10 + 40 + 160 + 27 + + + + + + + 10 + 75 + 160 + 27 + + + + CheckBox + + + + + + 10 + 110 + 160 + 27 + + + + PushButton + + + + + + + diff --git a/toxygen/ui/views/ms_left_column.ui b/toxygen/ui/views/ms_left_column.ui new file mode 100644 index 0000000..ffbff71 --- /dev/null +++ b/toxygen/ui/views/ms_left_column.ui @@ -0,0 +1,94 @@ + + + Form + + + + 0 + 0 + 270 + 500 + + + + PointingHandCursor + + + Form + + + + + 5 + 5 + 64 + 64 + + + + PointingHandCursor + + + TextLabel + + + + + + 0 + 75 + 150 + 25 + + + + + + + 150 + 75 + 120 + 25 + + + + + + + 0 + 77 + 20 + 20 + + + + TextLabel + + + + + + 0 + 100 + 270 + 400 + + + + + + + 0 + 100 + 270 + 30 + + + + PushButton + + + + + + diff --git a/toxygen/ui/views/network_settings_screen.ui b/toxygen/ui/views/network_settings_screen.ui new file mode 100644 index 0000000..f6e2960 --- /dev/null +++ b/toxygen/ui/views/network_settings_screen.ui @@ -0,0 +1,196 @@ + + + Form + + + + 0 + 0 + 400 + 545 + + + + + 400 + 545 + + + + + 400 + 545 + + + + Form + + + + + 30 + 20 + 150 + 30 + + + + CheckBox + + + + + + 210 + 20 + 150 + 30 + + + + CheckBox + + + + + + 30 + 140 + 150 + 30 + + + + CheckBox + + + + + + 30 + 190 + 150 + 25 + + + + RadioButton + + + + + + 30 + 230 + 150 + 25 + + + + RadioButton + + + + + + 30 + 100 + 150 + 30 + + + + CheckBox + + + + + + 30 + 280 + 60 + 20 + + + + TextLabel + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 30 + 330 + 60 + 20 + + + + TextLabel + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 30 + 380 + 60 + 20 + + + + Chat Url + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 30 + 430 + 340 + 40 + + + + PushButton + + + + + + 30 + 60 + 340 + 30 + + + + CheckBox + + + + + + 30 + 480 + 340 + 65 + + + + TextLabel + + + + + + diff --git a/toxygen/ui/views/notifications_settings_screen.ui b/toxygen/ui/views/notifications_settings_screen.ui new file mode 100644 index 0000000..67e2dc6 --- /dev/null +++ b/toxygen/ui/views/notifications_settings_screen.ui @@ -0,0 +1,71 @@ + + + Form + + + + 0 + 0 + 320 + 201 + + + + Form + + + + + 20 + 20 + 271 + 41 + + + + CheckBox + + + + + + 20 + 60 + 271 + 41 + + + + CheckBox + + + + + + 20 + 100 + 271 + 41 + + + + CheckBox + + + + + + 20 + 140 + 271 + 41 + + + + CheckBox + + + + + + diff --git a/toxygen/ui/views/peer_screen.ui b/toxygen/ui/views/peer_screen.ui new file mode 100644 index 0000000..086dd18 --- /dev/null +++ b/toxygen/ui/views/peer_screen.ui @@ -0,0 +1,202 @@ + + + Form + + + + 0 + 0 + 600 + 500 + + + + + 600 + 500 + + + + + 600 + 500 + + + + Form + + + + + 110 + 10 + 431 + 40 + + + + TextLabel + + + + + + 50 + 140 + 500 + 50 + + + + PushButton + + + + + + 50 + 100 + 500 + 23 + + + + CheckBox + + + + + + 50 + 300 + 500 + 161 + + + + GroupBox + + + + + + 40 + 40 + 251 + 23 + + + + RadioButton + + + true + + + + + + 40 + 80 + 251 + 23 + + + + RadioButton + + + + + + 40 + 120 + 251 + 23 + + + + RadioButton + + + + + + 380 + 100 + 101 + 41 + + + + PushButton + + + + + + + 50 + 60 + 67 + 20 + + + + TextLabel + + + + + + 130 + 60 + 411 + 20 + + + + TextLabel + + + + + + 50 + 210 + 500 + 50 + + + + PushButton + + + + + + 130 + 55 + 291 + 30 + + + + + + + diff --git a/toxygen/ui/views/profile_settings_screen.ui b/toxygen/ui/views/profile_settings_screen.ui new file mode 100644 index 0000000..1c899ab --- /dev/null +++ b/toxygen/ui/views/profile_settings_screen.ui @@ -0,0 +1,280 @@ + + + Form + + + + 0 + 0 + 900 + 680 + + + + Form + + + + + 30 + 10 + 161 + 30 + + + + TextLabel + + + + + + 30 + 90 + 161 + 30 + + + + TextLabel + + + + + + 30 + 50 + 421 + 30 + + + + + + + 30 + 130 + 421 + 30 + + + + + + + 520 + 30 + 311 + 30 + + + + + + + 40 + 180 + 131 + 20 + + + + TextLabel + + + + + + 40 + 210 + 831 + 60 + + + + TextLabel + + + true + + + + + + 40 + 280 + 371 + 30 + + + + PushButton + + + + + + 440 + 280 + 371 + 30 + + + + PushButton + + + + + + 520 + 80 + 321 + 34 + + + + PushButton + + + + + + 520 + 130 + 321 + 34 + + + + PushButton + + + + + + 60 + 380 + 161 + 30 + + + + TextLabel + + + + + + 50 + 420 + 421 + 30 + + + + + + + 50 + 470 + 421 + 30 + + + + + + + 500 + 420 + 381 + 20 + + + + TextLabel + + + + + + 60 + 580 + 381 + 20 + + + + TextLabel + + + + + + 40 + 630 + 831 + 34 + + + + PushButton + + + + + + 50 + 520 + 421 + 34 + + + + PushButton + + + + + + 500 + 470 + 381 + 20 + + + + TextLabel + + + + + + 40 + 330 + 371 + 34 + + + + PushButton + + + + + + 440 + 330 + 371 + 34 + + + + PushButton + + + + + + diff --git a/toxygen/ui/views/self_peer_screen.ui b/toxygen/ui/views/self_peer_screen.ui new file mode 100644 index 0000000..38e1f88 --- /dev/null +++ b/toxygen/ui/views/self_peer_screen.ui @@ -0,0 +1,119 @@ + + + Form + + + + 0 + 0 + 600 + 500 + + + + + 600 + 500 + + + + + 600 + 500 + + + + Form + + + + + 50 + 120 + 67 + 20 + + + + TextLabel + + + + + + 50 + 250 + 500 + 50 + + + + PushButton + + + + + + 140 + 110 + 400 + 40 + + + + + + + 50 + 40 + 67 + 20 + + + + TextLabel + + + + + + 50 + 190 + 67 + 20 + + + + TextLabel + + + + + + 140 + 190 + 411 + 20 + + + + TextLabel + + + + + + 50 + 330 + 500 + 50 + + + + PushButton + + + + + + diff --git a/toxygen/ui/views/update_settings_screen.ui b/toxygen/ui/views/update_settings_screen.ui new file mode 100644 index 0000000..76e7c57 --- /dev/null +++ b/toxygen/ui/views/update_settings_screen.ui @@ -0,0 +1,67 @@ + + + Form + + + + 0 + 0 + 400 + 120 + + + + + 400 + 120 + + + + + 400 + 120 + + + + Form + + + + + 25 + 5 + 350 + 20 + + + + TextLabel + + + + + + 25 + 30 + 350 + 30 + + + + + + + 25 + 70 + 350 + 30 + + + + PushButton + + + + + + diff --git a/toxygen/ui/views/video_settings_screen.ui b/toxygen/ui/views/video_settings_screen.ui new file mode 100644 index 0000000..cfa36fb --- /dev/null +++ b/toxygen/ui/views/video_settings_screen.ui @@ -0,0 +1,77 @@ + + + Form + + + + 0 + 0 + 400 + 120 + + + + + 400 + 120 + + + + + 400 + 120 + + + + Form + + + + + 25 + 5 + 350 + 20 + + + + TextLabel + + + + + + 25 + 30 + 350 + 30 + + + + + + + 25 + 70 + 350 + 30 + + + + PushButton + + + + + + 25 + 70 + 350 + 30 + + + + + + + diff --git a/toxygen/user_data/profile_manager.py b/toxygen/user_data/profile_manager.py index 6d7329f..824a191 100644 --- a/toxygen/user_data/profile_manager.py +++ b/toxygen/user_data/profile_manager.py @@ -33,18 +33,14 @@ class ProfileManager: if not os.path.exists(avatars_directory): os.makedirs(avatars_directory) - # ----------------------------------------------------------------------------------------------------------------- # Properties - # ----------------------------------------------------------------------------------------------------------------- def get_profile_saved_event(self): return self._profile_saved_event profile_saved_event = property(get_profile_saved_event) - # ----------------------------------------------------------------------------------------------------------------- # Public methods - # ----------------------------------------------------------------------------------------------------------------- def open_profile(self): with open(self._path, 'rb') as fl: diff --git a/toxygen/user_data/settings.py b/toxygen/user_data/settings.py index 074001c..94800bf 100644 --- a/toxygen/user_data/settings.py +++ b/toxygen/user_data/settings.py @@ -192,18 +192,14 @@ class Settings(dict): self.unlockScreen = False - # ----------------------------------------------------------------------------------------------------------------- # Properties - # ----------------------------------------------------------------------------------------------------------------- def get_settings_saved_event(self): return self._settings_saved_event settings_saved_event = property(get_settings_saved_event) - # ----------------------------------------------------------------------------------------------------------------- # Public methods - # ----------------------------------------------------------------------------------------------------------------- def save(self): text = json.dumps(self) @@ -252,9 +248,7 @@ class Settings(dict): self._path = new_path self.save() - # ----------------------------------------------------------------------------------------------------------------- # Static methods - # ----------------------------------------------------------------------------------------------------------------- @staticmethod def get_auto_profile(): @@ -387,9 +381,7 @@ class Settings(dict): } return retval - # ----------------------------------------------------------------------------------------------------------------- # Private methods - # ----------------------------------------------------------------------------------------------------------------- def _upgrade(self): default = Settings.get_default_settings() diff --git a/toxygen/wrapper_tests/__init__.py b/toxygen/wrapper_tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/wrapper_tests/socks.py b/toxygen/wrapper_tests/socks.py new file mode 100644 index 0000000..748fa8e --- /dev/null +++ b/toxygen/wrapper_tests/socks.py @@ -0,0 +1,391 @@ +"""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 + +Minor modifications made by Eugene Dementiev (http://www.dementiev.eu/) + +""" + +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") + +_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. + """ + data = self.recv(count) + 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, destaddr, destport): + """__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])) + # 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 + if type(destaddr) != type(b''): # python3 + destaddr_bytes = destaddr.encode(encoding='idna') + else: + destaddr_bytes = destaddr + req = req + chr(0x03).encode() + chr(len(destaddr_bytes)).encode() + destaddr_bytes + 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 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 __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: + recv = self.recv(1) + if not recv: + raise GeneralProxyError((1, _generalerrors[1])) + resp = resp + recv + # 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 = int(self.__proxy[2]) + else: + portnum = 1080 + _orgsocket.connect(self, (self.__proxy[1], portnum)) + self.__negotiatesocks5(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 + _orgsocket.connect(self,(self.__proxy[1], portnum)) + self.__negotiatehttp(destpair[0], destpair[1]) + elif self.__proxy[0] == None: + _orgsocket.connect(self, (destpair[0], destpair[1])) + else: + raise GeneralProxyError((4, _generalerrors[4])) diff --git a/toxygen/wrapper_tests/support_http.py b/toxygen/wrapper_tests/support_http.py new file mode 100644 index 0000000..d60ccbf --- /dev/null +++ b/toxygen/wrapper_tests/support_http.py @@ -0,0 +1,164 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +import os +import sys +import logging +from io import BytesIO +import urllib +import traceback + +global LOG +LOG = logging.getLogger('app.'+'ts') + +try: + import pycurl +except ImportError: + pycurl = None +try: + import requests +except ImportError: + requests = None + +lNO_PROXY = ['localhost', '127.0.0.1'] +CONNECT_TIMEOUT = 20.0 + +def bAreWeConnected(): + # FixMe: Linux only + sFile = f"/proc/{os.getpid()}/net/route" + if not os.path.isfile(sFile): return None + i = 0 + for elt in open(sFile, "r").readlines(): + if elt.startswith('Iface'): continue + if elt.startswith('lo'): continue + i += 1 + return i > 0 + +def pick_up_proxy_from_environ(): + retval = dict() + if os.environ.get('socks_proxy', ''): + # socks_proxy takes precedence over https/http + proxy = os.environ.get('socks_proxy', '') + i = proxy.find('//') + if i >= 0: proxy = proxy[i+2:] + retval['proxy_host'] = proxy.split(':')[0] + retval['proxy_port'] = proxy.split(':')[-1] + retval['proxy_type'] = 2 + retval['udp_enabled'] = False + elif os.environ.get('https_proxy', ''): + # https takes precedence over http + proxy = os.environ.get('https_proxy', '') + i = proxy.find('//') + if i >= 0: proxy = proxy[i+2:] + retval['proxy_host'] = proxy.split(':')[0] + retval['proxy_port'] = proxy.split(':')[-1] + retval['proxy_type'] = 1 + retval['udp_enabled'] = False + elif os.environ.get('http_proxy', ''): + proxy = os.environ.get('http_proxy', '') + i = proxy.find('//') + if i >= 0: proxy = proxy[i+2:] + retval['proxy_host'] = proxy.split(':')[0] + retval['proxy_port'] = proxy.split(':')[-1] + retval['proxy_type'] = 1 + retval['udp_enabled'] = False + else: + retval['proxy_host'] = '' + retval['proxy_port'] = '' + retval['proxy_type'] = 0 + retval['udp_enabled'] = True + return retval + +def download_url(url, settings=None): + if not bAreWeConnected(): return '' + + if settings is None: + settings = pick_up_proxy_from_environ() + + if pycurl: + LOG.debug('Downloading with pycurl: ' + str(url)) + buffer = BytesIO() + c = pycurl.Curl() + c.setopt(c.URL, url) + c.setopt(c.WRITEDATA, buffer) + # Follow redirect. + c.setopt(c.FOLLOWLOCATION, True) + + # cookie jar + cjar = os.path.join(os.environ['HOME'], '.local', 'jar.cookie') + if os.path.isfile(cjar): + c.setopt(c.COOKIEFILE, cjar) + # LARGS+=( --cookie-jar --junk-session-cookies ) + + #? c.setopt(c.ALTSVC_CTRL, 16) + + c.setopt(c.NOPROXY, ','.join(lNO_PROXY)) + #? c.setopt(c.CAINFO, certifi.where()) + if settings['proxy_type'] == 2 and settings['proxy_host']: + socks_proxy = 'socks5h://'+settings['proxy_host']+':'+str(settings['proxy_port']) + settings['udp_enabled'] = False + c.setopt(c.PROXY, socks_proxy) + c.setopt(c.PROXYTYPE, pycurl.PROXYTYPE_SOCKS5_HOSTNAME) + elif settings['proxy_type'] == 1 and settings['proxy_host']: + https_proxy = 'https://'+settings['proxy_host']+':'+str(settings['proxy_port']) + c.setopt(c.PROXY, https_proxy) + elif settings['proxy_type'] == 1 and settings['proxy_host']: + http_proxy = 'http://'+settings['proxy_host']+':'+str(settings['proxy_port']) + c.setopt(c.PROXY, http_proxy) + c.setopt(c.PROTOCOLS, c.PROTO_HTTPS) + try: + c.perform() + c.close() + #? assert c.getinfo(c.RESPONSE_CODE) < 300 + result = buffer.getvalue() + # Body is a byte string. + LOG.info('nodes loaded with pycurl: ' + str(url)) + return result + except Exception as ex: + LOG.error('TOX Downloading error with pycurl: ' + str(ex)) + LOG.error('\n' + traceback.format_exc()) + # drop through + + if requests: + LOG.debug('Downloading with requests: ' + str(url)) + try: + headers = dict() + headers['Content-Type'] = 'application/json' + proxies = dict() + if settings['proxy_type'] == 2 and settings['proxy_host']: + socks_proxy = 'socks5://'+settings['proxy_host']+':'+str(settings['proxy_port']) + settings['udp_enabled'] = False + proxies['https'] = socks_proxy + elif settings['proxy_type'] == 1 and settings['proxy_host']: + https_proxy = 'https://'+settings['proxy_host']+':'+str(settings['proxy_port']) + proxies['https'] = https_proxy + elif settings['proxy_type'] == 1 and settings['proxy_host']: + http_proxy = 'http://'+settings['proxy_host']+':'+str(settings['proxy_port']) + proxies['http'] = http_proxy + req = requests.get(url, + headers=headers, + proxies=proxies, + timeout=CONNECT_TIMEOUT) + # max_retries=3 + assert req.status_code < 300 + result = req.content + LOG.info('nodes loaded with requests: ' + str(url)) + return result + except Exception as ex: + LOG.error('TOX Downloading error with requests: ' + str(ex)) + # drop through + + if not settings['proxy_type']: # no proxy + LOG.debug('Downloading with urllib no proxy: ' + str(url)) + try: + req = urllib.request.Request(url) + req.add_header('Content-Type', 'application/json') + response = urllib.request.urlopen(req) + result = response.read() + LOG.info('nodes loaded with no proxy: ' + str(url)) + return result + except Exception as ex: + LOG.error('TOX Downloading ' + str(ex)) + return '' + + return '' + diff --git a/toxygen/wrapper_tests/support_onions.py b/toxygen/wrapper_tests/support_onions.py new file mode 100644 index 0000000..ba1d182 --- /dev/null +++ b/toxygen/wrapper_tests/support_onions.py @@ -0,0 +1,572 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +import getpass +import os +import re +import select +import shutil +import socket +import sys +import time + +if False: + import cepa as stem + from cepa.connection import MissingPassword + from cepa.control import Controller + from cepa.util.tor_tools import is_valid_fingerprint +else: + import stem + from stem.connection import MissingPassword + from stem.control import Controller + from stem.util.tor_tools import is_valid_fingerprint + +global LOG +import logging +import warnings + +warnings.filterwarnings('ignore') +LOG = logging.getLogger() + +bHAVE_TORR = shutil.which('tor-resolve') + +yKNOWN_ONIONS = """ + - facebookwkhpilnemxj7asaniu7vnjjbiltxjqhye3mhbshg7kx5tfyd # facebook + - duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad # ddg + - zkaan2xfbuxia2wpf7ofnkbz6r5zdbbvxbunvp5g2iebopbfc4iqmbad # hks +""" +# grep -B 1 '
  • 0 + +def sMapaddressResolv(target, iPort=9051, log_level=10): + if not stem: + LOG.warn('please install the stem Python package') + return '' + + try: + controller = oGetStemController(log_level=log_level) + + map_dict = {"0.0.0.0": target} + map_ret = controller.map_address(map_dict) + + return map_ret + except Exception as e: + LOG.exception(e) + return '' + +def vwait_for_controller(controller, wait_boot=10): + if bAreWeConnected() is False: + raise SystemExit("we are not connected") + percent = i = 0 + # You can call this while boostrapping + while percent < 100 and i < wait_boot: + bootstrap_status = controller.get_info("status/bootstrap-phase") + progress_percent = re.match('.* PROGRESS=([0-9]+).*', bootstrap_status) + percent = int(progress_percent.group(1)) + LOG.info(f"Bootstrapping {percent}%") + time.sleep(5) + i += 5 + +def bin_to_hex(raw_id, length=None): + if length is None: length = len(raw_id) + res = ''.join('{:02x}'.format(raw_id[i]) for i in range(length)) + return res.upper() + +def lIntroductionPoints(controller=None, lOnions=[], itimeout=120, log_level=10): + """now working !!! stem 1.8.x timeout must be huge >120 + 'Provides the descriptor for a hidden service. The **address** is the + '.onion' address of the hidden service ' + What about Services? + """ + try: + from cryptography.utils import int_from_bytes + except ImportError: + import cryptography.utils + + # guessing - not in the current cryptography but stem expects it + def int_from_bytes(**args): return int.to_bytes(*args) + cryptography.utils.int_from_bytes = int_from_bytes + # this will fai if the trick above didnt work + from stem.prereq import is_crypto_available + is_crypto_available(ed25519=True) + + from queue import Empty + + from stem import Timeout + from stem.client.datatype import LinkByFingerprint + from stem.descriptor.hidden_service import HiddenServiceDescriptorV3 + + if type(lOnions) not in [set, tuple, list]: + lOnions = list(lOnions) + if controller is None: + controller = oGetStemController(log_level=log_level) + l = [] + for elt in lOnions: + LOG.info(f"controller.get_hidden_service_descriptor {elt}") + try: + desc = controller.get_hidden_service_descriptor(elt, + await_result=True, + timeout=itimeout) + # LOG.log(40, f"{dir(desc)} get_hidden_service_descriptor") + # timeouts 20 sec + # mistakenly a HSv2 descriptor + hs_address = HiddenServiceDescriptorV3.from_str(str(desc)) # reparse as HSv3 + oInnerLayer = hs_address.decrypt(elt) + # LOG.log(40, f"{dir(oInnerLayer)}") + + # IntroductionPointV3 + n = oInnerLayer.introduction_points + if not n: + LOG.warn(f"NO introduction points for {elt}") + continue + LOG.info(f"{elt} {len(n)} introduction points") + lp = [] + for introduction_point in n: + for linkspecifier in introduction_point.link_specifiers: + if isinstance(linkspecifier, LinkByFingerprint): + # LOG.log(40, f"Getting fingerprint for {linkspecifier}") + if hasattr(linkspecifier, 'fingerprint'): + assert len(linkspecifier.value) == 20 + lp += [bin_to_hex(linkspecifier.value)] + LOG.info(f"{len(lp)} introduction points for {elt}") + l += lp + except (Empty, Timeout,) as e: # noqa + LOG.warn(f"Timed out getting introduction points for {elt}") + except stem.DescriptorUnavailable as e: + LOG.error(e) + except Exception as e: + LOG.exception(e) + return l + +def zResolveDomain(domain): + try: + ip = sTorResolve(domain) + except Exception as e: # noqa + ip = '' + if ip == '': + try: + lpair = getaddrinfo(domain, 443) + except Exception as e: + LOG.warn(f"{e}") + lpair = None + if lpair is None: + LOG.warn(f"TorResolv and getaddrinfo failed for {domain}") + return '' + ip = lpair[0] + return ip + +def sTorResolve(target, + verbose=False, + sHost='127.0.0.1', + iPort=9050, + SOCK_TIMEOUT_SECONDS=10.0, + SOCK_TIMEOUT_TRIES=3, + ): + MAX_INFO_RESPONSE_PACKET_LENGTH = 8 + if '@' in target: + LOG.warn(f"sTorResolve failed invalid hostname {target}") + return '' + target = target.strip('/') + seb = b"\x04\xf0\x00\x00\x00\x00\x00\x01\x00" + seb += bytes(target, 'US-ASCII') + b"\x00" + assert len(seb) == 10 + len(target), str(len(seb)) + repr(seb) + +# LOG.debug(f"0 Sending {len(seb)} to The TOR proxy {seb}") + + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect((sHost, iPort)) + + sock.settimeout(SOCK_TIMEOUT_SECONDS) + oRet = sock.sendall(seb) # noqa + + i = 0 + data = '' + while i < SOCK_TIMEOUT_TRIES: + i += 1 + time.sleep(3) + lReady = select.select([sock.fileno()], [], [], + SOCK_TIMEOUT_SECONDS) + if not lReady[0]: continue + try: + flags=socket.MSG_WAITALL + data = sock.recv(MAX_INFO_RESPONSE_PACKET_LENGTH, flags) + except socket.timeout: + LOG.warn(f"4 The TOR proxy {(sHost, iPort)}" \ + +" didnt reply in " + str(SOCK_TIMEOUT_SECONDS) + " sec." + +" #" +str(i)) + except Exception as e: + LOG.error("4 The TOR proxy " \ + +repr((sHost, iPort)) \ + +" errored with " + str(e) + +" #" +str(i)) + sock.close() + return '' + else: + if len(data) > 0: break + + if len(data) == 0: + if i > SOCK_TIMEOUT_TRIES: + sLabel = "5 No reply #" + else: + sLabel = "5 No data #" + LOG.warn(f"sTorResolve: {sLabel} {i} on {sHost}:{iPort}") + sock.close() + return '' + + assert len(data) >= 8 + packet_sf = data[1] + if packet_sf == 90: + # , "%d" % packet_sf + assert f"{packet_sf}" == "90", f"packet_sf = {packet_sf}" + return f"{data[4]}.{data[5]}.{data[6]}.{data[7]}" + else: + # 91 + LOG.warn(f"tor-resolve failed for {target} on {sHost}:{iPort}") + + os.system(f"tor-resolve -4 {target} > /tmp/e 2>/dev/null") +# os.system("strace tor-resolve -4 "+target+" 2>&1|grep '^sen\|^rec'") + + return '' + +def getaddrinfo(sHost, sPort): + # do this the explicit way = Ive seen the compact connect fail + # >>> sHost, sPort = 'l27.0.0.1', 33446 + # >>> sock.connect((sHost, sPort)) + # socket.gaierror: [Errno -2] Name or service not known + try: + lElts = socket.getaddrinfo(sHost, int(sPort), socket.AF_INET) + lElts = list(filter(lambda elt: elt[1] == socket.SOCK_DGRAM, lElts)) + assert len(lElts) == 1, repr(lElts) + lPair = lElts[0][-1] + assert len(lPair) == 2, repr(lPair) + assert type(lPair[1]) == int, repr(lPair) + except (socket.gaierror, OSError, BaseException) as e: + LOG.error(e) + return None + return lPair + +def icheck_torrc(sFile, oArgs): + l = open(sFile, 'rt').readlines() + a = {} + for elt in l: + elt = elt.strip() + if not elt or ' ' not in elt: continue + (k, v,) = elt.split(' ', 1) + a[k] = v + keys = a + + if 'HashedControlPassword' not in keys: + LOG.info('Add HashedControlPassword for security') + print('run: tor --hashcontrolpassword ') + if 'ExcludeExitNodes' in keys: + elt = 'BadNodes.ExcludeExitNodes.BadExit' + LOG.warn(f"Remove ExcludeNodes and move then to {oArgs.bad_nodes}") + print(f"move to the {elt} section as a list") + if 'GuardNodes' in keys: + elt = 'GoodNodes.GuardNodes' + LOG.warn(f"Remove GuardNodes and move then to {oArgs.good_nodes}") + print(f"move to the {elt} section as a list") + if 'ExcludeNodes' in keys: + elt = 'BadNodes.ExcludeNodes.BadExit' + LOG.warn(f"Remove ExcludeNodes and move then to {oArgs.bad_nodes}") + print(f"move to the {elt} section as a list") + if 'ControlSocket' not in keys and os.path.exists('/run/tor/control'): + LOG.info('Add ControlSocket /run/tor/control for us') + print('ControlSocket /run/tor/control GroupWritable RelaxDirModeCheck') + if 'UseMicrodescriptors' not in keys or keys['UseMicrodescriptors'] != '1': + LOG.info('Add UseMicrodescriptors 0 for us') + print('UseMicrodescriptors 0') + if 'AutomapHostsSuffixes' not in keys: + LOG.info('Add AutomapHostsSuffixes for onions') + print('AutomapHostsSuffixes .exit,.onion') + if 'AutoMapHostsOnResolve' not in keys: + LOG.info('Add AutoMapHostsOnResolve for onions') + print('AutoMapHostsOnResolve 1') + if 'VirtualAddrNetworkIPv4' not in keys: + LOG.info('Add VirtualAddrNetworkIPv4 for onions') + print('VirtualAddrNetworkIPv4 172.16.0.0/12') + return 0 + +def lExitExcluder(oArgs, iPort=9051, log_level=10): + """ + https://raw.githubusercontent.com/nusenu/noContactInfo_Exit_Excluder/main/exclude_noContactInfo_Exits.py + """ + if not stem: + LOG.warn('please install the stem Python package') + return '' + LOG.debug('lExcludeExitNodes') + + try: + controller = oGetStemController(log_level=log_level) + # generator + relays = controller.get_server_descriptors() + except Exception as e: + LOG.error(f'Failed to get relay descriptors {e}') + return None + + if controller.is_set('ExcludeExitNodes'): + LOG.info('ExcludeExitNodes is in use already.') + return None + + exit_excludelist=[] + LOG.debug("Excluded exit relays:") + for relay in relays: + if relay.exit_policy.is_exiting_allowed() and not relay.contact: + if is_valid_fingerprint(relay.fingerprint): + exit_excludelist.append(relay.fingerprint) + LOG.debug("https://metrics.torproject.org/rs.html#details/%s" % relay.fingerprint) + else: + LOG.warn('Invalid Fingerprint: %s' % relay.fingerprint) + + try: + controller.set_conf('ExcludeExitNodes', exit_excludelist) + LOG.info('Excluded a total of %s exit relays without ContactInfo from the exit position.' % len(exit_excludelist)) + except Exception as e: + LOG.exception('ExcludeExitNodes ' +str(e)) + return exit_excludelist + +if __name__ == '__main__': + target = 'duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad' + controller = oGetStemController(log_level=10) + lIntroductionPoints(controller, [target], itimeout=120) diff --git a/toxygen/wrapper_tests/support_testing.py b/toxygen/wrapper_tests/support_testing.py new file mode 100644 index 0000000..8e1ea48 --- /dev/null +++ b/toxygen/wrapper_tests/support_testing.py @@ -0,0 +1,914 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +import argparse +import contextlib +import inspect +import json +import logging +import os +import re +import select +import shutil +import socket +import sys +import time +import traceback +import unittest +from ctypes import * +from random import Random +import functools + +random = Random() + +try: + import coloredlogs + if 'COLOREDLOGS_LEVEL_STYLES' not in os.environ: + os.environ['COLOREDLOGS_LEVEL_STYLES'] = 'spam=22;debug=28;verbose=34;notice=220;warning=202;success=118,bold;error=124;critical=background=red' + # https://pypi.org/project/coloredlogs/ +except ImportError as e: + coloredlogs = False +try: + import stem +except ImportError as e: + stem = False +try: + import nmap +except ImportError as e: + nmap = False + +import wrapper +from wrapper.toxcore_enums_and_consts import TOX_CONNECTION, TOX_USER_STATUS + +from wrapper_tests.support_http import bAreWeConnected +from wrapper_tests.support_onions import (is_valid_fingerprint, + lIntroductionPoints, + oGetStemController, + sMapaddressResolv, sTorResolve) + +try: + from user_data.settings import get_user_config_path +except ImportError: + get_user_config_path = None + +# LOG=util.log +global LOG +LOG = logging.getLogger() + +def LOG_ERROR(l): print('ERRORc: '+l) +def LOG_WARN(l): print('WARNc: ' +l) +def LOG_INFO(l): print('INFOc: ' +l) +def LOG_DEBUG(l): print('DEBUGc: '+l) +def LOG_TRACE(l): pass # print('TRACE+ '+l) + +try: + from trepan.api import debug + from trepan.interfaces import server as Mserver +except: +# print('trepan3 TCP server NOT available.') + pass +else: +# print('trepan3 TCP server available.') + def trepan_handler(num=None, f=None): + connection_opts={'IO': 'TCP', 'PORT': 6666} + intf = Mserver.ServerInterface(connection_opts=connection_opts) + dbg_opts = { 'interface': intf } + print(f'Starting TCP server listening on port 6666.') + debug(dbg_opts=dbg_opts) + return + +# self._audio_thread.isAlive +iTHREAD_TIMEOUT = 1 +iTHREAD_SLEEP = 1 +iTHREAD_JOINS = 8 +iNODES = 6 + +lToxSamplerates = [8000, 12000, 16000, 24000, 48000] +lToxSampleratesK = [8, 12, 16, 24, 48] +lBOOLEANS = [ + 'local_discovery_enabled', + 'udp_enabled', + 'ipv6_enabled', + 'trace_enabled', + 'compact_mode', + 'allow_inline', + 'notifications', + 'sound_notifications', + 'calls_sound', + 'hole_punching_enabled', + 'dht_announcements_enabled', + 'save_history', + 'download_nodes_list' + 'core_logging', + ] + +sDIR = os.environ.get('TMPDIR', '/tmp') +sTOX_VERSION = "1000002018" +bHAVE_NMAP = shutil.which('nmap') +bHAVE_JQ = shutil.which('jq') +bHAVE_BASH = shutil.which('bash') +bHAVE_TORR = shutil.which('tor-resolve') + +lDEAD_BS = [ + # Failed to resolve "tox3.plastiras.org" + "tox3.plastiras.org", + 'tox.kolka.tech', + # IPs that do not reverse resolve + '49.12.229.145', + "46.101.197.175", + '114.35.245.150', + '172.93.52.70', + '195.123.208.139', + '205.185.115.131', + # IPs that do not rreverse resolve + 'yggnode.cf', '188.225.9.167', + '85-143-221-42.simplecloud.ru', '85.143.221.42', + # IPs that do not ping + '104.244.74.69', 'tox.plastiras.org', + '195.123.208.139', + 'gt.sot-te.ch', '32.226.5.82', + # suspicious IPs + 'tox.abilinski.com', '172.103.164.250', '172.103.164.250.tpia.cipherkey.com', + ] + + +def assert_main_thread(): + from PyQt5 import QtCore, QtWidgets + from qtpy.QtWidgets import QApplication + + # this "instance" method is very useful! + app_thread = QtWidgets.QApplication.instance().thread() + curr_thread = QtCore.QThread.currentThread() + if app_thread != curr_thread: + raise RuntimeError('attempt to call MainWindow.append_message from non-app thread') + +@contextlib.contextmanager +def ignoreStdout(): + devnull = os.open(os.devnull, os.O_WRONLY) + old_stdout = os.dup(1) + sys.stdout.flush() + os.dup2(devnull, 1) + os.close(devnull) + try: + yield + finally: + os.dup2(old_stdout, 1) + os.close(old_stdout) + +@contextlib.contextmanager +def ignoreStderr(): + devnull = os.open(os.devnull, os.O_WRONLY) + old_stderr = os.dup(2) + sys.stderr.flush() + os.dup2(devnull, 2) + os.close(devnull) + try: + yield + finally: + os.dup2(old_stderr, 2) + os.close(old_stderr) + +def clean_booleans(oArgs): + for key in lBOOLEANS: + if not hasattr(oArgs, key): continue + val = getattr(oArgs, key) + if type(val) == bool: continue + if val in ['False', 'false', '0']: + setattr(oArgs, key, False) + else: + setattr(oArgs, key, True) + +def on_log(iTox, level, filename, line, func, message, *data): + # LOG.debug(repr((level, filename, line, func, message,))) + tox_log_cb(level, filename, line, func, message) + +def tox_log_cb(level, filename, line, func, message, *args): + """ + * @param level The severity of the log message. + * @param filename The source file from which the message originated. + * @param line The source line from which the message originated. + * @param func The function from which the message originated. + * @param message The log message. + * @param user_data The user data pointer passed to tox_new in options. + """ + if type(func) == bytes: + func = str(func, 'utf-8') + message = str(message, 'UTF-8') + filename = str(filename, 'UTF-8') + + if filename == 'network.c': + if line == 660: return + # root WARNING 3network.c#944:b'send_packet'attempted to send message with network family 10 (probably IPv6) on IPv4 socket + if line == 944: return + i = message.find('07 = GET_NODES') + if i > 0: + return + if filename == 'TCP_common.c': return + + i = message.find(' | ') + if i > 0: + message = message[:i] + # message = filename +'#' +str(line) +':'+func +' '+message + + name = 'core' + # old level is meaningless + level = 10 # LOG.level + + # LOG._log(LOG.level, f"{level}: {message}", list()) + + i = message.find('(0: OK)') + if i > 0: + level = 10 # LOG.debug + else: + i = message.find('(1: ') + if i > 0: + level = 30 # LOG.warn + else: + level = 20 # LOG.info + + o = LOG.makeRecord(filename, level, func, line, message, list(), None) + # LOG.handle(o) + LOG_TRACE(f"{level}: {func}{line} {message}") + return + + elif level == 1: + LOG.critical(f"{level}: {message}") + elif level == 2: + LOG.error(f"{level}: {message}") + elif level == 3: + LOG.warn(f"{level}: {message}") + elif level == 4: + LOG.info(f"{level}: {message}") + elif level == 5: + LOG.debug(f"{level}: {message}") + else: + LOG_TRACE(f"{level}: {message}") + +def vAddLoggerCallback(tox_options, callback=None): + if callback is None: + wrapper.tox.Tox.libtoxcore.tox_options_set_log_callback( + tox_options._options_pointer, + POINTER(None)()) + tox_options.self_logger_cb = None + return + + c_callback = CFUNCTYPE(None, c_void_p, c_int, c_char_p, c_int, c_char_p, c_char_p, c_void_p) + tox_options.self_logger_cb = c_callback(callback) + wrapper.tox.Tox.libtoxcore.tox_options_set_log_callback( + tox_options._options_pointer, + tox_options.self_logger_cb) + +def get_video_indexes(): + # Linux + return [str(l[5:]) for l in os.listdir('/dev/') if l.startswith('video')] + +def get_audio(): + with ignoreStderr(): + import pyaudio + oPyA = pyaudio.PyAudio() + + input_devices = output_devices = 0 + for i in range(oPyA.get_device_count()): + device = oPyA.get_device_info_by_index(i) + if device["maxInputChannels"]: + input_devices += 1 + if device["maxOutputChannels"]: + output_devices += 1 + # {'index': 21, 'structVersion': 2, 'name': 'default', 'hostApi': 0, 'maxInputChannels': 64, 'maxOutputChannels': 64, 'defaultLowInputLatency': 0.008707482993197279, 'defaultLowOutputLatency': 0.008707482993197279, 'defaultHighInputLatency': 0.034829931972789115, 'defaultHighOutputLatency': 0.034829931972789115, 'defaultSampleRate': 44100.0} + audio = {'input': oPyA.get_default_input_device_info()['index'] if input_devices else -1, + 'output': oPyA.get_default_output_device_info()['index'] if output_devices else -1, + 'enabled': input_devices and output_devices} + return audio + +def oMainArgparser(_=None, iMode=0): + # 'Mode: 0=chat 1=chat+audio 2=chat+audio+video default: 0' + if not os.path.exists('/proc/sys/net/ipv6'): + bIpV6 = 'False' + else: + bIpV6 = 'True' + lIpV6Choices=[bIpV6, 'False'] + + sNodesJson = os.path.join(os.environ['HOME'], '.config', 'tox', 'DHTnodes.json') + if not os.path.exists(sNodesJson): sNodesJson = '' + + logfile = os.path.join(os.environ.get('TMPDIR', '/tmp'), 'toxygen.log') + if not os.path.exists(sNodesJson): logfile = '' + + parser = argparse.ArgumentParser(add_help=True) + parser.add_argument('--proxy_host', '--proxy-host', type=str, + # oddball - we want to use '' as a setting + default='0.0.0.0', + help='proxy host') + parser.add_argument('--proxy_port', '--proxy-port', default=0, type=int, + help='proxy port') + parser.add_argument('--proxy_type', '--proxy-type', default=0, type=int, + choices=[0,1,2], + help='proxy type 1=http, 2=socks') + parser.add_argument('--tcp_port', '--tcp-port', default=0, type=int, + help='tcp port') + parser.add_argument('--udp_enabled', type=str, default='True', + choices=['True', 'False'], + help='En/Disable udp') + parser.add_argument('--ipv6_enabled', type=str, default=bIpV6, + choices=lIpV6Choices, + help=f"En/Disable ipv6 - default {bIpV6}") + parser.add_argument('--trace_enabled',type=str, + default='True' if os.environ.get('DEBUG') else 'False', + choices=['True','False'], + help='Debugging from toxcore logger_trace or env DEBUG=1') + parser.add_argument('--download_nodes_list', type=str, default='False', + choices=['True', 'False'], + help='Download nodes list') + parser.add_argument('--nodes_json', type=str, + default=sNodesJson) + parser.add_argument('--network', type=str, + choices=['main', 'local'], + default='main') + parser.add_argument('--download_nodes_url', type=str, + default='https://nodes.tox.chat/json') + parser.add_argument('--logfile', default=logfile, + help='Filename for logging - start with + for stdout too') + parser.add_argument('--loglevel', default=logging.INFO, type=int, + # choices=[logging.info,logging.trace,logging.debug,logging.error] + help='Threshold for logging (lower is more) default: 20') + parser.add_argument('--mode', type=int, default=iMode, + choices=[0,1,2], + help='Mode: 0=chat 1=chat+audio 2=chat+audio+video default: 0') + parser.add_argument('--hole_punching_enabled',type=str, + default='False', choices=['True','False'], + help='En/Enable hole punching') + parser.add_argument('--dht_announcements_enabled',type=str, + default='True', choices=['True','False'], + help='En/Disable DHT announcements') + return parser + +def vSetupLogging(oArgs): + global LOG + logging._defaultFormatter = logging.Formatter(datefmt='%m-%d %H:%M:%S') + logging._defaultFormatter.default_time_format = '%m-%d %H:%M:%S' + logging._defaultFormatter.default_msec_format = '' + + add = None + kwargs = dict(level=oArgs.loglevel, + format='%(levelname)-8s %(message)s') + if oArgs.logfile: + add = oArgs.logfile.startswith('+') + sub = oArgs.logfile.startswith('-') + if add or sub: + oArgs.logfile = oArgs.logfile[1:] + kwargs['filename'] = oArgs.logfile + + if coloredlogs: + # https://pypi.org/project/coloredlogs/ + aKw = dict(level=oArgs.loglevel, + logger=LOG, + stream=sys.stdout, + fmt='%(name)s %(levelname)s %(message)s' + ) + coloredlogs.install(**aKw) + if oArgs.logfile: + oHandler = logging.FileHandler(oArgs.logfile) + LOG.addHandler(oHandler) + else: + logging.basicConfig(**kwargs) + if add: + oHandler = logging.StreamHandler(sys.stdout) + LOG.addHandler(oHandler) + + LOG.info(f"Setting loglevel to {oArgs.loglevel!s}") + + +def setup_logging(oArgs): + global LOG + if coloredlogs: + aKw = dict(level=oArgs.loglevel, + logger=LOG, + fmt='%(name)s %(levelname)s %(message)s') + if oArgs.logfile: + oFd = open(oArgs.logfile, 'wt') + setattr(oArgs, 'log_oFd', oFd) + aKw['stream'] = oFd + coloredlogs.install(**aKw) + if oArgs.logfile: + oHandler = logging.StreamHandler(stream=sys.stdout) + LOG.addHandler(oHandler) + else: + aKw = dict(level=oArgs.loglevel, + format='%(name)s %(levelname)-4s %(message)s') + if oArgs.logfile: + aKw['filename'] = oArgs.logfile + logging.basicConfig(**aKw) + + logging._defaultFormatter = logging.Formatter(datefmt='%m-%d %H:%M:%S') + logging._defaultFormatter.default_time_format = '%m-%d %H:%M:%S' + logging._defaultFormatter.default_msec_format = '' + + LOG.setLevel(oArgs.loglevel) +# LOG.trace = lambda l: LOG.log(0, repr(l)) + LOG.info(f"Setting loglevel to {oArgs.loglevel!s}") + +def signal_handler(num, f): + from trepan.api import debug + from trepan.interfaces import server as Mserver + connection_opts={'IO': 'TCP', 'PORT': 6666} + intf = Mserver.ServerInterface(connection_opts=connection_opts) + dbg_opts = {'interface': intf} + LOG.info('Starting TCP server listening on port 6666.') + debug(dbg_opts=dbg_opts) + return + +def merge_args_into_settings(args, settings): + if args: + if not hasattr(args, 'audio'): + LOG.warn('No audio ' +repr(args)) + settings['audio'] = getattr(args, 'audio') + if not hasattr(args, 'video'): + LOG.warn('No video ' +repr(args)) + settings['video'] = getattr(args, 'video') + for key in settings.keys(): + # proxy_type proxy_port proxy_host + not_key = 'not_' +key + if hasattr(args, key): + val = getattr(args, key) + if type(val) == bytes: + # proxy_host - ascii? + # filenames - ascii? + val = str(val, 'UTF-8') + settings[key] = val + elif hasattr(args, not_key): + val = not getattr(args, not_key) + settings[key] = val + clean_settings(settings) + return + +def clean_settings(self): + # failsafe to ensure C tox is bytes and Py settings is str + + # overrides + self['mirror_mode'] = False + # REQUIRED!! + if not os.path.exists('/proc/sys/net/ipv6'): + LOG.warn('Disabling IPV6 because /proc/sys/net/ipv6 does not exist') + self['ipv6_enabled'] = False + + if 'proxy_type' in self and self['proxy_type'] == 0: + self['proxy_host'] = '' + self['proxy_port'] = 0 + + if 'proxy_type' in self and self['proxy_type'] != 0 and \ + 'proxy_host' in self and self['proxy_host'] != '' and \ + 'proxy_port' in self and self['proxy_port'] != 0: + if 'udp_enabled' in self and self['udp_enabled']: + # We don't currently support UDP over proxy. + LOG.info("UDP enabled and proxy set: disabling UDP") + self['udp_enabled'] = False + if 'local_discovery_enabled' in self and self['local_discovery_enabled']: + LOG.info("local_discovery_enabled enabled and proxy set: disabling local_discovery_enabled") + self['local_discovery_enabled'] = False + if 'dht_announcements_enabled' in self and self['dht_announcements_enabled']: + LOG.info("dht_announcements_enabled enabled and proxy set: disabling dht_announcements_enabled") + self['dht_announcements_enabled'] = False + + if 'auto_accept_path' in self and \ + type(self['auto_accept_path']) == bytes: + self['auto_accept_path'] = str(self['auto_accept_path'], 'UTF-8') + + LOG.debug("Cleaned settings") + +def lSdSamplerates(iDev): + try: + import sounddevice as sd + except ImportError: + return [] + samplerates = (32000, 44100, 48000, 96000, ) + device = iDev + supported_samplerates = [] + for fs in samplerates: + try: + sd.check_output_settings(device=device, samplerate=fs) + except Exception as e: + # LOG.debug(f"Sample rate not supported {fs}" +' '+str(e)) + pass + else: + supported_samplerates.append(fs) + return supported_samplerates + +def _get_nodes_path(oArgs=None): + if oArgs and oArgs.nodes_json and os.path.isfile(oArgs.nodes_json): + LOG.debug("_get_nodes_path: " +oArgs.nodes_json) + default = oArgs.nodes_json + elif get_user_config_path: + default = os.path.join(get_user_config_path(), 'toxygen_nodes.json') + else: + # Windwoes + default = os.path.join(os.getenv('HOME'), '.config', 'tox', 'toxygen_nodes.json') + LOG.debug("_get_nodes_path: " +default) + return default + +DEFAULT_NODES_COUNT = 8 + +global aNODES +aNODES = {} + + +# @functools.lru_cache(maxsize=12) TypeError: unhashable type: 'Namespace' +def generate_nodes(oArgs=None, + nodes_count=DEFAULT_NODES_COUNT, + ipv='ipv4', + udp_not_tcp=True): + global aNODES + sKey = ipv + sKey += ',0' if udp_not_tcp else ',1' + if sKey in aNODES and aNODES[sKey]: + return aNODES[sKey] + sFile = _get_nodes_path(oArgs=oArgs) + assert os.path.exists(sFile), sFile + lNodes = generate_nodes_from_file(sFile, + nodes_count=nodes_count, + ipv=ipv, udp_not_tcp=udp_not_tcp) + assert lNodes + aNODES[sKey] = lNodes + return aNODES[sKey] + +aNODES_CACHE = {} +def generate_nodes_from_file(sFile, + nodes_count=DEFAULT_NODES_COUNT, + ipv='ipv4', + udp_not_tcp=True, + ): + """https://github.com/TokTok/c-toxcore/issues/469 +I had a conversation with @irungentoo on IRC about whether we really need to call tox_bootstrap() when having UDP disabled and why. The answer is yes, because in addition to TCP relays (tox_add_tcp_relay()), toxcore also needs to know addresses of UDP onion nodes in order to work correctly. The DHT, however, is not used when UDP is disabled. tox_bootstrap() function resolves the address passed to it as argument and calls onion_add_bs_node_path() and DHT_bootstrap() functions. Although calling DHT_bootstrap() is not really necessary as DHT is not used, we still need to resolve the address of the DHT node in order to populate the onion routes with onion_add_bs_node_path() call. +""" + global aNODES_CACHE + + key = ipv + key += ',0' if udp_not_tcp else ',1' + if key in aNODES_CACHE: + sorted_nodes = aNODES_CACHE[key] + else: + if not os.path.exists(sFile): + LOG.error("generate_nodes_from_file file not found " +sFile) + return [] + try: + with open(sFile, 'rt') as fl: + json_nodes = json.loads(fl.read())['nodes'] + except Exception as e: + LOG.error(f"generate_nodes_from_file error {sFile}\n{e}") + return [] + else: + LOG.debug("generate_nodes_from_file " +sFile) + + if udp_not_tcp: + nodes = [(node[ipv], node['port'], node['public_key'],) for + node in json_nodes if node[ipv] != 'NONE' \ + and node["status_udp"] in [True, "true"] + ] + else: + nodes = [] + elts = [(node[ipv], node['tcp_ports'], node['public_key'],) \ + for node in json_nodes if node[ipv] != 'NONE' \ + and node["status_tcp"] in [True, "true"] + ] + for (ipv, ports, public_key,) in elts: + for port in ports: + nodes += [(ipv, port, public_key)] + if not nodes: + LOG.warn(f'empty generate_nodes from {sFile} {json_nodes!r}') + return [] + sorted_nodes = nodes + aNODES_CACHE[key] = sorted_nodes + + random.shuffle(sorted_nodes) + if nodes_count is not None and len(sorted_nodes) > nodes_count: + sorted_nodes = sorted_nodes[-nodes_count:] + LOG.debug(f"generate_nodes_from_file {sFile} len={len(sorted_nodes)}") + return sorted_nodes + +def tox_bootstrapd_port(): + port = 33446 + sFile = '/etc/tox-bootstrapd.conf' + if os.path.exists(sFile): + with open(sFile, 'rt') as oFd: + for line in oFd.readlines(): + if line.startswith('port = '): + port = int(line[7:]) + return port + +def bootstrap_local(elts, lToxes, oArgs=None): + if os.path.exists('/run/tox-bootstrapd/tox-bootstrapd.pid'): + LOG.debug('/run/tox-bootstrapd/tox-bootstrapd.pid') + iRet = True + else: + iRet = os.system("netstat -nle4|grep -q :33") + if iRet > 0: + LOG.warn(f'bootstraping local No local DHT running') + LOG.info(f'bootstraping local') + return bootstrap_udp(elts, lToxes, oArgs) + +def lDNSClean(l): + global lDEAD_BS + # list(set(l).difference(set(lDEAD_BS))) + return [elt for elt in l if elt not in lDEAD_BS] + +def lExitExcluder(oArgs, iPort=9051): + """ + https://raw.githubusercontent.com/nusenu/noContactInfo_Exit_Excluder/main/exclude_noContactInfo_Exits.py + """ + if not stem: + LOG.warn('please install the stem Python package') + return '' + LOG.debug('lExcludeExitNodes') + + try: + controller = oGetStemController(log_level=10) + # generator + relays = controller.get_server_descriptors() + except Exception as e: + LOG.error(f'Failed to get relay descriptors {e}') + return None + + if controller.is_set('ExcludeExitNodes'): + LOG.info('ExcludeExitNodes is in use already.') + return None + + exit_excludelist=[] + LOG.debug("Excluded exit relays:") + for relay in relays: + if relay.exit_policy.is_exiting_allowed() and not relay.contact: + if is_valid_fingerprint(relay.fingerprint): + exit_excludelist.append(relay.fingerprint) + LOG.debug("https://metrics.torproject.org/rs.html#details/%s" % relay.fingerprint) + else: + LOG.warn('Invalid Fingerprint: %s' % relay.fingerprint) + + try: + controller.set_conf('ExcludeExitNodes', exit_excludelist) + LOG.info('Excluded a total of %s exit relays without ContactInfo from the exit position.' % len(exit_excludelist)) + except Exception as e: + LOG.exception('ExcludeExitNodes ' +str(e)) + return exit_excludelist + +aHOSTS = {} +@functools.lru_cache(maxsize=20) +def sDNSLookup(host): + global aHOSTS + ipv = 0 + if host in lDEAD_BS: +# LOG.warn(f"address skipped because in lDEAD_BS {host}") + return '' + if host in aHOSTS: + return aHOSTS[host] + + try: + s = host.replace('.','') + int(s) + ipv = 4 + except: + try: + s = host.replace(':','') + int(s) + ipv = 6 + except: pass + + if ipv > 0: +# LOG.debug(f"v={ipv} IP address {host}") + return host + + LOG.debug(f"sDNSLookup {host}") + ip = '' + if host.endswith('.tox') or host.endswith('.onion'): + if False and stem: + ip = sMapaddressResolv(host) + if ip: return ip + + ip = sTorResolve(host) + if ip: return ip + + if not bHAVE_TORR: + LOG.warn(f"onion address skipped because no tor-resolve {host}") + return '' + try: + sout = f"/tmp/TR{os.getpid()}.log" + i = os.system(f"tor-resolve -4 {host} > {sout}") + if not i: + LOG.warn(f"onion address skipped because tor-resolve on {host}") + return '' + ip = open(sout, 'rt').read() + if ip.endswith('failed.'): + LOG.warn(f"onion address skipped because tor-resolve failed on {host}") + return '' + LOG.debug(f"onion address tor-resolve {ip} on {host}") + return ip + except: + pass + else: + try: + ip = socket.gethostbyname(host) + LOG.debug(f"host={host} gethostbyname IP address {ip}") + if ip: + aHOSTS[host] = ip + return ip + # drop through + except: + # drop through + pass + + if ip == '': + try: + sout = f"/tmp/TR{os.getpid()}.log" + i = os.system(f"dig {host} +timeout=15|grep ^{host}|sed -e 's/.* //'> {sout}") + if not i: + LOG.warn(f"address skipped because dig failed on {host}") + return '' + ip = open(sout, 'rt').read().strip() + LOG.debug(f"address dig {ip} on {host}") + aHOSTS[host] = ip + return ip + except: + ip = host + LOG.debug(f'sDNSLookup {host} -> {ip}') + if ip and ip != host: + aHOSTS[host] = ip + return ip + +def bootstrap_udp(lelts, lToxes, oArgs=None): + lelts = lDNSClean(lelts) + socket.setdefaulttimeout(15.0) + for oTox in lToxes: + random.shuffle(lelts) + if hasattr(oTox, 'oArgs'): + oArgs = oTox.oArgs + if hasattr(oArgs, 'contents') and oArgs.contents.proxy_type != 0: + lelts = lelts[:1] + +# LOG.debug(f'bootstrap_udp DHT bootstraping {oTox.name} {len(lelts)}') + for largs in lelts: + assert len(largs) == 3 + host, port, key = largs + assert host; assert port; assert key + if host in lDEAD_BS: continue + ip = sDNSLookup(host) + if not ip: + LOG.warn(f'bootstrap_udp to host={host} port={port} did not resolve ip={ip}') + continue + + if type(port) == str: + port = int(port) + try: + assert len(key) == 64, key + # NOT ip + oRet = oTox.bootstrap(host, + port, + key) + except Exception as e: + if oArgs is None or ( + hasattr(oArgs, 'contents') and oArgs.contents.proxy_type == 0): + pass + # LOG.error(f'bootstrap_udp failed to host={host} port={port} {e}') + continue + if not oRet: + LOG.warn(f'bootstrap_udp failed to {host} : {oRet}') + elif oTox.self_get_connection_status() != TOX_CONNECTION['NONE']: + LOG.info(f'bootstrap_udp to {host} connected') + break + else: +# LOG.debug(f'bootstrap_udp to {host} not connected') + pass + +def bootstrap_tcp(lelts, lToxes, oArgs=None): + lelts = lDNSClean(lelts) + for oTox in lToxes: + if hasattr(oTox, 'oArgs'): oArgs = oTox.oArgs + random.shuffle(lelts) +# LOG.debug(f'bootstrap_tcp bootstapping {oTox.name} {len(lelts)}') + for (host, port, key,) in lelts: + assert host; assert port;assert key + if host in lDEAD_BS: continue + ip = sDNSLookup(host) + if not ip: + LOG.warn(f'bootstrap_tcp to {host} did not resolve ip={ip}') +# continue + ip = host + if host.endswith('.onion') and stem: + l = lIntroductionPoints(host) + if not l: + LOG.warn(f'bootstrap_tcp to {host} has no introduction points') + continue + if type(port) == str: + port = int(port) + try: + assert len(key) == 64, key + oRet = oTox.add_tcp_relay(ip, + port, + key) + except Exception as e: + LOG.error(f'bootstrap_tcp to {host} : ' +str(e)) + continue + if not oRet: + LOG.warn(f'bootstrap_tcp failed to {host} : {oRet}') + elif oTox.mycon_time == 1: + LOG.info(f'bootstrap_tcp to {host} not yet connected last=1') + elif oTox.mycon_status is False: + LOG.info(f'bootstrap_tcp to {host} not True' \ + +f" last={int(oTox.mycon_time)}" ) + elif oTox.self_get_connection_status() != TOX_CONNECTION['NONE']: + LOG.info(f'bootstrap_tcp to {host} connected' \ + +f" last={int(oTox.mycon_time)}" ) + break + else: + LOG.debug(f'bootstrap_tcp to {host} but not connected' \ + +f" last={int(oTox.mycon_time)}" ) + pass + +def iNmapInfoNmap(sProt, sHost, sPort, key=None, environ=None, cmd=''): + if sHost in ['-', 'NONE']: return 0 + if not nmap: return 0 + nmps = nmap.PortScanner + if sProt in ['socks', 'socks5', 'tcp4']: + prot = 'tcp' + cmd = f" -Pn -n -sT -p T:{sPort}" + else: + prot = 'udp' + cmd = f" -Pn -n -sU -p U:{sPort}" + LOG.debug(f"iNmapInfoNmap cmd={cmd}") + sys.stdout.flush() + o = nmps().scan(hosts=sHost, arguments=cmd) + aScan = o['scan'] + ip = list(aScan.keys())[0] + state = aScan[ip][prot][sPort]['state'] + LOG.info(f"iNmapInfoNmap: to {sHost} {state}") + return 0 + +def iNmapInfo(sProt, sHost, sPort, key=None, environ=None, cmd='nmap'): + if sHost in ['-', 'NONE']: return 0 + sFile = os.path.join("/tmp", f"{sHost}.{os.getpid()}.nmap") + if sProt in ['socks', 'socks5', 'tcp4']: + cmd += f" -Pn -n -sT -p T:{sPort} {sHost} | grep /tcp " + else: + cmd += f" -Pn -n -sU -p U:{sPort} {sHost} | grep /udp " + LOG.debug(f"iNmapInfo cmd={cmd}") + sys.stdout.flush() + iRet = os.system('sudo ' +cmd +f" >{sFile} 2>&1 ") + LOG.debug(f"iNmapInfo cmd={cmd} iRet={iRet}") + if iRet != 0: + return iRet + assert os.path.exists(sFile), sFile + with open(sFile, 'rt') as oFd: + l = oFd.readlines() + assert len(l) + l = [line for line in l if line and not line.startswith('WARNING:')] + s = '\n'.join([s.strip() for s in l]) + LOG.info(f"iNmapInfo: to {sHost}\n{s}") + return 0 + +def bootstrap_iNmapInfo(lElts, oArgs, protocol="tcp4", bIS_LOCAL=False, iNODES=iNODES, cmd='nmap'): + if not bIS_LOCAL and not bAreWeConnected(): + LOG.warn(f"bootstrap_iNmapInfo not local and NOT CONNECTED") + return True + if os.environ['USER'] != 'root': + LOG.warn(f"bootstrap_iNmapInfo not ROOT") + return True + + lRetval = [] + for elts in lElts[:iNODES]: + host, port, key = elts + ip = sDNSLookup(host) + if not ip: + LOG.info('bootstrap_iNmapInfo to {host} did not resolve ip={ip}') + continue + if type(port) == str: + port = int(port) + iRet = -1 + try: + if not nmap: + iRet = iNmapInfo(protocol, ip, port, key, cmd=cmd) + else: + iRet = iNmapInfoNmap(protocol, ip, port, key) + if iRet != 0: + LOG.warn('iNmapInfo to ' +repr(host) +' retval=' +str(iRet)) + lRetval += [False] + else: + LOG.debug('iNmapInfo to ' +repr(host) +' retval=' +str(iRet)) + lRetval += [True] + except Exception as e: + LOG.exception('iNmapInfo to {host} : ' +str(e) + ) + lRetval += [False] + return any(lRetval) + +def caseFactory(cases): + """We want the tests run in order.""" + if len(cases) > 1: + ordered_cases = sorted(cases, key=lambda f: inspect.findsource(f)[1]) + else: + ordered_cases = cases + return ordered_cases + +def suiteFactory(*testcases): + """We want the tests run in order.""" + linen = lambda f: getattr(tc, f).__code__.co_firstlineno + lncmp = lambda a, b: linen(a) - linen(b) + + test_suite = unittest.TestSuite() + for tc in testcases: + test_suite.addTest(unittest.makeSuite(tc, sortUsing=lncmp)) + return test_suite diff --git a/toxygen/wrapper_tests/tests_wrapper.py b/toxygen/wrapper_tests/tests_wrapper.py new file mode 100644 index 0000000..640916d --- /dev/null +++ b/toxygen/wrapper_tests/tests_wrapper.py @@ -0,0 +1,1885 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- +# +# @file tests.py +# @author Wei-Ning Huang (AZ) +# +# Copyright (C) 2013 - 2014 Wei-Ning Huang (AZ) +# All Rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# + +"""Originaly from https://github.com/oxij/PyTox c-toxcore-02 branch +which itself was forked from https://github.com/aitjcize/PyTox/ + +Modified to work with +""" + +import ctypes +import faulthandler +import hashlib +import logging +import os +import random +import re +import sys +import threading +import traceback +import unittest +from ctypes import * + +faulthandler.enable() + +import warnings + +warnings.filterwarnings('ignore') + +try: + from io import BytesIO + + import certifi + import pycurl +except ImportError: + pycurl = None + +try: + import coloredlogs + os.environ['COLOREDLOGS_LEVEL_STYLES'] = 'spam=22;debug=28;verbose=34;notice=220;warning=202;success=118,bold;error=124;critical=background=red' +except ImportError as e: + logging.log(logging.DEBUG, f"coloredlogs not available: {e}") + coloredlogs = None + +try: + import color_runner +except ImportError as e: + logging.log(logging.DEBUG, f"color_runner not available: {e}") + color_runner = None + +import wrapper +import wrapper.toxcore_enums_and_consts as enums +from wrapper.tox import Tox +from wrapper.toxcore_enums_and_consts import (TOX_ADDRESS_SIZE, TOX_CONNECTION, + TOX_FILE_CONTROL, + TOX_MESSAGE_TYPE, + TOX_SECRET_KEY_SIZE, + TOX_USER_STATUS) + +try: + import support_testing as ts +except ImportError: + import wrapper_tests.support_testing as ts + +try: + from tests.toxygen_tests import test_sound_notification + bIS_NOT_TOXYGEN = False +except ImportError: + bIS_NOT_TOXYGEN = True + +# from PyQt5 import QtCore +import time + +sleep = time.sleep + +global LOG +LOG = logging.getLogger('TestS') +# just print to stdout so there is no complications from logging. +def LOG_ERROR(l): print('EROR+ '+l) +def LOG_WARN(l): print('WARN+ '+l) +def LOG_INFO(l): print('INFO+ '+l) +def LOG_DEBUG(l): print('DEBUG+ '+l) +def LOG_TRACE(l): pass # print('TRAC+ '+l) + +ADDR_SIZE = 38 * 2 +CLIENT_ID_SIZE = 32 * 2 +THRESHOLD = 25 + +global oTOX_OPTIONS +oTOX_OPTIONS = {} + +bIS_LOCAL = 'new' in sys.argv or 'main' in sys.argv or 'newlocal' in sys.argv + +# Patch unittest for Python version <= 2.6 +if not hasattr(unittest, 'skip'): + def unittest_skip(reason): + def _wrap1(func): + def _wrap2(self, *args, **kwargs): + pass + return _wrap2 + return _wrap1 + unittest.skip = unittest_skip + +if not hasattr(unittest, 'expectedFailureIf'): + def unittest_expectedFailureIf(condition, reason): + def _wrap1(test_item): + def _wrap2(self, *args, **kwargs): + if condition: + test_item.__unittest_expecting_failure__ = True + pass + return _wrap2 + return _wrap1 + + unittest.expectedFailureIf = unittest_expectedFailureIf + +def expectedFailure(test_item): + test_item.__unittest_expecting_failure__ = True + return test_item + +class ToxOptions(): + def __init__(self): + self.ipv6_enabled = True + self.udp_enabled = True + self.proxy_type = 0 + self.proxy_host = '' + self.proxy_port = 0 + self.start_port = 0 + self.end_port = 0 + self.tcp_port = 0 + self.savedata_type = 0 # 1=toxsave, 2=secretkey + self.savedata_data = b'' + self.savedata_length = 0 + self.local_discovery_enabled = False + self.dht_announcements_enabled = True + self.hole_punching_enabled = False + self.experimental_thread_safety = False + +class App(): + def __init__(self): + self.mode = 0 +oAPP = App() + +class AliceTox(Tox): + + def __init__(self, opts, app=None): + + super(AliceTox, self).__init__(opts, app=app) + self._address = self.self_get_address() + self.name = 'alice' + self._opts = opts + self._app = app + +class BobTox(Tox): + + def __init__(self, opts, app=None): + super(BobTox, self).__init__(opts, app=app) + self._address = self.self_get_address() + self.name = 'bob' + self._opts = opts + self._app = app + +class BaseThread(threading.Thread): + + def __init__(self, name=None, target=None): + if name: + super().__init__(name=name, target=target) + else: + super().__init__(target=target) + self._stop_thread = False + self.name = name + + def stop_thread(self, timeout=-1): + self._stop_thread = True + if timeout < 0: + timeout = ts.iTHREAD_TIMEOUT + i = 0 + while i < ts.iTHREAD_JOINS: + self.join(timeout) + if not self.is_alive(): break + i = i + 1 + else: + LOG.warning(f"{self.name} BLOCKED") + +class ToxIterateThread(BaseThread): + + def __init__(self, tox): + super().__init__(name='ToxIterateThread') + self._tox = tox + + def run(self): + while not self._stop_thread: + self._tox.iterate() + sleep(self._tox.iteration_interval() / 1000) + +global bob, alice +bob = alice = None + +def prepare(self): + global bob, alice + def bobs_on_self_connection_status(iTox, connection_state, *args): + status = connection_state + self.bob.dht_connected = status + self.bob.mycon_time = time.time() + try: + if status != TOX_CONNECTION['NONE']: + LOG_DEBUG(f"bobs_on_self_connection_status TRUE {status}" \ + +f" last={int(self.bob.mycon_time)}" ) + self.bob.mycon_status = True + else: + LOG_DEBUG(f"bobs_on_self_connection_status FALSE {status}" \ + +f" last={int(self.bob.mycon_time)}" ) + self.bob.mycon_status = False + except Exception as e: + LOG_ERROR(f"bobs_on_self_connection_status {e}") + else: + if self.bob.self_get_connection_status() != status: + LOG_WARN(f"bobs_on_self_connection_status DISAGREE {status}") + + def alices_on_self_connection_status(iTox, connection_state, *args): + #FixMe connection_num + status = connection_state + self.alice.dht_connected = status + self.alice.mycon_time = time.time() + try: + if status != TOX_CONNECTION['NONE']: + LOG_DEBUG(f"alices_on_self_connection_status TRUE {status}" \ + +f" last={int(self.alice.mycon_time)}" ) + self.alice.mycon_status = True + else: + LOG_WARN(f"alices_on_self_connection_status FALSE {status}" \ + +f" last={int(self.alice.mycon_time)}" ) + self.alice.mycon_status = False + except Exception as e: + LOG_ERROR(f"alices_on_self_connection_status error={e}") + else: + if self.alice.self_get_connection_status() != status: + LOG_WARN(f"alices_on_self_connection_status != {status}") + self.alice.dht_connected = status + + opts = oToxygenToxOptions(oTOX_OARGS) + alice = AliceTox(opts, app=oAPP) + alice.oArgs = opts + alice.dht_connected = -1 + alice.mycon_status = False + alice.mycon_time = 1 + alice.callback_self_connection_status(alices_on_self_connection_status) + + bob = BobTox(opts, app=oAPP) + bob.oArgs = opts + bob.dht_connected = -1 + bob.mycon_status = False + bob.mycon_time = 1 + bob.callback_self_connection_status(bobs_on_self_connection_status) + if not bIS_LOCAL and not ts.bAreWeConnected(): + LOG.warning(f"doOnce not local and NOT CONNECTED") + return [bob, alice] + +class ToxSuite(unittest.TestCase): + failureException = RuntimeError + + @classmethod + def setUpClass(cls): + global oTOX_OARGS + assert oTOX_OPTIONS + assert oTOX_OARGS + + if not hasattr(cls, 'alice') and not hasattr(cls, 'bob'): + l = prepare(cls) + assert l + cls.bob, cls.alice = l + if not hasattr(cls.bob, '_main_loop'): + cls.bob._main_loop = ToxIterateThread(cls.bob) + cls.bob._main_loop.start() + LOG.debug(f"cls.bob._main_loop: ") # {threading.enumerate()} + if not hasattr(cls.alice, '_main_loop'): + cls.alice._main_loop = ToxIterateThread(cls.alice) + cls.alice._main_loop.start() + LOG.debug(f"cls.alice._main_loop: ") # {threading.enumerate()} + + cls.lUdp = ts.generate_nodes( + oArgs=oTOX_OARGS, + nodes_count=2*ts.iNODES, + ipv='ipv4', + udp_not_tcp=True) + + cls.lTcp = ts.generate_nodes( + oArgs=oTOX_OARGS, + nodes_count=2*ts.iNODES, + ipv='ipv4', + udp_not_tcp=False) + + @classmethod + def tearDownClass(cls): + cls.bob._main_loop.stop_thread() + cls.alice._main_loop.stop_thread() + if False: + cls.alice.kill() + cls.bob.kill() + del cls.bob + del cls.alice + + def setUp(self): + """ + """ + if hasattr(self, 'baid') and self.baid >= 0 and \ + self.baid in self.bob.self_get_friend_list(): + LOG.warn(f"setUp ALICE IS ALREADY IN BOBS FRIEND LIST") + elif self.bob.self_get_friend_list_size() >= 1: + LOG.warn(f"setUp BOB STILL HAS A FRIEND LIST") + + if hasattr(self, 'abid') and self.abid >= 0 and \ + self.abid in self.alice.self_get_friend_list(): + LOG.warn(f"setUp BOB IS ALREADY IN ALICES FRIEND LIST") + elif self.alice.self_get_friend_list_size() >= 1: + LOG.warn(f"setUp ALICE STILL HAS A FRIEND LIST") + + def tearDown(self): + """ + """ + if hasattr(self, 'baid') and self.baid >= 0 and \ + self.baid in self.bob.self_get_friend_list(): + LOG.warn(f"tearDown ALICE IS STILL IN BOBS FRIEND LIST") + elif self.bob.self_get_friend_list_size() >= 1: + LOG.warn(f"tearDown BOBS STILL HAS A FRIEND LIST") + + if hasattr(self, 'abid') and self.abid >= 0 and \ + self.abid in self.alice.self_get_friend_list(): + LOG.warn(f"tearDown BOB IS STILL IN ALICES FRIEND LIST") + elif self.bob.self_get_friend_list_size() >= 1: + LOG.warn(f"tearDown ALICE STILL HAS A FRIEND LIST") + + def run(self, result=None): + """ Stop after first error """ + if not result.errors: + super(ToxSuite, self).run(result) + + def get_connection_status(self): + if self.bob.mycon_time == -1 or self.alice.mycon_time == -1: + pass + # drop through + elif self.bob.dht_connected == TOX_CONNECTION['NONE']: + return False + elif self.alice.dht_connected == TOX_CONNECTION['NONE']: + return False + + # if not self.connected + if self.bob.self_get_connection_status() == TOX_CONNECTION['NONE']: + return False + if self.alice.self_get_connection_status() == TOX_CONNECTION['NONE']: + return False + return True + + def loop(self, n): + """ + t:iterate + t:iteration_interval + """ + interval = self.bob.iteration_interval() + for i in range(n): + self.alice.iterate() + self.bob.iterate() + sleep(interval / 1000.0) + + def call_bootstrap(self, num=None, lToxes=None, i=0): + if num == None: num=ts.iNODES +# LOG.debug(f"call_bootstrap network={oTOX_OARGS.network}") + if oTOX_OARGS.network in ['new', 'newlocal', 'localnew']: + ts.bootstrap_local(self.lUdp, [self.alice, self.bob]) + elif not ts.bAreWeConnected(): + LOG.warning('we are NOT CONNECTED') + else: + random.shuffle(self.lUdp) + if oTOX_OARGS.proxy_port > 0: + lElts = self.lUdp[:1] + else: + lElts = self.lUdp[:num+i] + LOG.debug(f"call_bootstrap ts.bootstrap_udp {len(lElts)}") + if lToxes is None: lToxes = [self.alice, self.bob] + ts.bootstrap_udp(lElts, lToxes) + random.shuffle(self.lTcp) + lElts = self.lTcp[:num+i] + LOG.debug(f"call_bootstrap ts.bootstrap_tcp {len(lElts)}") + ts.bootstrap_tcp(lElts, lToxes) + + def loop_until_connected(self, num=None): + """ + t:on_self_connection_status + t:self_get_connection_status + """ + i = 0 + bRet = None + while i <= THRESHOLD : + if (self.alice.mycon_status and self.bob.mycon_status): + bRet = True + break + if i % 5 == 0: + j = i//5 + self.call_bootstrap(num, lToxes=None, i=j) + s = '' + if i == 0: s = '\n' + LOG.info(s+"loop_until_connected " \ + +" #" + str(i) \ + +" BOB=" +repr(self.bob.self_get_connection_status()) \ + +" ALICE=" +repr(self.alice.self_get_connection_status()) + +f" BOBS={self.bob.mycon_status}" \ + +f" ALICES={self.alice.mycon_status}" \ + +f" last={int(self.bob.mycon_time)}" ) + if (self.alice.mycon_status and self.bob.mycon_status): + bRet = True + break + if (self.alice.self_get_connection_status() and + self.bob.self_get_connection_status()): + LOG_WARN(f"loop_until_connected disagree status() DISAGREE" \ + +f' self.bob.mycon_status={self.bob.mycon_status}' \ + +f' alice.mycon_status={self.alice.mycon_status}' \ + +f" last={int(self.bob.mycon_time)}" ) + bRet = True + break + i += 1 + self.loop(100) + else: + bRet = False + + if bRet or \ + ( self.bob.self_get_connection_status() != TOX_CONNECTION['NONE'] and \ + self.alice.self_get_connection_status() != TOX_CONNECTION['NONE'] ): + LOG.info(f"loop_until_connected returning True {i}" \ + +f" BOB={self.bob.self_get_connection_status()}" \ + +f" ALICE={self.alice.self_get_connection_status()}" \ + +f" last={int(self.bob.mycon_time)}" ) + return True + else: + LOG.warning(f"loop_until_connected returning False {i}" \ + +f" BOB={self.bob.self_get_connection_status()}" \ + +f" ALICE={self.alice.self_get_connection_status()}" \ + +f" last={int(self.bob.mycon_time)}" ) + return False + + def wait_obj_attr(self, obj, attr): + return wait_otox_attrs(self, obj, [attr]) + + def wait_objs_attr(self, objs, attr): + i = 0 + while i <= THRESHOLD: + if i % 5 == 0: + num = None + j = i//5 + self.call_bootstrap(num, objs, i=j) + LOG.debug("wait_objs_attr " +repr(objs) \ + +" for " +repr(attr) \ + +" " +str(i)) + if all([getattr(obj, attr) for obj in objs]): + return True + self.loop(100) + i += 1 + else: + LOG.error(f"wait_obj_attr i >= {THRESHOLD}") + + return all([getattr(obj, attr) for obj in objs]) + + def wait_otox_attrs(self, obj, attrs): + i = 0 + while i <= THRESHOLD: + if i % 5 == 0: + num = None + j = 0 + if obj.mycon_time == 1: + num = 4 + j = i//5 + self.call_bootstrap(num, [obj], i=j) + LOG.debug(f"wait_otox_attrs {obj.name} for {attrs} {i}" \ + +f" last={int(obj.mycon_time)}") + if all([getattr(obj, attr) is not None for attr in attrs]): + return True + self.loop(100) + i += 1 + else: + LOG.warning(f"wait_otox_attrs i >= {THRESHOLD}") + + return all([getattr(obj, attr) for attr in attrs]) + + def wait_ensure_exec(self, method, args): + i = 0 + oRet = None + while i <= THRESHOLD: + if i % 5 == 0: + j = i//5 + self.call_bootstrap(num=None, lToxes=None, i=j) + LOG.debug("wait_ensure_exec " \ + +" " +str(method) + +" " +str(i)) + try: + oRet = method(*args) + if oRet: + LOG.info(f"wait_ensure_exec oRet {oRet!r}") + return True + except ArgumentError as e: + # ArgumentError('This client is currently NOT CONNECTED to the friend.') + # dunno + LOG.warning(f"wait_ensure_exec ArgumentError {e}") + return False + except Exception as e: + LOG.warning(f"wait_ensure_exec EXCEPTION {e}") + return False + sleep(3) + i += 1 + else: + LOG.error(f"wait_ensure_exec i >= {1*THRESHOLD}") + return False + + return oRet + + def bob_add_alice_as_friend_norequest(self): + if hasattr(self, 'baid') and self.baid >= 0 and \ + self.baid in self.bob.self_get_friend_list(): + LOG.warn('Alice is already in bobs friend list') + return True + if self.bob.self_get_friend_list_size() >= 1: + LOG.warn(f'Bob has a friend list {self.bob.self_get_friend_list()}') + return True + + MSG = 'Hi, this is Bob.' + iRet = self.bob.friend_add_norequest(self.alice._address) + self.baid = self.bob.friend_by_public_key(self.alice._address) + assert self.baid >= 0, self.baid + assert self.bob.friend_exists(self.baid), "bob.friend_exists" + assert not self.bob.friend_exists(self.baid + 1) + assert self.baid in self.bob.self_get_friend_list() + assert self.bob.self_get_friend_list_size() >= 1 + return iRet >= 0 + + def alice_add_bob_as_friend_norequest(self): + if hasattr(self, 'abid') and self.abid >= 0 and \ + self.abid in self.alice.self_get_friend_list(): + LOG.warn('Alice is already in Bobs friend list') + return True + if self.alice.self_get_friend_list_size() >= 1: + LOG.warn(f'Alice has a friend list {self.alice.self_get_friend_list()}') + + MSG = 'Hi Bob, this is Alice.' + iRet = self.alice.friend_add_norequest(self.bob._address) + self.abid = self.alice.friend_by_public_key(self.bob._address) + assert self.abid >= 0, self.abid + assert self.abid in self.alice.self_get_friend_list() + assert self.alice.friend_exists(self.abid), "alice.friend_exists" + assert not self.alice.friend_exists(self.abid + 1) + assert self.alice.self_get_friend_list_size() >= 1 + return iRet >= 0 + + def both_add_as_friend_norequest(self): + assert self.bob_add_alice_as_friend_norequest() + if not hasattr(self, 'baid') or self.baid < 0: + raise AssertionError("both_add_as_friend_norequest bob, 'baid'") + + assert self.alice_add_bob_as_friend_norequest() + if not hasattr(self, 'abid') or self.abid < 0: + raise AssertionError("both_add_as_friend_norequest alice, 'abid'") + + #: Test last online + assert self.alice.friend_get_last_online(self.abid) is not None + assert self.bob.friend_get_last_online(self.baid) is not None + return True + + def bob_add_alice_as_friend(self): + """ + t:friend_add + t:on_friend_request + t:friend_by_public_key + """ + MSG = 'Alice, this is Bob.' + sSlot = 'friend_request' + + def alices_on_friend_request(iTox, + public_key, + message_data, + message_data_size, + *largs): + LOG_DEBUG(f"alices_on_friend_request: " +repr(message_data)) + try: + assert str(message_data, 'UTF-8') == MSG + LOG_INFO(f"alices_on_friend_request: friend_added = True ") + except Exception as e: + LOG_WARN(f"alices_on_friend_request: Exception {e}") + # return + setattr(self.bob, sSlot, True) + + setattr(self.bob, sSlot, None) + inum = -1 + self.alice.callback_friend_request(alices_on_friend_request) + try: + inum = self.bob.friend_add(self.alice._address, bytes(MSG, 'UTF-8')) + if not inum >= 0: + LOG.warning('bob.friend_add !>= 0 ' +repr(inum)) + if not self.wait_otox_attrs(self.bob, [sSlot]): + return False + except Exception as e: + LOG.error(f"bob.friend_add EXCEPTION {e}") + return False + finally: + self.bob.callback_friend_message(None) + + self.baid = self.bob.friend_by_public_key(self.alice._address) + assert self.baid >= 0, self.baid + assert self.bob.friend_exists(self.baid) + assert not self.bob.friend_exists(self.baid + 1) + assert self.baid in self.bob.self_get_friend_list() + assert self.bob.self_get_friend_list_size() >= 1 + return True + + def alice_add_bob_as_friend(self): + """ + t:friend_add + t:on_friend_request + t:friend_by_public_key + """ + MSG = 'Bob, this is Alice.' + sSlot = 'friend_request' + + def bobs_on_friend_request(iTox, + public_key, + message_data, + message_data_size, + *largs): + LOG_DEBUG(f"bobs_on_friend_request: " +repr(message_data)) + try: + assert str(message_data, 'UTF-8') == MSG + LOG_INFO(f"bobs_on_friend_request: friend_added = True ") + except Exception as e: + LOG_WARN(f"bobs_on_friend_request: Exception {e}") + # return + else: + setattr(self.alice, sSlot, True) + + setattr(self.alice, sSlot, None) + inum = -1 + self.bob.callback_friend_request(bobs_on_friend_request) + try: + inum = self.alice.friend_add(self.bob._address, bytes(MSG, 'UTF-8')) + if not inum >= 0: + LOG.warning('alice.friend_add !>= 0 ' +repr(inum)) + if not self.wait_obj_attr(self.alice, sSlot): + return False + except Exception as e: + LOG.error(f"alice.friend_add EXCEPTION {e}") + return False + finally: + self.bob.callback_friend_message(None) + self.abid = self.alice.friend_by_public_key(self.bob._address) + assert self.abid >= 0, self.abid + assert self.alice.friend_exists(self.abid) + assert not self.alice.friend_exists(self.abid + 1) + assert self.abid in self.alice.self_get_friend_list() + assert self.alice.self_get_friend_list_size() >= 1 + return True + + def both_add_as_friend(self): + assert self.bob_add_alice_as_friend() + assert self.alice_add_bob_as_friend() + + #: Test last online + assert self.alice.friend_get_last_online(self.abid) is not None + assert self.bob.friend_get_last_online(self.baid) is not None + + def bob_add_alice_as_friend_and_status(self): + if oTOX_OARGS.bIS_LOCAL: + assert self.bob_add_alice_as_friend_norequest() + else: + assert self.bob_add_alice_as_friend() + + #: Wait until both are online + self.bob.friend_conn_status = False + def bobs_on_friend_connection_status(iTox, friend_id, iStatus, *largs): + LOG_INFO(f"bobs_on_friend_connection_status {friend_id} ?>=0" +repr(iStatus)) + if iStatus > 0: + self.bob.friend_conn_status = True + + self.bob.friend_status = None + def bobs_on_friend_status(iTox, friend_id, iStatus, *largs): + LOG_INFO(f"bobs_on_friend_status {friend_id} ?>=0" +repr(iStatus)) + if iStatus > 0: + self.bob.friend_status = True + + self.alice.friend_conn_status = None + def alices_on_friend_connection_status(iTox, friend_id, iStatus, *largs): + LOG_INFO(f"alices_on_friend_connection_status {friend_id} ?>=0 " +repr(iStatus)) + if iStatus > 0: + self.alice.friend_conn_status = True + + self.alice.friend_status = False + def alices_on_friend_status(iTox, friend_id, iStatus, *largs): + LOG_INFO(f"alices_on_friend_status {friend_id} ?>=0 " +repr(iStatus)) + if iStatus > 0: + self.alice.friend_status = True + + self.alice.callback_friend_connection_status(alices_on_friend_connection_status) + self.alice.callback_friend_status(alices_on_friend_status) + try: + LOG.info("bob_add_alice_as_friend_and_status waiting for alice connections") + if not self.wait_otox_attrs(self.alice, + ['friend_conn_status', + 'friend_status']): + return False + + self.bob.callback_friend_connection_status(bobs_on_friend_connection_status) + self.bob.callback_friend_status(bobs_on_friend_status) + + LOG.info("bob_add_alice_as_friend_and_status waiting for bob connections") + if not self.wait_otox_attrs(self.bob, + ['friend_conn_status', + 'friend_status']): + return False + except Exception as e: + LOG.error(f"bob_add_alice_as_friend_and_status ERROR {e}") + return False + finally: + self.alice.callback_friend_connection_status(None) + self.bob.callback_friend_connection_status(None) + self.alice.callback_friend_status(None) + self.bob.callback_friend_status(None) + return True + + def friend_delete(self, fname, baid): + #: Test delete friend + assert getattr(self, fname).friend_exists(baid) + getattr(self, fname).friend_delete(baid) + self.loop(50) + assert not self.bob.friend_exists(baid) + + def warn_if_no_cb(self, alice, sSlot): + if not hasattr(alice, sSlot+'_cb') or \ + not getattr(alice, sSlot+'_cb'): + LOG.warning(f"self.bob.{sSlot}_cb NOT EXIST") + + def warn_if_cb(self, alice, sSlot): + if hasattr(self.bob, sSlot+'_cb') and \ + getattr(self.bob, sSlot+'_cb'): + LOG.warning(f"self.bob.{sSlot}_cb EXIST") + + # tests are executed in order + def test_notice_log(self): # works + notice = '/var/lib/tor/.SelekTOR/3xx/cache/9050/notice.log' + if True or os.path.exists(notice): + iRet = os.system(f"sudo sed -e '1,/.notice. Bootstrapped 100%/d' {notice}" + \ + "| grep 'Tried for 120 seconds to get a connection to :0.'") + if iRet == 0: + raise SystemExit("seconds to get a connection to :0") + else: + LOG.debug(f"checked {notice}") + + def test_tests_logging(self): # works + with self.assertLogs('foo', level='INFO') as cm: + logging.getLogger('foo').info('first message') + logging.getLogger('foo.bar').error('second message') + logging.getLogger('foo.bar.baz').debug('third message') + self.assertEqual(cm.output, ['INFO:foo:first message', + 'ERROR:foo.bar:second message']) + + def test_tests_start(self): # works + LOG.info("test_tests_start " ) + port = ts.tox_bootstrapd_port() + + assert len(self.bob._address) == 2*TOX_ADDRESS_SIZE, len(self.bob._address) + assert len(self.alice._address) == 2*TOX_ADDRESS_SIZE, \ + len(self.alice._address) + + def test_bootstrap_local_netstat(self): # works + """ + t:bootstrap + """ + if oTOX_OARGS.network not in ['new', 'newlocal', 'local']: + return + + port = ts.tox_bootstrapd_port() + if not port: + return + iStatus = os.system(f"""netstat -nle4 | grep :{port}""") + if iStatus == 0: + LOG.info(f"bootstrap_local_netstat port {port} iStatus={iStatus}") + else: + LOG.warning(f"bootstrap_local_netstat NOT {port} iStatus={iStatus}") + + @unittest.skipIf(not bIS_LOCAL, "local test") + def test_bootstrap_local(self): # works + """ + t:bootstrap + """ + # get port from /etc/tox-bootstrapd.conf 33445 + self.call_bootstrap() + # ts.bootstrap_local(self, self.lUdp) + i = 0 + iStatus = -1 + while i < 10: + i = i + 1 + iStatus = self.bob.self_get_connection_status() + if iStatus != TOX_CONNECTION['NONE']: + break + sleep(3) + else: + pass + + o1 = self.alice.self_get_dht_id() + assert len(o1) == 64 + o2 = self.bob.self_get_dht_id() + assert len(o2) == 64 + +# if o1 != o2: LOG.warning(f"bootstrap_local DHT NOT same {o1} {o2} iStatus={iStatus}") + + iStatus = self.bob.self_get_connection_status() + if iStatus != TOX_CONNECTION['NONE']: + LOG.info(f"bootstrap_local connected iStatus={iStatus}") + return True + iStatus = self.alice.self_get_connection_status() + if iStatus != TOX_CONNECTION['NONE']: + LOG.info(f"bootstrap_local connected iStatus={iStatus}") + return True + LOG.warning(f"bootstrap_local NOT CONNECTED iStatus={iStatus}") + return False + + def test_bootstrap_iNmapInfo(self): # works + if os.environ['USER'] != 'root': + return + if oTOX_OARGS.network in ['new', 'newlocal', 'localnew']: + lElts = self.lUdp + elif oTOX_OARGS.proxy_port > 0: + lElts = self.lTcp + else: + lElts = self.lUdp + lRetval = [] + random.shuffle(lElts) + # assert + ts.bootstrap_iNmapInfo(lElts, oTOX_OARGS, bIS_LOCAL, iNODES=8) + + def test_self_get_secret_key(self): # works + """ + t:self_get_secret_key + """ + # test_self_get_secret_key + CRYPTO_SECRET_KEY_SIZE = 32 + secret_key = create_string_buffer(CRYPTO_SECRET_KEY_SIZE) + oRet0 = self.alice.self_get_secret_key(secret_key) + assert oRet0, repr(oRet0) + LOG.info('test_self_get_secret_key ' +repr(oRet0)) + assert len(str(oRet0)) + del secret_key + + def test_self_get_public_keys(self): # works + """ + t:self_get_secret_key + t:self_get_public_key + """ + + LOG.info('test_self_get_public_keys self.alice.self_get_secret_key') + oRet0 = self.alice.self_get_secret_key() + assert len(oRet0) + LOG.info('test_self_get_public_keys ' +repr(oRet0)) + oRet1 = self.alice.self_get_public_key() + assert len(oRet1) + LOG.info('test_self_get_public_keys ' +repr(oRet1)) + assert oRet0 != oRet1, repr(oRet0) +' != ' +repr(oRet1) + + def test_self_name(self): # works + """ + t:self_set_name + t:self_get_name + t:self_get_name_size + """ + self.alice.self_set_name('Alice') + assert self.alice.self_get_name() == 'Alice' + assert self.alice.self_get_name_size() == len('Alice') + self.bob.self_set_name('Bob') + assert self.bob.self_get_name() == 'Bob' + assert self.bob.self_get_name_size() == len('Bob') + + @unittest.skip('loud') + @unittest.skipIf(bIS_NOT_TOXYGEN or oTOX_OARGS.mode == 0, 'not testing in toxygen') + def test_sound_notification(self): # works + """ + Plays sound notification + :param type of notification + """ + from tests.toxygen_tests import test_sound_notification + test_sound_notification(self) + + def test_address(self): # works + """ + t:self_get_address + t:self_get_nospam + t:self_set_nospam + t:self_get_keys + """ + assert len(self.alice.self_get_address()) == ADDR_SIZE + assert len(self.bob.self_get_address()) == ADDR_SIZE + + self.alice.self_set_nospam(0x12345678) + assert self.alice.self_get_nospam() == 0x12345678 + self.loop(50) + + if hasattr(self.alice, 'self_get_keys'): + pk, sk = self.alice.self_get_keys() + assert pk == self.alice.self_get_address()[:CLIENT_ID_SIZE] + + def test_status_message(self): # works + MSG = 'Happy' + self.alice.self_set_status_message(MSG) + self.loop(100) + assert self.alice.self_get_status_message() == MSG, \ + self.alice.self_get_status_message() +' is not ' +MSG + assert self.alice.self_get_status_message_size() == len(MSG) + + def test_loop_until_connected(self): # works + assert self.loop_until_connected() + + def test_self_get_udp_port(self): # works + """ + t:self_get_udp_port + """ + if hasattr(oTOX_OPTIONS, 'udp_port') and oTOX_OPTIONS.udp_port: + o = self.alice.self_get_udp_port() + LOG.info('self_get_udp_port alice ' +repr(o)) + assert o > 0 + o = self.bob.self_get_udp_port() + LOG.info('self_get_udp_port bob ' +repr(o)) + assert o > 0 + + def test_self_get_tcp_port(self): # works + """ + t:self_get_tcp_port + """ + if hasattr(oTOX_OPTIONS, 'tcp_port') and oTOX_OPTIONS.tcp_port: + # errors if tcp_port <= 0 + o = self.alice.self_get_tcp_port() + LOG.info('self_get_tcp_port ' +repr(o)) + o = self.bob.self_get_tcp_port() + LOG.info('self_get_tcp_port ' +repr(o)) + + def test_get_dht_id(self): # works + """ + t:self_get_dht_id + """ + o1 = self.alice.self_get_dht_id() + assert len(o1) == 64 + o2 = self.bob.self_get_dht_id() + assert len(o2) == 64 + + def test_bob_assert_connection_status(self): # works + if self.bob.self_get_connection_status() == TOX_CONNECTION['NONE']: + RuntimeError("ERROR: NOT CONNECTED " \ + +repr(self.bob.self_get_connection_status())) + + def test_alice_assert_connection_status(self): # works + if self.alice.self_get_connection_status() == TOX_CONNECTION['NONE']: + RuntimeError("ERROR: NOT CONNECTED " \ + +repr(self.alice.self_get_connection_status())) + + def test_bob_assert_mycon_status(self): # works + if self.bob.mycon_status == False: + RuntimeError("ERROR: NOT CONNECTED " \ + +repr(self.bob.mycon_status)) + + def test_alice_assert_mycon_status(self): # works + if self.alice.mycon_status == False: + RuntimeError("ERROR: NOT CONNECTED " \ + +repr(self.alice.mycon_status)) + + def test_bob_add_alice_as_friend_norequest(self): # works + assert len(self.bob.self_get_friend_list()) == 0 + assert self.bob_add_alice_as_friend_norequest() + #: Test last online + assert self.bob.friend_get_last_online(self.baid) is not None + self.bob.friend_delete(self.baid) + + def test_alice_add_bob_as_friend_norequest(self): # works + assert len(self.alice.self_get_friend_list()) == 0 + assert self.alice_add_bob_as_friend_norequest() + assert len(self.alice.self_get_friend_list()) != 0 + #: Test last online + assert self.alice.friend_get_last_online(self.abid) is not None + self.alice.friend_delete(self.abid) + + def test_both_add_as_friend_norequest(self): # works + assert len(self.bob.self_get_friend_list()) == 0 + assert len(self.alice.self_get_friend_list()) == 0 + self.both_add_as_friend_norequest() + + self.bob.friend_delete(self.baid) + self.alice.friend_delete(self.abid) + assert len(self.bob.self_get_friend_list()) == 0 + assert len(self.alice.self_get_friend_list()) == 0 + + def test_bob_add_alice_as_friend_and_status(self): + self.bob_add_alice_as_friend_and_status() + self.bob.friend_delete(self.baid) + + @unittest.skip('malloc_consolidate(): invalid chunk size') +# @unittest.skipIf(bIS_LOCAL, "local test") +# @expectedFailure # (bIS_LOCAL, "local test") + def test_bob_add_alice_as_friend(self): # fails + assert len(self.bob.self_get_friend_list()) == 0 + try: + assert self.bob_add_alice_as_friend() + #: Test last online + assert self.bob.friend_get_last_online(self.baid) is not None + except AssertionError as e: + #WTF? + self.bob.friend_delete(self.baid) + raise RuntimeError(f"Failed test {e}") + finally: + self.bob.friend_delete(self.baid) + assert len(self.bob.self_get_friend_list()) == 0 + + @unittest.skip('malloc_consolidate(): invalid chunk size') +# @unittest.skipIf(bIS_LOCAL, "local test") +# @expectedFailure + def test_alice_add_bob_as_friend(self): # fails + assert len(self.bob.self_get_friend_list()) == 0 + try: + assert self.alice_add_bob_as_friend() + #: Test last online + assert self.alice.friend_get_last_online(self.abid) is not None + except AssertionError as e: + raise RuntimeError(f"Failed test {e}") + except Exception as e: + LOG.error(f"test_alice_add_bob_as_friend EXCEPTION {e}") + raise + finally: + self.alice.friend_delete(self.abid) + assert len(self.alice.self_get_friend_list()) == 0 + +# @unittest.skipIf(bIS_LOCAL, "local test") + @expectedFailure + def test_both_add_as_friend(self): # works + try: + self.both_add_as_friend() + except AssertionError as e: + raise RuntimeError(f"Failed test {e}") + except Exception as e: + LOG.error(f"test_both_add_as_friend EXCEPTION {e}") + raise + finally: + self.bob.friend_delete(self.baid) + self.alice.friend_delete(self.abid) + assert len(self.bob.self_get_friend_list()) == 0 + assert len(self.alice.self_get_friend_list()) == 0 + + @unittest.skip('unfinished') + def test_bob_add_alice_as_friend_and_status(self): + assert self.bob_add_alice_as_friend_and_status() + self.bob.friend_delete(self.baid) + +#? @unittest.skip('fails') + @expectedFailure + def test_on_friend_status_message(self): # fails + """ + t:self_set_status_message + t:self_get_status_message + t:self_get_status_message_size + t:friend_set_status_message + t:friend_get_status_message + t:friend_get_status_message_size + t:on_friend_status_message + """ + MSG = 'Happy' + sSlot = 'friend_status_message' + + def bob_on_friend_status_message(iTox, friend_id, new_status_message, new_status_size, *largs): + try: + assert str(new_status_message, 'UTF-8') == MSG + assert friend_id == self.baid + except Exception as e: + LOG_ERROR(f"BOB_ON_friend_status_message EXCEPTION {e}") + else: + LOG_INFO(f"BOB_ON_friend_status_message {friend_id}" \ + +repr(new_status_message)) + setattr(self.bob, sSlot, True) + + setattr(self.bob, sSlot, None) + try: + if oTOX_OARGS.bIS_LOCAL: + assert self.bob_add_alice_as_friend_norequest() + else: + assert self.bob_add_alice_as_friend() + + self.bob.callback_friend_status_message(bob_on_friend_status_message) + self.warn_if_no_cb(self.bob, sSlot) + self.alice.self_set_status_message(MSG) + assert self.wait_otox_attrs(self.bob, [sSlot]) + + assert self.bob.friend_get_status_message(self.baid) == MSG + assert self.bob.friend_get_status_message_size(self.baid) == len(MSG) + + except AssertionError as e: + raise RuntimeError(f"Failed test {e}") + except Exception as e: + LOG.error(f"test_on_friend_status_message EXCEPTION {e}") + raise + finally: + self.alice.callback_friend_status(None) + self.bob.friend_delete(self.baid) + + @expectedFailure + def test_friend(self): # works + """ + t:friend_delete + t:friend_exists + t:friend_get_public_key + t:self_get_friend_list + t:self_get_friend_list_size + t:self_set_name + t:friend_get_name + t:friend_get_name_size + t:on_friend_name + """ + + assert len(self.bob.self_get_friend_list()) == 0 + assert len(self.alice.self_get_friend_list()) == 0 + #: Test friend request + if oTOX_OARGS.bIS_LOCAL: + assert self.bob_add_alice_as_friend_norequest() + assert self.alice_add_bob_as_friend_norequest() + else: + # no not connected error + assert self.bob_add_alice_as_friend() + assert self.alice_add_bob_as_friend() + try: + assert self.bob.friend_get_public_key(self.baid) == \ + self.alice.self_get_address()[:CLIENT_ID_SIZE] + + #: Test friend_get_public_key + assert self.alice.friend_get_public_key(self.abid) == \ + self.bob.self_get_address()[:CLIENT_ID_SIZE] + except AssertionError as e: + raise RuntimeError(f"Failed test {e}") + except Exception as e: + LOG.error(f"test_friend EXCEPTION {e}") + raise + finally: + self.bob.friend_delete(self.baid) + self.alice.friend_delete(self.abid) + +# @unittest.skip('fails') +# @unittest.skipIf(not bIS_LOCAL and not ts.bAreWeConnected(), 'NOT CONNECTED') + @expectedFailure + def test_user_status(self): + """ + t:self_get_status + t:self_set_status + t:friend_get_status + t:friend_get_status + t:on_friend_status + """ + sSlot = 'friend_status' + if oTOX_OARGS.bIS_LOCAL: + assert self.bob_add_alice_as_friend_norequest() + else: + assert self.bob_add_alice_as_friend() + + sSTATUS = TOX_USER_STATUS['NONE'] + setattr(self.bob, sSlot, None) + def bobs_on_friend_set_status(iTox, friend_id, new_status, *largs): + LOG_INFO(f"bobs_on_friend_set_status {friend_id} {new_status}") + try: + assert friend_id == self.baid + assert new_status in [TOX_USER_STATUS['BUSY'], TOX_USER_STATUS['AWAY']] + except Exception as e: + LOG_WARN(f"bobs_on_friend_set_status EXCEPTION {e}") + setattr(self.bob, sSlot, True) + + try: + if not self.get_connection_status(): + LOG.warning(f"test_user_status NOT CONNECTED self.get_connection_status") + self.loop_until_connected() + + self.bob.callback_friend_status(bobs_on_friend_set_status) + self.warn_if_no_cb(self.bob, sSlot) + sSTATUS = TOX_USER_STATUS['BUSY'] + self.alice.self_set_status(sSTATUS) + sSTATUS = TOX_USER_STATUS['AWAY'] + self.alice.self_set_status(sSTATUS) + assert self.wait_otox_attrs(self.bob, [sSlot]) + # wait_obj_attr count >= 15 for friend_status + + self.alice.self_set_status(TOX_USER_STATUS['NONE']) + assert self.alice.self_get_status() == TOX_USER_STATUS['NONE'] + assert self.bob.friend_get_status(self.baid) == TOX_USER_STATUS['NONE'] + + except AssertionError as e: + raise RuntimeError(f"Failed test {e}") + + except Exception as e: + LOG.error(f"test_user_status EXCEPTION {e}") + raise + finally: + self.bob.callback_friend_status(None) + self.warn_if_cb(self.bob, sSlot) + self.bob.friend_delete(self.baid) + + @unittest.skip('crashes') + def test_connection_status(self): + """ + t:friend_get_connection_status + t:on_friend_connection_status + """ + LOG.info("test_connection_status ") + if oTOX_OARGS.bIS_LOCAL: + assert self.bob_add_alice_as_friend_norequest() + else: + assert self.bob_add_alice_as_friend() + + sSlot = 'friend_connection_status' + setattr(self.bob, sSlot, None) + def bobs_on_friend_connection_status(iTox, friend_id, iStatus, *largs): + setattr(self.bob, sSlot, True) + LOG_INFO(f"bobs_on_friend_connection_status " +repr(iStatus)) + try: + assert friend_id == self.baid + except Exception as e: + LOG.error(f"bobs_on_friend_connection_status ERROR {e}") + + opts = oToxygenToxOptions(oTOX_OARGS) + try: + setattr(self.bob, sSlot, True) + self.bob.callback_friend_connection_status(bobs_on_friend_connection_status) + + LOG.info("test_connection_status killing alice") + self.alice.kill() #! bang + LOG.info("test_connection_status making alice") + self.alice = Tox(opts, app=oAPP) + LOG.info("test_connection_status maked alice") + + assert self.wait_otox_attrs(self.bob, [sSlot]) + except AssertionError as e: + raise + except Exception as e: + LOG.error(f"bobs_on_friend_connection_status {e}") + raise + finally: + self.bob.callback_friend_connection_status(None) + + #? assert self.bob.friend_get_connection_status(self.aid) is False + self.bob.friend_delete(self.baid) + +#? @unittest.skip('fails') + def test_friend_name(self): # fails + """ + t:self_set_name + t:friend_get_name + t:friend_get_name_size + t:on_friend_name + """ + + sSlot= 'friend_name' + #: Test friend request + + LOG.info("test_friend_name") + if oTOX_OARGS.bIS_LOCAL: + assert self.bob_add_alice_as_friend_norequest() + else: + assert self.bob_add_alice_as_friend() + + if not self.get_connection_status(): + LOG.warning(f"test_friend_message NOT CONNECTED") + self.loop_until_connected() + + #: Test friend name + NEWNAME = 'Jenny' + + def bobs_on_friend_name(iTox, fid, newname, iNameSize, *largs): + LOG_INFO(f"bobs_on_friend_name {sSlot} {fid}") + try: + assert fid == self.baid + assert str(newname, 'UTF-8') == NEWNAME + except Exception as e: + LOG.error(f"bobs_on_friend_name EXCEPTION {e}") + setattr(self.bob, sSlot, True) + + setattr(self.bob, sSlot, None) + self.bob.callback_friend_name(bobs_on_friend_name) + self.warn_if_no_cb(self.bob, sSlot) + try: + self.alice.self_set_name(NEWNAME) + assert self.wait_otox_attrs(self.bob, [sSlot]) + + assert self.bob.friend_get_name(self.baid) == NEWNAME + assert self.bob.friend_get_name_size(self.baid) == len(NEWNAME) + + except AssertionError as e: + raise RuntimeError(f"test_friend Failed test {e}") + + except Exception as e: + LOG.error(f"test_friend EXCEPTION {e}") + raise + + finally: + self.bob.callback_friend_name(None) + if hasattr(self.bob, sSlot + '_cb') and \ + getattr(self.bob, sSlot + '_cb'): + LOG.warning(sSlot + ' EXISTS') + + self.bob.friend_delete(self.baid) + + # wait_ensure_exec ArgumentError This client is currently not connected to the friend. + def test_friend_message(self): # fails + """ + t:on_friend_action + t:on_friend_message + t:friend_send_message + """ + + #: Test message + MSG = 'Hi, Bob!' + sSlot = 'friend_message' + if oTOX_OARGS.bIS_LOCAL: + assert self.both_add_as_friend_norequest() + else: + assert self.both_add_as_friend() + + if not self.get_connection_status(): + LOG.warning(f"test_friend_message NOT CONNECTED") + self.loop_until_connected() + + iRet = self.bob.friend_get_connection_status(self.baid) + if iRet == TOX_CONNECTION['NONE']: + LOG.error("bob.friend_get_connection_status") + raise RuntimeError("bob.friend_get_connection_status") + iRet = self.alice.friend_get_connection_status(self.abid) + if iRet == TOX_CONNECTION['NONE']: + LOG.error("alice.friend_get_connection_status") + raise RuntimeError("alice.friend_get_connection_status") + + def alices_on_friend_message(iTox, fid, msg_type, message, iSize, *largs): + LOG_DEBUG(f"alices_on_friend_message {fid} {message}") + try: + assert fid == self.alice.abid + assert msg_type == TOX_MESSAGE_TYPE['NORMAL'] + assert str(message, 'UTF-8') == MSG + except Exception as e: + LOG_ERROR(f"alices_on_friend_message EXCEPTION {e}") + else: + LOG_INFO(f"alices_on_friend_message {message}") + setattr(self.alice, sSlot, True) + + setattr(self.alice, sSlot, None) + try: + self.alice.callback_friend_message(alices_on_friend_message) + self.warn_if_no_cb(self.alice, sSlot) + + # dunno - both This client is currently NOT CONNECTED to the friend. + if True: + iMesId = self.bob.friend_send_message( + self.baid, + TOX_MESSAGE_TYPE['NORMAL'], + bytes(MSG, 'UTF-8')) + # ArgumentError('This client is currently NOT CONNECTED to the friend.') + else: + iMesId = self.wait_ensure_exec(self.bob.friend_send_message, + [self.baid, + TOX_MESSAGE_TYPE['NORMAL'], + bytes(MSG, 'UTF-8')]) + assert iMesId >= 0 + assert self.wait_otox_attrs(self.alice, [sSlot]) + except ArgumentError as e: + # ArgumentError('This client is currently NOT CONNECTED to the friend.') + # dunno + LOG.error(f"test_friend_message {e}") + raise + except AssertionError as e: + LOG.warning(f"test_friend_message {e}") + raise RuntimeError(f"Failed test test_friend_message {e}") + except Exception as e: + LOG.error(f"test_friend_message {e}") + raise + finally: + self.alice.callback_friend_message(None) + self.warn_if_cb(self.alice, sSlot) + self.bob.friend_delete(self.baid) + self.alice.friend_delete(self.abid) + +#? @unittest.skip('fails') + def test_friend_action(self): + """ + t:on_friend_action + t:on_friend_message + t:friend_send_message + """ + + if oTOX_OARGS.bIS_LOCAL: + assert self.both_add_as_friend_norequest() + else: + assert self.both_add_as_friend() + + if not self.get_connection_status(): + LOG.warning(f"test_friend_message NOT CONNECTED") + self.loop_until_connected() + + iRet = self.bob.friend_get_connection_status(self.baid) + if iRet == TOX_CONNECTION['NONE']: + LOG.error("bob.friend_get_connection_status") + raise RuntimeError("bob.friend_get_connection_status") + iRet = self.alice.friend_get_connection_status(self.abid) + if iRet == TOX_CONNECTION['NONE']: + LOG.error("alice.friend_get_connection_status") + raise RuntimeError("alice.friend_get_connection_status") + + BID = self.baid + #: Test action + ACTION = 'Kick' + sSlot = 'friend_read_action' + setattr(self.bob, sSlot, None) + sSlot = 'friend_read_receipt' + setattr(self.bob, sSlot, None) + def alices_on_friend_action(iTox, fid, msg_type, action, *largs): + sSlot = 'friend_read_action' + LOG_DEBUG(f"alices_on_friend_action") + try: + assert fid == self.bob.baid + assert msg_type == TOX_MESSAGE_TYPE['ACTION'] + assert action == ACTION + except Exception as e: + LOG_ERROR(f"alices_on_friend_action EXCEPTION {e}") + else: + LOG_INFO(f"alices_on_friend_action {message}") + setattr(self.bob, sSlot, True) + + sSlot = 'friend_read_action' + setattr(self.alice, sSlot, None) + sSlot = 'friend_read_receipt' + setattr(self.alice, sSlot, None) + def alices_on_read_reciept(iTox, fid, msg_id, *largs): + LOG_DEBUG(f"alices_on_read_reciept") + sSlot = 'friend_read_receipt' + try: + assert fid == BID + except Exception as e: + LOG_ERROR(f"alices_on_read_reciept {e}") + else: + LOG_INFO(f"alices_on_read_reciept {fid}") + setattr(self.alice, sSlot, True) + + sSlot = 'friend_read_receipt' + try: + sSlot = 'friend_read_action' + setattr(self.bob, sSlot, False) + sSlot = 'friend_read_receipt' + setattr(self.alice, sSlot, False) + + self.alice.callback_friend_read_receipt(alices_on_read_reciept) #was alices_on_friend_action + self.warn_if_no_cb(self.alice, sSlot) + assert self.wait_ensure_exec(self.bob.friend_send_message, + [self.baid, + TOX_MESSAGE_TYPE['ACTION'], + bytes(ACTION, 'UTF-8')]) + assert self.wait_otox_attrs(self.alice, [sSlot]) + except AssertionError as e: + raise RuntimeError(f"Failed test {e}") + except ArgumentError as e: + # ArgumentError('This client is currently NOT CONNECTED to the friend.') + # dunno + LOG.warning(f"test_friend_action {e}") + except Exception as e: + LOG.error(f"test_friend_action {e}") + raise + finally: + self.alice.callback_friend_read_receipt(None) + self.bob.friend_delete(self.baid) + self.alice.friend_delete(self.abid) + + @unittest.skip('fails') + def test_alice_typing_status(self): + """ + t:on_friend_read_receipt + t:on_friend_typing + t:self_set_typing + t:friend_get_typing + t:friend_get_last_online + """ + + sSlot = 'friend_typing' + # works + LOG.info("test_typing_status bob adding alice") + if oTOX_OARGS.bIS_LOCAL: + assert self.both_add_as_friend_norequest() + else: + assert self.both_add_as_friend() + + BID = self.baid + + #: Test typing status + def bob_on_friend_typing(iTox, fid, is_typing, *largs): + try: + assert fid == BID + assert is_typing is True + assert self.bob.friend_get_typing(fid) is True + except Exception as e: + LOG.error(f"BOB_ON_friend_typing {e}") + raise + else: + LOG_INFO(f"BOB_ON_friend_typing" + str(fid)) + setattr(self.bob, sSlot, True) + + setattr(self.bob, sSlot, None) + try: + if not self.get_connection_status(): + LOG.warning(f"test_friend_message NOT CONNECTED") + self.loop_until_connected() + + self.bob.callback_friend_typing(bob_on_friend_typing) + self.alice.self_set_typing(self.abid, True) + assert self.wait_otox_attrs(self.bob, [sSlot]) + if not hasattr(self.bob, sSlot+'_cb') or \ + not getattr(self.bob, sSlot+'_cb'): + LOG.warning(f"self.bob.{sSlot}_cb NOT EXIST") + except AssertionError as e: + raise RuntimeError(f"Failed test {e}") + except Exception as e: + LOG.error(f"test_alice_typing_status error={e}") + raise + finally: + self.bob.callback_friend_typing(None) + self.bob.friend_delete(self.baid) + self.alice.friend_delete(self.abid) + + @unittest.skip('unfinished') + def test_file_transfer(self): # unfinished + """ + t:file_send + t:file_send_chunk + t:file_control + t:file_seek + t:file_get_file_id + t:on_file_recv + t:on_file_recv_control + t:on_file_recv_chunk + t:on_file_chunk_request + """ + + if oTOX_OARGS.bIS_LOCAL: + assert self.bob_add_alice_as_friend_norequest() + else: + assert self.bob_add_alice_as_friend() + + BID = self.baid + + FRIEND_NUMBER = self.baid + FILE_NUMBER = 1 + FILE = os.urandom(1024 * 1024) + FILE_NAME = b"/tmp/test.bin" + if not os.path.exists(FILE_NAME): + with open(FILE_NAME, 'wb') as oFd: + oFd.write(FILE) + FILE_SIZE = len(FILE) + OFFSET = 567 + + m = hashlib.md5() + m.update(FILE[OFFSET:]) + FILE_DIGEST = m.hexdigest() + + CONTEXT = { 'FILE': bytes(), 'RECEIVED': 0, 'START': False, 'SENT': 0 } + + def alice_on_file_recv(iTox, fid, file_number, kind, size, filename): + LOG_DEBUG(f"ALICE_ON_file_recv fid={fid} {file_number}") + try: + assert size == FILE_SIZE + assert filename == FILE_NAME + retv = self.alice.file_seek(fid, file_number, OFFSET) + assert retv is True + self.alice.file_control(fid, file_number, TOX_FILE_CONTROL['RESUME']) + except Exception as e: + LOG_ERROR(f"ALICE_ON_file_recv {e}") + else: + LOG_INFO(f"ALICE_ON_file_recv " + str(fid)) + + def alice_on_file_recv_control(iTox, fid, file_number, control, *largs): + # TOX_FILE_CONTROL = { 'RESUME': 0, 'PAUSE': 1, 'CANCEL': 2,} + LOG_DEBUG(f"ALICE_ON_file_recv_control fid={fid} {file_number} {control}") + try: + assert FILE_NUMBER == file_number + # FixMe _FINISHED? + if False and control == TOX_FILE_CONTROL['RESUME']: + # assert CONTEXT['RECEIVED'] == FILE_SIZE + # m = hashlib.md5() + # m.update(CONTEXT['FILE']) + # assert m.hexdigest() == FILE_DIGEST + self.alice.completed = True + except Exception as e: + LOG_ERROR(f"ALICE_ON_file_recv {e}") + else: + LOG_INFO(f"ALICE_ON_file_recv " + str(fid)) + + self.alice.completed = False + def alice_on_file_recv_chunk(iTox, fid, file_number, position, iNumBytes, *largs): + LOG_DEBUG(f"ALICE_ON_file_recv_chunk {fid} {file_number}") + # FixMe - use file_number and iNumBytes to get data? + data = '' + try: + if data is None: + assert CONTEXT['RECEIVED'] == (FILE_SIZE - OFFSET) + m = hashlib.md5() + m.update(CONTEXT['FILE']) + assert m.hexdigest() == FILE_DIGEST + self.alice.completed = True + self.alice.file_control(fid, file_number, TOX_FILE_CONTROL['CANCEL']) + return + + CONTEXT['FILE'] += data + CONTEXT['RECEIVED'] += len(data) + # if CONTEXT['RECEIVED'] < FILE_SIZE: + # assert self.file_data_remaining( + # fid, file_number, 1) == FILE_SIZE - CONTEXT['RECEIVED'] + except Exception as e: + LOG_ERROR(f"ALICE_ON_file_recv_chunk {e}") + else: + LOG_INFO(f"ALICE_ON_file_recv_chunk {fid}") + + # AliceTox.on_file_send_request = on_file_send_request + # AliceTox.on_file_control = on_file_control + # AliceTox.on_file_data = on_file_data + + LOG.info(f"test_file_transfer: baid={self.baid}") + try: + self.alice.callback_file_recv(alice_on_file_recv) + self.alice.callback_file_recv_control(alice_on_file_recv_control) + self.alice.callback_file_recv_chunk(alice_on_file_recv_chunk) + + self.bob.completed = False + def bob_on_file_recv_control2(iTox, fid, file_number, control): + LOG_DEBUG(f"BOB_ON_file_recv_control2 {fid} {file_number} control={control}") + if control == TOX_FILE_CONTROL['RESUME']: + CONTEXT['START'] = True + elif control == TOX_FILE_CONTROL['CANCEL']: + self.bob.completed = True + pass + + def bob_on_file_chunk_request(iTox, fid, file_number, position, length, *largs): + LOG_DEBUG(f"BOB_ON_file_chunk_request {fid} {file_number}") + if length == 0: + return + data = FILE[position:(position + length)] + self.bob.file_send_chunk(fid, file_number, position, data) + + sSlot = 'file_recv_control' + self.bob.callback_file_recv_control(bob_on_file_recv_control2) + self.bob.callback_file_chunk_request(bob_on_file_chunk_request) + + # was FILE_ID = FILE_NAME + FILE_ID = 32*'1' # + FILE_NAME = b'test.in' + + if not self.get_connection_status(): + LOG.warning(f"test_file_transfer NOT CONNECTED") + self.loop_until_connected() + + i = 0 + iKind = 0 + while i < 2: + i += 1 + try: + FN = self.bob.file_send(self.baid, iKind, FILE_SIZE, FILE_ID, FILE_NAME) + LOG.info(f"test_file_transfer bob.file_send {FN}") + except ArgumentError as e: + LOG.debug(f"test_file_transfer bob.file_send {e} {i}") + # ctypes.ArgumentError: This client is currently not connected to the friend. + raise + else: + break + self.loop(100) + sleep(1) + else: + LOG.error(f"test_file_transfer bob.file_send 2") + raise RuntimeError(f"test_file_transfer bob.file_send {THRESHOLD // 2}") + + # UINT32_MAX + FID = self.bob.file_get_file_id(self.baid, FN) + hexFID = "".join([hex(ord(c))[2:].zfill(2) for c in FILE_NAME]) + assert FID.startswith(hexFID.upper()) + + if not self.wait_obj_attrs(self.bob, ['completed']): + LOG.warning(f"test_file_transfer Bob not completed") + return False + if not self.wait_obj_attrs(self.alice, ['completed']): + LOG.warning(f"test_file_transfer Alice not completed") + return False + return True + + except (ArgumentError, ValueError,) as e: + # ValueError: non-hexadecimal number found in fromhex() arg at position 0 + LOG_ERROR(f"test_file_transfer: {e}") + raise + + except Exception as e: + LOG_ERROR(f"test_file_transfer:: {e}") + LOG_DEBUG('\n' + traceback.format_exc()) + raise + + finally: + self.bob.friend_delete(self.baid) + self.alice.callback_file_recv(None) + self.alice.callback_file_recv_control(None) + self.alice.callback_file_recv_chunk(None) + self.bob.callback_file_recv_control(None) + self.bob.callback_file_chunk_request(None) + + LOG_INFO(f"test_file_transfer:: self.wait_objs_attr completed") + + @unittest.skip('crashes') + def test_tox_savedata(self): # works sorta + # but "{addr} != {self.alice.self_get_address()}" + """ + t:get_savedata_size + t:get_savedata + """ + # Fatal Python error: Aborted + # "/var/local/src/toxygen_wrapper/wrapper/tox.py", line 180 in kill + return + + assert self.alice.get_savedata_size() > 0 + data = self.alice.get_savedata() + assert data is not None + addr = self.alice.self_get_address() + # self._address + + try: + LOG.info("test_tox_savedata alice.kill") + # crashes + self.alice.kill() + except: + pass + + oArgs = oTOX_OARGS + opts = oToxygenToxOptions(oArgs) + opts.savedata_data = data + opts.savedata_length = len(data) + + self.alice = Tox(tox_options=opts) + if addr != self.alice.self_get_address(): + LOG.warning("test_tox_savedata " + + f"{addr} != {self.alice.self_get_address()}") + else: + LOG.info("passed test_tox_savedata") + +def vOargsToxPreamble(oArgs, Tox, ToxTest): + + ts.vSetupLogging(oArgs) + + methods = set([x for x in dir(Tox) if not x[0].isupper() + and not x[0] == '_']) + docs = "".join([getattr(ToxTest, x).__doc__ for x in dir(ToxTest) + if getattr(ToxTest, x).__doc__ is not None]) + + tested = set(re.findall(r't:(.*?)\n', docs)) + not_tested = methods.difference(tested) + + logging.info('Test Coverage: %.2f%%' % (len(tested) * 100.0 / len(methods))) + if len(not_tested): + logging.info('Not tested:\n %s' % "\n ".join(sorted(list(not_tested)))) + +### + +def iMain(oArgs): + failfast=True + + vOargsToxPreamble(oArgs, Tox, ToxSuite) + # https://stackoverflow.com/questions/35930811/how-to-sort-unittest-testcases-properly/35930812#35930812 + cases = ts.suiteFactory(*ts.caseFactory([ToxSuite])) + if color_runner: + runner = color_runner.runner.TextTestRunner(verbosity=2, failfast=failfast) + else: + runner = unittest.TextTestRunner(verbosity=2, failfast=failfast, warnings='ignore') + runner.run(cases) + +def oToxygenToxOptions(oArgs): + data = None + tox_options = wrapper.tox.Tox.options_new() + if oArgs.proxy_type: + tox_options.contents.proxy_type = int(oArgs.proxy_type) + tox_options.contents.proxy_host = bytes(oArgs.proxy_host, 'UTF-8') + tox_options.contents.proxy_port = int(oArgs.proxy_port) + tox_options.contents.udp_enabled = False + else: + tox_options.contents.udp_enabled = oArgs.udp_enabled + if not os.path.exists('/proc/sys/net/ipv6'): + oArgs.ipv6_enabled = False + else: + tox_options.contents.ipv6_enabled = oArgs.ipv6_enabled + + tox_options.contents.tcp_port = int(oArgs.tcp_port) + tox_options.contents.dht_announcements_enabled = oArgs.dht_announcements_enabled + tox_options.contents.hole_punching_enabled = oArgs.hole_punching_enabled + + # overrides + tox_options.contents.local_discovery_enabled = False + tox_options.contents.experimental_thread_safety = False + # REQUIRED!! + if oArgs.ipv6_enabled and not os.path.exists('/proc/sys/net/ipv6'): + LOG.warning('Disabling IPV6 because /proc/sys/net/ipv6 does not exist' + repr(oArgs.ipv6_enabled)) + tox_options.contents.ipv6_enabled = False + else: + tox_options.contents.ipv6_enabled = bool(oArgs.ipv6_enabled) + + if data: # load existing profile + tox_options.contents.savedata_type = enums.TOX_SAVEDATA_TYPE['TOX_SAVE'] + tox_options.contents.savedata_data = c_char_p(data) + tox_options.contents.savedata_length = len(data) + else: # create new profile + tox_options.contents.savedata_type = enums.TOX_SAVEDATA_TYPE['NONE'] + tox_options.contents.savedata_data = None + tox_options.contents.savedata_length = 0 + + #? tox_options.contents.log_callback = LOG + if tox_options._options_pointer: + # LOG.debug("Adding logging to tox_options._options_pointer ") + ts.vAddLoggerCallback(tox_options, ts.on_log) + else: + LOG.warning("No tox_options._options_pointer " +repr(tox_options._options_pointer)) + + return tox_options + +def oArgparse(lArgv): + parser = ts.oMainArgparser() + parser.add_argument('profile', type=str, nargs='?', default=None, + help='Path to Tox profile') + oArgs = parser.parse_args(lArgv) + + for key in ts.lBOOLEANS: + if key not in oArgs: continue + val = getattr(oArgs, key) + setattr(oArgs, key, bool(val)) + + if hasattr(oArgs, 'sleep'): + if oArgs.sleep == 'qt': + pass # broken or gevent.sleep(idle_period) + elif oArgs.sleep == 'gevent': + pass # broken or gevent.sleep(idle_period) + else: + oArgs.sleep = 'time' + + return oArgs + +def main(lArgs=None): + global oTOX_OARGS + if lArgs is None: lArgs = [] + oArgs = oArgparse(lArgs) + global bIS_LOCAL + bIS_LOCAL = oArgs.network in ['newlocal', 'localnew', 'local'] + oTOX_OARGS = oArgs + setattr(oTOX_OARGS, 'bIS_LOCAL', bIS_LOCAL) + bIS_LOCAL = True + setattr(oTOX_OARGS, 'bIS_LOCAL', bIS_LOCAL) + # oTOX_OPTIONS = ToxOptions() + global oTOX_OPTIONS + oTOX_OPTIONS = oToxygenToxOptions(oArgs) + if coloredlogs: + # https://pypi.org/project/coloredlogs/ + coloredlogs.install(level=oArgs.loglevel, + logger=LOG, + # %(asctime)s,%(msecs)03d %(hostname)s [%(process)d] + fmt='%(name)s %(levelname)s %(message)s' + ) + else: + logging.basicConfig(level=oArgs.loglevel) # logging.INFO + + return iMain(oArgs) + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) + +# Ran 33 tests in 51.733s