Compare commits

..

10 commits

Author SHA1 Message Date
emdee
dcde8e3d1e Misc fixes 2023-07-14 14:46:18 +00:00
emdee
948335c8a0 fixed qweechat 2022-11-23 19:23:21 +00:00
emdee
0b1eaa1391 Added toxygen/third_party/qweechat 2022-11-20 18:44:17 +00:00
emdee
424e15b31c add third_party 2022-11-20 18:16:31 +00:00
emdee
db37d29dc8 add third_party 2022-11-20 18:15:46 +00:00
emdee
f1d8ce105c Added qweechat 2022-11-20 01:11:51 +00:00
emdee
1e5618060a isort 2022-11-17 15:26:55 +00:00
emdee
1b8b26eafc Fixes 2022-11-05 01:16:25 +00:00
emdee
a073dd9bc9 Oops 2022-10-27 07:18:09 +00:00
emdee
5df00c3ccd Fixed 2022-10-27 07:07:28 +00:00
73 changed files with 3428 additions and 395 deletions

43
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,43 @@
name: CI
on:
- push
- pull_request
jobs:
build:
strategy:
matrix:
python-version:
- "3.7"
- "3.8"
- "3.9"
- "3.10"
name: Python ${{ matrix.python-version }}
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install bandit flake8 pylint
- name: Lint with flake8
run: make flake8
# - name: Lint with pylint
# run: make pylint
- name: Lint with bandit
run: make bandit

View file

@ -46,14 +46,38 @@ Toxygen is powerful cross-platform [Tox](https://tox.chat/) client written in pu
## Forked
This hard-forked from https://github.com/toxygen-project/toxygen
This hard-forked from the dead https://github.com/toxygen-project/toxygen
```next_gen``` branch.
https://git.macaw.me/emdee/toxygen_wrapper needs packaging
https://git.plastiras.org/emdee/toxygen_wrapper needs packaging
is making a dependency. Just download it and copy the two directories
```wrapper``` and ```wrapper_tests``` into ```toxygen/toxygen```.
See ToDo.md to the current ToDo list.
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!
You can have a [weechat](https://github.com/weechat/qweechat)
console so that you can have IRC and jabber in a window as well as Tox.
There's a copy of qweechat in ```thirdparty/qweechat``` backported to
PyQt5 and integrated into toxygen. Follow the normal instructions for
adding a ```relay``` to [weechat](https://github.com/weechat/weechat)
```
/relay add ipv4.ssl.weechat 9001
/relay start ipv4.ssl.weechat
```
or
```
/relay add weechat 9000
/relay start weechat
```
and use the Plugins/Weechat Console to start weechat under Toxygen.
Then use th File/Connect menu item of the console to connect to weechat.
Weechat has a Jabber plugin to enable XMPP:
```
/python load jabber.el
/help jabber
```
so you can have Tox, IRC and XMPP in the same application!
Work on Tox on this project is suspended until the
[MultiDevice](https://git.plastiras.org/emdee/tox_profile/wiki/MultiDevice-Announcements-POC) problem is solved. Fork me!

View file

@ -45,8 +45,8 @@ line.
## check toxygen_wrapper
1. I've broken out toxygen_wrapper to be standalone,
https://git.macaw.me/emdee/toxygen_wrapper but the tox.py
https://git.plastiras.org/emdee/toxygen_wrapper but the tox.py
needs each call double checking.
2. https://git.macaw.me/emdee/toxygen_wrapper needs packaging
2. https://git.plastiras.org/emdee/toxygen_wrapper needs packaging
and making a dependency.

6
requirements.txt Normal file
View file

@ -0,0 +1,6 @@
PyQt5
PyAudio
numpy
opencv-python
pydenticon
cv2

View file

@ -71,7 +71,7 @@ setup(name='Toxygen',
version=version,
description='Toxygen - Tox client',
long_description='Toxygen is powerful Tox client written in Python3',
url='https://git.macaw.me/emdee/toxygen/',
url='https://git.plastiras.org/emdee/toxygen/',
keywords='toxygen Tox messenger',
author='Ingvar',
maintainer='',

View file

@ -4,7 +4,7 @@ import sys
import traceback
from random import shuffle
import threading
from time import sleep
from time import sleep, time
from gevent import monkey; monkey.patch_all(); del monkey # noqa
import gevent
@ -153,7 +153,7 @@ class App:
def __init__(self, version, oArgs):
global LOG
self._args = oArgs
self._oArgs = oArgs
self.oArgs = oArgs
self._path = path_to_profile = oArgs.profile
uri = oArgs.uri
logfile = oArgs.logfile
@ -220,11 +220,11 @@ class App:
# this throws everything as errors
if not self._select_and_load_profile():
return 2
if hasattr(self._oArgs, 'update') and self._oArgs.update:
if hasattr(self._args, 'update') and self._args.update:
if self._try_to_update(): return 3
self._load_app_styles()
if self._oArgs.language != 'English':
if self._args.language != 'English':
# > /var/local/src/toxygen/toxygen/app.py(303)_load_app_translations()->None
# -> self._app.translator = translator
# (Pdb) Fatal Python error: Segmentation fault
@ -285,7 +285,7 @@ class App:
self._app.quit()
del self._app.quit
del self._app
sys.stderr.write('quit raising SystemExit' +'\n')
# hanging on gevents
# Thread 1 "python3.9" received signal SIGSEGV, Segmentation fault.
@ -309,13 +309,13 @@ class App:
if hasattr(self, '_tray') and self._tray:
self._tray.hide()
self._settings.close()
LOG.debug(f"stop_app: Killing {self._tox}")
self._kill_toxav()
self._kill_tox()
del self._tox
oArgs = self._oArgs
oArgs = self._args
if hasattr(oArgs, 'log_oFd'):
LOG.debug(f"Closing {oArgs.log_oFd}")
oArgs.log_oFd.close()
@ -326,20 +326,20 @@ class App:
# -----------------------------------------------------------------------------------------------------------------
def _load_base_style(self):
if self._oArgs.theme in ['', 'default']: return
if self._args.theme in ['', 'default']: return
if qdarkstyle:
LOG.debug("_load_base_style qdarkstyle " +self._oArgs.theme)
LOG.debug("_load_base_style qdarkstyle " +self._args.theme)
# QDarkStyleSheet
if self._oArgs.theme == 'light':
if self._args.theme == 'light':
from qdarkstyle.light.palette import LightPalette
style = qdarkstyle.load_stylesheet(palette=LightPalette)
else:
from qdarkstyle.dark.palette import DarkPalette
style = qdarkstyle.load_stylesheet(palette=DarkPalette)
else:
LOG.debug("_load_base_style qss " +self._oArgs.theme)
name = self._oArgs.theme + '.qss'
LOG.debug("_load_base_style qss " +self._args.theme)
name = self._args.theme + '.qss'
with open(util.join_path(util.get_styles_directory(), name)) as fl:
style = fl.read()
style += '\n' +sSTYLE
@ -353,9 +353,9 @@ class App:
if self._settings['theme'] != theme:
continue
if qdarkstyle:
LOG.debug("_load_base_style qdarkstyle " +self._oArgs.theme)
LOG.debug("_load_base_style qdarkstyle " +self._args.theme)
# QDarkStyleSheet
if self._oArgs.theme == 'light':
if self._args.theme == 'light':
from qdarkstyle.light.palette import LightPalette
style = qdarkstyle.load_stylesheet(palette=LightPalette)
else:
@ -372,7 +372,7 @@ class App:
LOG.debug('_load_app_styles: loading theme file ' + file_path)
style += '\n' +sSTYLE
self._app.setStyleSheet(style)
LOG.info('_load_app_styles: loaded theme ' +self._oArgs.theme)
LOG.info('_load_app_styles: loaded theme ' +self._args.theme)
break
def _load_login_screen_translations(self):
@ -487,7 +487,7 @@ class App:
LOG.debug(f"_start_threads init: {te()!r}")
# starting threads for tox iterate and toxav iterate
self._main_loop = threads.ToxIterateThread(self._tox)
self._main_loop = threads.ToxIterateThread(self._tox, app=self)
self._main_loop.start()
self._av_loop = threads.ToxAVIterateThread(self._tox.AV)
@ -519,7 +519,7 @@ class App:
def _select_profile(self):
LOG.debug("_select_profile")
if self._oArgs.language != 'English':
if self._args.language != 'English':
self._load_login_screen_translations()
ls = LoginScreen()
profiles = ProfileManager.find_profiles()
@ -558,7 +558,7 @@ class App:
util_ui.tr('Error'))
return False
name = profile_name or 'toxygen_user'
assert self._oArgs
assert self._args
self._path = profile_path
if result.password:
self._toxes.set_password(result.password)
@ -660,7 +660,7 @@ class App:
def _create_dependencies(self):
LOG.info(f"_create_dependencies toxygen version {self._version}")
if hasattr(self._oArgs, 'update') and self._oArgs.update:
if hasattr(self._args, 'update') and self._args.update:
self._backup_service = BackupService(self._settings,
self._profile_manager)
self._smiley_loader = SmileyLoader(self._settings)
@ -772,13 +772,13 @@ class App:
self._ms.show()
# FixMe:
self._log = lambda line: LOG.log(self._oArgs.loglevel,
self._log = lambda line: LOG.log(self._args.loglevel,
self._ms.status(line))
# self._ms._log = self._log # was used in callbacks.py
if False:
self.status_handler = logging.Handler()
self.status_handler.setLevel(logging.INFO) # self._oArgs.loglevel
self.status_handler.setLevel(logging.INFO) # self._args.loglevel
self.status_handler.handle = self._ms.status
self._init_callbacks()
@ -797,9 +797,9 @@ class App:
def _create_tox(self, data, settings_):
LOG.info("_create_tox calling tox_factory")
assert self._oArgs
assert self._args
retval = tox_factory(data=data, settings=settings_,
args=self._oArgs, app=self)
args=self._args, app=self)
LOG.debug("_create_tox succeeded")
self._tox = retval
return retval
@ -845,22 +845,21 @@ class App:
sleep(interval / 1000.0)
def _test_tox(self):
self.test_net()
self.test_net(iMax=8)
self._ms.log_console()
def test_net(self, lElts=None, oThread=None, iMax=4):
LOG.debug("test_net " +self._oArgs.network)
# bootstrap
LOG.debug('Calling generate_nodes: udp')
lNodes = ts.generate_nodes(oArgs=self._oArgs,
LOG.debug('test_net: Calling generate_nodes: udp')
lNodes = ts.generate_nodes(oArgs=self._args,
ipv='ipv4',
udp_not_tcp=True)
self._settings['current_nodes_udp'] = lNodes
if not lNodes:
LOG.warn('empty generate_nodes udp')
LOG.debug('Calling generate_nodes: tcp')
lNodes = ts.generate_nodes(oArgs=self._oArgs,
LOG.debug('test_net: Calling generate_nodes: tcp')
lNodes = ts.generate_nodes(oArgs=self._args,
ipv='ipv4',
udp_not_tcp=False)
self._settings['current_nodes_tcp'] = lNodes
@ -868,8 +867,8 @@ class App:
LOG.warn('empty generate_nodes tcp')
# if oThread and oThread._stop_thread: return
LOG.debug("test_net network=" +self._oArgs.network +' iMax=' +str(iMax))
if self._oArgs.network not in ['local', 'localnew', 'newlocal']:
LOG.debug("test_net network=" +self._args.network +' iMax=' +str(iMax))
if self._args.network not in ['local', 'localnew', 'newlocal']:
b = ts.bAreWeConnected()
if b is None:
i = os.system('ip route|grep ^def')
@ -878,38 +877,39 @@ class App:
else:
b = True
if not b:
LOG.warn("No default route for network " +self._oArgs.network)
LOG.warn("No default route for network " +self._args.network)
text = 'You have no default route - are you connected?'
reply = util_ui.question(text, "Are you connected?")
if not reply: return
iMax = 1
else:
LOG.debug("Have default route for network " +self._oArgs.network)
LOG.debug("Have default route for network " +self._args.network)
lUdpElts = self._settings['current_nodes_udp']
if self._oArgs.proxy_type <= 0 and not lUdpElts:
if self._args.proxy_type <= 0 and not lUdpElts:
title = 'test_net Error'
text = 'Error: ' + str('No UDP nodes')
util_ui.message_box(text, title)
return
lTcpElts = self._settings['current_nodes_tcp']
if self._oArgs.proxy_type > 0 and not lTcpElts:
if self._args.proxy_type > 0 and not lTcpElts:
title = 'test_net Error'
text = 'Error: ' + str('No TCP nodes')
util_ui.message_box(text, title)
return
LOG.debug(f"test_net {self._oArgs.network} lenU={len(lUdpElts)} lenT={len(lTcpElts)} iMax= {iMax}")
LOG.debug(f"test_net {self._args.network} lenU={len(lUdpElts)} lenT={len(lTcpElts)} iMax={iMax}")
i = 0
while i < iMax:
# if oThread and oThread._stop_thread: return
i = i + 1
LOG.debug(f"bootstrapping status # {i}")
self._test_bootstrap(lUdpElts)
if hasattr(self._oArgs, 'proxy_type') and self._oArgs.proxy_type > 0:
LOG.debug(f"bootstrapping status proxy={self._args.proxy_type} # {i}")
if self._args.proxy_type == 0:
self._test_bootstrap(lUdpElts)
else:
self._test_bootstrap([lUdpElts[0]])
LOG.debug(f"relaying status # {i}")
self._test_relays(self._settings['current_nodes_tcp'])
status = self._tox.self_get_connection_status()
LOG.debug(f"connecting status # {i}" +' : ' +repr(status))
if status > 0:
LOG.info(f"Connected # {i}" +' : ' +repr(status))
break
@ -956,8 +956,8 @@ class App:
LOG.debug(f"_test_relays {len(lElts)}")
ts.bootstrap_tcp(lElts[:iNODES], [self._tox])
def _test_socks(self, lElts=None):
LOG.debug("_test_socks")
def _test_nmap(self, lElts=None):
LOG.debug("_test_nmap")
if not self._tox: return
title = 'Extended Test Suite'
text = 'Run the Extended Test Suite?\nThe program may freeze for 1-10 minutes.'
@ -968,14 +968,19 @@ class App:
if not reply: return
if lElts is None:
lElts = self._settings['current_nodes_tcp']
if self._args.proxy_type == 0:
sProt = "udp4"
lElts = self._settings['current_nodes_tcp']
else:
sProt = "tcp4"
lElts = self._settings['current_nodes_tcp']
shuffle(lElts)
try:
bootstrap_iNodeInfo(lElts)
ts.bootstrap_iNmapInfo(lElts, self._args, sProt)
self._ms.log_console()
except Exception as e:
# json.decoder.JSONDecodeError
LOG.error(f"test_tox ' +' : {e}")
LOG.error('_test_tox(): ' \
LOG.error(f"test_nmap ' +' : {e}")
LOG.error('_test_nmap(): ' \
+'\n' + traceback.format_exc())
title = 'Test Suite Error'
text = 'Error: ' + str(e)
@ -986,16 +991,16 @@ class App:
def _test_main(self):
from tests.tests_socks import main as tests_main
LOG.debug("_test_socks")
LOG.debug("_test_main")
if not self._tox: return
title = 'Extended Test Suite'
text = 'Run the Extended Test Suite?\nThe program may freeze for 20-60 minutes.'
reply = util_ui.question(text, title)
if reply:
if hasattr(self._oArgs, 'proxy_type') and self._oArgs.proxy_type:
lArgs = ['--proxy_host', self._oArgs.proxy_host,
'--proxy_port', str(self._oArgs.proxy_port),
'--proxy_type', str(self._oArgs.proxy_type), ]
if hasattr(self._args, 'proxy_type') and self._args.proxy_type:
lArgs = ['--proxy_host', self._args.proxy_host,
'--proxy_port', str(self._args.proxy_port),
'--proxy_type', str(self._args.proxy_type), ]
else:
lArgs = list()
try:

View file

@ -299,6 +299,7 @@ class AV(common.tox_save.ToxAvSave):
self._video_width = s['video']['width']
self._video_height = s['video']['height']
# dunno
if True or s['video']['device'] == -1:
self._video = screen_sharing.DesktopGrabber(s['video']['x'],
s['video']['y'],
@ -404,6 +405,7 @@ class AV(common.tox_save.ToxAvSave):
if self._calls[friend_num].out_audio:
try:
# app.av.calls ERROR Error send_audio: One of the frame parameters was invalid. E.g. the resolution may be too small or too large, or the audio sampling rate may be unsupported
# app.av.calls ERROR Error send_audio audio_send_frame: This client is currently not in a call with the friend.
self._toxav.audio_send_frame(friend_num,
pcm,
count,
@ -412,9 +414,9 @@ class AV(common.tox_save.ToxAvSave):
except Exception as e:
LOG.error(f"Error send_audio audio_send_frame: {e}")
LOG.debug(f"send_audio self._audio_rate_tox={self._audio_rate_tox} self._audio_channels={self._audio_channels}")
invoke_in_main_thread(util_ui.message_box,
str(e),
util_ui.tr("Error send_audio audio_send_frame"))
# invoke_in_main_thread(util_ui.message_box,
# str(e),
# util_ui.tr("Error send_audio audio_send_frame"))
pass
def send_audio(self):
@ -432,9 +434,10 @@ class AV(common.tox_save.ToxAvSave):
else:
self.send_audio_data(pcm, count)
except:
pass
LOG_DEBUG(f"error send_audio {i}")
else:
LOG_TRACE(f"send_audio {i}")
i += 1
LOG.debug(f"send_audio {i}")
sleep(0.01)
def send_video(self):
@ -454,7 +457,7 @@ class AV(common.tox_save.ToxAvSave):
LOG.warn(f"send_video video_send_frame _video.read result={result} frame={frame}")
continue
else:
LOG.debug(f"send_video video_send_frame _video.read result={result}")
LOG_TRACE(f"send_video video_send_frame _video.read result={result}")
height, width, channels = frame.shape
friends = []
for friend_num in self._calls:
@ -463,7 +466,7 @@ class AV(common.tox_save.ToxAvSave):
if len(friends) == 0:
LOG.warn(f"send_video video_send_frame no friends")
else:
LOG.debug(f"send_video video_send_frame {friends}")
LOG_TRACE(f"send_video video_send_frame {friends}")
friend_num = friends[0]
try:
y, u, v = self.convert_bgr_to_yuv(frame)

View file

@ -23,7 +23,7 @@ def download_nodes_list(settings, oArgs):
if not settings['download_nodes_list']:
return ''
if not ts.bAreWeConnected():
return ''
return ''
url = settings['download_nodes_url']
path = _get_nodes_path(oArgs=oArgs)
# dont download blindly so we can edit the file and not block on startup

View file

@ -1,6 +1,6 @@
from pydenticon import Generator
import hashlib
from pydenticon import Generator
# -----------------------------------------------------------------------------------------------------------------
# Typing notifications

View file

@ -136,8 +136,10 @@ class Contact(basecontact.BaseContact):
"""
:return list of unsent messages for saving
"""
messages = filter(lambda m: m.type in (MESSAGE_TYPE['TEXT'], MESSAGE_TYPE['ACTION'])
and m.author.type == MESSAGE_AUTHOR['NOT_SENT'], self._corr)
message = list(filter(lambda m: m.author is not None
and m.author.type == MESSAGE_AUTHOR['NOT_SENT']
and m.tox_message_id == tox_message_id,
self._corr))[0]
return list(messages)
def mark_as_sent(self, tox_message_id):
@ -146,6 +148,7 @@ class Contact(basecontact.BaseContact):
and m.tox_message_id == tox_message_id, self._corr))[0]
message.mark_as_sent()
except Exception as ex:
# wrapped C/C++ object of type QLabel has been deleted
LOG.error(f"Mark as sent: {ex!s}")
# -----------------------------------------------------------------------------------------------------------------

View file

@ -6,6 +6,18 @@ global LOG
import logging
LOG = logging.getLogger(__name__)
# callbacks can be called in any thread so were being careful
def LOG_ERROR(l): print('EROR< '+l)
def LOG_WARN(l): print('WARN< '+l)
def LOG_INFO(l):
bIsVerbose = hasattr(__builtins__, 'app') and app.oArgs.loglevel <= 20-1
if bIsVerbose: print('INFO< '+l)
def LOG_DEBUG(l):
bIsVerbose = hasattr(__builtins__, 'app') and app.oArgs.loglevel <= 10-1
if bIsVerbose: print('DBUG< '+l)
def LOG_TRACE(l):
bIsVerbose = hasattr(__builtins__, 'app') and app.oArgs.loglevel < 10-1
pass # print('TRACE+ '+l)
class ContactProvider(tox_save.ToxSave):
@ -24,6 +36,7 @@ class ContactProvider(tox_save.ToxSave):
try:
public_key = self._tox.friend_get_public_key(friend_number)
except Exception as e:
LOG_WARN(f"get_friend_by_number NO {friend_number} {e} ")
return None
return self.get_friend_by_public_key(public_key)
@ -33,6 +46,7 @@ class ContactProvider(tox_save.ToxSave):
return friend
friend = self._friend_factory.create_friend_by_public_key(public_key)
self._add_to_cache(public_key, friend)
LOG_INFO(f"get_friend_by_public_key ADDED {friend} ")
return friend
@ -40,6 +54,7 @@ class ContactProvider(tox_save.ToxSave):
try:
friend_numbers = self._tox.self_get_friend_list()
except Exception as e:
LOG_WARN(f"get_all_friends NO {friend_numbers} {e} ")
return None
friends = map(lambda n: self.get_friend_by_number(n), friend_numbers)
@ -50,38 +65,69 @@ class ContactProvider(tox_save.ToxSave):
# -----------------------------------------------------------------------------------------------------------------
def get_all_groups(self):
"""from callbacks"""
try:
group_numbers = range(self._tox.group_get_number_groups())
len_groups = self._tox.group_get_number_groups()
group_numbers = range(len_groups)
except Exception as e:
return None
groups = map(lambda n: self.get_group_by_number(n), group_numbers)
return list(groups)
groups = list(map(lambda n: self.get_group_by_number(n), group_numbers))
# failsafe in case there are bogus None groups?
fgroups = list(filter(lambda x: x, groups))
if len(fgroups) != len_groups:
LOG_WARN(f"are there are bogus None groups in libtoxcore? {len(fgroups)} != {len_groups}")
for group_num in group_numbers:
group = self.get_group_by_number(group_num)
if group is None:
LOG_ERROR(f"there are bogus None groups in libtoxcore {group_num}!")
# fixme: do something
groups = fgroups
return groups
def get_group_by_number(self, group_number):
group = None
try:
if True:
# original code
public_key = self._tox.group_get_chat_id(group_number)
# LOG.info(f"group_get_chat_id {group_number} {public_key}")
return self.get_group_by_public_key(public_key)
LOG_INFO(f"CP.group_get_number {group_number} ")
# original code
chat_id = self._tox.group_get_chat_id(group_number)
if chat_id is None:
LOG_ERROR(f"get_group_by_number NULL chat_id ({group_number})")
elif chat_id == '-1':
LOG_ERROR(f"get_group_by_number <0 chat_id ({group_number})")
else:
# guessing
chat_id = self._tox.group_get_chat_id(group_number)
# LOG.info(f"group_get_chat_id {group_number} {chat_id}")
group = self.get_contact_by_tox_id(chat_id)
return group
LOG_INFO(f"group_get_number {group_number} {chat_id}")
group = self.get_group_by_chat_id(chat_id)
if group is None or group == '-1':
LOG_WARN(f"get_group_by_number leaving {group} ({group_number})")
#? iRet = self._tox.group_leave(group_number)
# invoke in main thread?
# self._contacts_manager.delete_group(group_number)
return group
except Exception as e:
LOG.warn(f"group_get_chat_id {group_number} {e}")
LOG_WARN(f"group_get_number {group_number} {e}")
return None
def get_group_by_chat_id(self, chat_id):
group = self._get_contact_from_cache(chat_id)
if group is not None:
return group
group = self._group_factory.create_group_by_chat_id(chat_id)
if group is None:
LOG_ERROR(f"get_group_by_chat_id NULL chat_id={chat_id}")
else:
self._add_to_cache(chat_id, group)
return group
def get_group_by_public_key(self, public_key):
group = self._get_contact_from_cache(public_key)
if group is not None:
return group
group = self._group_factory.create_group_by_public_key(public_key)
self._add_to_cache(public_key, group)
if group is None:
LOG_ERROR(f"get_group_by_public_key NULL group public_key={get_group_by_chat_id}")
else:
self._add_to_cache(public_key, group)
return group

View file

@ -54,7 +54,8 @@ class ContactsManager(ToxSave):
self._tox_dns = tox_dns
self._messages_items_factory = messages_items_factory
self._messages = screen.messages
self._contacts, self._active_contact = [], -1
self._contacts = []
self._active_contact = -1
self._active_contact_changed = Event()
self._sorting = settings['sorting']
self._filter_string = ''
@ -92,17 +93,16 @@ class ContactsManager(ToxSave):
return self.get_curr_contact().number == group_number
def is_contact_active(self, contact):
if not self._active_contact:
if self._active_contact == -1:
# LOG.debug("No self._active_contact")
return False
if self._active_contact not in self._contacts:
LOG.warn(f"_active_contact={self._active_contact} not in contacts len={len(self._contacts)}")
if self._active_contact >= len(self._contacts):
LOG.warn(f"ERROR _active_contact={self._active_contact} >= contacts len={len(self._contacts)}")
return False
if not self._contacts[self._active_contact]:
LOG.debug(f"{self._contacts[self._active_contact]} {contact.tox_id}")
LOG.warn(f"ERROR NULL {self._contacts[self._active_contact]} {contact.tox_id}")
return False
LOG.debug(f"{self._contacts[self._active_contact].tox_id} == {contact.tox_id}")
return self._contacts[self._active_contact].tox_id == contact.tox_id
# -----------------------------------------------------------------------------------------------------------------
@ -145,7 +145,7 @@ class ContactsManager(ToxSave):
current_contact.remove_messages_widgets() # TODO: if required
self._unsubscribe_from_events(current_contact)
if self._active_contact + 1 and self._active_contact != value:
if self._active_contact >= 0 and self._active_contact != value:
try:
current_contact.curr_text = self._screen.messageEdit.toPlainText()
except:
@ -180,7 +180,7 @@ class ContactsManager(ToxSave):
self._set_current_contact_data(contact)
self._active_contact_changed(contact)
except Exception as ex: # no friend found. ignore
LOG.warn(f"no friend found. Friend value: {value!s}")
LOG.warn(f"no friend found. Friend value: {value!s}")
LOG.error('in set active: ' + str(ex))
# gulp raise
@ -368,7 +368,10 @@ class ContactsManager(ToxSave):
"""
friend = self._contacts[num]
self._cleanup_contact_data(friend)
self._tox.friend_delete(friend.number)
try:
self._tox.friend_delete(friend.number)
except Exception as e:
LOG.warn(f"'There was no friend with the given friend number {e}")
self._delete_contact(num)
def add_friend(self, tox_id):
@ -418,8 +421,10 @@ class ContactsManager(ToxSave):
def add_group(self, group_number):
index = len(self._contacts)
group = self._contact_provider.get_group_by_number(group_number)
if not group:
LOG.warn(f"CM.add_group: NO group {group_number}")
if group is None:
LOG.warn(f"CM.add_group: NULL group from group_number={group_number}")
elif 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}")
self._contacts.append(group)
@ -517,7 +522,8 @@ class ContactsManager(ToxSave):
title = 'Friend add exception'
text = 'Friend request exception with ' + str(ex)
self._log(text)
LOG.error(traceback.format_exc())
LOG.exception(text)
LOG.warn(f"DELETE {sToxPkOrId} ?")
retval = str(ex)
title = util_ui.tr(title)
text = util_ui.tr(text)
@ -586,9 +592,11 @@ class ContactsManager(ToxSave):
self.set_active(0)
# filter(lambda c: not c.has_avatar(), self._contacts)
for (i, contact) in enumerate(self._contacts):
if not contact:
LOG.warn("_load_contacts NULL contact {i}")
if contact is None:
LOG.warn(f"_load_contacts NULL contact {i}")
LOG.info(f"_load_contacts deleting NULL {self._contacts[i]}")
del self._contacts[i]
#? self.save_profile()
continue
if contact.has_avatar(): continue
contact.reset_avatar(self._settings['identicons'])

View file

@ -1,5 +1,5 @@
from contacts.friend import Friend
from common.tox_save import ToxSave
from contacts.friend import Friend
class FriendFactory(ToxSave):

View file

@ -17,9 +17,11 @@ class GroupFactory(ToxSave):
self._db = db
self._items_factory = items_factory
def create_group_by_chat_id(self, chat_id):
return self.create_group_by_public_key(chat_id)
def create_group_by_public_key(self, public_key):
group_number = self._get_group_number_by_chat_id(public_key)
return self.create_group_by_number(group_number)
def create_group_by_number(self, group_number):

View file

@ -1,11 +1,11 @@
from wrapper.toxcore_enums_and_consts import TOX_FILE_KIND, TOX_FILE_CONTROL
from os.path import basename, getsize, exists, dirname
from os import remove, rename, chdir
from os import chdir, remove, rename
from os.path import basename, dirname, exists, getsize
from time import time
from wrapper.tox import Tox
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
FILE_TRANSFER_STATE = {
'RUNNING': 0,

View file

@ -255,7 +255,7 @@ class GroupsService(tox_save.ToxSave):
# -----------------------------------------------------------------------------------------------------------------
def _add_new_group_by_number(self, group_number):
LOG.debug(f"_add_new_group_by_number {group_number}")
LOG.debug(f"_add_new_group_by_number group_number={group_number}")
self._contacts_manager.add_group(group_number)
def _get_group_by_number(self, group_number):
@ -281,14 +281,16 @@ class GroupsService(tox_save.ToxSave):
if invite in self._group_invites:
self._group_invites.remove(invite)
def _join_gc_via_invite(self, invite_data, friend_number, nick, status, password):
# status should be dropped
def _join_gc_via_invite(self, invite_data, friend_number, nick, status='', password=''):
LOG.debug(f"_join_gc_via_invite friend_number={friend_number} nick={nick} datalen={len(invite_data)}")
if nick is None:
nick = ''
if invite_data is None:
invite_data = b''
try:
group_number = self._tox.group_invite_accept(invite_data, friend_number, nick, status, password)
# status should be dropped
group_number = self._tox.group_invite_accept(invite_data, friend_number, nick, password=password)
except Exception as e:
LOG.error(f"_join_gc_via_invite ERROR {e}")
return

View file

@ -66,7 +66,7 @@ class History:
with open(file_name, 'wt') as fl:
fl.write(history)
LOG.info(f"wrote history to {file_name}")
def delete_message(self, message):
contact = self._contacts_manager.get_curr_contact()
if message.type in (MESSAGE_TYPE['TEXT'], MESSAGE_TYPE['ACTION']):

View file

@ -1,5 +1,5 @@
from messenger.messages import *
import utils.util as util
from messenger.messages import *
class HistoryLogsGenerator:

View file

@ -171,7 +171,7 @@ def setup_default_video():
video['output_devices'] = default_video
return video
def main_parser():
def main_parser(_=None, iMode=2):
import cv2
if not os.path.exists('/proc/sys/net/ipv6'):
bIpV6 = 'False'
@ -182,32 +182,17 @@ def main_parser():
audio = setup_default_audio()
default_video = setup_default_video()
logfile = os.path.join(os.environ.get('TMPDIR', '/tmp'), 'toxygen.log')
parser = argparse.ArgumentParser()
# parser = argparse.ArgumentParser()
parser = ts.oMainArgparser()
parser.add_argument('--version', action='store_true', help='Prints Toxygen version')
parser.add_argument('--clean', action='store_true', help='Delete toxcore libs from libs folder')
parser.add_argument('--reset', action='store_true', help='Reset default profile')
parser.add_argument('--uri', type=str, default='',
help='Add specified Tox ID to friends')
parser.add_argument('--logfile', default=logfile,
help='Filename for logging')
parser.add_argument('--loglevel', type=int, default=logging.INFO,
help='Threshold for logging (lower is more) default: 20')
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=https, 2=socks')
parser.add_argument('--tcp_port', '--tcp-port', default=0, type=int,
help='tcp port')
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=2,
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')
@ -216,15 +201,6 @@ def main_parser():
parser.add_argument('--local_discovery_enabled',type=str,
default='False', choices=['True','False'],
help='Look on the local lan')
parser.add_argument('--udp_enabled',type=str,
default='True', choices=['True','False'],
help='En/Disable udp')
parser.add_argument('--trace_enabled',type=str,
default='False', choices=['True','False'],
help='Debugging from toxcore logger_trace')
parser.add_argument('--ipv6_enabled',type=str,
default=bIpV6, choices=lIpV6Choices,
help='En/Disable ipv6')
parser.add_argument('--compact_mode',type=str,
default='True', choices=['True','False'],
help='Compact mode')
@ -243,28 +219,12 @@ def main_parser():
parser.add_argument('--core_logging',type=str,
default='False', choices=['True','False'],
help='Dis/Enable Toxcore notifications')
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')
parser.add_argument('--save_history',type=str,
default='True', choices=['True','False'],
help='En/Disable save history')
parser.add_argument('--update', type=int, default=0,
choices=[0,0],
help='Update program (broken)')
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='')
parser.add_argument('--download_nodes_url', type=str,
default='https://nodes.tox.chat/json')
parser.add_argument('--network', type=str,
choices=['old', 'main', 'new', 'local', 'newlocal'],
default='old')
parser.add_argument('--video_input', type=str,
default=-1,
choices=default_video['output_devices'],
@ -353,14 +313,7 @@ def main(lArgs):
if getattr(default_ns, key) == getattr(oArgs, key):
delattr(oArgs, key)
for key in ts.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)
ts.clean_booleans(oArgs)
aArgs = A()
for key in oArgs.__dict__.keys():

View file

@ -1,7 +1,7 @@
from history.database import MESSAGE_AUTHOR
import os.path
from ui.messages_widgets import *
from history.database import MESSAGE_AUTHOR
from ui.messages_widgets import *
MESSAGE_TYPE = {
'TEXT': 0,

View file

@ -30,7 +30,7 @@ class Messenger(tox_save.ToxSave):
def __repr__(self):
return "<Messenger>"
def get_last_message(self):
contact = self._contacts_manager.get_curr_contact()
if contact is None:
@ -89,7 +89,7 @@ class Messenger(tox_save.ToxSave):
text = 'Error: ' + str(e)
assert_main_thread()
util_ui.message_box(text, title)
def send_message_to_friend(self, text, message_type, friend_number=None):
"""
Send message
@ -200,7 +200,7 @@ class Messenger(tox_save.ToxSave):
return
if peer_id and peer_id < 0:
return
assert_main_thread()
# FixMe: peer_id is None?
group_peer_contact = self._contacts_manager.get_or_create_group_peer_contact(group_number, peer_id)
@ -353,11 +353,12 @@ class Messenger(tox_save.ToxSave):
LOG.warn("_add_message null contact")
return
if self._contacts_manager.is_contact_active(contact): # add message to list
# LOG.debug("_add_message is_contact_active(contact)")
self._create_message_item(text_message)
self._screen.messages.scrollToBottom()
self._contacts_manager.get_curr_contact().append_message(text_message)
else:
LOG.debug("_add_message not is_contact_active(contact)")
# LOG.debug("_add_message not is_contact_active(contact)")
contact.inc_messages()
contact.append_message(text_message)
if not contact.visibility:

View file

@ -529,27 +529,35 @@ def group_invite(window, settings, tray, profile, groups_service, contacts_provi
def group_self_join(contacts_provider, contacts_manager, groups_service):
sSlot = 'group_self_join'
def wrapped(tox, group_number, user_data):
if group_number is None:
LOG_ERROR(f"group_self_join NULL group_number #{group_number}")
return
LOG_DEBUG(f"group_self_join #{group_number}")
key = f"group_number {group_number}"
if bTooSoon(key, sSlot, 10): return
LOG_DEBUG(f"group_self_join #{group_number}")
group = contacts_provider.get_group_by_number(group_number)
if group is None:
LOG_ERROR(f"group_self_join NULL group #{group}")
return
invoke_in_main_thread(group.set_status, TOX_USER_STATUS['NONE'])
invoke_in_main_thread(groups_service.update_group_info, group)
invoke_in_main_thread(contacts_manager.update_filtration)
return wrapped
def group_peer_join(contacts_provider, groups_service):
sSlot = "group_peer_join"
def wrapped(tox, group_number, peer_id, user_data):
key = f"group_peer_join #{group_number} peer_id={peer_id}"
if bTooSoon(key, sSlot, 20): return
group = contacts_provider.get_group_by_number(group_number)
if group is None:
LOG_ERROR(f"group_peer_join NULL group #{group} group_number={group_number}")
return
if peer_id > group._peers_limit:
LOG_ERROR(key +f" {peer_id} > {group._peers_limit}")
return
LOG_DEBUG(key)
LOG_DEBUG(f"group_peer_join group={group}")
group.add_peer(peer_id)
invoke_in_main_thread(groups_service.generate_peers_list)
invoke_in_main_thread(groups_service.update_group_info, group)

View file

@ -122,7 +122,7 @@ class ToxIterateThread(BaseQThread):
super().__init__()
self._tox = tox
self._app = app
def run(self):
LOG_DEBUG('ToxIterateThread run: ')
while not self._stop_thread:
@ -134,15 +134,14 @@ class ToxIterateThread(BaseQThread):
LOG_ERROR(f"ToxIterateThread run: {e}")
else:
sleep(iMsec / 1000.0)
global iLAST_CONN
if not iLAST_CONN:
iLAST_CONN = time.time()
# TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
# and segv
if \
time.time() - iLAST_CONN > iLAST_DELTA and \
if time.time() - iLAST_CONN > iLAST_DELTA and \
ts.bAreWeConnected() and \
self._tox.self_get_status() == TOX_USER_STATUS['NONE'] and \
self._tox.self_get_connection_status() == TOX_CONNECTION['NONE']:
@ -150,7 +149,7 @@ class ToxIterateThread(BaseQThread):
LOG_INFO(f"ToxIterateThread calling test_net")
invoke_in_main_thread(
self._app.test_net, oThread=self, iMax=2)
class ToxAVIterateThread(BaseQThread):
def __init__(self, toxav):

View file

@ -49,12 +49,12 @@ def tox_log_cb(iTox, level, file, line, func, message, *args):
except Exception as e:
LOG_ERROR(f"tox_log_cb {e}")
#tox_log_handler (context=0x24763d0,
# level=LOGGER_LEVEL_TRACE, file=0x7fffe599fb99 "TCP_common.c", line=203,
# func=0x7fffe599fc50 <__func__.2> "read_TCP_packet",
# message=0x7fffba7fabd0 "recv buffer has 0 bytes, but requested 10 bytes",
#tox_log_handler (context=0x24763d0,
# level=LOGGER_LEVEL_TRACE, file=0x7fffe599fb99 "TCP_common.c", line=203,
# func=0x7fffe599fc50 <__func__.2> "read_TCP_packet",
# message=0x7fffba7fabd0 "recv buffer has 0 bytes, but requested 10 bytes",
# userdata=0x0) at /var/local/src/c-toxcore/toxcore/tox.c:78
def tox_factory(data=None, settings=None, args=None, app=None):
"""
:param data: user data from .tox file. None = no saved data, create new profile

View file

@ -88,9 +88,9 @@ class PluginLoader:
if is_active:
try:
instance.start()
self._app.LOG('INFO: Started Plugin ' +short_name)
self._app._log('INFO: Started Plugin ' +short_name)
except Exception as e:
self._app.LOG.error(f"Starting Plugin ' +short_name +' {e}")
self._app._log.error(f"Starting Plugin ' +short_name +' {e}")
# else: LOG.info('Defined Plugin ' +short_name)
except Exception as ex:
LOG.error('in module ' + short_name + ' Exception: ' + str(ex))
@ -150,7 +150,7 @@ class PluginLoader:
if key in self._plugins and hasattr(self._plugins[key], 'instance'):
return self._plugins[key].instance.get_window()
except Exception as e:
self._app.LOG('WARN: ' +key +' _plugins no slot instance: ' +str(e))
self._app._log('WARN: ' +key +' _plugins no slot instance: ' +str(e))
return None
@ -202,7 +202,7 @@ class PluginLoader:
continue
if not hasattr(plugin.instance, 'get_message_menu'):
name = plugin.instance.get_short_name()
self._app.LOG('WARN: get_message_menu not found: ' + name)
self._app._log('WARN: get_message_menu not found: ' + name)
continue
try:
result.extend(plugin.instance.get_message_menu(menu, selected_text))
@ -222,9 +222,9 @@ class PluginLoader:
def reload(self):
path = util.get_plugins_directory()
if not os.path.exists(path):
self._app.LOG('WARN: Plugin directory not found: ' + path)
self._app._log('WARN: Plugin directory not found: ' + path)
return
self.stop()
self._app.LOG('INFO: Reloading plugins from ' +path)
self._app._log('INFO: Reloading plugins from ' +path)
self.load()

View file

@ -1,3 +1,4 @@
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
import os
from PyQt5 import QtCore, QtWidgets
import utils.ui as util_ui
@ -19,7 +20,7 @@ def path_to_data(name):
return os.path.dirname(os.path.realpath(__file__)) + '/' + name + '/'
def log(name, data):
def log(name, data=''):
"""
:param name: plugin unique name
:param data: data for saving in log
@ -47,7 +48,7 @@ class PluginSuperClass(tox_save.ToxSave):
name = name.strip()
short_name = short_name.strip()
if not name or not short_name:
raise NameError('Wrong name')
raise NameError('Wrong name or not name or not short_name')
self._name = name
self._short_name = short_name[:MAX_SHORT_NAME_LENGTH]
self._translator = None # translator for plugin's GUI
@ -74,7 +75,7 @@ class PluginSuperClass(tox_save.ToxSave):
"""
return self.__doc__
def get_menu(self, row_number):
def get_menu(self, menu, row_number=None):
"""
This method creates items for menu which called on right click in list of friends
:param row_number: number of selected row in list of contacts

0
toxygen/third_party/__init__.py vendored Normal file
View file

View file

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2011-2022 Sébastien Helleu <flashcode@flashtux.org>
#
# This file is part of QWeeChat, a Qt remote GUI for WeeChat.
#
# QWeeChat 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.
#
# QWeeChat 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 QWeeChat. If not, see <http://www.gnu.org/licenses/>.
#

61
toxygen/third_party/qweechat/about.py vendored Normal file
View file

@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
#
# about.py - about dialog box
#
# Copyright (C) 2011-2022 Sébastien Helleu <flashcode@flashtux.org>
#
# This file is part of QWeeChat, a Qt remote GUI for WeeChat.
#
# QWeeChat 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.
#
# QWeeChat 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 QWeeChat. If not, see <http://www.gnu.org/licenses/>.
#
"""About dialog box."""
from PyQt5 import QtCore, QtWidgets as QtGui
from third_party.qweechat.version import qweechat_version
class AboutDialog(QtGui.QDialog):
"""About dialog."""
def __init__(self, app_name, author, weechat_site, *args):
QtGui.QDialog.__init__(*(self,) + args)
self.setModal(True)
self.setWindowTitle('About')
close_button = QtGui.QPushButton('Close')
close_button.pressed.connect(self.close)
hbox = QtGui.QHBoxLayout()
hbox.addStretch(1)
hbox.addWidget(close_button)
hbox.addStretch(1)
vbox = QtGui.QVBoxLayout()
messages = [
f'<b>{app_name}</b> {qweechat_version()}',
f'© 2011-2022 {author}',
'',
f'<a href="{weechat_site}">{weechat_site}</a>',
'',
]
for msg in messages:
label = QtGui.QLabel(msg)
label.setAlignment(QtCore.Qt.AlignHCenter)
vbox.addWidget(label)
vbox.addLayout(hbox)
self.setLayout(vbox)
self.show()

250
toxygen/third_party/qweechat/buffer.py vendored Normal file
View file

@ -0,0 +1,250 @@
# -*- coding: utf-8 -*-
#
# buffer.py - management of WeeChat buffers/nicklist
#
# Copyright (C) 2011-2022 Sébastien Helleu <flashcode@flashtux.org>
#
# This file is part of QWeeChat, a Qt remote GUI for WeeChat.
#
# QWeeChat 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.
#
# QWeeChat 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 QWeeChat. If not, see <http://www.gnu.org/licenses/>.
#
"""Management of WeeChat buffers/nicklist."""
from pkg_resources import resource_filename
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import pyqtSignal
Signal = pyqtSignal
from third_party.qweechat.chat import ChatTextEdit
from third_party.qweechat.input import InputLineEdit
from third_party.qweechat.weechat import color
class GenericListWidget(QtWidgets.QListWidget):
"""Generic QListWidget with dynamic size."""
def __init__(self, *args):
super().__init__(*args)
self.setMaximumWidth(100)
self.setTextElideMode(QtCore.Qt.ElideNone)
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setFocusPolicy(QtCore.Qt.NoFocus)
pal = self.palette()
pal.setColor(QtGui.QPalette.Highlight, QtGui.QColor('#ddddff'))
pal.setColor(QtGui.QPalette.HighlightedText, QtGui.QColor('black'))
self.setPalette(pal)
def auto_resize(self):
size = self.sizeHintForColumn(0)
if size > 0:
size += 4
self.setMaximumWidth(size)
def clear(self, *args):
"""Re-implement clear to set dynamic size after clear."""
QtWidgets.QListWidget.clear(*(self,) + args)
self.auto_resize()
def addItem(self, *args):
"""Re-implement addItem to set dynamic size after add."""
QtWidgets.QListWidget.addItem(*(self,) + args)
self.auto_resize()
def insertItem(self, *args):
"""Re-implement insertItem to set dynamic size after insert."""
QtWidgets.QListWidget.insertItem(*(self,) + args)
self.auto_resize()
class BufferListWidget(GenericListWidget):
"""Widget with list of buffers."""
def switch_prev_buffer(self):
if self.currentRow() > 0:
self.setCurrentRow(self.currentRow() - 1)
else:
self.setCurrentRow(self.count() - 1)
def switch_next_buffer(self):
if self.currentRow() < self.count() - 1:
self.setCurrentRow(self.currentRow() + 1)
else:
self.setCurrentRow(0)
class BufferWidget(QtWidgets.QWidget):
"""
Widget with (from top to bottom):
title, chat + nicklist (optional) + prompt/input.
"""
def __init__(self, display_nicklist=False):
super().__init__()
# title
self.title = QtWidgets.QLineEdit()
self.title.setFocusPolicy(QtCore.Qt.NoFocus)
# splitter with chat + nicklist
self.chat_nicklist = QtWidgets.QSplitter()
self.chat_nicklist.setSizePolicy(QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Expanding)
self.chat = ChatTextEdit(debug=False)
self.chat_nicklist.addWidget(self.chat)
self.nicklist = GenericListWidget()
if not display_nicklist:
self.nicklist.setVisible(False)
self.chat_nicklist.addWidget(self.nicklist)
# prompt + input
self.hbox_edit = QtWidgets.QHBoxLayout()
self.hbox_edit.setContentsMargins(0, 0, 0, 0)
self.hbox_edit.setSpacing(0)
self.input = InputLineEdit(self.chat)
self.hbox_edit.addWidget(self.input)
prompt_input = QtWidgets.QWidget()
prompt_input.setLayout(self.hbox_edit)
# vbox with title + chat/nicklist + prompt/input
vbox = QtWidgets.QVBoxLayout()
vbox.setContentsMargins(0, 0, 0, 0)
vbox.setSpacing(0)
vbox.addWidget(self.title)
vbox.addWidget(self.chat_nicklist)
vbox.addWidget(prompt_input)
self.setLayout(vbox)
def set_title(self, title):
"""Set buffer title."""
self.title.clear()
if title is not None:
self.title.setText(title)
def set_prompt(self, prompt):
"""Set prompt."""
if self.hbox_edit.count() > 1:
self.hbox_edit.takeAt(0)
if prompt is not None:
label = QtWidgets.QLabel(prompt)
label.setContentsMargins(0, 0, 5, 0)
self.hbox_edit.insertWidget(0, label)
class Buffer(QtCore.QObject):
"""A WeeChat buffer."""
bufferInput = Signal(str, str)
def __init__(self, data=None):
QtCore.QObject.__init__(self)
self.data = data or {}
self.nicklist = {}
self.widget = BufferWidget(display_nicklist=self.data.get('nicklist',
0))
self.update_title()
self.update_prompt()
self.widget.input.textSent.connect(self.input_text_sent)
def pointer(self):
"""Return pointer on buffer."""
return self.data.get('__path', [''])[0]
def update_title(self):
"""Update title."""
try:
self.widget.set_title(
color.remove(self.data['title']))
except Exception: # noqa: E722
# TODO: Debug print the exception to be fixed.
# traceback.print_exc()
self.widget.set_title(None)
def update_prompt(self):
"""Update prompt."""
try:
self.widget.set_prompt(self.data['local_variables']['nick'])
except Exception: # noqa: E722
self.widget.set_prompt(None)
def input_text_sent(self, text):
"""Called when text has to be sent to buffer."""
if self.data:
self.bufferInput.emit(self.data['full_name'], text)
def nicklist_add_item(self, parent, group, prefix, name, visible):
"""Add a group/nick in nicklist."""
if group:
self.nicklist[name] = {
'visible': visible,
'nicks': []
}
else:
self.nicklist[parent]['nicks'].append({
'prefix': prefix,
'name': name,
'visible': visible,
})
def nicklist_remove_item(self, parent, group, name):
"""Remove a group/nick from nicklist."""
if group:
if name in self.nicklist:
del self.nicklist[name]
else:
if parent in self.nicklist:
self.nicklist[parent]['nicks'] = [
nick for nick in self.nicklist[parent]['nicks']
if nick['name'] != name
]
def nicklist_update_item(self, parent, group, prefix, name, visible):
"""Update a group/nick in nicklist."""
if group:
if name in self.nicklist:
self.nicklist[name]['visible'] = visible
else:
if parent in self.nicklist:
for nick in self.nicklist[parent]['nicks']:
if nick['name'] == name:
nick['prefix'] = prefix
nick['visible'] = visible
break
def nicklist_refresh(self):
"""Refresh nicklist."""
self.widget.nicklist.clear()
for group in sorted(self.nicklist):
for nick in sorted(self.nicklist[group]['nicks'],
key=lambda n: n['name']):
prefix_color = {
'': '',
' ': '',
'+': 'yellow',
}
col = prefix_color.get(nick['prefix'], 'green')
if col:
icon = QtGui.QIcon(
resource_filename(__name__,
'data/icons/bullet_%s_8x8.png' %
col))
else:
pixmap = QtGui.QPixmap(8, 8)
pixmap.fill()
icon = QtGui.QIcon(pixmap)
item = QtWidgets.QListWidgetItem(icon, nick['name'])
self.widget.nicklist.addItem(item)
self.widget.nicklist.setVisible(True)

142
toxygen/third_party/qweechat/chat.py vendored Normal file
View file

@ -0,0 +1,142 @@
# -*- coding: utf-8 -*-
#
# chat.py - chat area
#
# Copyright (C) 2011-2022 Sébastien Helleu <flashcode@flashtux.org>
#
# This file is part of QWeeChat, a Qt remote GUI for WeeChat.
#
# QWeeChat 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.
#
# QWeeChat 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 QWeeChat. If not, see <http://www.gnu.org/licenses/>.
#
"""Chat area."""
import datetime
from PyQt5 import QtCore, QtWidgets, QtGui
from third_party.qweechat import config
from third_party.qweechat.weechat import color
class ChatTextEdit(QtWidgets.QTextEdit):
"""Chat area."""
def __init__(self, debug, *args):
QtWidgets.QTextEdit.__init__(*(self,) + args)
self.debug = debug
self.readOnly = True
self.setFocusPolicy(QtCore.Qt.NoFocus)
self.setFontFamily('monospace')
self._textcolor = self.textColor()
self._bgcolor = QtGui.QColor('#FFFFFF')
self._setcolorcode = {
'F': (self.setTextColor, self._textcolor),
'B': (self.setTextBackgroundColor, self._bgcolor)
}
self._setfont = {
'*': self.setFontWeight,
'_': self.setFontUnderline,
'/': self.setFontItalic
}
self._fontvalues = {
False: {
'*': QtGui.QFont.Normal,
'_': False,
'/': False
},
True: {
'*': QtGui.QFont.Bold,
'_': True,
'/': True
}
}
self._color = color.Color(config.color_options(), self.debug)
def display(self, time, prefix, text, forcecolor=None):
if time == 0:
now = datetime.datetime.now()
else:
now = datetime.datetime.fromtimestamp(float(time))
self.setTextColor(QtGui.QColor('#999999'))
self.insertPlainText(now.strftime('%H:%M '))
prefix = self._color.convert(prefix)
text = self._color.convert(text)
if forcecolor:
if prefix:
prefix = '\x01(F%s)%s' % (forcecolor, prefix)
text = '\x01(F%s)%s' % (forcecolor, text)
if prefix:
self._display_with_colors(prefix + ' ')
if text:
self._display_with_colors(text)
if text[-1:] != '\n':
self.insertPlainText('\n')
else:
self.insertPlainText('\n')
self.scroll_bottom()
def _display_with_colors(self, string):
self.setTextColor(self._textcolor)
self.setTextBackgroundColor(self._bgcolor)
self._reset_attributes()
items = string.split('\x01')
for i, item in enumerate(items):
if i > 0 and item.startswith('('):
pos = item.find(')')
if pos >= 2:
action = item[1]
code = item[2:pos]
if action == '+':
# set attribute
self._set_attribute(code[0], True)
elif action == '-':
# remove attribute
self._set_attribute(code[0], False)
else:
# reset attributes and color
if code == 'r':
self._reset_attributes()
self._setcolorcode[action][0](
self._setcolorcode[action][1])
else:
# set attributes + color
while code.startswith(('*', '!', '/', '_', '|',
'r')):
if code[0] == 'r':
self._reset_attributes()
elif code[0] in self._setfont:
self._set_attribute(
code[0],
not self._font[code[0]])
code = code[1:]
if code:
self._setcolorcode[action][0](
QtGui.QColor(code))
item = item[pos+1:]
if len(item) > 0:
self.insertPlainText(item)
def _reset_attributes(self):
self._font = {}
for attr in self._setfont:
self._set_attribute(attr, False)
def _set_attribute(self, attr, value):
self._font[attr] = value
self._setfont[attr](self._fontvalues[self._font[attr]][attr])
def scroll_bottom(self):
scroll = self.verticalScrollBar()
scroll.setValue(scroll.maximum())

136
toxygen/third_party/qweechat/config.py vendored Normal file
View file

@ -0,0 +1,136 @@
# -*- coding: utf-8 -*-
#
# config.py - configuration for QWeeChat
#
# Copyright (C) 2011-2022 Sébastien Helleu <flashcode@flashtux.org>
#
# This file is part of QWeeChat, a Qt remote GUI for WeeChat.
#
# QWeeChat 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.
#
# QWeeChat 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 QWeeChat. If not, see <http://www.gnu.org/licenses/>.
#
"""Configuration for QWeeChat."""
import configparser
import os
from pathlib import Path
CONFIG_DIR = '%s/.config/qweechat' % os.getenv('HOME')
CONFIG_FILENAME = '%s/qweechat.conf' % CONFIG_DIR
CONFIG_DEFAULT_RELAY_LINES = 50
CONFIG_DEFAULT_SECTIONS = ('relay', 'look', 'color')
CONFIG_DEFAULT_OPTIONS = (('relay.hostname', '127.0.0.1'),
('relay.port', '9000'),
('relay.ssl', 'off'),
('relay.password', ''),
('relay.autoconnect', 'off'),
('relay.lines', str(CONFIG_DEFAULT_RELAY_LINES)),
('look.debug', 'off'),
('look.statusbar', 'on'))
# Default colors for WeeChat color options (option name, #rgb value)
CONFIG_DEFAULT_COLOR_OPTIONS = (
('separator', '#000066'), # 0
('chat', '#000000'), # 1
('chat_time', '#999999'), # 2
('chat_time_delimiters', '#000000'), # 3
('chat_prefix_error', '#FF6633'), # 4
('chat_prefix_network', '#990099'), # 5
('chat_prefix_action', '#000000'), # 6
('chat_prefix_join', '#00CC00'), # 7
('chat_prefix_quit', '#CC0000'), # 8
('chat_prefix_more', '#CC00FF'), # 9
('chat_prefix_suffix', '#330099'), # 10
('chat_buffer', '#000000'), # 11
('chat_server', '#000000'), # 12
('chat_channel', '#000000'), # 13
('chat_nick', '#000000'), # 14
('chat_nick_self', '*#000000'), # 15
('chat_nick_other', '#000000'), # 16
('', '#000000'), # 17 (nick1 -- obsolete)
('', '#000000'), # 18 (nick2 -- obsolete)
('', '#000000'), # 19 (nick3 -- obsolete)
('', '#000000'), # 20 (nick4 -- obsolete)
('', '#000000'), # 21 (nick5 -- obsolete)
('', '#000000'), # 22 (nick6 -- obsolete)
('', '#000000'), # 23 (nick7 -- obsolete)
('', '#000000'), # 24 (nick8 -- obsolete)
('', '#000000'), # 25 (nick9 -- obsolete)
('', '#000000'), # 26 (nick10 -- obsolete)
('chat_host', '#666666'), # 27
('chat_delimiters', '#9999FF'), # 28
('chat_highlight', '#3399CC'), # 29
('chat_read_marker', '#000000'), # 30
('chat_text_found', '#000000'), # 31
('chat_value', '#000000'), # 32
('chat_prefix_buffer', '#000000'), # 33
('chat_tags', '#000000'), # 34
('chat_inactive_window', '#000000'), # 35
('chat_inactive_buffer', '#000000'), # 36
('chat_prefix_buffer_inactive_buffer', '#000000'), # 37
('chat_nick_offline', '#000000'), # 38
('chat_nick_offline_highlight', '#000000'), # 39
('chat_nick_prefix', '#000000'), # 40
('chat_nick_suffix', '#000000'), # 41
('emphasis', '#000000'), # 42
('chat_day_change', '#000000'), # 43
)
config_color_options = []
def read():
"""Read config file."""
global config_color_options
config = configparser.RawConfigParser()
if os.path.isfile(CONFIG_FILENAME):
config.read(CONFIG_FILENAME)
# add missing sections/options
for section in CONFIG_DEFAULT_SECTIONS:
if not config.has_section(section):
config.add_section(section)
for option in reversed(CONFIG_DEFAULT_OPTIONS):
section, name = option[0].split('.', 1)
if not config.has_option(section, name):
config.set(section, name, option[1])
section = 'color'
for option in reversed(CONFIG_DEFAULT_COLOR_OPTIONS):
if option[0] and not config.has_option(section, option[0]):
config.set(section, option[0], option[1])
# build list of color options
config_color_options = []
for option in CONFIG_DEFAULT_COLOR_OPTIONS:
if option[0]:
config_color_options.append(config.get('color', option[0]))
else:
config_color_options.append('#000000')
return config
def write(config):
"""Write config file."""
Path(CONFIG_DIR).mkdir(mode=0o0700, parents=True, exist_ok=True)
with open(CONFIG_FILENAME, 'w') as cfg:
config.write(cfg)
def color_options():
"""Return color options."""
global config_color_options
return config_color_options

View file

@ -0,0 +1,128 @@
# -*- coding: utf-8 -*-
#
# connection.py - connection window
#
# Copyright (C) 2011-2022 Sébastien Helleu <flashcode@flashtux.org>
#
# This file is part of QWeeChat, a Qt remote GUI for WeeChat.
#
# QWeeChat 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.
#
# QWeeChat 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 QWeeChat. If not, see <http://www.gnu.org/licenses/>.
#
"""Connection window."""
from PyQt5 import QtGui, QtWidgets
class ConnectionDialog(QtWidgets.QDialog):
"""Connection window."""
def __init__(self, values, *args):
super().__init__(*args)
self.values = values
self.setModal(True)
self.setWindowTitle('Connect to WeeChat')
grid = QtWidgets.QGridLayout()
grid.setSpacing(10)
self.fields = {}
focus = None
# hostname
grid.addWidget(QtWidgets.QLabel('<b>Hostname</b>'), 0, 0)
line_edit = QtWidgets.QLineEdit()
line_edit.setFixedWidth(200)
value = self.values.get('hostname', '')
if value in ['None', None]: value = ''
line_edit.insert(value)
grid.addWidget(line_edit, 0, 1)
self.fields['hostname'] = line_edit
if not focus and not value:
focus = 'hostname'
# port / SSL
grid.addWidget(QtWidgets.QLabel('<b>Port</b>'), 1, 0)
line_edit = QtWidgets.QLineEdit()
line_edit.setFixedWidth(200)
value = self.values.get('port', '')
if value in ['None', None]:
value = '0'
elif type(value) == int:
value = str(value)
line_edit.insert(value)
grid.addWidget(line_edit, 1, 1)
self.fields['port'] = line_edit
if not focus and not value:
focus = 'port'
ssl = QtWidgets.QCheckBox('SSL')
ssl.setChecked(self.values['ssl'] == 'on')
grid.addWidget(ssl, 1, 2)
self.fields['ssl'] = ssl
# password
grid.addWidget(QtWidgets.QLabel('<b>Password</b>'), 2, 0)
line_edit = QtWidgets.QLineEdit()
line_edit.setFixedWidth(200)
line_edit.setEchoMode(QtWidgets.QLineEdit.Password)
value = self.values.get('password', '')
if value in ['None', None]: value = ''
line_edit.insert(value)
grid.addWidget(line_edit, 2, 1)
self.fields['password'] = line_edit
if not focus and not value:
focus = 'password'
# TOTP (Time-Based One-Time Password)
label = QtWidgets.QLabel('TOTP')
label.setToolTip('Time-Based One-Time Password (6 digits)')
grid.addWidget(label, 3, 0)
line_edit = QtWidgets.QLineEdit()
line_edit.setPlaceholderText('6 digits')
validator = QtGui.QIntValidator(0, 999999, self)
line_edit.setValidator(validator)
line_edit.setFixedWidth(80)
value = self.values.get('totp', '')
line_edit.insert(value)
grid.addWidget(line_edit, 3, 1)
self.fields['totp'] = line_edit
if not focus and not value:
focus = 'totp'
# lines
grid.addWidget(QtWidgets.QLabel('Lines'), 4, 0)
line_edit = QtWidgets.QLineEdit()
line_edit.setFixedWidth(200)
validator = QtGui.QIntValidator(0, 2147483647, self)
line_edit.setValidator(validator)
line_edit.setFixedWidth(80)
value = self.values.get('lines', '')
line_edit.insert(value)
grid.addWidget(line_edit, 4, 1)
self.fields['lines'] = line_edit
if not focus and not value:
focus = 'lines'
self.dialog_buttons = QtWidgets.QDialogButtonBox()
self.dialog_buttons.setStandardButtons(
QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel)
self.dialog_buttons.rejected.connect(self.close)
grid.addWidget(self.dialog_buttons, 5, 0, 1, 2)
self.setLayout(grid)
self.show()
if focus:
self.fields[focus].setFocus()

View file

@ -0,0 +1,41 @@
Copyright and license for images
================================
Files: weechat.png, bullet_green_8x8.png, bullet_yellow_8x8.png
Copyright (C) 2011-2022 Sébastien Helleu <flashcode@flashtux.org>
Released under GPLv3.
Files: application-exit.png, dialog-close.png, dialog-ok-apply.png,
dialog-password.png, dialog-warning.png, document-save.png,
edit-find.png, help-about.png, network-connect.png,
network-disconnect.png, preferences-other.png
Files come from Debian package "oxygen-icon-theme":
The Oxygen Icon Theme
Copyright (C) 2007 Nuno Pinheiro <nuno@oxygen-icons.org>
Copyright (C) 2007 David Vignoni <david@icon-king.com>
Copyright (C) 2007 David Miller <miller@oxygen-icons.org>
Copyright (C) 2007 Johann Ollivier Lapeyre <johann@oxygen-icons.org>
Copyright (C) 2007 Kenneth Wimer <kwwii@bootsplash.org>
Copyright (C) 2007 Riccardo Iaconelli <riccardo@oxygen-icons.org>
and others
License:
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 3 of the License, or (at your option) any later version.
This library 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library. If not, see <http://www.gnu.org/licenses/>.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 813 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 713 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

51
toxygen/third_party/qweechat/debug.py vendored Normal file
View file

@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
#
# debug.py - debug window
#
# Copyright (C) 2011-2022 Sébastien Helleu <flashcode@flashtux.org>
#
# This file is part of QWeeChat, a Qt remote GUI for WeeChat.
#
# QWeeChat 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.
#
# QWeeChat 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 QWeeChat. If not, see <http://www.gnu.org/licenses/>.
#
"""Debug window."""
from PyQt5 import QtWidgets
from third_party.qweechat.chat import ChatTextEdit
from third_party.qweechat.input import InputLineEdit
class DebugDialog(QtWidgets.QDialog):
"""Debug dialog."""
def __init__(self, *args):
QtWidgets.QDialog.__init__(*(self,) + args)
self.resize(1024, 768)
self.setWindowTitle('Debug console')
self.chat = ChatTextEdit(debug=True)
self.input = InputLineEdit(self.chat)
vbox = QtWidgets.QVBoxLayout()
vbox.addWidget(self.chat)
vbox.addWidget(self.input)
self.setLayout(vbox)
self.show()
def display_lines(self, lines):
for line in lines:
self.chat.display(*line[0], **line[1])

96
toxygen/third_party/qweechat/input.py vendored Normal file
View file

@ -0,0 +1,96 @@
# -*- coding: utf-8 -*-
#
# input.py - input line for chat and debug window
#
# Copyright (C) 2011-2022 Sébastien Helleu <flashcode@flashtux.org>
#
# This file is part of QWeeChat, a Qt remote GUI for WeeChat.
#
# QWeeChat 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.
#
# QWeeChat 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 QWeeChat. If not, see <http://www.gnu.org/licenses/>.
#
"""Input line for chat and debug window."""
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtCore import pyqtSignal
Signal = pyqtSignal
class InputLineEdit(QtWidgets.QLineEdit):
"""Input line."""
bufferSwitchPrev = Signal()
bufferSwitchNext = Signal()
textSent = Signal(str)
def __init__(self, scroll_widget):
super().__init__()
self.scroll_widget = scroll_widget
self._history = []
self._history_index = -1
self.returnPressed.connect(self._input_return_pressed)
def keyPressEvent(self, event):
key = event.key()
modifiers = event.modifiers()
scroll = self.scroll_widget.verticalScrollBar()
if modifiers == QtCore.Qt.ControlModifier:
if key == QtCore.Qt.Key_PageUp:
self.bufferSwitchPrev.emit()
elif key == QtCore.Qt.Key_PageDown:
self.bufferSwitchNext.emit()
else:
QtWidgets.QLineEdit.keyPressEvent(self, event)
elif modifiers == QtCore.Qt.AltModifier:
if key in (QtCore.Qt.Key_Left, QtCore.Qt.Key_Up):
self.bufferSwitchPrev.emit()
elif key in (QtCore.Qt.Key_Right, QtCore.Qt.Key_Down):
self.bufferSwitchNext.emit()
elif key == QtCore.Qt.Key_PageUp:
scroll.setValue(scroll.value() - (scroll.pageStep() / 10))
elif key == QtCore.Qt.Key_PageDown:
scroll.setValue(scroll.value() + (scroll.pageStep() / 10))
elif key == QtCore.Qt.Key_Home:
scroll.setValue(scroll.minimum())
elif key == QtCore.Qt.Key_End:
scroll.setValue(scroll.maximum())
else:
QtWidgets.QLineEdit.keyPressEvent(self, event)
elif key == QtCore.Qt.Key_PageUp:
scroll.setValue(scroll.value() - scroll.pageStep())
elif key == QtCore.Qt.Key_PageDown:
scroll.setValue(scroll.value() + scroll.pageStep())
elif key == QtCore.Qt.Key_Up:
self._history_navigate(-1)
elif key == QtCore.Qt.Key_Down:
self._history_navigate(1)
else:
QtWidgets.QLineEdit.keyPressEvent(self, event)
def _input_return_pressed(self):
self._history.append(self.text())
self._history_index = len(self._history)
self.textSent.emit(self.text())
self.clear()
def _history_navigate(self, direction):
if self._history:
self._history_index += direction
if self._history_index < 0:
self._history_index = 0
return
if self._history_index > len(self._history) - 1:
self._history_index = len(self._history)
self.clear()
return
self.setText(self._history[self._history_index])

358
toxygen/third_party/qweechat/network.py vendored Normal file
View file

@ -0,0 +1,358 @@
# -*- coding: utf-8 -*-
#
# network.py - I/O with WeeChat/relay
#
# Copyright (C) 2011-2022 Sébastien Helleu <flashcode@flashtux.org>
#
# This file is part of QWeeChat, a Qt remote GUI for WeeChat.
#
# QWeeChat 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.
#
# QWeeChat 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 QWeeChat. If not, see <http://www.gnu.org/licenses/>.
#
"""I/O with WeeChat/relay."""
import hashlib
import secrets
import struct
from PyQt5 import QtCore, QtNetwork
from PyQt5.QtCore import pyqtSignal
Signal = pyqtSignal
from third_party.qweechat import config
from third_party.qweechat.debug import DebugDialog
# list of supported hash algorithms on our side
# (the hash algorithm will be negotiated with the remote WeeChat)
_HASH_ALGOS_LIST = [
'plain',
'sha256',
'sha512',
'pbkdf2+sha256',
'pbkdf2+sha512',
]
_HASH_ALGOS = ':'.join(_HASH_ALGOS_LIST)
# handshake with remote WeeChat (before init)
_PROTO_HANDSHAKE = f'(handshake) handshake password_hash_algo={_HASH_ALGOS}\n'
# initialize with the password (plain text)
_PROTO_INIT_PWD = 'init password=%(password)s%(totp)s\n' # nosec
# initialize with the hashed password
_PROTO_INIT_HASH = ('init password_hash='
'%(algo)s:%(salt)s%(iter)s:%(hash)s%(totp)s\n')
_PROTO_SYNC_CMDS = [
# get buffers
'(listbuffers) hdata buffer:gui_buffers(*) number,full_name,short_name,'
'type,nicklist,title,local_variables',
# get lines
'(listlines) hdata buffer:gui_buffers(*)/own_lines/last_line(-%(lines)d)/'
'data date,displayed,prefix,message',
# get nicklist for all buffers
'(nicklist) nicklist',
# enable synchronization
'sync',
]
STATUS_DISCONNECTED = 'disconnected'
STATUS_CONNECTING = 'connecting'
STATUS_AUTHENTICATING = 'authenticating'
STATUS_CONNECTED = 'connected'
NETWORK_STATUS = {
STATUS_DISCONNECTED: {
'label': 'Disconnected',
'color': '#aa0000',
'icon': 'dialog-close.png',
},
STATUS_CONNECTING: {
'label': 'Connecting…',
'color': '#dd5f00',
'icon': 'dialog-warning.png',
},
STATUS_AUTHENTICATING: {
'label': 'Authenticating…',
'color': '#007fff',
'icon': 'dialog-password.png',
},
STATUS_CONNECTED: {
'label': 'Connected',
'color': 'green',
'icon': 'dialog-ok-apply.png',
},
}
class Network(QtCore.QObject):
"""I/O with WeeChat/relay."""
statusChanged = Signal(str, str)
messageFromWeechat = Signal(QtCore.QByteArray)
def __init__(self, *args):
super().__init__(*args)
self._init_connection()
self.debug_lines = []
self.debug_dialog = None
self._lines = config.CONFIG_DEFAULT_RELAY_LINES
self._buffer = QtCore.QByteArray()
self._socket = QtNetwork.QSslSocket()
self._socket.connected.connect(self._socket_connected)
self._socket.readyRead.connect(self._socket_read)
self._socket.disconnected.connect(self._socket_disconnected)
def _init_connection(self):
self.status = STATUS_DISCONNECTED
self._hostname = None
self._port = None
self._ssl = None
self._password = None
self._totp = None
self._handshake_received = False
self._handshake_timer = None
self._handshake_timer = False
self._pwd_hash_algo = None
self._pwd_hash_iter = 0
self._server_nonce = None
def set_status(self, status):
"""Set current status."""
self.status = status
self.statusChanged.emit(status, None)
def pbkdf2(self, hash_name, salt):
"""Return hashed password with PBKDF2-HMAC."""
return hashlib.pbkdf2_hmac(
hash_name,
password=self._password.encode('utf-8'),
salt=salt,
iterations=self._pwd_hash_iter,
).hex()
def _build_init_command(self):
"""Build the init command to send to WeeChat."""
totp = f',totp={self._totp}' if self._totp else ''
if self._pwd_hash_algo == 'plain':
cmd = _PROTO_INIT_PWD % {
'password': self._password,
'totp': totp,
}
else:
client_nonce = secrets.token_bytes(16)
salt = self._server_nonce + client_nonce
pwd_hash = None
iterations = ''
if self._pwd_hash_algo == 'pbkdf2+sha512':
pwd_hash = self.pbkdf2('sha512', salt)
iterations = f':{self._pwd_hash_iter}'
elif self._pwd_hash_algo == 'pbkdf2+sha256':
pwd_hash = self.pbkdf2('sha256', salt)
iterations = f':{self._pwd_hash_iter}'
elif self._pwd_hash_algo == 'sha512':
pwd = salt + self._password.encode('utf-8')
pwd_hash = hashlib.sha512(pwd).hexdigest()
elif self._pwd_hash_algo == 'sha256':
pwd = salt + self._password.encode('utf-8')
pwd_hash = hashlib.sha256(pwd).hexdigest()
if not pwd_hash:
return None
cmd = _PROTO_INIT_HASH % {
'algo': self._pwd_hash_algo,
'salt': bytearray(salt).hex(),
'iter': iterations,
'hash': pwd_hash,
'totp': totp,
}
return cmd
def _build_sync_command(self):
"""Build the sync commands to send to WeeChat."""
cmd = '\n'.join(_PROTO_SYNC_CMDS) + '\n'
return cmd % {'lines': self._lines}
def handshake_timer_expired(self):
if self.status == STATUS_AUTHENTICATING:
self._pwd_hash_algo = 'plain'
self.send_to_weechat(self._build_init_command())
self.sync_weechat()
self.set_status(STATUS_CONNECTED)
def _socket_connected(self):
"""Slot: socket connected."""
self.set_status(STATUS_AUTHENTICATING)
self.send_to_weechat(_PROTO_HANDSHAKE)
self._handshake_timer = QtCore.QTimer()
self._handshake_timer.setSingleShot(True)
self._handshake_timer.setInterval(2000)
self._handshake_timer.timeout.connect(self.handshake_timer_expired)
self._handshake_timer.start()
def _socket_read(self):
"""Slot: data available on socket."""
data = self._socket.readAll()
self._buffer.append(data)
while len(self._buffer) >= 4:
remainder = None
length = struct.unpack('>i', self._buffer[0:4].data())[0]
if len(self._buffer) < length:
# partial message, just wait for end of message
break
# more than one message?
if length < len(self._buffer):
# save beginning of another message
remainder = self._buffer[length:]
self._buffer = self._buffer[0:length]
self.messageFromWeechat.emit(self._buffer)
if not self.is_connected():
return
self._buffer.clear()
if remainder:
self._buffer.append(remainder)
def _socket_disconnected(self):
"""Slot: socket disconnected."""
if self._handshake_timer:
self._handshake_timer.stop()
self._init_connection()
self.set_status(STATUS_DISCONNECTED)
def is_connected(self):
"""Return True if the socket is connected, False otherwise."""
return self._socket.state() == QtNetwork.QAbstractSocket.ConnectedState
def is_ssl(self):
"""Return True if SSL is used, False otherwise."""
return self._ssl
def connect_weechat(self, hostname, port, ssl, password, totp, lines):
"""Connect to WeeChat."""
self._hostname = hostname
try:
self._port = int(port)
except ValueError:
self._port = 0
self._ssl = ssl
self._password = password
self._totp = totp
try:
self._lines = int(lines)
except ValueError:
self._lines = config.CONFIG_DEFAULT_RELAY_LINES
if self._socket.state() == QtNetwork.QAbstractSocket.ConnectedState:
return
if self._socket.state() != QtNetwork.QAbstractSocket.UnconnectedState:
self._socket.abort()
if self._ssl:
self._socket.ignoreSslErrors()
self._socket.connectToHostEncrypted(self._hostname, self._port)
else:
self._socket.connectToHost(self._hostname, self._port)
self.set_status(STATUS_CONNECTING)
def disconnect_weechat(self):
"""Disconnect from WeeChat."""
if self._socket.state() == QtNetwork.QAbstractSocket.UnconnectedState:
self.set_status(STATUS_DISCONNECTED)
return
if self._socket.state() == QtNetwork.QAbstractSocket.ConnectedState:
self.send_to_weechat('quit\n')
self._socket.waitForBytesWritten(1000)
else:
self.set_status(STATUS_DISCONNECTED)
self._socket.abort()
def send_to_weechat(self, message):
"""Send a message to WeeChat."""
self.debug_print(0, '<==', message, forcecolor='#AA0000')
self._socket.write(message.encode('utf-8'))
def init_with_handshake(self, response):
"""Initialize with WeeChat using the handshake response."""
self._pwd_hash_algo = response['password_hash_algo']
self._pwd_hash_iter = int(response['password_hash_iterations'])
self._server_nonce = bytearray.fromhex(response['nonce'])
if self._pwd_hash_algo:
cmd = self._build_init_command()
if cmd:
self.send_to_weechat(cmd)
self.sync_weechat()
self.set_status(STATUS_CONNECTED)
return
# failed to initialize: disconnect
self.disconnect_weechat()
def desync_weechat(self):
"""Desynchronize from WeeChat."""
self.send_to_weechat('desync\n')
def sync_weechat(self):
"""Synchronize with WeeChat."""
self.send_to_weechat(self._build_sync_command())
def status_label(self, status):
"""Return the label for a given status."""
return NETWORK_STATUS.get(status, {}).get('label', '')
def status_color(self, status):
"""Return the color for a given status."""
return NETWORK_STATUS.get(status, {}).get('color', 'black')
def status_icon(self, status):
"""Return the name of icon for a given status."""
return NETWORK_STATUS.get(status, {}).get('icon', '')
def get_options(self):
"""Get connection options."""
return {
'hostname': self._hostname,
'port': self._port,
'ssl': 'on' if self._ssl else 'off',
'password': self._password,
'lines': str(self._lines),
}
def debug_print(self, *args, **kwargs):
"""Display a debug message."""
self.debug_lines.append((args, kwargs))
if self.debug_dialog:
self.debug_dialog.chat.display(*args, **kwargs)
def _debug_dialog_closed(self, result):
"""Called when debug dialog is closed."""
self.debug_dialog = None
def debug_input_text_sent(self, text):
"""Send debug buffer input to WeeChat."""
if self.network.is_connected():
text = str(text)
pos = text.find(')')
if text.startswith('(') and pos >= 0:
text = '(debug_%s)%s' % (text[1:pos], text[pos+1:])
else:
text = '(debug) %s' % text
self.network.debug_print(0, '<==', text, forcecolor='#AA0000')
self.network.send_to_weechat(text + '\n')
def open_debug_dialog(self):
"""Open a dialog with debug messages."""
if not self.debug_dialog:
self.debug_dialog = DebugDialog()
self.debug_dialog.input.textSent.connect(
self.debug_input_text_sent)
self.debug_dialog.finished.connect(self._debug_dialog_closed)
self.debug_dialog.display_lines(self.debug_lines)
self.debug_dialog.chat.scroll_bottom()

View file

@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
#
# preferences.py - preferences dialog box
#
# Copyright (C) 2011-2022 Sébastien Helleu <flashcode@flashtux.org>
#
# This file is part of QWeeChat, a Qt remote GUI for WeeChat.
#
# QWeeChat 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.
#
# QWeeChat 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 QWeeChat. If not, see <http://www.gnu.org/licenses/>.
#
"""Preferences dialog box."""
from PyQt5 import QtCore, QtWidgets as QtGui
class PreferencesDialog(QtGui.QDialog):
"""Preferences dialog."""
def __init__(self, *args):
QtGui.QDialog.__init__(*(self,) + args)
self.setModal(True)
self.setWindowTitle('Preferences')
close_button = QtGui.QPushButton('Close')
close_button.pressed.connect(self.close)
hbox = QtGui.QHBoxLayout()
hbox.addStretch(1)
hbox.addWidget(close_button)
hbox.addStretch(1)
vbox = QtGui.QVBoxLayout()
label = QtGui.QLabel('Not yet implemented!')
label.setAlignment(QtCore.Qt.AlignHCenter)
vbox.addWidget(label)
label = QtGui.QLabel('')
label.setAlignment(QtCore.Qt.AlignHCenter)
vbox.addWidget(label)
vbox.addLayout(hbox)
self.setLayout(vbox)
self.show()

569
toxygen/third_party/qweechat/qweechat.py vendored Normal file
View file

@ -0,0 +1,569 @@
# -*- coding: utf-8 -*-
#
# qweechat.py - WeeChat remote GUI using Qt toolkit
#
# Copyright (C) 2011-2022 Sébastien Helleu <flashcode@flashtux.org>
#
# This file is part of QWeeChat, a Qt remote GUI for WeeChat.
#
# QWeeChat 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.
#
# QWeeChat 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 QWeeChat. If not, see <http://www.gnu.org/licenses/>.
#
"""
QWeeChat is a WeeChat remote GUI using Qt toolkit.
It requires requires WeeChat 0.3.7 or newer, running on local or remote host.
"""
#
# History:
#
# 2011-05-27, Sébastien Helleu <flashcode@flashtux.org>:
# start dev
#
import sys
import traceback
from pkg_resources import resource_filename
from PyQt5 import QtCore, QtGui, QtWidgets
from third_party.qweechat import config
from third_party.qweechat.about import AboutDialog
from third_party.qweechat.buffer import BufferListWidget, Buffer
from third_party.qweechat.connection import ConnectionDialog
from third_party.qweechat.network import Network, STATUS_DISCONNECTED
from third_party.qweechat.preferences import PreferencesDialog
from third_party.qweechat.weechat import protocol
APP_NAME = 'QWeeChat'
AUTHOR = 'Sébastien Helleu'
WEECHAT_SITE = 'https://weechat.org/'
# not QFrame
class MainWindow(QtWidgets.QMainWindow):
"""Main window."""
def __init__(self, *args):
super().__init__(*args)
self.config = config.read()
self.resize(1000, 600)
self.setWindowTitle(APP_NAME)
self.about_dialog = None
self.connection_dialog = None
self.preferences_dialog = None
# network
self.network = Network()
self.network.statusChanged.connect(self._network_status_changed)
self.network.messageFromWeechat.connect(self._network_weechat_msg)
# list of buffers
self.list_buffers = BufferListWidget()
self.list_buffers.currentRowChanged.connect(self._buffer_switch)
# default buffer
self.buffers = [Buffer()]
self.stacked_buffers = QtWidgets.QStackedWidget()
self.stacked_buffers.addWidget(self.buffers[0].widget)
# splitter with buffers + chat/input
splitter = QtWidgets.QSplitter()
splitter.addWidget(self.list_buffers)
splitter.addWidget(self.stacked_buffers)
self.list_buffers.setSizePolicy(QtWidgets.QSizePolicy.Preferred,
QtWidgets.QSizePolicy.Preferred)
self.stacked_buffers.setSizePolicy(QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Expanding)
# MainWindow
self.setCentralWidget(splitter)
if self.config.getboolean('look', 'statusbar'):
self.statusBar().visible = True
self.statusBar().visible = True
# actions for menu and toolbar
actions_def = {
'connect': [
'network-connect.png',
'Connect to WeeChat',
'Ctrl+O',
self.open_connection_dialog,
],
'disconnect': [
'network-disconnect.png',
'Disconnect from WeeChat',
'Ctrl+D',
self.network.disconnect_weechat,
],
'debug': [
'edit-find.png',
'Open debug console window',
'Ctrl+B',
self.network.open_debug_dialog,
],
'preferences': [
'preferences-other.png',
'Change preferences',
'Ctrl+P',
self.open_preferences_dialog,
],
'about': [
'help-about.png',
'About QWeeChat',
'Ctrl+H',
self.open_about_dialog,
],
'save connection': [
'document-save.png',
'Save connection configuration',
'Ctrl+S',
self.save_connection,
],
'quit': [
'application-exit.png',
'Quit application',
'Ctrl+Q',
self.close,
],
}
self.actions = {}
for name, action in list(actions_def.items()):
self.actions[name] = QtWidgets.QAction(
QtGui.QIcon(
resource_filename(__name__, 'data/icons/%s' % action[0])),
name.capitalize(), self)
self.actions[name].setToolTip(f'{action[1]} ({action[2]})')
self.actions[name].setShortcut(action[2])
self.actions[name].triggered.connect(action[3])
# menu
self.menu = self.menuBar()
menu_file = self.menu.addMenu('&File')
menu_file.addActions([self.actions['connect'],
self.actions['disconnect'],
self.actions['preferences'],
self.actions['save connection'],
self.actions['quit']])
menu_window = self.menu.addMenu('&Window')
menu_window.addAction(self.actions['debug'])
name = 'toggle'
menu_window.addAction(
QtWidgets.QAction(QtGui.QIcon(
resource_filename(__name__, 'data/icons/%s' % 'weechat.png')),
name.capitalize(), self))
#? .triggered.connect(self.onMyToolBarButtonClick)
menu_help = self.menu.addMenu('&Help')
menu_help.addAction(self.actions['about'])
self.network_status = QtWidgets.QLabel()
self.network_status.setFixedHeight(20)
self.network_status.setFixedWidth(200)
self.network_status.setContentsMargins(0, 0, 10, 0)
self.network_status.setAlignment(QtCore.Qt.AlignRight)
if hasattr(self.menu, 'setCornerWidget'):
self.menu.setCornerWidget(self.network_status,
QtCore.Qt.TopRightCorner)
self.network_status_set(STATUS_DISCONNECTED)
# toolbar
toolbar = self.addToolBar('toolBar')
toolbar.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon)
toolbar.addActions([self.actions['connect'],
self.actions['disconnect'],
self.actions['debug'],
self.actions['preferences'],
self.actions['about'],
self.actions['quit']])
self.toolbar = toolbar
self.buffers[0].widget.input.setFocus()
# open debug dialog
if self.config.getboolean('look', 'debug'):
self.network.open_debug_dialog()
# auto-connect to relay
if self.config.getboolean('relay', 'autoconnect'):
self.network.connect_weechat(
hostname=self.config.get('relay', 'hostname', fallback='127.0.0.1'),
port=self.config.get('relay', 'port', fallback='9000'),
ssl=self.config.getboolean('relay', 'ssl', fallback=False),
password=self.config.get('relay', 'password', fallback=''),
totp=self.config.get('relay', 'password', fallback=''),
lines=self.config.get('relay', 'lines', fallback=''),
)
self.show()
def _buffer_switch(self, index):
"""Switch to a buffer."""
if index >= 0:
self.stacked_buffers.setCurrentIndex(index)
self.stacked_buffers.widget(index).input.setFocus()
def buffer_input(self, full_name, text):
"""Send buffer input to WeeChat."""
if self.network.is_connected():
message = 'input %s %s\n' % (full_name, text)
self.network.send_to_weechat(message)
self.network.debug_print(0, '<==', message, forcecolor='#AA0000')
def open_preferences_dialog(self):
"""Open a dialog with preferences."""
# TODO: implement the preferences dialog box
self.preferences_dialog = PreferencesDialog(self)
def save_connection(self):
"""Save connection configuration."""
if self.network:
options = self.network.get_options()
for option in options:
self.config.set('relay', option, options[option])
def open_about_dialog(self):
"""Open a dialog with info about QWeeChat."""
self.about_dialog = AboutDialog(APP_NAME, AUTHOR, WEECHAT_SITE, self)
def open_connection_dialog(self):
"""Open a dialog with connection settings."""
values = {}
for option in ('hostname', 'port', 'ssl', 'password', 'lines'):
val = self.config.get('relay', option, fallback='')
if val in [None, 'None']: val = ''
if option == 'port' and val in [None, 'None']: val = 0
values[option] = val
self.connection_dialog = ConnectionDialog(values, self)
self.connection_dialog.dialog_buttons.accepted.connect(
self.connect_weechat)
def connect_weechat(self):
"""Connect to WeeChat."""
self.network.connect_weechat(
hostname=self.connection_dialog.fields['hostname'].text(),
port=self.connection_dialog.fields['port'].text(),
ssl=self.connection_dialog.fields['ssl'].isChecked(),
password=self.connection_dialog.fields['password'].text(),
totp=self.connection_dialog.fields['totp'].text(),
lines=int(self.connection_dialog.fields['lines'].text()),
)
hostname=self.connection_dialog.fields['hostname'].text()
port = self.connection_dialog.fields['port'].text()
ssl=self.connection_dialog.fields['ssl'].isChecked()
password = '' # self.connection_dialog.fields['password'].text()
self.config.set('relay', 'port', port)
self.config.set('relay', 'hostname', hostname)
self.config.set('relay', 'password', password)
self.connection_dialog.close()
def _network_status_changed(self, status, extra):
"""Called when the network status has changed."""
if self.config.getboolean('look', 'statusbar'):
self.statusBar().showMessage(status)
self.network.debug_print(0, '', status, forcecolor='#0000AA')
self.network_status_set(status)
def network_status_set(self, status):
"""Set the network status."""
pal = self.network_status.palette()
try:
pal.setColor(self.network_status.foregroundRole(),
self.network.status_color(status))
except:
# dunno
pass
ssl = ' (SSL)' if status != STATUS_DISCONNECTED \
and self.network.is_ssl() else ''
self.network_status.setPalette(pal)
icon = self.network.status_icon(status)
if icon:
self.network_status.setText(
'<img src="%s"> %s' %
(resource_filename(__name__, 'data/icons/%s' % icon),
self.network.status_label(status) + ssl))
else:
self.network_status.setText(status.capitalize())
if status == STATUS_DISCONNECTED:
self.actions['connect'].setEnabled(True)
self.actions['disconnect'].setEnabled(False)
else:
self.actions['connect'].setEnabled(False)
self.actions['disconnect'].setEnabled(True)
def _network_weechat_msg(self, message):
"""Called when a message is received from WeeChat."""
self.network.debug_print(
0, '==>',
'message (%d bytes):\n%s'
% (len(message),
protocol.hex_and_ascii(message.data(), 20)),
forcecolor='#008800',
)
try:
proto = protocol.Protocol()
message = proto.decode(message.data())
if message.uncompressed:
self.network.debug_print(
0, '==>',
'message uncompressed (%d bytes):\n%s'
% (message.size_uncompressed,
protocol.hex_and_ascii(message.uncompressed, 20)),
forcecolor='#008800')
self.network.debug_print(0, '', 'Message: %s' % message)
self.parse_message(message)
except Exception: # noqa: E722
print('Error while decoding message from WeeChat:\n%s'
% traceback.format_exc())
self.network.disconnect_weechat()
def _parse_handshake(self, message):
"""Parse a WeeChat message with handshake response."""
for obj in message.objects:
if obj.objtype != 'htb':
continue
self.network.init_with_handshake(obj.value)
break
def _parse_listbuffers(self, message):
"""Parse a WeeChat message with list of buffers."""
for obj in message.objects:
if obj.objtype != 'hda' or obj.value['path'][-1] != 'buffer':
continue
self.list_buffers.clear()
while self.stacked_buffers.count() > 0:
buf = self.stacked_buffers.widget(0)
self.stacked_buffers.removeWidget(buf)
self.buffers = []
for item in obj.value['items']:
buf = self.create_buffer(item)
self.insert_buffer(len(self.buffers), buf)
self.list_buffers.setCurrentRow(0)
self.buffers[0].widget.input.setFocus()
def _parse_line(self, message):
"""Parse a WeeChat message with a buffer line."""
for obj in message.objects:
lines = []
if obj.objtype != 'hda' or obj.value['path'][-1] != 'line_data':
continue
for item in obj.value['items']:
if message.msgid == 'listlines':
ptrbuf = item['__path'][0]
else:
ptrbuf = item['buffer']
index = [i for i, b in enumerate(self.buffers)
if b.pointer() == ptrbuf]
if index:
lines.append(
(index[0],
(item['date'], item['prefix'],
item['message']))
)
if message.msgid == 'listlines':
lines.reverse()
for line in lines:
self.buffers[line[0]].widget.chat.display(*line[1])
def _parse_nicklist(self, message):
"""Parse a WeeChat message with a buffer nicklist."""
buffer_refresh = {}
for obj in message.objects:
if obj.objtype != 'hda' or \
obj.value['path'][-1] != 'nicklist_item':
continue
group = '__root'
for item in obj.value['items']:
index = [i for i, b in enumerate(self.buffers)
if b.pointer() == item['__path'][0]]
if index:
if not index[0] in buffer_refresh:
self.buffers[index[0]].nicklist = {}
buffer_refresh[index[0]] = True
if item['group']:
group = item['name']
self.buffers[index[0]].nicklist_add_item(
group, item['group'], item['prefix'], item['name'],
item['visible'])
for index in buffer_refresh:
self.buffers[index].nicklist_refresh()
def _parse_nicklist_diff(self, message):
"""Parse a WeeChat message with a buffer nicklist diff."""
buffer_refresh = {}
for obj in message.objects:
if obj.objtype != 'hda' or \
obj.value['path'][-1] != 'nicklist_item':
continue
group = '__root'
for item in obj.value['items']:
index = [i for i, b in enumerate(self.buffers)
if b.pointer() == item['__path'][0]]
if not index:
continue
buffer_refresh[index[0]] = True
if item['_diff'] == ord('^'):
group = item['name']
elif item['_diff'] == ord('+'):
self.buffers[index[0]].nicklist_add_item(
group, item['group'], item['prefix'], item['name'],
item['visible'])
elif item['_diff'] == ord('-'):
self.buffers[index[0]].nicklist_remove_item(
group, item['group'], item['name'])
elif item['_diff'] == ord('*'):
self.buffers[index[0]].nicklist_update_item(
group, item['group'], item['prefix'], item['name'],
item['visible'])
for index in buffer_refresh:
self.buffers[index].nicklist_refresh()
def _parse_buffer_opened(self, message):
"""Parse a WeeChat message with a new buffer (opened)."""
for obj in message.objects:
if obj.objtype != 'hda' or obj.value['path'][-1] != 'buffer':
continue
for item in obj.value['items']:
buf = self.create_buffer(item)
index = self.find_buffer_index_for_insert(item['next_buffer'])
self.insert_buffer(index, buf)
def _parse_buffer(self, message):
"""Parse a WeeChat message with a buffer event
(anything except a new buffer).
"""
for obj in message.objects:
if obj.objtype != 'hda' or obj.value['path'][-1] != 'buffer':
continue
for item in obj.value['items']:
index = [i for i, b in enumerate(self.buffers)
if b.pointer() == item['__path'][0]]
if not index:
continue
index = index[0]
if message.msgid == '_buffer_type_changed':
self.buffers[index].data['type'] = item['type']
elif message.msgid in ('_buffer_moved', '_buffer_merged',
'_buffer_unmerged'):
buf = self.buffers[index]
buf.data['number'] = item['number']
self.remove_buffer(index)
index2 = self.find_buffer_index_for_insert(
item['next_buffer'])
self.insert_buffer(index2, buf)
elif message.msgid == '_buffer_renamed':
self.buffers[index].data['full_name'] = item['full_name']
self.buffers[index].data['short_name'] = item['short_name']
elif message.msgid == '_buffer_title_changed':
self.buffers[index].data['title'] = item['title']
self.buffers[index].update_title()
elif message.msgid == '_buffer_cleared':
self.buffers[index].widget.chat.clear()
elif message.msgid.startswith('_buffer_localvar_'):
self.buffers[index].data['local_variables'] = \
item['local_variables']
self.buffers[index].update_prompt()
elif message.msgid == '_buffer_closing':
self.remove_buffer(index)
def parse_message(self, message):
"""Parse a WeeChat message."""
if message.msgid.startswith('debug'):
self.network.debug_print(0, '', '(debug message, ignored)')
elif message.msgid == 'handshake':
self._parse_handshake(message)
elif message.msgid == 'listbuffers':
self._parse_listbuffers(message)
elif message.msgid in ('listlines', '_buffer_line_added'):
self._parse_line(message)
elif message.msgid in ('_nicklist', 'nicklist'):
self._parse_nicklist(message)
elif message.msgid == '_nicklist_diff':
self._parse_nicklist_diff(message)
elif message.msgid == '_buffer_opened':
self._parse_buffer_opened(message)
elif message.msgid.startswith('_buffer_'):
self._parse_buffer(message)
elif message.msgid == '_upgrade':
self.network.desync_weechat()
elif message.msgid == '_upgrade_ended':
self.network.sync_weechat()
else:
print(f"Unknown message with id {message.msgid}")
def create_buffer(self, item):
"""Create a new buffer."""
buf = Buffer(item)
buf.bufferInput.connect(self.buffer_input)
buf.widget.input.bufferSwitchPrev.connect(
self.list_buffers.switch_prev_buffer)
buf.widget.input.bufferSwitchNext.connect(
self.list_buffers.switch_next_buffer)
return buf
def insert_buffer(self, index, buf):
"""Insert a buffer in list."""
self.buffers.insert(index, buf)
self.list_buffers.insertItem(index, '%s'
% (buf.data['local_variables']['name']))
self.stacked_buffers.insertWidget(index, buf.widget)
def remove_buffer(self, index):
"""Remove a buffer."""
if self.list_buffers.currentRow == index and index > 0:
self.list_buffers.setCurrentRow(index - 1)
self.list_buffers.takeItem(index)
self.stacked_buffers.removeWidget(self.stacked_buffers.widget(index))
self.buffers.pop(index)
def find_buffer_index_for_insert(self, next_buffer):
"""Find position to insert a buffer in list."""
index = -1
if next_buffer == '0x0':
index = len(self.buffers)
else:
elts = [i for i, b in enumerate(self.buffers)
if b.pointer() == next_buffer]
if len(elts):
index = elts[0]
if index < 0:
print('Warning: unable to find position for buffer, using end of '
'list by default')
index = len(self.buffers)
return index
def closeEvent(self, event):
"""Called when QWeeChat window is closed."""
self.network.disconnect_weechat()
if self.network.debug_dialog:
self.network.debug_dialog.close()
config.write(self.config)
QtWidgets.QFrame.closeEvent(self, event)
def main():
app = QtWidgets.QApplication(sys.argv)
app.setStyle(QtWidgets.QStyleFactory.create('Cleanlooks'))
app.setWindowIcon(QtGui.QIcon(
resource_filename(__name__, 'data/icons/weechat.png')))
main_win = MainWindow()
main_win.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()

30
toxygen/third_party/qweechat/version.py vendored Normal file
View file

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
#
# version.py - version of QWeeChat
#
# Copyright (C) 2011-2022 Sébastien Helleu <flashcode@flashtux.org>
#
# This file is part of QWeeChat, a Qt remote GUI for WeeChat.
#
# QWeeChat 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.
#
# QWeeChat 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 QWeeChat. If not, see <http://www.gnu.org/licenses/>.
#
"""Version of QWeeChat."""
VERSION = '0.0.1-dev'
def qweechat_version():
"""Return QWeeChat version."""
return VERSION

View file

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2011-2022 Sébastien Helleu <flashcode@flashtux.org>
#
# This file is part of QWeeChat, a Qt remote GUI for WeeChat.
#
# QWeeChat 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.
#
# QWeeChat 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 QWeeChat. If not, see <http://www.gnu.org/licenses/>.
#

View file

@ -0,0 +1,201 @@
# -*- coding: utf-8 -*-
#
# color.py - remove/replace colors in WeeChat strings
#
# Copyright (C) 2011-2022 Sébastien Helleu <flashcode@flashtux.org>
#
# This file is part of QWeeChat, a Qt remote GUI for WeeChat.
#
# QWeeChat 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.
#
# QWeeChat 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 QWeeChat. If not, see <http://www.gnu.org/licenses/>.
#
"""Remove/replace colors in WeeChat strings."""
import re
import logging
RE_COLOR_ATTRS = r'[*!/_|]*'
RE_COLOR_STD = r'(?:%s\d{2})' % RE_COLOR_ATTRS
RE_COLOR_EXT = r'(?:@%s\d{5})' % RE_COLOR_ATTRS
RE_COLOR_ANY = r'(?:%s|%s)' % (RE_COLOR_STD, RE_COLOR_EXT)
# \x19: color code, \x1A: set attribute, \x1B: remove attribute, \x1C: reset
RE_COLOR = re.compile(
r'(\x19(?:\d{2}|F%s|B\d{2}|B@\d{5}|E|\\*%s(~%s)?|@\d{5}|b.|\x1C))|\x1A.|'
r'\x1B.|\x1C'
% (RE_COLOR_ANY, RE_COLOR_ANY, RE_COLOR_ANY))
TERMINAL_COLORS = \
'000000cd000000cd00cdcd000000cdcd00cd00cdcde5e5e5' \
'4d4d4dff000000ff00ffff000000ffff00ff00ffffffffff' \
'00000000002a0000550000800000aa0000d4002a00002a2a' \
'002a55002a80002aaa002ad400550000552a005555005580' \
'0055aa0055d400800000802a0080550080800080aa0080d4' \
'00aa0000aa2a00aa5500aa8000aaaa00aad400d40000d42a' \
'00d45500d48000d4aa00d4d42a00002a002a2a00552a0080' \
'2a00aa2a00d42a2a002a2a2a2a2a552a2a802a2aaa2a2ad4' \
'2a55002a552a2a55552a55802a55aa2a55d42a80002a802a' \
'2a80552a80802a80aa2a80d42aaa002aaa2a2aaa552aaa80' \
'2aaaaa2aaad42ad4002ad42a2ad4552ad4802ad4aa2ad4d4' \
'55000055002a5500555500805500aa5500d4552a00552a2a' \
'552a55552a80552aaa552ad455550055552a555555555580' \
'5555aa5555d455800055802a5580555580805580aa5580d4' \
'55aa0055aa2a55aa5555aa8055aaaa55aad455d40055d42a' \
'55d45555d48055d4aa55d4d480000080002a800055800080' \
'8000aa8000d4802a00802a2a802a55802a80802aaa802ad4' \
'80550080552a8055558055808055aa8055d480800080802a' \
'8080558080808080aa8080d480aa0080aa2a80aa5580aa80' \
'80aaaa80aad480d40080d42a80d45580d48080d4aa80d4d4' \
'aa0000aa002aaa0055aa0080aa00aaaa00d4aa2a00aa2a2a' \
'aa2a55aa2a80aa2aaaaa2ad4aa5500aa552aaa5555aa5580' \
'aa55aaaa55d4aa8000aa802aaa8055aa8080aa80aaaa80d4' \
'aaaa00aaaa2aaaaa55aaaa80aaaaaaaaaad4aad400aad42a' \
'aad455aad480aad4aaaad4d4d40000d4002ad40055d40080' \
'd400aad400d4d42a00d42a2ad42a55d42a80d42aaad42ad4' \
'd45500d4552ad45555d45580d455aad455d4d48000d4802a' \
'd48055d48080d480aad480d4d4aa00d4aa2ad4aa55d4aa80' \
'd4aaaad4aad4d4d400d4d42ad4d455d4d480d4d4aad4d4d4' \
'0808081212121c1c1c2626263030303a3a3a4444444e4e4e' \
'5858586262626c6c6c7676768080808a8a8a9494949e9e9e' \
'a8a8a8b2b2b2bcbcbcc6c6c6d0d0d0dadadae4e4e4eeeeee'
# WeeChat basic colors (color name, index in terminal colors)
WEECHAT_BASIC_COLORS = (
('default', 0), ('black', 0), ('darkgray', 8), ('red', 1),
('lightred', 9), ('green', 2), ('lightgreen', 10), ('brown', 3),
('yellow', 11), ('blue', 4), ('lightblue', 12), ('magenta', 5),
('lightmagenta', 13), ('cyan', 6), ('lightcyan', 14), ('gray', 7),
('white', 0))
log = logging.getLogger(__name__)
class Color():
def __init__(self, color_options, debug=False):
self.color_options = color_options
self.debug = debug
def _rgb_color(self, index):
color = TERMINAL_COLORS[index*6:(index*6)+6]
col_r = int(color[0:2], 16) * 0.85
col_g = int(color[2:4], 16) * 0.85
col_b = int(color[4:6], 16) * 0.85
return '%02x%02x%02x' % (col_r, col_g, col_b)
def _convert_weechat_color(self, color):
try:
index = int(color)
return '\x01(Fr%s)' % self.color_options[index]
except Exception: # noqa: E722
log.debug('Error decoding WeeChat color "%s"', color)
return ''
def _convert_terminal_color(self, fg_bg, attrs, color):
try:
index = int(color)
return '\x01(%s%s#%s)' % (fg_bg, attrs, self._rgb_color(index))
except Exception: # noqa: E722
log.debug('Error decoding terminal color "%s"', color)
return ''
def _convert_color_attr(self, fg_bg, color):
extended = False
if color[0].startswith('@'):
extended = True
color = color[1:]
attrs = ''
# keep_attrs = False
while color.startswith(('*', '!', '/', '_', '|')):
# TODO: manage the "keep attributes" flag
# if color[0] == '|':
# keep_attrs = True
attrs += color[0]
color = color[1:]
if extended:
return self._convert_terminal_color(fg_bg, attrs, color)
try:
index = int(color)
return self._convert_terminal_color(fg_bg, attrs,
WEECHAT_BASIC_COLORS[index][1])
except Exception: # noqa: E722
log.debug('Error decoding color "%s"', color)
return ''
def _attrcode_to_char(self, code):
codes = {
'\x01': '*',
'\x02': '!',
'\x03': '/',
'\x04': '_',
}
return codes.get(code, '')
def _convert_color(self, match):
color = match.group(0)
if color[0] == '\x19':
if color[1] == 'b':
# bar code, ignored
return ''
if color[1] == '\x1C':
# reset
return '\x01(Fr)\x01(Br)'
if color[1] in ('F', 'B'):
# foreground or background
return self._convert_color_attr(color[1], color[2:])
if color[1] == '*':
# foreground with optional background
items = color[2:].split(',')
str_col = self._convert_color_attr('F', items[0])
if len(items) > 1:
str_col += self._convert_color_attr('B', items[1])
return str_col
if color[1] == '@':
# direct ncurses pair number, ignored
return ''
if color[1] == 'E':
# text emphasis, ignored
return ''
if color[1:].isdigit():
return self._convert_weechat_color(int(color[1:]))
elif color[0] == '\x1A':
# set attribute
return '\x01(+%s)' % self._attrcode_to_char(color[1])
elif color[0] == '\x1B':
# remove attribute
return '\x01(-%s)' % self._attrcode_to_char(color[1])
elif color[0] == '\x1C':
# reset
return '\x01(Fr)\x01(Br)'
# should never be executed!
return match.group(0)
def _convert_color_debug(self, match):
group = match.group(0)
for code in (0x01, 0x02, 0x03, 0x04, 0x19, 0x1A, 0x1B):
group = group.replace(chr(code), '<x%02X>' % code)
return group
def convert(self, text):
if not text:
return ''
if self.debug:
return RE_COLOR.sub(self._convert_color_debug, text)
return RE_COLOR.sub(self._convert_color, text)
def remove(text):
"""Remove colors in a WeeChat string."""
if not text:
return ''
return re.sub(RE_COLOR, '', text)

View file

@ -0,0 +1,361 @@
# -*- coding: utf-8 -*-
#
# protocol.py - decode binary messages received from WeeChat/relay
#
# Copyright (C) 2011-2022 Sébastien Helleu <flashcode@flashtux.org>
#
# This file is part of QWeeChat, a Qt remote GUI for WeeChat.
#
# QWeeChat 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.
#
# QWeeChat 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 QWeeChat. If not, see <http://www.gnu.org/licenses/>.
#
#
# For info about protocol and format of messages, please read document
# "WeeChat Relay Protocol", available at: https://weechat.org/doc/
#
# History:
#
# 2011-11-23, Sébastien Helleu <flashcode@flashtux.org>:
# start dev
#
"""Decode binary messages received from WeeChat/relay."""
import collections
import struct
import zlib
class WeechatDict(collections.OrderedDict):
def __str__(self):
return '{%s}' % ', '.join(
['%s: %s' % (repr(key), repr(self[key])) for key in self])
class WeechatObject:
def __init__(self, objtype, value, separator='\n'):
self.objtype = objtype
self.value = value
self.separator = separator
self.indent = ' ' if separator == '\n' else ''
self.separator1 = '\n%s' % self.indent if separator == '\n' else ''
def _str_value(self, val):
if isinstance(val, str) and val is not None:
return '\'%s\'' % val
return str(val)
def _str_value_hdata(self):
lines = ['%skeys: %s%s%spath: %s' % (self.separator1,
str(self.value['keys']),
self.separator,
self.indent,
str(self.value['path']))]
for i, item in enumerate(self.value['items']):
lines.append(' item %d:%s%s' % (
(i + 1), self.separator,
self.separator.join(
['%s%s: %s' % (self.indent * 2, key,
self._str_value(value))
for key, value in item.items()])))
return '\n'.join(lines)
def _str_value_infolist(self):
lines = ['%sname: %s' % (self.separator1, self.value['name'])]
for i, item in enumerate(self.value['items']):
lines.append(' item %d:%s%s' % (
(i + 1), self.separator,
self.separator.join(
['%s%s: %s' % (self.indent * 2, key,
self._str_value(value))
for key, value in item.items()])))
return '\n'.join(lines)
def _str_value_other(self):
return self._str_value(self.value)
def __str__(self):
obj_cb = {
'hda': self._str_value_hdata,
'inl': self._str_value_infolist,
}
return '%s: %s' % (self.objtype,
obj_cb.get(self.objtype, self._str_value_other)())
class WeechatObjects(list):
def __init__(self, separator='\n'):
super().__init__()
self.separator = separator
def __str__(self):
return self.separator.join([str(obj) for obj in self])
class WeechatMessage:
def __init__(self, size, size_uncompressed, compression, uncompressed,
msgid, objects):
self.size = size
self.size_uncompressed = size_uncompressed
self.compression = compression
self.uncompressed = uncompressed
self.msgid = msgid
self.objects = objects
def __str__(self):
if self.compression != 0:
return 'size: %d/%d (%d%%), id=\'%s\', objects:\n%s' % (
self.size, self.size_uncompressed,
100 - ((self.size * 100) // self.size_uncompressed),
self.msgid, self.objects)
return 'size: %d, id=\'%s\', objects:\n%s' % (self.size,
self.msgid,
self.objects)
class Protocol:
"""Decode binary message received from WeeChat/relay."""
def __init__(self):
self.data = ''
self._obj_cb = {
'chr': self._obj_char,
'int': self._obj_int,
'lon': self._obj_long,
'str': self._obj_str,
'buf': self._obj_buffer,
'ptr': self._obj_ptr,
'tim': self._obj_time,
'htb': self._obj_hashtable,
'hda': self._obj_hdata,
'inf': self._obj_info,
'inl': self._obj_infolist,
'arr': self._obj_array,
}
def _obj_type(self):
"""Read type in data (3 chars)."""
if len(self.data) < 3:
self.data = ''
return ''
objtype = self.data[0:3].decode()
self.data = self.data[3:]
return objtype
def _obj_len_data(self, length_size):
"""Read length (1 or 4 bytes), then value with this length."""
if len(self.data) < length_size:
self.data = ''
return None
if length_size == 1:
length = struct.unpack('B', self.data[0:1])[0]
self.data = self.data[1:]
else:
length = self._obj_int()
if length < 0:
return None
if length > 0:
value = self.data[0:length]
self.data = self.data[length:]
else:
value = ''
return value
def _obj_char(self):
"""Read a char in data."""
if len(self.data) < 1:
return 0
value = struct.unpack('b', self.data[0:1])[0]
self.data = self.data[1:]
return value
def _obj_int(self):
"""Read an integer in data (4 bytes)."""
if len(self.data) < 4:
self.data = ''
return 0
value = struct.unpack('>i', self.data[0:4])[0]
self.data = self.data[4:]
return value
def _obj_long(self):
"""Read a long integer in data (length on 1 byte + value as string)."""
value = self._obj_len_data(1)
if value is None:
return None
return int(value)
def _obj_str(self):
"""Read a string in data (length on 4 bytes + content)."""
value = self._obj_len_data(4)
if value in ("", None):
return ""
return value.decode()
def _obj_buffer(self):
"""Read a buffer in data (length on 4 bytes + data)."""
return self._obj_len_data(4)
def _obj_ptr(self):
"""Read a pointer in data (length on 1 byte + value as string)."""
value = self._obj_len_data(1)
if value is None:
return None
return '0x%s' % value
def _obj_time(self):
"""Read a time in data (length on 1 byte + value as string)."""
value = self._obj_len_data(1)
if value is None:
return None
return int(value)
def _obj_hashtable(self):
"""
Read a hashtable in data
(type for keys + type for values + count + items).
"""
type_keys = self._obj_type()
type_values = self._obj_type()
count = self._obj_int()
hashtable = WeechatDict()
for _ in range(count):
key = self._obj_cb[type_keys]()
value = self._obj_cb[type_values]()
hashtable[key] = value
return hashtable
def _obj_hdata(self):
"""Read a hdata in data."""
path = self._obj_str()
keys = self._obj_str()
count = self._obj_int()
list_path = path.split('/') if path else []
list_keys = keys.split(',') if keys else []
keys_types = []
dict_keys = WeechatDict()
for key in list_keys:
items = key.split(':')
keys_types.append(items)
dict_keys[items[0]] = items[1]
items = []
for _ in range(count):
item = WeechatDict()
item['__path'] = []
pointers = []
for _ in enumerate(list_path):
pointers.append(self._obj_ptr())
for key, objtype in keys_types:
item[key] = self._obj_cb[objtype]()
item['__path'] = pointers
items.append(item)
return {
'path': list_path,
'keys': dict_keys,
'count': count,
'items': items,
}
def _obj_info(self):
"""Read an info in data."""
name = self._obj_str()
value = self._obj_str()
return (name, value)
def _obj_infolist(self):
"""Read an infolist in data."""
name = self._obj_str()
count_items = self._obj_int()
items = []
for _ in range(count_items):
count_vars = self._obj_int()
variables = WeechatDict()
for _ in range(count_vars):
var_name = self._obj_str()
var_type = self._obj_type()
var_value = self._obj_cb[var_type]()
variables[var_name] = var_value
items.append(variables)
return {
'name': name,
'items': items
}
def _obj_array(self):
"""Read an array of values in data."""
type_values = self._obj_type()
count_values = self._obj_int()
values = []
for _ in range(count_values):
values.append(self._obj_cb[type_values]())
return values
def decode(self, data, separator='\n'):
"""Decode binary data and return list of objects."""
self.data = data
size = len(self.data)
size_uncompressed = size
uncompressed = None
# uncompress data (if it is compressed)
compression = struct.unpack('b', self.data[4:5])[0]
if compression:
uncompressed = zlib.decompress(self.data[5:])
size_uncompressed = len(uncompressed) + 5
uncompressed = b'%s%s%s' % (struct.pack('>i', size_uncompressed),
struct.pack('b', 0), uncompressed)
self.data = uncompressed
else:
uncompressed = self.data[:]
# skip length and compression flag
self.data = self.data[5:]
# read id
msgid = self._obj_str()
if msgid is None:
msgid = ''
# read objects
objects = WeechatObjects(separator=separator)
while len(self.data) > 0:
objtype = self._obj_type()
value = self._obj_cb[objtype]()
objects.append(WeechatObject(objtype, value, separator=separator))
return WeechatMessage(size, size_uncompressed, compression,
uncompressed, msgid, objects)
def hex_and_ascii(data, bytes_per_line=10):
"""Convert a QByteArray to hex + ascii output."""
num_lines = ((len(data) - 1) // bytes_per_line) + 1
if num_lines == 0:
return ''
lines = []
for i in range(num_lines):
str_hex = []
str_ascii = []
for j in range(bytes_per_line):
# We can't easily iterate over individual bytes, so we are going to
# do it this way.
index = (i*bytes_per_line) + j
char = data[index:index+1]
if not char:
char = b'x'
byte = struct.unpack('B', char)[0]
str_hex.append(b'%02X' % int(byte))
if 32 <= byte <= 127:
str_ascii.append(char)
else:
str_ascii.append(b'.')
fmt = b'%%-%ds %%s' % ((bytes_per_line * 3) - 1)
lines.append(fmt % (b' '.join(str_hex),
b''.join(str_ascii)))
return b'\n'.join(lines)

View file

@ -0,0 +1,252 @@
# -*- coding: utf-8 -*-
#
# testproto.py - command-line program for testing WeeChat/relay protocol
#
# Copyright (C) 2013-2022 Sébastien Helleu <flashcode@flashtux.org>
#
# This file is part of QWeeChat, a Qt remote GUI for WeeChat.
#
# QWeeChat 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.
#
# QWeeChat 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 QWeeChat. If not, see <http://www.gnu.org/licenses/>.
#
"""Command-line program for testing WeeChat/relay protocol."""
import argparse
import os
import select
import shlex
import socket
import struct
import sys
import time
import traceback
from qweechat.weechat import protocol
qweechat_version = '0.1'
NAME = 'qweechat-testproto'
class TestProto(object):
"""Test of WeeChat/relay protocol."""
def __init__(self, args):
self.args = args
self.sock = None
self.has_quit = False
self.address = '{self.args.hostname}/{self.args.port} ' \
'(IPv{0})'.format(6 if self.args.ipv6 else 4, self=self)
def connect(self):
"""
Connect to WeeChat/relay.
Return True if OK, False if error.
"""
inet = socket.AF_INET6 if self.args.ipv6 else socket.AF_INET
try:
self.sock = socket.socket(inet, socket.SOCK_STREAM)
self.sock.connect((self.args.hostname, self.args.port))
except Exception:
if self.sock:
self.sock.close()
print('Failed to connect to', self.address)
return False
print(f'Connected to {self.address} socket {self.sock}')
return True
def send(self, messages):
"""
Send a text message to WeeChat/relay.
Return True if OK, False if error.
"""
try:
for msg in messages.split(b'\n'):
if msg == b'quit':
self.has_quit = True
self.sock.sendall(msg + b'\n')
sys.stdout.write(
(b'\x1b[33m<-- ' + msg + b'\x1b[0m\n').decode())
except Exception: # noqa: E722
traceback.print_exc()
print('Failed to send message')
return False
return True
def decode(self, message):
"""
Decode a binary message received from WeeChat/relay.
Return True if OK, False if error.
"""
try:
proto = protocol.Protocol()
msgd = proto.decode(message,
separator=b'\n' if self.args.debug > 0
else ', ')
print('')
if self.args.debug >= 2 and msgd.uncompressed:
# display raw message
print('\x1b[32m--> message uncompressed ({0} bytes):\n'
'{1}\x1b[0m'
''.format(msgd.size_uncompressed,
protocol.hex_and_ascii(msgd.uncompressed, 20)))
# display decoded message
print('\x1b[32m--> {0}\x1b[0m'.format(msgd))
except Exception: # noqa: E722
traceback.print_exc()
print('Error while decoding message from WeeChat')
return False
return True
def send_stdin(self):
"""
Send commands from standard input if some data is available.
Return True if OK (it's OK if stdin has no commands),
False if error.
"""
inr = select.select([sys.stdin], [], [], 0)[0]
if inr:
data = os.read(sys.stdin.fileno(), 4096)
if data:
if not self.send(data.strip()):
self.sock.close()
return False
# open stdin to read user commands
sys.stdin = open('/dev/tty')
return True
def mainloop(self):
"""
Main loop: read keyboard, send commands, read socket,
decode/display binary messages received from WeeChat/relay.
Return 0 if OK, 4 if send error, 5 if decode error.
"""
if self.has_quit:
return 0
message = b''
recvbuf = b''
prompt = b'\x1b[36mrelay> \x1b[0m'
sys.stdout.write(prompt.decode())
sys.stdout.flush()
try:
while not self.has_quit:
inr = select.select([sys.stdin, self.sock], [], [], 1)[0]
for _file in inr:
if _file == sys.stdin:
buf = os.read(_file.fileno(), 4096)
if buf:
message += buf
if b'\n' in message:
messages = message.split(b'\n')
msgsent = b'\n'.join(messages[:-1])
if msgsent and not self.send(msgsent):
return 4
message = messages[-1]
sys.stdout.write((prompt + message).decode())
# sys.stdout.write(prompt + message)
sys.stdout.flush()
else:
buf = _file.recv(4096)
if buf:
recvbuf += buf
while len(recvbuf) >= 4:
remainder = None
length = struct.unpack('>i', recvbuf[0:4])[0]
if len(recvbuf) < length:
# partial message, just wait for the
# end of message
break
# more than one message?
if length < len(recvbuf):
# save beginning of another message
remainder = recvbuf[length:]
recvbuf = recvbuf[0:length]
if not self.decode(recvbuf):
return 5
if remainder:
recvbuf = remainder
else:
recvbuf = b''
sys.stdout.write((prompt + message).decode())
sys.stdout.flush()
except Exception: # noqa: E722
traceback.print_exc()
self.send(b'quit')
return 0
def __del__(self):
print('Closing connection with', self.address)
time.sleep(0.5)
self.sock.close()
def main():
"""Main function."""
# parse command line arguments
parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
fromfile_prefix_chars='@',
description='Command-line program for testing WeeChat/relay protocol.',
epilog='''
Environment variable "QWEECHAT_PROTO_OPTIONS" can be set with default options.
Argument "@file.txt" can be used to read default options in a file.
Some commands can be piped to the script, for example:
echo "init password=xxxx" | {name} localhost 5000
{name} localhost 5000 < commands.txt
The script returns:
0: OK
2: wrong arguments (command line)
3: connection error
4: send error (message sent to WeeChat)
5: decode error (message received from WeeChat)
'''.format(name=NAME))
parser.add_argument('-6', '--ipv6', action='store_true',
help='connect using IPv6')
parser.add_argument('-d', '--debug', action='count', default=0,
help='debug mode: long objects view '
'(-dd: display raw messages)')
parser.add_argument('-v', '--version', action='version',
version=qweechat_version)
parser.add_argument('hostname',
help='hostname (or IP address) of machine running '
'WeeChat/relay')
parser.add_argument('port', type=int,
help='port of machine running WeeChat/relay')
if len(sys.argv) == 1:
parser.print_help()
sys.exit(0)
_args = parser.parse_args(
shlex.split(os.getenv('QWEECHAT_PROTO_OPTIONS') or '') + sys.argv[1:])
test = TestProto(_args)
# connect to WeeChat/relay
if not test.connect():
sys.exit(3)
# send commands from standard input if some data is available
if not test.send_stdin():
sys.exit(4)
# main loop (wait commands, display messages received)
returncode = test.mainloop()
del test
sys.exit(returncode)
if __name__ == "__main__":
main()

View file

@ -63,7 +63,7 @@ class IncomingCallWidget(widgets.CenteredWidget):
self.accept_video.clicked.connect(self.accept_call_with_video)
self.decline.clicked.connect(self.decline_call)
output_device_index = self._settings._args.audio['output']
output_device_index = self._settings._oArgs.audio['output']
if False and self._settings['calls_sound']:
class SoundPlay(QtCore.QThread):

View file

@ -44,7 +44,7 @@ class MessagesItemsFactory:
self._messages.setItemWidget(elem, item)
return item
# File "/var/local/src/toxygen/toxygen/file_transfers/file_transfers_handler.py", line 216, in transfer_finished
# self._file_transfers_message_service.add_inline_message(transfer, index)
# File "/var/local/src/toxygen/toxygen/file_transfers/file_transfers_messages_service.py", line 47, in add_inline_message

View file

@ -1,9 +1,8 @@
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
import os
import logging
from PyQt5 import uic
from PyQt5 import QtWidgets, QtGui
from PyQt5 import QtCore, QtGui, QtWidgets
from qtpy.QtGui import (QColor, QTextCharFormat, QFont, QSyntaxHighlighter)
from ui.contact_items import *
@ -13,6 +12,7 @@ import utils.util as util
import utils.ui as util_ui
from user_data.settings import Settings
import logging
global LOG
LOG = logging.getLogger('app.'+'mains')
@ -63,7 +63,7 @@ else:
else:
bg = 'black'
def hl_format(color, style=''):
"""Return a QTextCharFormat with the given attributes.
unused
"""
@ -94,7 +94,6 @@ else:
'inprompt': hl_format('lightBlue', 'bold'),
'outprompt': hl_format('lightRed', 'bold'),
}
class QTextEditLogger(logging.Handler):
def __init__(self, parent, app):
@ -177,6 +176,7 @@ class MainWindow(QtWidgets.QMainWindow):
iMAX = settings['width'] * 2/3 / settings['message_font_size']
self._me = LogDialog(self, app)
self._pe = None
self._we = None
def set_dependencies(self, widget_factory, tray, contacts_manager, messenger, profile, plugins_loader,
file_transfer_handler, history_loader, calls_manager, groups_service, toxes, app):
@ -250,6 +250,8 @@ class MainWindow(QtWidgets.QMainWindow):
self.actionLog_console.setObjectName("actionLog_console")
self.actionPython_console = QtWidgets.QAction(window)
self.actionPython_console.setObjectName("actionLog_console")
self.actionWeechat_console = QtWidgets.QAction(window)
self.actionWeechat_console.setObjectName("actionLog_console")
self.updateSettings = QtWidgets.QAction(window)
self.actionSettings = QtWidgets.QAction(window)
self.actionSettings.setObjectName("actionSettings")
@ -290,6 +292,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.menuPlugins.addAction(self.reloadToxchat)
self.menuPlugins.addAction(self.actionLog_console)
self.menuPlugins.addAction(self.actionPython_console)
self.menuPlugins.addAction(self.actionWeechat_console)
self.menuAbout.addAction(self.actionAbout_program)
@ -307,6 +310,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.actionAbout_program.triggered.connect(self.about_program)
self.actionLog_console.triggered.connect(self.log_console)
self.actionPython_console.triggered.connect(self.python_console)
self.actionWeechat_console.triggered.connect(self.weechat_console)
self.actionNetwork.triggered.connect(self.network_settings)
self.actionAdd_friend.triggered.connect(self.add_contact_triggered)
self.createGC.triggered.connect(self.create_gc)
@ -361,6 +365,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.actionAbout_program.setText(util_ui.tr("About program"))
self.actionLog_console.setText(util_ui.tr("Console Log"))
self.actionPython_console.setText(util_ui.tr("Python Console"))
self.actionWeechat_console.setText(util_ui.tr("Weechat Console"))
self.actionTest_tox.setText(util_ui.tr("Bootstrap"))
self.actionTest_nmap.setText(util_ui.tr("Test Nodes"))
self.actionTest_main.setText(util_ui.tr("Test Program"))
@ -655,44 +660,113 @@ class MainWindow(QtWidgets.QMainWindow):
self._me.show()
def python_console(self):
if PythonConsole:
app = self._app
if app and app._settings:
size = app._settings['message_font_size']
font_name = app._settings['font']
else:
size = 12
font_name = "Courier New"
if not PythonConsole: return
app = self._app
if app and app._settings:
size = app._settings['message_font_size']
font_name = app._settings['font']
else:
size = 12
font_name = "Courier New"
size = font_width = 10
font_name = "DejaVu Sans Mono"
size = font_width = 10
font_name = "DejaVu Sans Mono"
try:
if not self._pe:
self._pe = PythonConsole(formats=aFORMATS)
self._pe.setWindowTitle('variable: app is the application')
try:
if not self._pe:
self._pe = PythonConsole(formats=aFORMATS)
self._pe.setWindowTitle('variable: app is the application')
# self._pe.edit.setStyleSheet('foreground: white; background-color: black;}')
# Fix the pyconsole geometry
font = self._pe.edit.document().defaultFont()
# Fix the pyconsole geometry
font = self._pe.edit.document().defaultFont()
font.setFamily(font_name)
font.setBold(True)
if font_width is None:
font_width = QFontMetrics(font).width('M')
self._pe.setFont(font)
geometry = self._pe.geometry()
geometry.setWidth(font_width*50+20)
geometry.setHeight(font_width*24*13/8)
self._pe.setGeometry(geometry)
self._pe.resize(font_width*50+20, font_width*24*13/8)
self._pe.show()
self._pe.eval_queued()
# or self._pe.eval_in_thread()
return
except Exception as e:
LOG.debug(e)
def weechat_console(self):
if self._we:
self._we.show()
return
LOG.info("Loading WeechatConsole")
from third_party.qweechat import qweechat
from third_party.qweechat import config
try:
# WeeChat backported from PySide6 to PyQt5
LOG.info("Adding WeechatConsole")
class WeechatConsole(qweechat.MainWindow):
def __init__(self, *args):
qweechat.MainWindow.__init__(self, *args)
def closeEvent(self, event):
"""Called when QWeeChat window is closed."""
self.network.disconnect_weechat()
if self.network.debug_dialog:
self.network.debug_dialog.close()
qweechat.config.write(self.config)
except Exception as e:
LOG.exception(f"ERROR WeechatConsole {e}")
MainWindow = None
return
app = self._app
if app and app._settings:
size = app._settings['message_font_size']
font_name = app._settings['font']
else:
size = 12
font_name = "Courier New"
font_name = "DejaVu Sans Mono"
try:
LOG.info("Creating WeechatConsole")
self._we = WeechatConsole()
self._we.show()
self._we.setWindowTitle('File/Connect to 127.0.0.1:9000')
# Fix the pyconsole geometry
try:
font = self._we.buffers[0].widget.chat.defaultFont()
font.setFamily(font_name)
font.setBold(True)
if font_width is None:
font_width = QFontMetrics(font).width('M')
self._pe.setFont(font)
geometry = self._pe.geometry()
geometry.setWidth(font_width*80+20)
geometry.setHeight(font_width*40)
self._pe.setGeometry(geometry)
self._pe.resize(font_width*80+20, font_width*40)
self._pe.show()
self._pe.eval_queued()
# or self._pe.eval_in_thread()
return
self._we.setFont(font)
except Exception as e:
LOG.debug(e)
self._me.show()
# LOG.debug(e)
font_width = size
geometry = self._we.geometry()
geometry.setWidth(font_width*80+20)
geometry.setHeight(int(font_width*(2+24)*11/8))
self._we.setGeometry(geometry)
#? QtCore.QSize()
self._we.resize(font_width*80+20, int(font_width*(2+24)*11/8))
self._we.list_buffers.setSizePolicy(QtWidgets.QSizePolicy.Preferred,
QtWidgets.QSizePolicy.Preferred)
self._we.stacked_buffers.setSizePolicy(QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Expanding)
LOG.info("Showing WeechatConsole")
self._we.show()
# or self._we.eval_in_thread()
return
except Exception as e:
LOG.exception(f"Error creating WeechatConsole {e}")
def about_program(self):
# TODO: replace with window

View file

@ -10,6 +10,8 @@ import utils.util as util
import utils.ui as util_ui
from stickers.stickers import load_stickers
import logging
LOG = logging.getLogger('app.'+'msw')
class MessageArea(QtWidgets.QPlainTextEdit):
"""User types messages here"""
@ -36,7 +38,7 @@ class MessageArea(QtWidgets.QPlainTextEdit):
self.pasteEvent(url.toString())
else:
self.pasteEvent()
elif event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
modifiers = event.modifiers()
if modifiers & QtCore.Qt.ControlModifier or modifiers & QtCore.Qt.ShiftModifier:
@ -51,10 +53,10 @@ class MessageArea(QtWidgets.QPlainTextEdit):
LOG.error(f"keyPressEvent ERROR send_message to {self._messenger}")
util_ui.message_box(str(e),
util_ui.tr(f"keyPressEvent ERROR send_message to {self._messenger}"))
elif event.key() == QtCore.Qt.Key_Up and not self.toPlainText():
self.appendPlainText(self._messenger.get_last_message())
elif event.key() == QtCore.Qt.Key_Tab and self._contacts_manager.is_active_a_group():
text = self.toPlainText()
text_cursor = self.textCursor()

View file

@ -27,6 +27,7 @@ class AddContact(CenteredWidget):
uic.loadUi(get_views_path('add_contact_screen'), self)
self._update_ui(tox_id)
self._adding = False
self._bootstrap = False
def _update_ui(self, tox_id):
self.toxIdLineEdit = LineEdit(self)
@ -80,6 +81,7 @@ class AddBootstrap(CenteredWidget):
uic.loadUi(get_views_path('add_bootstrap_screen'), self)
self._update_ui(tox_id)
self._adding = False
self._bootstrap = False
def _update_ui(self, tox_id):
self.toxIdLineEdit = LineEdit(self)

View file

@ -7,7 +7,7 @@ import re
from ui.widgets import *
from messenger.messages import MESSAGE_AUTHOR
from file_transfers.file_transfers import *
from PyQt5 import QtCore, QtGui, QtWidgets
class MessageBrowser(QtWidgets.QTextBrowser):
@ -39,7 +39,16 @@ class MessageBrowser(QtWidgets.QTextBrowser):
font.setPixelSize(settings['message_font_size'])
font.setBold(False)
self.setFont(font)
self.resize(width, self.document().size().height())
try:
# was self.resize(width, self.document().size().height())
# guessing QSize
self.resize(QtCore.QSize(width, int(self.document().size().height())))
except TypeError as e:
# TypeError: arguments did not match any overloaded call:
# 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)

View file

@ -33,7 +33,7 @@ class PeerScreen(CenteredWidget):
self.statusCircle.update(self._peer.status)
self.peerNameLabel.setText(self._peer.name)
self.ignorePeerCheckBox.setChecked(self._peer.is_muted)
self.ignorePeerCheckBox.clicked.connect(self._toggle_ignore)
self.sendPrivateMessagePushButton.clicked.connect(self._send_private_message)
self.copyPublicKeyPushButton.clicked.connect(self._copy_public_key)

View file

@ -3,6 +3,9 @@ from PyQt5 import QtCore, QtGui, QtWidgets
import utils.ui as util_ui
import logging
global LOG
LOG = logging.getLogger('app')
class DataLabel(QtWidgets.QLabel):
"""
Label with elided text
@ -11,13 +14,17 @@ class DataLabel(QtWidgets.QLabel):
try:
text = ''.join('\u25AF' if len(bytes(str(c), 'utf-8')) >= 4 else c for c in str(text))
except Exception as e:
logging.error(f"DataLabel::setText: {e}")
LOG.error(f"DataLabel::setText: {e}")
return
metrics = QtGui.QFontMetrics(self.font())
text = metrics.elidedText(str(text), QtCore.Qt.ElideRight, self.width())
super().setText(text)
try:
metrics = QtGui.QFontMetrics(self.font())
text = metrics.elidedText(str(text), QtCore.Qt.ElideRight, self.width())
except Exception as e:
# RuntimeError: wrapped C/C++ object of type DataLabel has been deleted
text = str(text)
super().setText(text)
class ComboBox(QtWidgets.QComboBox):

View file

@ -138,7 +138,8 @@ class Settings(dict):
self._profile_path = path.replace('.json', '.tox')
self._toxes = toxes
self._app = app
self._oArgs = app._oArgs
self._args = app._args
self._oArgs = app._args
self._log = lambda l: LOG.log(self._oArgs.loglevel, l)
self._settings_saved_event = Event()
@ -155,29 +156,29 @@ class Settings(dict):
text = title + path
LOG.error(title +str(ex))
util_ui.message_box(text, title)
info = Settings.get_default_settings(app._oArgs)
info = Settings.get_default_settings(app._args)
user_data.settings.clean_settings(info)
else:
LOG.debug('get_default_settings for: ' + repr(path))
info = Settings.get_default_settings(app._oArgs)
info = Settings.get_default_settings(app._args)
if not os.path.exists(path):
merge_args_into_settings(app._oArgs, info)
merge_args_into_settings(app._args, info)
else:
aC = self._changed(app._oArgs, info)
aC = self._changed(app._args, info)
if aC:
title = 'Override profile with commandline - '
if path:
title += os.path.basename(path)
text = 'Override profile with command-line settings? \n'
# text += '\n'.join([str(key) +'=' +str(val) for
# key,val in self._changed(app._oArgs).items()])
# key,val in self._changed(app._args).items()])
text += repr(aC)
reply = util_ui.question(text, title)
if reply:
merge_args_into_settings(app._oArgs, info)
info['audio'] = getattr(app._oArgs, 'audio')
info['video'] = getattr(app._oArgs, 'video')
merge_args_into_settings(app._args, info)
info['audio'] = getattr(app._args, 'audio')
info['video'] = getattr(app._args, 'video')
super().__init__(info)
self._upgrade()

View file

@ -1,11 +1,11 @@
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
import datetime
import os
import time
import platform
import re
import shutil
import sys
import re
import platform
import datetime
import time
def cached(func):

View file

@ -1,5 +1,5 @@
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
# You need a libs directory beside this directory
# You need a libs directory beside this directory
# and you need to link your libtoxcore.so and libtoxav.so
# and libtoxencryptsave.so into ../libs/
# Link all 3 to libtoxcore.so if you have only libtoxcore.so

View file

@ -14,6 +14,14 @@ except ImportError:
sLIBS_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)),
'libs')
# environment variable TOXCORE_LIBS overrides
d = os.environ.get('TOXCORE_LIBS', '')
if d and os.path.exists(d):
sLIBS_DIR = d
if os.environ.get('DEBUG', ''):
print ('DBUG: Setting TOXCORE_LIBS to ' +d)
del d
class LibToxCore:
def __init__(self):
@ -28,7 +36,6 @@ class LibToxCore:
# libtoxcore and libsodium may be installed in your os
# give libs/ precedence
libFile = os.path.join(sLIBS_DIR, libtoxcore)
assert os.path.isfile(libFile), libFile
if os.path.isfile(libFile):
self._libtoxcore = CDLL(libFile)
else:
@ -48,7 +55,6 @@ class LibToxAV:
self._libtoxav = CDLL('libtoxcore.dylib')
else:
libFile = os.path.join(sLIBS_DIR, 'libtoxav.so')
assert os.path.isfile(libFile), libFile
if os.path.isfile(libFile):
self._libtoxav = CDLL(libFile)
else:
@ -70,7 +76,6 @@ class LibToxEncryptSave:
self._lib_tox_encrypt_save = CDLL('libtoxcore.dylib')
else:
libFile = os.path.join(sLIBS_DIR, 'libtoxencryptsave.so')
assert os.path.isfile(libFile), libFile
if os.path.isfile(libFile):
self._lib_tox_encrypt_save = CDLL(libFile)
else:

View file

@ -3,19 +3,30 @@ from ctypes import *
from datetime import datetime
try:
from wrapper.toxcore_enums_and_consts import *
from wrapper.toxav import ToxAV
from wrapper.libtox import LibToxCore
from wrapper.toxav import ToxAV
from wrapper.toxcore_enums_and_consts import *
except:
from toxcore_enums_and_consts import *
from toxav import ToxAV
from libtox import LibToxCore
from toxav import ToxAV
from toxcore_enums_and_consts import *
# callbacks can be called in any thread so were being careful
# tox.py can be called by callbacks
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)
def LOG_INFO(a):
bVERBOSE = hasattr(__builtins__, 'app') and app.oArgs.loglevel <= 20
if bVERBOSE: print('INFO> '+a)
def LOG_DEBUG(a):
bVERBOSE = hasattr(__builtins__, 'app') and app.oArgs.loglevel <= 10
if bVERBOSE: print('DBUG> '+a)
def LOG_TRACE(a):
bVERBOSE = hasattr(__builtins__, 'app') and app.oArgs.loglevel < 10
if bVERBOSE: print('TRAC> '+a)
UINT32_MAX = 2 ** 32 -1
class ToxError(RuntimeError): pass
global aTIMES
aTIMES=dict()
@ -57,7 +68,6 @@ class ToxOptions(Structure):
]
class GroupChatSelfPeerInfo(Structure):
_fields_ = [
('nick', c_char_p),
@ -109,11 +119,11 @@ class Tox:
raise MemoryError('The function was unable to allocate enough '
'memory to store the internal structures for the Tox object.')
if tox_err_new == TOX_ERR_NEW['PORT_ALLOC']:
raise RuntimeError('The function was unable to bind to a port. This may mean that all ports have '
raise ToxError('The function was unable to bind to a port. This may mean that all ports have '
'already been bound, e.g. by other Tox instances, or it may mean a permission error.'
' You may be able to gather more information from errno.')
if tox_err_new == TOX_ERR_NEW['TCP_SERVER_ALLOC']:
raise RuntimeError('The function was unable to bind the tcp server port.')
raise ToxError('The function was unable to bind the tcp server port.')
if tox_err_new == TOX_ERR_NEW['PROXY_BAD_TYPE']:
raise ArgumentError('proxy_type was invalid.')
if tox_err_new == TOX_ERR_NEW['PROXY_BAD_HOST']:
@ -165,7 +175,7 @@ class Tox:
def kill(self):
if hasattr(self, 'AV'): del self.AV
LOG_DEBUG(f"tox_kill")
LOG_INFO(f"tox_kill")
try:
Tox.libtoxcore.tox_kill(self._tox_pointer)
except Exception as e:
@ -213,7 +223,7 @@ class Tox:
return result
if tox_err_options_new == TOX_ERR_OPTIONS_NEW['MALLOC']:
raise MemoryError('The function failed to allocate enough memory for the options struct.')
raise RuntimeError('The function did not return OK for the options struct.')
raise ToxError('The function did not return OK for the options struct.')
@staticmethod
def options_free(tox_options):
@ -295,7 +305,7 @@ class Tox:
raise ArgumentError('One of the arguments to the function was NULL when it was not expected.')
if tox_err_bootstrap == TOX_ERR_BOOTSTRAP['BAD_HOST']:
raise ArgumentError('The address could not be resolved to an IP '
'address, or the IP address passed was invalid.')
'address, or the address passed was invalid.')
if tox_err_bootstrap == TOX_ERR_BOOTSTRAP['BAD_PORT']:
raise ArgumentError('The port passed was invalid. The valid port range is (1, 65535).')
# me - this seems wrong - should be False
@ -405,6 +415,9 @@ class Tox:
# Internal client information (Tox address/id)
# -----------------------------------------------------------------------------------------------------------------
def self_get_toxid(self, address=None):
return self.self_get_address(address)
def self_get_address(self, address=None):
"""
Writes the Tox friend address of the client to a byte array. The address is not in human-readable format. If a
@ -652,7 +665,7 @@ class Tox:
raise ArgumentError('The friend was already there, but the nospam value was different.')
if tox_err_friend_add == TOX_ERR_FRIEND_ADD['MALLOC']:
raise MemoryError('A memory allocation failed when trying to increase the friend list size.')
raise RuntimeError('The function did not return OK for the friend add.')
raise ToxError('The function did not return OK for the friend add.')
def friend_add_norequest(self, public_key):
"""Add a friend without sending a friend request.
@ -698,7 +711,7 @@ class Tox:
raise ArgumentError('The friend was already there, but the nospam value was different.')
if tox_err_friend_add == TOX_ERR_FRIEND_ADD['MALLOC']:
raise MemoryError('A memory allocation failed when trying to increase the friend list size.')
raise RuntimeError('The function did not return OK for the friend add.')
raise ToxError('The function did not return OK for the friend add.')
def friend_delete(self, friend_number):
"""
@ -744,13 +757,14 @@ class Tox:
raise ArgumentError('One of the arguments to the function was NULL when it was not expected.')
if tox_err_friend_by_public_key == TOX_ERR_FRIEND_BY_PUBLIC_KEY['NOT_FOUND']:
raise ArgumentError('No friend with the given Public Key exists on the friend list.')
raise RuntimeError('The function did not return OK for the friend by public key.')
raise ToxError('The function did not return OK for the friend by public key.')
def friend_exists(self, friend_number):
"""
Checks if a friend with the given friend number exists and returns true if it does.
"""
return bool(Tox.libtoxcore.tox_friend_exists(self._tox_pointer, c_uint32(friend_number)))
# bool() -> TypeError: 'str' object cannot be interpreted as an integer
return Tox.libtoxcore.tox_friend_exists(self._tox_pointer, c_uint32(friend_number))
def self_get_friend_list_size(self):
"""
@ -819,7 +833,7 @@ class Tox:
return result
elif tox_err_last_online == TOX_ERR_FRIEND_GET_LAST_ONLINE['FRIEND_NOT_FOUND']:
raise ArgumentError('No friend with the given number exists on the friend list.')
raise RuntimeError('The function did not return OK')
raise ToxError('The function did not return OK')
# -----------------------------------------------------------------------------------------------------------------
# Friend-specific state queries (can also be received through callbacks)
@ -832,7 +846,7 @@ class Tox:
The return value is equal to the `length` argument received by the last `friend_name` callback.
"""
tox_err_friend_query = c_int()
LOG_DEBUG(f"tox_friend_get_name_size")
LOG_TRACE(f"tox_friend_get_name_size")
result = Tox.libtoxcore.tox_friend_get_name_size(self._tox_pointer,
c_uint32(friend_number),
byref(tox_err_friend_query))
@ -845,7 +859,7 @@ class Tox:
' NULL, these functions return an error in that case.')
elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['FRIEND_NOT_FOUND']:
raise ArgumentError('The friend_number did not designate a valid friend.')
raise RuntimeError('The function did not return OK')
raise ToxError('The function did not return OK')
def friend_get_name(self, friend_number, name=None):
"""
@ -874,7 +888,7 @@ class Tox:
' NULL, these functions return an error in that case.')
elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['FRIEND_NOT_FOUND']:
raise ArgumentError('The friend_number did not designate a valid friend.')
raise RuntimeError('The function did not return OK')
raise ToxError('The function did not return OK')
def callback_friend_name(self, callback):
"""
@ -907,7 +921,7 @@ class Tox:
:return: length of the friend's status message
"""
tox_err_friend_query = c_int()
LOG_DEBUG(f"tox_friend_get_status_message_size")
LOG_TRACE(f"tox_friend_get_status_message_size")
result = Tox.libtoxcore.tox_friend_get_status_message_size(self._tox_pointer, c_uint32(friend_number),
byref(tox_err_friend_query))
tox_err_friend_query = tox_err_friend_query.value
@ -949,7 +963,7 @@ class Tox:
' NULL, these functions return an error in that case.')
elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['FRIEND_NOT_FOUND']:
raise ArgumentError('The friend_number did not designate a valid friend.')
raise RuntimeError('The function did not return OK')
raise ToxError('The function did not return OK')
def callback_friend_status_message(self, callback):
"""
@ -1044,7 +1058,7 @@ class Tox:
' NULL, these functions return an error in that case.')
elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['FRIEND_NOT_FOUND']:
raise ArgumentError('The friend_number did not designate a valid friend.')
raise RuntimeError('The function did not return OK for friend get connection status.')
raise ToxError('The function did not return OK for friend get connection status.')
def callback_friend_connection_status(self, callback):
"""
@ -1138,27 +1152,31 @@ class Tox:
return bool(result)
if tox_err_set_typing == TOX_ERR_SET_TYPING['FRIEND_NOT_FOUND']:
raise ArgumentError('The friend number did not designate a valid friend.')
raise RuntimeError('The function did not return OK for set typing.')
raise ToxError('The function did not return OK for set typing.')
def friend_send_message(self, friend_number, message_type, message):
"""
Send a text chat message to an online friend.
"""Send a text chat message to an online friend.
This function creates a chat message packet and pushes it into the send queue.
The message length may not exceed TOX_MAX_MESSAGE_LENGTH. Larger messages must be split by the client and sent
as separate messages. Other clients can then reassemble the fragments. Messages may not be empty.
The message length may not exceed
TOX_MAX_MESSAGE_LENGTH. Larger messages must be split by the
client and sent as separate messages. Other clients can then
reassemble the fragments. Messages may not be empty.
The return value of this function is the message ID. If a read receipt is received, the triggered
`friend_read_receipt` event will be passed this message ID.
The return value of this function is the message ID. If a read
receipt is received, the triggered `friend_read_receipt` event
will be passed this message ID.
Message IDs are unique per friend. The first message ID is 0. Message IDs are incremented by 1 each time a
message is sent. If UINT32_MAX messages were sent, the next message ID is 0.
Message IDs are unique per friend. The first message ID is 0.
Message IDs are incremented by 1 each time a message is sent.
If UINT32_MAX messages were sent, the next message ID is 0.
:param friend_number: The friend number of the friend to send the message to.
:param message_type: Message type (TOX_MESSAGE_TYPE).
:param message: A non-None message text.
:return: message ID
"""
tox_err_friend_send_message = c_int()
LOG_DEBUG(f"tox_friend_send_message")
@ -1180,7 +1198,7 @@ class Tox:
raise ArgumentError('Message length exceeded TOX_MAX_MESSAGE_LENGTH.')
elif tox_err_friend_send_message == TOX_ERR_FRIEND_SEND_MESSAGE['EMPTY']:
raise ArgumentError('Attempted to send a zero-length message.')
raise RuntimeError('The function did not return OK for friend send message.')
raise ToxError('The function did not return OK for friend send message.')
def callback_friend_read_receipt(self, callback):
"""
@ -1310,15 +1328,15 @@ class Tox:
elif tox_err_file_control == TOX_ERR_FILE_CONTROL['NOT_FOUND']:
raise ArgumentError('No file transfer with the given file number was found for the given friend.')
elif tox_err_file_control == TOX_ERR_FILE_CONTROL['NOT_PAUSED']:
raise RuntimeError('A RESUME control was sent, but the file transfer is running normally.')
raise ToxError('A RESUME control was sent, but the file transfer is running normally.')
elif tox_err_file_control == TOX_ERR_FILE_CONTROL['DENIED']:
raise RuntimeError('A RESUME control was sent, but the file transfer was paused by the other party. Only '
raise ToxError('A RESUME control was sent, but the file transfer was paused by the other party. Only '
'the party that paused the transfer can resume it.')
elif tox_err_file_control == TOX_ERR_FILE_CONTROL['ALREADY_PAUSED']:
raise RuntimeError('A PAUSE control was sent, but the file transfer was already paused.')
raise ToxError('A PAUSE control was sent, but the file transfer was already paused.')
elif tox_err_file_control == TOX_ERR_FILE_CONTROL['SENDQ']:
raise RuntimeError('Packet queue is full.')
raise RuntimeError('The function did not return OK for file control.')
raise ToxError('Packet queue is full.')
raise ToxError('The function did not return OK for file control.')
def callback_file_recv_control(self, callback):
"""
@ -1381,8 +1399,8 @@ class Tox:
elif tox_err_file_seek == TOX_ERR_FILE_SEEK['INVALID_POSITION']:
raise ArgumentError('Seek position was invalid')
elif tox_err_file_seek == TOX_ERR_FILE_SEEK['SENDQ']:
raise RuntimeError('Packet queue is full.')
raise RuntimeError('The function did not return OK')
raise ToxError('Packet queue is full.')
raise ToxError('The function did not return OK')
def file_get_file_id(self, friend_number, file_number, file_id=None):
"""
@ -1490,9 +1508,9 @@ class Tox:
if err_file == TOX_ERR_FILE_SEND['NAME_TOO_LONG']:
raise ArgumentError('Filename length exceeded TOX_MAX_FILENAME_LENGTH bytes.')
if err_file == TOX_ERR_FILE_SEND['TOO_MANY']:
raise RuntimeError('Too many ongoing transfers. The maximum number of concurrent file transfers is 256 per'
raise ToxError('Too many ongoing transfers. The maximum number of concurrent file transfers is 256 per'
'friend per direction (sending and receiving).')
raise RuntimeError('The function did not return OK')
raise ToxError('The function did not return OK')
def file_send_chunk(self, friend_number, file_number, position, data):
"""
@ -1535,10 +1553,10 @@ class Tox:
'adjusted according to maximum transmission unit and the expected end of the file. '
'Trying to send less or more than requested will return this error.')
elif tox_err_file_send_chunk == TOX_ERR_FILE_SEND_CHUNK['SENDQ']:
raise RuntimeError('Packet queue is full.')
raise ToxError('Packet queue is full.')
elif tox_err_file_send_chunk == TOX_ERR_FILE_SEND_CHUNK['WRONG_POSITION']:
raise ArgumentError('Position parameter was wrong.')
raise RuntimeError('The function did not return OK')
raise ToxError('The function did not return OK')
def callback_file_chunk_request(self, callback):
"""
@ -1688,8 +1706,8 @@ class Tox:
elif tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['TOO_LONG']:
raise ArgumentError('Packet data length exceeded TOX_MAX_CUSTOM_PACKET_SIZE.')
elif tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['SENDQ']:
raise RuntimeError('Packet queue is full.')
raise RuntimeError('The function did not return OK')
raise ToxError('Packet queue is full.')
raise ToxError('The function did not return OK')
def friend_send_lossless_packet(self, friend_number, data):
"""
@ -1726,7 +1744,7 @@ class Tox:
elif tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['TOO_LONG']:
raise ArgumentError('Packet data length exceeded TOX_MAX_CUSTOM_PACKET_SIZE.')
elif tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['SENDQ']:
raise RuntimeError('Packet queue is full.')
raise ToxError('Packet queue is full.')
def callback_friend_lossy_packet(self, callback):
"""
@ -1808,8 +1826,8 @@ class Tox:
if tox_err_get_port == TOX_ERR_GET_PORT['OK']:
return result
if tox_err_get_port == TOX_ERR_GET_PORT['NOT_BOUND']:
raise RuntimeError('The instance was not bound to any port.')
raise RuntimeError('The function did not return OK')
raise ToxError('The instance was not bound to any port.')
raise ToxError('The function did not return OK')
def self_get_tcp_port(self):
"""
@ -1823,8 +1841,8 @@ class Tox:
if tox_err_get_port == TOX_ERR_GET_PORT['OK']:
return result
if tox_err_get_port == TOX_ERR_GET_PORT['NOT_BOUND']:
raise RuntimeError('The instance was not bound to any port.')
raise RuntimeError('The function did not return OK')
raise ToxError('The instance was not bound to any port.')
raise ToxError('The function did not return OK')
# -----------------------------------------------------------------------------------------------------------------
# Group chat instance management
@ -1865,18 +1883,26 @@ class Tox:
else:
nick_length = len(nick)
cnick = c_char_p(nick)
result = Tox.libtoxcore.tox_group_new(self._tox_pointer, privacy_state,
result = Tox.libtoxcore.tox_group_new(self._tox_pointer,
privacy_state,
group_name,
len(group_name),
cnick, nick_length,
cnick,
nick_length,
byref(error))
if error.value:
LOG_ERROR(f"group_new {error.value}")
raise RuntimeError("group_new {error.value}")
# -1 TOX_ERR_GROUP_NEW_TOO_LONG
# -2 TOX_ERR_GROUP_NEW_EMPTY
# -3 TOX_ERR_GROUP_NEW_INIT
# -4 TOX_ERR_GROUP_NEW_STATE
# -5 TOX_ERR_GROUP_NEW_ANNOUNCE
if error.value in TOX_ERR_GROUP_NEW:
LOG_ERROR(f"group_new {error.value} {TOX_ERR_GROUP_NEW[error.value]}")
raise ToxError(f"group_new {error.value}")
return result
def group_join(self, chat_id, password, nick, status):
def group_join(self, chat_id, password, nick, status=''):
"""Joins a group chat with specified Chat ID.
This function creates a new group chat object, adds it to the
@ -1917,8 +1943,8 @@ class Tox:
byref(error))
if error.value:
LOG_ERROR(f"group_join {error.value}")
raise RuntimeError("group_join {error.value}")
LOG_ERROR(f"group_join {error.value} {TOX_ERR_GROUP_JOIN[error.value]}")
raise ToxError(f"group_join {error.value} {TOX_ERR_GROUP_JOIN[error.value]}")
return result
def group_reconnect(self, group_number):
@ -1937,7 +1963,7 @@ class Tox:
result = Tox.libtoxcore.tox_group_reconnect(self._tox_pointer, group_number, byref(error))
if error.value:
LOG_ERROR(f"group_reconnect {error.value}")
raise RuntimeError(f"group_reconnect {error.value}")
raise ToxError(f"group_reconnect {error.value}")
return result
def group_is_connected(self, group_number):
@ -1946,7 +1972,7 @@ class Tox:
result = Tox.libtoxcore.tox_group_is_connected(self._tox_pointer, group_number, byref(error))
if error.value:
LOG_ERROR(f"group_is_connected {error.value}")
raise RuntimeError("group_is_connected {error.value}")
raise ToxError("group_is_connected {error.value}")
return result
def group_disconnect(self, group_number):
@ -1955,10 +1981,10 @@ class Tox:
result = Tox.libtoxcore.tox_group_disconnect(self._tox_pointer, group_number, byref(error))
if error.value:
LOG_ERROR(f"group_disconnect {error.value}")
raise RuntimeError("group_disconnect {error.value}")
raise ToxError("group_disconnect {error.value}")
return result
def group_leave(self, group_number, message=''):
def group_leave(self, group_number, message=None):
"""Leaves a group.
This function sends a parting packet containing a custom
@ -1979,10 +2005,10 @@ class Tox:
f = Tox.libtoxcore.tox_group_leave
f.restype = c_bool
result = f(self._tox_pointer, group_number, message,
len(message) if message is not None else 0, byref(error))
len(message) if message else 0, byref(error))
if error.value:
LOG_ERROR(f"group_leave {error.value}")
raise RuntimeError("group_leave {error.value}")
raise ToxError("group_leave {error.value}")
return result
# -----------------------------------------------------------------------------------------------------------------
@ -2008,7 +2034,7 @@ class Tox:
result = Tox.libtoxcore.tox_group_self_set_name(self._tox_pointer, group_number, name, len(name), byref(error))
if error.value:
LOG_ERROR(f"group_self_set_name {error.value}")
raise RuntimeError("group_self_set_name {error.value}")
raise ToxError("group_self_set_name {error.value}")
return result
def group_self_get_name_size(self, group_number):
@ -2021,11 +2047,11 @@ class Tox:
"""
error = c_int()
LOG_DEBUG(f"tox_group_self_get_name_size")
LOG_TRACE(f"tox_group_self_get_name_size")
result = Tox.libtoxcore.tox_group_self_get_name_size(self._tox_pointer, group_number, byref(error))
if error.value:
LOG_ERROR(f"group_self_get_name_size {error.value}")
raise RuntimeError("group_self_get_name_size {error.value}")
raise ToxError("group_self_get_name_size {error.value}")
return result
def group_self_get_name(self, group_number):
@ -2048,7 +2074,7 @@ class Tox:
result = Tox.libtoxcore.tox_group_self_get_name(self._tox_pointer, group_number, name, byref(error))
if error.value:
LOG_ERROR(f"group_self_get_name {error.value}")
raise RuntimeError("group_self_get_name {error.value}")
raise ToxError("group_self_get_name {error.value}")
return str(name[:size], 'utf-8', errors='ignore')
def group_self_set_status(self, group_number, status):
@ -2063,7 +2089,7 @@ class Tox:
result = Tox.libtoxcore.tox_group_self_set_status(self._tox_pointer, group_number, status, byref(error))
if error.value:
LOG_ERROR(f"group_self_set_status {error.value}")
raise RuntimeError("group_self_set_status {error.value}")
raise ToxError("group_self_set_status {error.value}")
return result
def group_self_get_status(self, group_number):
@ -2077,7 +2103,7 @@ class Tox:
result = Tox.libtoxcore.tox_group_self_get_status(self._tox_pointer, group_number, byref(error))
if error.value:
LOG_ERROR(f"group_self_get_status {error.value}")
raise RuntimeError("group_self_get_status {error.value}")
raise ToxError("group_self_get_status {error.value}")
return result
def group_self_get_role(self, group_number):
@ -2091,7 +2117,7 @@ class Tox:
result = Tox.libtoxcore.tox_group_self_get_role(self._tox_pointer, group_number, byref(error))
if error.value:
LOG_ERROR(f" {error.value}")
raise RuntimeError(f" {error.value}")
raise ToxError(f" {error.value}")
return result
def group_self_get_peer_id(self, group_number):
@ -2105,7 +2131,7 @@ class Tox:
result = Tox.libtoxcore.tox_group_self_get_peer_id(self._tox_pointer, group_number, byref(error))
if error.value:
LOG_ERROR(f" {error.value}")
raise RuntimeError("tox_group_self_get_peer_id {error.value}")
raise ToxError("tox_group_self_get_peer_id {error.value}")
return result
def group_self_get_public_key(self, group_number):
@ -2127,8 +2153,8 @@ class Tox:
result = Tox.libtoxcore.tox_group_self_get_public_key(self._tox_pointer, group_number,
key, byref(error))
if error.value:
LOG_ERROR(f" {error.value}")
raise RuntimeError(f" {error.value}")
LOG_ERROR(f" {TOX_ERR_FRIEND_GET_PUBLIC_KEY[error.value]}")
raise ToxError(f"{TOX_ERR_FRIEND_GET_PUBLIC_KEY[error.value]}")
return bin_to_string(key, TOX_GROUP_PEER_PUBLIC_KEY_SIZE)
# -----------------------------------------------------------------------------------------------------------------
@ -2148,7 +2174,7 @@ class Tox:
result = Tox.libtoxcore.tox_group_peer_get_name_size(self._tox_pointer, group_number, peer_id, byref(error))
if error.value:
LOG_ERROR(f" {error.value}")
raise RuntimeError(f" {error.value}")
raise ToxError(f" {error.value}")
LOG_TRACE(f"tox_group_peer_get_name_size")
return result
@ -2175,7 +2201,7 @@ class Tox:
result = Tox.libtoxcore.tox_group_peer_get_name(self._tox_pointer, group_number, peer_id, name, byref(error))
if error.value:
LOG_ERROR(f" {error.value}")
raise RuntimeError(f"tox_group_peer_get_name {error.value}")
raise ToxError(f"tox_group_peer_get_name {error.value}")
sRet = str(name[:], 'utf-8', errors='ignore')
return sRet
@ -2193,7 +2219,7 @@ class Tox:
result = Tox.libtoxcore.tox_group_peer_get_status(self._tox_pointer, group_number, peer_id, byref(error))
if error.value:
LOG_ERROR(f" {error.value}")
raise RuntimeError(f" {error.value}")
raise ToxError(f" {error.value}")
return result
def group_peer_get_role(self, group_number, peer_id):
@ -2210,7 +2236,7 @@ class Tox:
result = Tox.libtoxcore.tox_group_peer_get_role(self._tox_pointer, group_number, peer_id, byref(error))
if error.value:
LOG_ERROR(f" {error.value}")
raise RuntimeError(f" {error.value}")
raise ToxError(f" {error.value}")
return result
def group_peer_get_public_key(self, group_number, peer_id):
@ -2234,7 +2260,7 @@ class Tox:
key, byref(error))
if error.value:
LOG_ERROR(f" {error.value}")
raise RuntimeError(f" {error.value}")
raise ToxError(f" {error.value}")
return bin_to_string(key, TOX_GROUP_PEER_PUBLIC_KEY_SIZE)
def callback_group_peer_name(self, callback, user_data):
@ -2300,7 +2326,7 @@ class Tox:
else:
if error.value:
LOG_ERROR(f"group_set_topic {error.value}")
raise RuntimeError("group_set_topic {error.value}")
raise ToxError("group_set_topic {error.value}")
return result
def group_get_topic_size(self, group_number):
@ -2313,8 +2339,8 @@ class Tox:
"""
error = c_int()
LOG_TRACE(f"tox_group_get_topic_size")
try:
LOG_DEBUG(f"tox_group_get_topic_size")
result = Tox.libtoxcore.tox_group_get_topic_size(self._tox_pointer, group_number, byref(error))
except Exception as e:
LOG_WARN(f" Exception {e}")
@ -2322,8 +2348,7 @@ class Tox:
else:
if error.value:
LOG_ERROR(f" {error.value}")
raise RuntimeError(f" {error.value}")
LOG_DEBUG(f"tox_group_get_topic_size")
raise ToxError(f" {error.value}")
return result
def group_get_topic(self, group_number):
@ -2343,7 +2368,7 @@ class Tox:
result = Tox.libtoxcore.tox_group_get_topic(self._tox_pointer, group_number, topic, byref(error))
if error.value:
LOG_ERROR(f" {error.value}")
raise RuntimeError(f" {error.value}")
raise ToxError(f" {error.value}")
return str(topic[:size], 'utf-8', errors='ignore')
def group_get_name_size(self, group_number):
@ -2352,11 +2377,10 @@ class Tox:
return value is unspecified.
"""
error = c_int()
LOG_DEBUG(f"tox_group_get_name_size")
result = Tox.libtoxcore.tox_group_get_name_size(self._tox_pointer, group_number, byref(error))
if error.value:
LOG_ERROR(f" {error.value}")
raise RuntimeError(f" {error.value}")
raise ToxError(f" {error.value}")
LOG_TRACE(f"tox_group_get_name_size")
return int(result)
@ -2375,7 +2399,7 @@ class Tox:
name, byref(error))
if error.value:
LOG_ERROR(f" {error.value}")
raise RuntimeError(f" {error.value}")
raise ToxError(f" {error.value}")
return str(name[:size], 'utf-8', errors='ignore')
def group_get_chat_id(self, group_number):
@ -2385,14 +2409,23 @@ class Tox:
:return chat id.
"""
LOG_INFO(f"tox_group_get_id group_number={group_number}")
error = c_int()
buff = create_string_buffer(TOX_GROUP_CHAT_ID_SIZE)
result = Tox.libtoxcore.tox_group_get_chat_id(self._tox_pointer,
group_number,
buff, byref(error))
if error.value:
LOG_ERROR(f"tox_group_get_chat_id {error.value}")
raise RuntimeError(f" {error.value}")
if error.value == 1:
LOG_ERROR(f"tox_group_get_chat_id ERROR GROUP_STATE_QUERIES_GROUP_NOT_FOUND group_number={group_number}")
else:
LOG_ERROR(f"tox_group_get_chat_id group_number={group_number} error={error.value}")
raise ToxError(f"tox_group_get_chat_id {error.value}")
#
# QObject::setParent: Cannot set parent, new parent is in a different thread
# QObject::installEventFilter(): Cannot filter events for objects in a different thread.
# QBasicTimer::start: Timers cannot be started from another thread
LOG_TRACE(f"tox_group_get_chat_id")
return bin_to_string(buff, TOX_GROUP_CHAT_ID_SIZE)
@ -2409,12 +2442,13 @@ class Tox:
return result
def groups_get_list(self):
groups_list_size = self.group_get_number_groups()
groups_list = create_string_buffer(sizeof(c_uint32) * groups_list_size)
groups_list = POINTER(c_uint32)(groups_list)
LOG_DEBUG(f"tox_groups_get_list")
Tox.libtoxcore.tox_groups_get_list(self._tox_pointer, groups_list)
return groups_list[0:groups_list_size]
raise NotImplementedError('tox_groups_get_list')
# groups_list_size = self.group_get_number_groups()
# groups_list = create_string_buffer(sizeof(c_uint32) * groups_list_size)
# groups_list = POINTER(c_uint32)(groups_list)
# LOG_DEBUG(f"tox_groups_get_list")
# Tox.libtoxcore.tox_groups_get_list(self._tox_pointer, groups_list)
# return groups_list[0:groups_list_size]
def group_get_privacy_state(self, group_number):
"""
@ -2432,7 +2466,7 @@ class Tox:
result = Tox.libtoxcore.tox_group_get_privacy_state(self._tox_pointer, group_number, byref(error))
if error.value:
LOG_ERROR(f" {error.value}")
raise RuntimeError(f" {error.value}")
raise ToxError(f" {error.value}")
return result
def group_get_peer_limit(self, group_number):
@ -2451,7 +2485,7 @@ class Tox:
result = Tox.libtoxcore.tox_group_get_peer_limit(self._tox_pointer, group_number, byref(error))
if error.value:
LOG_ERROR(f" {error.value}")
raise RuntimeError(f" {error.value}")
raise ToxError(f" {error.value}")
return result
def group_get_password_size(self, group_number):
@ -2461,11 +2495,11 @@ class Tox:
"""
error = c_int()
LOG_DEBUG(f"tox_group_get_password_size")
LOG_TRACE(f"tox_group_get_password_size")
result = Tox.libtoxcore.tox_group_get_password_size(self._tox_pointer, group_number, byref(error))
if error.value:
LOG_ERROR(f" {error.value}")
raise RuntimeError(f" {error.value}")
raise ToxError(f" {error.value}")
return result
def group_get_password(self, group_number):
@ -2490,7 +2524,7 @@ class Tox:
password, byref(error))
if error.value:
LOG_ERROR(f" {error.value}")
raise RuntimeError(f" {error.value}")
raise ToxError(f" {error.value}")
return str(password[:size], 'utf-8', errors='ignore')
def callback_group_topic(self, callback, user_data):
@ -2603,7 +2637,7 @@ class Tox:
len(data), byref(error))
if error.value:
LOG_ERROR(f" {error.value}")
raise RuntimeError(f" {error.value}")
raise ToxError(f" {error.value}")
return result
def group_send_private_message(self, group_number, peer_id, message_type, message):
@ -2630,11 +2664,11 @@ class Tox:
message_type, message,
len(message), byref(error))
if error.value:
LOG_ERROR(f" {error.value}")
raise RuntimeError(f" {error.value}")
LOG_ERROR(f"group_send_private_message {TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE[error.value]}")
raise ToxError(f"group_send_private_message {TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE[error.value]}")
return result
def group_send_message(self, group_number, type, message):
def group_send_message(self, group_number, type_, message):
"""
Send a text chat message to the group.
@ -2646,7 +2680,7 @@ class Tox:
then reassemble the fragments. Messages may not be empty.
:param group_number: The group number of the group the message is intended for.
:param type: Message type (normal, action, ...).
:param type_: Message type (normal, action, ...).
:param message: A non-NULL pointer to the first element of a byte array containing the message text.
:return True on success.
@ -2660,7 +2694,7 @@ class Tox:
# bool tox_group_send_message(const Tox *tox, uint32_t group_number, Tox_Message_Type type, const uint8_t *message, size_t length, uint32_t *message_id, Tox_Err_Group_Send_Message *error)
result = Tox.libtoxcore.tox_group_send_message(self._tox_pointer,
group_number,
type,
type_,
message,
len(message),
# dunno
@ -2668,7 +2702,7 @@ class Tox:
byref(error))
if error.value:
LOG_ERROR(f" {error.value}")
raise RuntimeError(f" {error.value}")
raise ToxError(f" {error.value}")
return result
# -----------------------------------------------------------------------------------------------------------------
@ -2755,10 +2789,10 @@ class Tox:
if error.value:
s = sGetError(error.value, TOX_ERR_GROUP_INVITE_FRIEND)
LOG_ERROR(f"group_invite_friend {error.value} {s}")
raise RuntimeError(f"group_invite_friend {error.value} {s}")
raise ToxError(f"group_invite_friend {error.value} {s}")
return result
# API change
# API change - this no longer exists
# @staticmethod
# def group_self_peer_info_new():
# error = c_int()
@ -2767,7 +2801,8 @@ class Tox:
# result = f(byref(error))
# return result
def group_invite_accept(self, invite_data, friend_number, nick, status, password=None):
# status should be dropped
def group_invite_accept(self, invite_data, friend_number, nick, status='', password=None):
"""
Accept an invite to a group chat that the client previously received from a friend. The invite
is only valid while the inviter is present in the group.
@ -2780,21 +2815,38 @@ class Tox:
error = c_int()
f = Tox.libtoxcore.tox_group_invite_accept
f.restype = c_uint32
nick = bytes(nick, 'utf-8')
invite_data = bytes(invite_data, 'utf-8')
try:
nick = bytes(nick, 'utf-8')
except:
nick = b''
try:
if password is not None:
password = bytes(password, 'utf-8')
except:
password = None
invite_data = invite_data or b''
if False: # API change
peer_info = self.group_self_peer_info_new()
peer_info.contents.nick = c_char_p(nick)
peer_info.contents.nick_length = len(nick)
peer_info.contents.user_status = status
result = f(self._tox_pointer, c_uint32(friend_number), invite_data, len(invite_data),
nick, len(nick),
password, len(password) if password is not None else 0,
byref(error))
LOG_INFO(f"group_invite_accept friend_number={friend_number} nick={nick} {invite_data}")
try:
assert type(invite_data) == bytes
result = f(self._tox_pointer,
c_uint32(friend_number),
invite_data, len(invite_data),
c_char_p(nick), len(nick),
c_char_p(password), len(password) if password is not None else 0,
byref(error))
except Exception as e:
LOG_ERROR(f"group_invite_accept ERROR {e}")
raise ToxError(f"group_invite_accept ERROR {e}")
if error.value:
LOG_ERROR(f" {error.value}")
raise RuntimeError(f" {error.value}")
# The invite data is not in the expected format.
LOG_ERROR(f"group_invite_accept {TOX_ERR_GROUP_INVITE_ACCEPT[error.value]}")
raise ToxError(f"group_invite_accept {TOX_ERR_GROUP_INVITE_ACCEPT[error.value]} {error.value}")
return result
def callback_group_invite(self, callback, user_data):
@ -2815,7 +2867,6 @@ class Tox:
Tox.libtoxcore.tox_callback_group_invite(self._tox_pointer, POINTER(None)())
self.group_invite_cb = None
return
LOG_DEBUG(f"tox_callback_group_invite")
c_callback = CFUNCTYPE(None, c_void_p, c_uint32, POINTER(c_uint8), c_size_t,
POINTER(c_uint8), c_size_t, c_void_p)
self.group_invite_cb = c_callback(callback)
@ -2953,7 +3004,7 @@ class Tox:
len(password), byref(error))
if error.value:
LOG_ERROR(f" {error.value}")
raise RuntimeError(f" {error.value}")
raise ToxError(f" {error.value}")
return result
def group_founder_set_privacy_state(self, group_number, privacy_state):
@ -2978,7 +3029,7 @@ class Tox:
byref(error))
if error.value:
LOG_ERROR(f" {error.value}")
raise RuntimeError(f" {error.value}")
raise ToxError(f" {error.value}")
return result
def group_founder_set_peer_limit(self, group_number, max_peers):
@ -3002,7 +3053,7 @@ class Tox:
byref(error))
if error.value:
LOG_ERROR(f" {error.value}")
raise RuntimeError(f" {error.value}")
raise ToxError(f" {error.value}")
return result
# -----------------------------------------------------------------------------------------------------------------
@ -3029,7 +3080,7 @@ class Tox:
result = Tox.libtoxcore.tox_group_mod_set_role(self._tox_pointer, group_number, peer_id, role, byref(error))
if error.value:
LOG_ERROR(f" {error.value}")
raise RuntimeError(f" {error.value}")
raise ToxError(f" {error.value}")
return result
def callback_group_moderation(self, callback, user_data):
@ -3037,9 +3088,12 @@ class Tox:
Set the callback for the `group_moderation` event. Pass NULL to unset.
This event is triggered when a moderator or founder executes a moderation event.
(tox_data->tox, group_number, source_peer_number, target_peer_number,
(Tox_Group_Mod_Event)mod_type, tox_data->user_data);
TOX_GROUP_MOD_EVENT = [0,1,2,3,4] TOX_GROUP_MOD_EVENT['MODERATOR']
"""
LOG_DEBUG(f"callback_group_moderation")
# LOG_DEBUG(f"callback_group_moderation")
if callback is None:
self.group_moderation_cb = None
LOG_DEBUG(f"tox_callback_group_moderation")
@ -3056,6 +3110,9 @@ class Tox:
LOG_DEBUG(f"tox_callback_group_moderation")
def group_toggle_set_ignore(self, group_number, peer_id, ignore):
return group_set_ignore(self, group_number, peer_id, ignore)
def group_set_ignore(self, group_number, peer_id, ignore):
"""
Ignore or unignore a peer.
@ -3067,12 +3124,9 @@ class Tox:
"""
error = c_int()
LOG_DEBUG(f"tox_group_toggle_set_ignore")
result = Tox.libtoxcore.tox_group_toggle_set_ignore(self._tox_pointer, group_number, peer_id, ignore, byref(error))
LOG_DEBUG(f"tox_group_set_ignore")
result = Tox.libtoxcore.tox_group_set_ignore(self._tox_pointer, group_number, peer_id, ignore, byref(error))
if error.value:
LOG_ERROR(f"tox_group_toggle_set_ignore {error.value}")
raise RuntimeError("tox_group_toggle_set_ignore {error.value}")
LOG_ERROR(f"tox_group_set_ignore {error.value}")
raise ToxError("tox_group_set_ignore {error.value}")
return result
# ToDo from JF/toxcore
# tox_group_set_ignore

View file

@ -1,11 +1,13 @@
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
from ctypes import c_int, POINTER, c_void_p, byref, ArgumentError, c_uint32, CFUNCTYPE, c_size_t, c_uint8, c_uint16
from ctypes import c_char_p, c_int32, c_bool, cast
from ctypes import (CFUNCTYPE, POINTER, ArgumentError, byref, c_bool, c_char_p,
c_int, c_int32, c_size_t, c_uint8, c_uint16, c_uint32,
c_void_p, cast)
from wrapper.libtox import LibToxAV
from wrapper.toxav_enums import *
def LOG_ERROR(a): print('EROR> '+a)
def LOG_WARN(a): print('WARN> '+a)
def LOG_INFO(a): print('INFO> '+a)
@ -262,7 +264,7 @@ class ToxAV:
24000, or 48000.
"""
toxav_err_send_frame = c_int()
LOG_DEBUG(f"toxav_audio_send_frame")
LOG_TRACE(f"toxav_audio_send_frame")
assert sampling_rate in [8000, 12000, 16000, 24000, 48000]
result = self.libtoxav.toxav_audio_send_frame(self._toxav_pointer,
c_uint32(friend_number),
@ -305,7 +307,7 @@ class ToxAV:
:param v: V (Chroma) plane data.
"""
toxav_err_send_frame = c_int()
LOG_DEBUG(f"toxav_video_send_frame")
LOG_TRACE(f"toxav_video_send_frame")
result = self.libtoxav.toxav_video_send_frame(self._toxav_pointer, c_uint32(friend_number), c_uint16(width),
c_uint16(height), c_char_p(y), c_char_p(u), c_char_p(v),
byref(toxav_err_send_frame))
@ -391,7 +393,7 @@ class ToxAV:
self.libtoxav.toxav_callback_video_receive_frame(self._toxav_pointer, POINTER(None)(), user_data)
self.video_receive_frame_cb = None
return
LOG_DEBUG(f"toxav_callback_video_receive_frame")
c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint16, c_uint16,
POINTER(c_uint8), POINTER(c_uint8), POINTER(c_uint8),

View file

@ -1,7 +1,5 @@
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
from ctypes import c_size_t, create_string_buffer, byref, c_int, ArgumentError, c_char_p, c_bool
try:
from wrapper import libtox
from wrapper.toxencryptsave_enums_and_consts import *
@ -9,6 +7,10 @@ except:
import libtox
from toxencryptsave_enums_and_consts import *
from ctypes import (ArgumentError, byref, c_bool, c_char_p, c_int, c_size_t,
create_string_buffer)
class ToxEncryptSave:
def __init__(self):