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 ## 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. ```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 is making a dependency. Just download it and copy the two directories
```wrapper``` and ```wrapper_tests``` into ```toxygen/toxygen```. ```wrapper``` and ```wrapper_tests``` into ```toxygen/toxygen```.
See ToDo.md to the current ToDo list. See ToDo.md to the current ToDo list.
Work on this project is suspended until the 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! [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 ## check toxygen_wrapper
1. I've broken out toxygen_wrapper to be standalone, 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. 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. 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, version=version,
description='Toxygen - Tox client', description='Toxygen - Tox client',
long_description='Toxygen is powerful Tox client written in Python3', 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', keywords='toxygen Tox messenger',
author='Ingvar', author='Ingvar',
maintainer='', maintainer='',

View file

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

View file

@ -299,6 +299,7 @@ class AV(common.tox_save.ToxAvSave):
self._video_width = s['video']['width'] self._video_width = s['video']['width']
self._video_height = s['video']['height'] self._video_height = s['video']['height']
# dunno
if True or s['video']['device'] == -1: if True or s['video']['device'] == -1:
self._video = screen_sharing.DesktopGrabber(s['video']['x'], self._video = screen_sharing.DesktopGrabber(s['video']['x'],
s['video']['y'], s['video']['y'],
@ -404,6 +405,7 @@ class AV(common.tox_save.ToxAvSave):
if self._calls[friend_num].out_audio: if self._calls[friend_num].out_audio:
try: 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: 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, self._toxav.audio_send_frame(friend_num,
pcm, pcm,
count, count,
@ -412,9 +414,9 @@ class AV(common.tox_save.ToxAvSave):
except Exception as e: except Exception as e:
LOG.error(f"Error send_audio audio_send_frame: {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}") 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, # invoke_in_main_thread(util_ui.message_box,
str(e), # str(e),
util_ui.tr("Error send_audio audio_send_frame")) # util_ui.tr("Error send_audio audio_send_frame"))
pass pass
def send_audio(self): def send_audio(self):
@ -432,9 +434,10 @@ class AV(common.tox_save.ToxAvSave):
else: else:
self.send_audio_data(pcm, count) self.send_audio_data(pcm, count)
except: except:
pass LOG_DEBUG(f"error send_audio {i}")
else:
LOG_TRACE(f"send_audio {i}")
i += 1 i += 1
LOG.debug(f"send_audio {i}")
sleep(0.01) sleep(0.01)
def send_video(self): 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}") LOG.warn(f"send_video video_send_frame _video.read result={result} frame={frame}")
continue continue
else: 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 height, width, channels = frame.shape
friends = [] friends = []
for friend_num in self._calls: for friend_num in self._calls:
@ -463,7 +466,7 @@ class AV(common.tox_save.ToxAvSave):
if len(friends) == 0: if len(friends) == 0:
LOG.warn(f"send_video video_send_frame no friends") LOG.warn(f"send_video video_send_frame no friends")
else: else:
LOG.debug(f"send_video video_send_frame {friends}") LOG_TRACE(f"send_video video_send_frame {friends}")
friend_num = friends[0] friend_num = friends[0]
try: try:
y, u, v = self.convert_bgr_to_yuv(frame) y, u, v = self.convert_bgr_to_yuv(frame)

View file

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

View file

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

View file

@ -6,6 +6,18 @@ global LOG
import logging import logging
LOG = logging.getLogger(__name__) 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): class ContactProvider(tox_save.ToxSave):
@ -24,6 +36,7 @@ class ContactProvider(tox_save.ToxSave):
try: try:
public_key = self._tox.friend_get_public_key(friend_number) public_key = self._tox.friend_get_public_key(friend_number)
except Exception as e: except Exception as e:
LOG_WARN(f"get_friend_by_number NO {friend_number} {e} ")
return None return None
return self.get_friend_by_public_key(public_key) return self.get_friend_by_public_key(public_key)
@ -33,6 +46,7 @@ class ContactProvider(tox_save.ToxSave):
return friend return friend
friend = self._friend_factory.create_friend_by_public_key(public_key) friend = self._friend_factory.create_friend_by_public_key(public_key)
self._add_to_cache(public_key, friend) self._add_to_cache(public_key, friend)
LOG_INFO(f"get_friend_by_public_key ADDED {friend} ")
return friend return friend
@ -40,6 +54,7 @@ class ContactProvider(tox_save.ToxSave):
try: try:
friend_numbers = self._tox.self_get_friend_list() friend_numbers = self._tox.self_get_friend_list()
except Exception as e: except Exception as e:
LOG_WARN(f"get_all_friends NO {friend_numbers} {e} ")
return None return None
friends = map(lambda n: self.get_friend_by_number(n), friend_numbers) friends = map(lambda n: self.get_friend_by_number(n), friend_numbers)
@ -50,37 +65,68 @@ class ContactProvider(tox_save.ToxSave):
# ----------------------------------------------------------------------------------------------------------------- # -----------------------------------------------------------------------------------------------------------------
def get_all_groups(self): def get_all_groups(self):
"""from callbacks"""
try: 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: except Exception as e:
return None return None
groups = map(lambda n: self.get_group_by_number(n), group_numbers) groups = list(map(lambda n: self.get_group_by_number(n), group_numbers))
# failsafe in case there are bogus None groups?
return list(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): def get_group_by_number(self, group_number):
group = None
try: try:
if True: LOG_INFO(f"CP.group_get_number {group_number} ")
# original code # 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)
else:
# guessing
chat_id = self._tox.group_get_chat_id(group_number) chat_id = self._tox.group_get_chat_id(group_number)
# LOG.info(f"group_get_chat_id {group_number} {chat_id}") if chat_id is None:
group = self.get_contact_by_tox_id(chat_id) 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:
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 return group
except Exception as e: 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 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): def get_group_by_public_key(self, public_key):
group = self._get_contact_from_cache(public_key) group = self._get_contact_from_cache(public_key)
if group is not None: if group is not None:
return group return group
group = self._group_factory.create_group_by_public_key(public_key) group = self._group_factory.create_group_by_public_key(public_key)
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) self._add_to_cache(public_key, group)
return group return group

View file

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

View file

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

View file

@ -17,9 +17,11 @@ class GroupFactory(ToxSave):
self._db = db self._db = db
self._items_factory = items_factory 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): def create_group_by_public_key(self, public_key):
group_number = self._get_group_number_by_chat_id(public_key) group_number = self._get_group_number_by_chat_id(public_key)
return self.create_group_by_number(group_number) return self.create_group_by_number(group_number)
def create_group_by_number(self, 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 import chdir, remove, rename
from os.path import basename, getsize, exists, dirname from os.path import basename, dirname, exists, getsize
from os import remove, rename, chdir
from time import time from time import time
from wrapper.tox import Tox
from common.event import Event from common.event import Event
from middleware.threads import invoke_in_main_thread 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 = { FILE_TRANSFER_STATE = {
'RUNNING': 0, 'RUNNING': 0,

View file

@ -255,7 +255,7 @@ class GroupsService(tox_save.ToxSave):
# ----------------------------------------------------------------------------------------------------------------- # -----------------------------------------------------------------------------------------------------------------
def _add_new_group_by_number(self, group_number): 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) self._contacts_manager.add_group(group_number)
def _get_group_by_number(self, 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: if invite in self._group_invites:
self._group_invites.remove(invite) 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)}") LOG.debug(f"_join_gc_via_invite friend_number={friend_number} nick={nick} datalen={len(invite_data)}")
if nick is None: if nick is None:
nick = '' nick = ''
if invite_data is None: if invite_data is None:
invite_data = b'' invite_data = b''
try: 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: except Exception as e:
LOG.error(f"_join_gc_via_invite ERROR {e}") LOG.error(f"_join_gc_via_invite ERROR {e}")
return return

View file

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

View file

@ -171,7 +171,7 @@ def setup_default_video():
video['output_devices'] = default_video video['output_devices'] = default_video
return video return video
def main_parser(): def main_parser(_=None, iMode=2):
import cv2 import cv2
if not os.path.exists('/proc/sys/net/ipv6'): if not os.path.exists('/proc/sys/net/ipv6'):
bIpV6 = 'False' bIpV6 = 'False'
@ -182,32 +182,17 @@ def main_parser():
audio = setup_default_audio() audio = setup_default_audio()
default_video = setup_default_video() 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('--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('--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('--reset', action='store_true', help='Reset default profile')
parser.add_argument('--uri', type=str, default='', parser.add_argument('--uri', type=str, default='',
help='Add specified Tox ID to friends') 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, parser.add_argument('--auto_accept_path', '--auto-accept-path', type=str,
default=os.path.join(os.environ['HOME'], 'Downloads'), default=os.path.join(os.environ['HOME'], 'Downloads'),
help="auto_accept_path") 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') help='Mode: 0=chat 1=chat+audio 2=chat+audio+video default: 0')
parser.add_argument('--font', type=str, default="Courier", parser.add_argument('--font', type=str, default="Courier",
help='Message font') help='Message font')
@ -216,15 +201,6 @@ def main_parser():
parser.add_argument('--local_discovery_enabled',type=str, parser.add_argument('--local_discovery_enabled',type=str,
default='False', choices=['True','False'], default='False', choices=['True','False'],
help='Look on the local lan') 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, parser.add_argument('--compact_mode',type=str,
default='True', choices=['True','False'], default='True', choices=['True','False'],
help='Compact mode') help='Compact mode')
@ -243,28 +219,12 @@ def main_parser():
parser.add_argument('--core_logging',type=str, parser.add_argument('--core_logging',type=str,
default='False', choices=['True','False'], default='False', choices=['True','False'],
help='Dis/Enable Toxcore notifications') 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, parser.add_argument('--save_history',type=str,
default='True', choices=['True','False'], default='True', choices=['True','False'],
help='En/Disable save history') help='En/Disable save history')
parser.add_argument('--update', type=int, default=0, parser.add_argument('--update', type=int, default=0,
choices=[0,0], choices=[0,0],
help='Update program (broken)') 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, parser.add_argument('--video_input', type=str,
default=-1, default=-1,
choices=default_video['output_devices'], choices=default_video['output_devices'],
@ -353,14 +313,7 @@ def main(lArgs):
if getattr(default_ns, key) == getattr(oArgs, key): if getattr(default_ns, key) == getattr(oArgs, key):
delattr(oArgs, key) delattr(oArgs, key)
for key in ts.lBOOLEANS: ts.clean_booleans(oArgs)
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)
aArgs = A() aArgs = A()
for key in oArgs.__dict__.keys(): for key in oArgs.__dict__.keys():

View file

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

View file

@ -353,11 +353,12 @@ class Messenger(tox_save.ToxSave):
LOG.warn("_add_message null contact") LOG.warn("_add_message null contact")
return return
if self._contacts_manager.is_contact_active(contact): # add message to list 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._create_message_item(text_message)
self._screen.messages.scrollToBottom() self._screen.messages.scrollToBottom()
self._contacts_manager.get_curr_contact().append_message(text_message) self._contacts_manager.get_curr_contact().append_message(text_message)
else: else:
LOG.debug("_add_message not is_contact_active(contact)") # LOG.debug("_add_message not is_contact_active(contact)")
contact.inc_messages() contact.inc_messages()
contact.append_message(text_message) contact.append_message(text_message)
if not contact.visibility: 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): def group_self_join(contacts_provider, contacts_manager, groups_service):
sSlot = 'group_self_join' sSlot = 'group_self_join'
def wrapped(tox, group_number, user_data): 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}" key = f"group_number {group_number}"
if bTooSoon(key, sSlot, 10): return if bTooSoon(key, sSlot, 10): return
LOG_DEBUG(f"group_self_join #{group_number}")
group = contacts_provider.get_group_by_number(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(group.set_status, TOX_USER_STATUS['NONE'])
invoke_in_main_thread(groups_service.update_group_info, group) invoke_in_main_thread(groups_service.update_group_info, group)
invoke_in_main_thread(contacts_manager.update_filtration) invoke_in_main_thread(contacts_manager.update_filtration)
return wrapped return wrapped
def group_peer_join(contacts_provider, groups_service): def group_peer_join(contacts_provider, groups_service):
sSlot = "group_peer_join" sSlot = "group_peer_join"
def wrapped(tox, group_number, peer_id, user_data): def wrapped(tox, group_number, peer_id, user_data):
key = f"group_peer_join #{group_number} peer_id={peer_id}" key = f"group_peer_join #{group_number} peer_id={peer_id}"
if bTooSoon(key, sSlot, 20): return if bTooSoon(key, sSlot, 20): return
group = contacts_provider.get_group_by_number(group_number) 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: if peer_id > group._peers_limit:
LOG_ERROR(key +f" {peer_id} > {group._peers_limit}") LOG_ERROR(key +f" {peer_id} > {group._peers_limit}")
return return
LOG_DEBUG(key) LOG_DEBUG(f"group_peer_join group={group}")
group.add_peer(peer_id) group.add_peer(peer_id)
invoke_in_main_thread(groups_service.generate_peers_list) invoke_in_main_thread(groups_service.generate_peers_list)
invoke_in_main_thread(groups_service.update_group_info, group) invoke_in_main_thread(groups_service.update_group_info, group)

View file

@ -141,8 +141,7 @@ class ToxIterateThread(BaseQThread):
# TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes # TRAC> TCP_common.c#203:read_TCP_packet recv buffer has 0 bytes, but requested 10 bytes
# and segv # and segv
if \ if time.time() - iLAST_CONN > iLAST_DELTA and \
time.time() - iLAST_CONN > iLAST_DELTA and \
ts.bAreWeConnected() and \ ts.bAreWeConnected() and \
self._tox.self_get_status() == TOX_USER_STATUS['NONE'] and \ self._tox.self_get_status() == TOX_USER_STATUS['NONE'] and \
self._tox.self_get_connection_status() == TOX_CONNECTION['NONE']: self._tox.self_get_connection_status() == TOX_CONNECTION['NONE']:

View file

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

View file

@ -1,3 +1,4 @@
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
import os import os
from PyQt5 import QtCore, QtWidgets from PyQt5 import QtCore, QtWidgets
import utils.ui as util_ui import utils.ui as util_ui
@ -19,7 +20,7 @@ def path_to_data(name):
return os.path.dirname(os.path.realpath(__file__)) + '/' + name + '/' return os.path.dirname(os.path.realpath(__file__)) + '/' + name + '/'
def log(name, data): def log(name, data=''):
""" """
:param name: plugin unique name :param name: plugin unique name
:param data: data for saving in log :param data: data for saving in log
@ -47,7 +48,7 @@ class PluginSuperClass(tox_save.ToxSave):
name = name.strip() name = name.strip()
short_name = short_name.strip() short_name = short_name.strip()
if not name or not short_name: 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._name = name
self._short_name = short_name[:MAX_SHORT_NAME_LENGTH] self._short_name = short_name[:MAX_SHORT_NAME_LENGTH]
self._translator = None # translator for plugin's GUI self._translator = None # translator for plugin's GUI
@ -74,7 +75,7 @@ class PluginSuperClass(tox_save.ToxSave):
""" """
return self.__doc__ 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 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 :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.accept_video.clicked.connect(self.accept_call_with_video)
self.decline.clicked.connect(self.decline_call) 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']: if False and self._settings['calls_sound']:
class SoundPlay(QtCore.QThread): class SoundPlay(QtCore.QThread):

View file

@ -1,9 +1,8 @@
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- # -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
import os import os
import logging
from PyQt5 import uic from PyQt5 import uic
from PyQt5 import QtWidgets, QtGui from PyQt5 import QtCore, QtGui, QtWidgets
from qtpy.QtGui import (QColor, QTextCharFormat, QFont, QSyntaxHighlighter) from qtpy.QtGui import (QColor, QTextCharFormat, QFont, QSyntaxHighlighter)
from ui.contact_items import * from ui.contact_items import *
@ -13,6 +12,7 @@ import utils.util as util
import utils.ui as util_ui import utils.ui as util_ui
from user_data.settings import Settings from user_data.settings import Settings
import logging
global LOG global LOG
LOG = logging.getLogger('app.'+'mains') LOG = logging.getLogger('app.'+'mains')
@ -95,7 +95,6 @@ else:
'outprompt': hl_format('lightRed', 'bold'), 'outprompt': hl_format('lightRed', 'bold'),
} }
class QTextEditLogger(logging.Handler): class QTextEditLogger(logging.Handler):
def __init__(self, parent, app): def __init__(self, parent, app):
super().__init__() super().__init__()
@ -177,6 +176,7 @@ class MainWindow(QtWidgets.QMainWindow):
iMAX = settings['width'] * 2/3 / settings['message_font_size'] iMAX = settings['width'] * 2/3 / settings['message_font_size']
self._me = LogDialog(self, app) self._me = LogDialog(self, app)
self._pe = None self._pe = None
self._we = None
def set_dependencies(self, widget_factory, tray, contacts_manager, messenger, profile, plugins_loader, def set_dependencies(self, widget_factory, tray, contacts_manager, messenger, profile, plugins_loader,
file_transfer_handler, history_loader, calls_manager, groups_service, toxes, app): 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.actionLog_console.setObjectName("actionLog_console")
self.actionPython_console = QtWidgets.QAction(window) self.actionPython_console = QtWidgets.QAction(window)
self.actionPython_console.setObjectName("actionLog_console") self.actionPython_console.setObjectName("actionLog_console")
self.actionWeechat_console = QtWidgets.QAction(window)
self.actionWeechat_console.setObjectName("actionLog_console")
self.updateSettings = QtWidgets.QAction(window) self.updateSettings = QtWidgets.QAction(window)
self.actionSettings = QtWidgets.QAction(window) self.actionSettings = QtWidgets.QAction(window)
self.actionSettings.setObjectName("actionSettings") self.actionSettings.setObjectName("actionSettings")
@ -290,6 +292,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.menuPlugins.addAction(self.reloadToxchat) self.menuPlugins.addAction(self.reloadToxchat)
self.menuPlugins.addAction(self.actionLog_console) self.menuPlugins.addAction(self.actionLog_console)
self.menuPlugins.addAction(self.actionPython_console) self.menuPlugins.addAction(self.actionPython_console)
self.menuPlugins.addAction(self.actionWeechat_console)
self.menuAbout.addAction(self.actionAbout_program) self.menuAbout.addAction(self.actionAbout_program)
@ -307,6 +310,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.actionAbout_program.triggered.connect(self.about_program) self.actionAbout_program.triggered.connect(self.about_program)
self.actionLog_console.triggered.connect(self.log_console) self.actionLog_console.triggered.connect(self.log_console)
self.actionPython_console.triggered.connect(self.python_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.actionNetwork.triggered.connect(self.network_settings)
self.actionAdd_friend.triggered.connect(self.add_contact_triggered) self.actionAdd_friend.triggered.connect(self.add_contact_triggered)
self.createGC.triggered.connect(self.create_gc) 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.actionAbout_program.setText(util_ui.tr("About program"))
self.actionLog_console.setText(util_ui.tr("Console Log")) self.actionLog_console.setText(util_ui.tr("Console Log"))
self.actionPython_console.setText(util_ui.tr("Python Console")) 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_tox.setText(util_ui.tr("Bootstrap"))
self.actionTest_nmap.setText(util_ui.tr("Test Nodes")) self.actionTest_nmap.setText(util_ui.tr("Test Nodes"))
self.actionTest_main.setText(util_ui.tr("Test Program")) self.actionTest_main.setText(util_ui.tr("Test Program"))
@ -655,7 +660,7 @@ class MainWindow(QtWidgets.QMainWindow):
self._me.show() self._me.show()
def python_console(self): def python_console(self):
if PythonConsole: if not PythonConsole: return
app = self._app app = self._app
if app and app._settings: if app and app._settings:
size = app._settings['message_font_size'] size = app._settings['message_font_size']
@ -681,10 +686,10 @@ class MainWindow(QtWidgets.QMainWindow):
font_width = QFontMetrics(font).width('M') font_width = QFontMetrics(font).width('M')
self._pe.setFont(font) self._pe.setFont(font)
geometry = self._pe.geometry() geometry = self._pe.geometry()
geometry.setWidth(font_width*80+20) geometry.setWidth(font_width*50+20)
geometry.setHeight(font_width*40) geometry.setHeight(font_width*24*13/8)
self._pe.setGeometry(geometry) self._pe.setGeometry(geometry)
self._pe.resize(font_width*80+20, font_width*40) self._pe.resize(font_width*50+20, font_width*24*13/8)
self._pe.show() self._pe.show()
self._pe.eval_queued() self._pe.eval_queued()
@ -692,7 +697,76 @@ class MainWindow(QtWidgets.QMainWindow):
return return
except Exception as e: except Exception as e:
LOG.debug(e) LOG.debug(e)
self._me.show()
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._we.setFont(font)
except Exception as e:
# 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): def about_program(self):
# TODO: replace with window # TODO: replace with window

View file

@ -10,6 +10,8 @@ import utils.util as util
import utils.ui as util_ui import utils.ui as util_ui
from stickers.stickers import load_stickers from stickers.stickers import load_stickers
import logging
LOG = logging.getLogger('app.'+'msw')
class MessageArea(QtWidgets.QPlainTextEdit): class MessageArea(QtWidgets.QPlainTextEdit):
"""User types messages here""" """User types messages here"""

View file

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

View file

@ -7,7 +7,7 @@ import re
from ui.widgets import * from ui.widgets import *
from messenger.messages import MESSAGE_AUTHOR from messenger.messages import MESSAGE_AUTHOR
from file_transfers.file_transfers import * from file_transfers.file_transfers import *
from PyQt5 import QtCore, QtGui, QtWidgets
class MessageBrowser(QtWidgets.QTextBrowser): class MessageBrowser(QtWidgets.QTextBrowser):
@ -39,7 +39,16 @@ class MessageBrowser(QtWidgets.QTextBrowser):
font.setPixelSize(settings['message_font_size']) font.setPixelSize(settings['message_font_size'])
font.setBold(False) font.setBold(False)
self.setFont(font) 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.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse | QtCore.Qt.LinksAccessibleByMouse)
self.anchorClicked.connect(self.on_anchor_clicked) self.anchorClicked.connect(self.on_anchor_clicked)

View file

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

View file

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

View file

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

View file

@ -14,6 +14,14 @@ except ImportError:
sLIBS_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), sLIBS_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)),
'libs') '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: class LibToxCore:
def __init__(self): def __init__(self):
@ -28,7 +36,6 @@ class LibToxCore:
# libtoxcore and libsodium may be installed in your os # libtoxcore and libsodium may be installed in your os
# give libs/ precedence # give libs/ precedence
libFile = os.path.join(sLIBS_DIR, libtoxcore) libFile = os.path.join(sLIBS_DIR, libtoxcore)
assert os.path.isfile(libFile), libFile
if os.path.isfile(libFile): if os.path.isfile(libFile):
self._libtoxcore = CDLL(libFile) self._libtoxcore = CDLL(libFile)
else: else:
@ -48,7 +55,6 @@ class LibToxAV:
self._libtoxav = CDLL('libtoxcore.dylib') self._libtoxav = CDLL('libtoxcore.dylib')
else: else:
libFile = os.path.join(sLIBS_DIR, 'libtoxav.so') libFile = os.path.join(sLIBS_DIR, 'libtoxav.so')
assert os.path.isfile(libFile), libFile
if os.path.isfile(libFile): if os.path.isfile(libFile):
self._libtoxav = CDLL(libFile) self._libtoxav = CDLL(libFile)
else: else:
@ -70,7 +76,6 @@ class LibToxEncryptSave:
self._lib_tox_encrypt_save = CDLL('libtoxcore.dylib') self._lib_tox_encrypt_save = CDLL('libtoxcore.dylib')
else: else:
libFile = os.path.join(sLIBS_DIR, 'libtoxencryptsave.so') libFile = os.path.join(sLIBS_DIR, 'libtoxencryptsave.so')
assert os.path.isfile(libFile), libFile
if os.path.isfile(libFile): if os.path.isfile(libFile):
self._lib_tox_encrypt_save = CDLL(libFile) self._lib_tox_encrypt_save = CDLL(libFile)
else: else:

View file

@ -3,19 +3,30 @@ from ctypes import *
from datetime import datetime from datetime import datetime
try: try:
from wrapper.toxcore_enums_and_consts import *
from wrapper.toxav import ToxAV
from wrapper.libtox import LibToxCore from wrapper.libtox import LibToxCore
from wrapper.toxav import ToxAV
from wrapper.toxcore_enums_and_consts import *
except: except:
from toxcore_enums_and_consts import *
from toxav import ToxAV
from libtox import LibToxCore 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_ERROR(a): print('EROR> '+a)
def LOG_WARN(a): print('WARN> '+a) def LOG_WARN(a): print('WARN> '+a)
def LOG_INFO(a): print('INFO> '+a) def LOG_INFO(a):
def LOG_DEBUG(a): print('DBUG> '+a) bVERBOSE = hasattr(__builtins__, 'app') and app.oArgs.loglevel <= 20
def LOG_TRACE(a): pass # print('TRAC> '+a) 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 global aTIMES
aTIMES=dict() aTIMES=dict()
@ -57,7 +68,6 @@ class ToxOptions(Structure):
] ]
class GroupChatSelfPeerInfo(Structure): class GroupChatSelfPeerInfo(Structure):
_fields_ = [ _fields_ = [
('nick', c_char_p), ('nick', c_char_p),
@ -109,11 +119,11 @@ class Tox:
raise MemoryError('The function was unable to allocate enough ' raise MemoryError('The function was unable to allocate enough '
'memory to store the internal structures for the Tox object.') 'memory to store the internal structures for the Tox object.')
if tox_err_new == TOX_ERR_NEW['PORT_ALLOC']: 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.' '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.') ' You may be able to gather more information from errno.')
if tox_err_new == TOX_ERR_NEW['TCP_SERVER_ALLOC']: 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']: if tox_err_new == TOX_ERR_NEW['PROXY_BAD_TYPE']:
raise ArgumentError('proxy_type was invalid.') raise ArgumentError('proxy_type was invalid.')
if tox_err_new == TOX_ERR_NEW['PROXY_BAD_HOST']: if tox_err_new == TOX_ERR_NEW['PROXY_BAD_HOST']:
@ -165,7 +175,7 @@ class Tox:
def kill(self): def kill(self):
if hasattr(self, 'AV'): del self.AV if hasattr(self, 'AV'): del self.AV
LOG_DEBUG(f"tox_kill") LOG_INFO(f"tox_kill")
try: try:
Tox.libtoxcore.tox_kill(self._tox_pointer) Tox.libtoxcore.tox_kill(self._tox_pointer)
except Exception as e: except Exception as e:
@ -213,7 +223,7 @@ class Tox:
return result return result
if tox_err_options_new == TOX_ERR_OPTIONS_NEW['MALLOC']: if tox_err_options_new == TOX_ERR_OPTIONS_NEW['MALLOC']:
raise MemoryError('The function failed to allocate enough memory for the options struct.') 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 @staticmethod
def options_free(tox_options): 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.') 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']: if tox_err_bootstrap == TOX_ERR_BOOTSTRAP['BAD_HOST']:
raise ArgumentError('The address could not be resolved to an IP ' 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']: if tox_err_bootstrap == TOX_ERR_BOOTSTRAP['BAD_PORT']:
raise ArgumentError('The port passed was invalid. The valid port range is (1, 65535).') raise ArgumentError('The port passed was invalid. The valid port range is (1, 65535).')
# me - this seems wrong - should be False # me - this seems wrong - should be False
@ -405,6 +415,9 @@ class Tox:
# Internal client information (Tox address/id) # 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): 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 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.') raise ArgumentError('The friend was already there, but the nospam value was different.')
if tox_err_friend_add == TOX_ERR_FRIEND_ADD['MALLOC']: 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 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): def friend_add_norequest(self, public_key):
"""Add a friend without sending a friend request. """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.') raise ArgumentError('The friend was already there, but the nospam value was different.')
if tox_err_friend_add == TOX_ERR_FRIEND_ADD['MALLOC']: 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 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): 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.') 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']: 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 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): def friend_exists(self, friend_number):
""" """
Checks if a friend with the given friend number exists and returns true if it does. 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): def self_get_friend_list_size(self):
""" """
@ -819,7 +833,7 @@ class Tox:
return result return result
elif tox_err_last_online == TOX_ERR_FRIEND_GET_LAST_ONLINE['FRIEND_NOT_FOUND']: 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 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) # 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. The return value is equal to the `length` argument received by the last `friend_name` callback.
""" """
tox_err_friend_query = c_int() 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, result = Tox.libtoxcore.tox_friend_get_name_size(self._tox_pointer,
c_uint32(friend_number), c_uint32(friend_number),
byref(tox_err_friend_query)) byref(tox_err_friend_query))
@ -845,7 +859,7 @@ class Tox:
' NULL, these functions return an error in that case.') ' NULL, these functions return an error in that case.')
elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['FRIEND_NOT_FOUND']: elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['FRIEND_NOT_FOUND']:
raise ArgumentError('The friend_number did not designate a valid friend.') 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): def friend_get_name(self, friend_number, name=None):
""" """
@ -874,7 +888,7 @@ class Tox:
' NULL, these functions return an error in that case.') ' NULL, these functions return an error in that case.')
elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['FRIEND_NOT_FOUND']: elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['FRIEND_NOT_FOUND']:
raise ArgumentError('The friend_number did not designate a valid friend.') 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): def callback_friend_name(self, callback):
""" """
@ -907,7 +921,7 @@ class Tox:
:return: length of the friend's status message :return: length of the friend's status message
""" """
tox_err_friend_query = c_int() 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), result = Tox.libtoxcore.tox_friend_get_status_message_size(self._tox_pointer, c_uint32(friend_number),
byref(tox_err_friend_query)) byref(tox_err_friend_query))
tox_err_friend_query = tox_err_friend_query.value tox_err_friend_query = tox_err_friend_query.value
@ -949,7 +963,7 @@ class Tox:
' NULL, these functions return an error in that case.') ' NULL, these functions return an error in that case.')
elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['FRIEND_NOT_FOUND']: elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['FRIEND_NOT_FOUND']:
raise ArgumentError('The friend_number did not designate a valid friend.') 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): def callback_friend_status_message(self, callback):
""" """
@ -1044,7 +1058,7 @@ class Tox:
' NULL, these functions return an error in that case.') ' NULL, these functions return an error in that case.')
elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['FRIEND_NOT_FOUND']: elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['FRIEND_NOT_FOUND']:
raise ArgumentError('The friend_number did not designate a valid friend.') 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): def callback_friend_connection_status(self, callback):
""" """
@ -1138,27 +1152,31 @@ class Tox:
return bool(result) return bool(result)
if tox_err_set_typing == TOX_ERR_SET_TYPING['FRIEND_NOT_FOUND']: if tox_err_set_typing == TOX_ERR_SET_TYPING['FRIEND_NOT_FOUND']:
raise ArgumentError('The friend number did not designate a valid friend.') 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): 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. 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 The message length may not exceed
as separate messages. Other clients can then reassemble the fragments. Messages may not be empty. 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 The return value of this function is the message ID. If a read
`friend_read_receipt` event will be passed this message ID. 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 IDs are unique per friend. The first message ID is 0.
message is sent. If UINT32_MAX messages were sent, the next 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 friend_number: The friend number of the friend to send the message to.
:param message_type: Message type (TOX_MESSAGE_TYPE). :param message_type: Message type (TOX_MESSAGE_TYPE).
:param message: A non-None message text. :param message: A non-None message text.
:return: message ID :return: message ID
""" """
tox_err_friend_send_message = c_int() tox_err_friend_send_message = c_int()
LOG_DEBUG(f"tox_friend_send_message") LOG_DEBUG(f"tox_friend_send_message")
@ -1180,7 +1198,7 @@ class Tox:
raise ArgumentError('Message length exceeded TOX_MAX_MESSAGE_LENGTH.') raise ArgumentError('Message length exceeded TOX_MAX_MESSAGE_LENGTH.')
elif tox_err_friend_send_message == TOX_ERR_FRIEND_SEND_MESSAGE['EMPTY']: elif tox_err_friend_send_message == TOX_ERR_FRIEND_SEND_MESSAGE['EMPTY']:
raise ArgumentError('Attempted to send a zero-length message.') 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): def callback_friend_read_receipt(self, callback):
""" """
@ -1310,15 +1328,15 @@ class Tox:
elif tox_err_file_control == TOX_ERR_FILE_CONTROL['NOT_FOUND']: 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.') 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']: 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']: 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.') 'the party that paused the transfer can resume it.')
elif tox_err_file_control == TOX_ERR_FILE_CONTROL['ALREADY_PAUSED']: 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']: elif tox_err_file_control == TOX_ERR_FILE_CONTROL['SENDQ']:
raise RuntimeError('Packet queue is full.') raise ToxError('Packet queue is full.')
raise RuntimeError('The function did not return OK for file control.') raise ToxError('The function did not return OK for file control.')
def callback_file_recv_control(self, callback): def callback_file_recv_control(self, callback):
""" """
@ -1381,8 +1399,8 @@ class Tox:
elif tox_err_file_seek == TOX_ERR_FILE_SEEK['INVALID_POSITION']: elif tox_err_file_seek == TOX_ERR_FILE_SEEK['INVALID_POSITION']:
raise ArgumentError('Seek position was invalid') raise ArgumentError('Seek position was invalid')
elif tox_err_file_seek == TOX_ERR_FILE_SEEK['SENDQ']: elif tox_err_file_seek == TOX_ERR_FILE_SEEK['SENDQ']:
raise RuntimeError('Packet queue is full.') raise ToxError('Packet queue is full.')
raise RuntimeError('The function did not return OK') raise ToxError('The function did not return OK')
def file_get_file_id(self, friend_number, file_number, file_id=None): 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']: if err_file == TOX_ERR_FILE_SEND['NAME_TOO_LONG']:
raise ArgumentError('Filename length exceeded TOX_MAX_FILENAME_LENGTH bytes.') raise ArgumentError('Filename length exceeded TOX_MAX_FILENAME_LENGTH bytes.')
if err_file == TOX_ERR_FILE_SEND['TOO_MANY']: 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).') '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): 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. ' '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.') 'Trying to send less or more than requested will return this error.')
elif tox_err_file_send_chunk == TOX_ERR_FILE_SEND_CHUNK['SENDQ']: 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']: elif tox_err_file_send_chunk == TOX_ERR_FILE_SEND_CHUNK['WRONG_POSITION']:
raise ArgumentError('Position parameter was wrong.') 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): 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']: elif tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['TOO_LONG']:
raise ArgumentError('Packet data length exceeded TOX_MAX_CUSTOM_PACKET_SIZE.') raise ArgumentError('Packet data length exceeded TOX_MAX_CUSTOM_PACKET_SIZE.')
elif tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['SENDQ']: elif tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['SENDQ']:
raise RuntimeError('Packet queue is full.') raise ToxError('Packet queue is full.')
raise RuntimeError('The function did not return OK') raise ToxError('The function did not return OK')
def friend_send_lossless_packet(self, friend_number, data): 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']: elif tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['TOO_LONG']:
raise ArgumentError('Packet data length exceeded TOX_MAX_CUSTOM_PACKET_SIZE.') raise ArgumentError('Packet data length exceeded TOX_MAX_CUSTOM_PACKET_SIZE.')
elif tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['SENDQ']: 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): def callback_friend_lossy_packet(self, callback):
""" """
@ -1808,8 +1826,8 @@ class Tox:
if tox_err_get_port == TOX_ERR_GET_PORT['OK']: if tox_err_get_port == TOX_ERR_GET_PORT['OK']:
return result return result
if tox_err_get_port == TOX_ERR_GET_PORT['NOT_BOUND']: if tox_err_get_port == TOX_ERR_GET_PORT['NOT_BOUND']:
raise RuntimeError('The instance was not bound to any port.') raise ToxError('The instance was not bound to any port.')
raise RuntimeError('The function did not return OK') raise ToxError('The function did not return OK')
def self_get_tcp_port(self): def self_get_tcp_port(self):
""" """
@ -1823,8 +1841,8 @@ class Tox:
if tox_err_get_port == TOX_ERR_GET_PORT['OK']: if tox_err_get_port == TOX_ERR_GET_PORT['OK']:
return result return result
if tox_err_get_port == TOX_ERR_GET_PORT['NOT_BOUND']: if tox_err_get_port == TOX_ERR_GET_PORT['NOT_BOUND']:
raise RuntimeError('The instance was not bound to any port.') raise ToxError('The instance was not bound to any port.')
raise RuntimeError('The function did not return OK') raise ToxError('The function did not return OK')
# ----------------------------------------------------------------------------------------------------------------- # -----------------------------------------------------------------------------------------------------------------
# Group chat instance management # Group chat instance management
@ -1865,18 +1883,26 @@ class Tox:
else: else:
nick_length = len(nick) nick_length = len(nick)
cnick = c_char_p(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, group_name,
len(group_name), len(group_name),
cnick, nick_length, cnick,
nick_length,
byref(error)) byref(error))
if error.value: if error.value:
LOG_ERROR(f"group_new {error.value}") # -1 TOX_ERR_GROUP_NEW_TOO_LONG
raise RuntimeError("group_new {error.value}") # -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 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. """Joins a group chat with specified Chat ID.
This function creates a new group chat object, adds it to the This function creates a new group chat object, adds it to the
@ -1917,8 +1943,8 @@ class Tox:
byref(error)) byref(error))
if error.value: if error.value:
LOG_ERROR(f"group_join {error.value}") LOG_ERROR(f"group_join {error.value} {TOX_ERR_GROUP_JOIN[error.value]}")
raise RuntimeError("group_join {error.value}") raise ToxError(f"group_join {error.value} {TOX_ERR_GROUP_JOIN[error.value]}")
return result return result
def group_reconnect(self, group_number): 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)) result = Tox.libtoxcore.tox_group_reconnect(self._tox_pointer, group_number, byref(error))
if error.value: if error.value:
LOG_ERROR(f"group_reconnect {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 return result
def group_is_connected(self, group_number): 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)) result = Tox.libtoxcore.tox_group_is_connected(self._tox_pointer, group_number, byref(error))
if error.value: if error.value:
LOG_ERROR(f"group_is_connected {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 return result
def group_disconnect(self, group_number): 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)) result = Tox.libtoxcore.tox_group_disconnect(self._tox_pointer, group_number, byref(error))
if error.value: if error.value:
LOG_ERROR(f"group_disconnect {error.value}") LOG_ERROR(f"group_disconnect {error.value}")
raise RuntimeError("group_disconnect {error.value}") raise ToxError("group_disconnect {error.value}")
return result return result
def group_leave(self, group_number, message=''): def group_leave(self, group_number, message=None):
"""Leaves a group. """Leaves a group.
This function sends a parting packet containing a custom This function sends a parting packet containing a custom
@ -1979,10 +2005,10 @@ class Tox:
f = Tox.libtoxcore.tox_group_leave f = Tox.libtoxcore.tox_group_leave
f.restype = c_bool f.restype = c_bool
result = f(self._tox_pointer, group_number, message, 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: if error.value:
LOG_ERROR(f"group_leave {error.value}") LOG_ERROR(f"group_leave {error.value}")
raise RuntimeError("group_leave {error.value}") raise ToxError("group_leave {error.value}")
return result 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)) result = Tox.libtoxcore.tox_group_self_set_name(self._tox_pointer, group_number, name, len(name), byref(error))
if error.value: if error.value:
LOG_ERROR(f"group_self_set_name {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 return result
def group_self_get_name_size(self, group_number): def group_self_get_name_size(self, group_number):
@ -2021,11 +2047,11 @@ class Tox:
""" """
error = c_int() 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)) result = Tox.libtoxcore.tox_group_self_get_name_size(self._tox_pointer, group_number, byref(error))
if error.value: if error.value:
LOG_ERROR(f"group_self_get_name_size {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 return result
def group_self_get_name(self, group_number): 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)) result = Tox.libtoxcore.tox_group_self_get_name(self._tox_pointer, group_number, name, byref(error))
if error.value: if error.value:
LOG_ERROR(f"group_self_get_name {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') return str(name[:size], 'utf-8', errors='ignore')
def group_self_set_status(self, group_number, status): 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)) result = Tox.libtoxcore.tox_group_self_set_status(self._tox_pointer, group_number, status, byref(error))
if error.value: if error.value:
LOG_ERROR(f"group_self_set_status {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 return result
def group_self_get_status(self, group_number): 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)) result = Tox.libtoxcore.tox_group_self_get_status(self._tox_pointer, group_number, byref(error))
if error.value: if error.value:
LOG_ERROR(f"group_self_get_status {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 return result
def group_self_get_role(self, group_number): 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)) result = Tox.libtoxcore.tox_group_self_get_role(self._tox_pointer, group_number, byref(error))
if error.value: if error.value:
LOG_ERROR(f" {error.value}") LOG_ERROR(f" {error.value}")
raise RuntimeError(f" {error.value}") raise ToxError(f" {error.value}")
return result return result
def group_self_get_peer_id(self, group_number): 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)) result = Tox.libtoxcore.tox_group_self_get_peer_id(self._tox_pointer, group_number, byref(error))
if error.value: if error.value:
LOG_ERROR(f" {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 return result
def group_self_get_public_key(self, group_number): 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, result = Tox.libtoxcore.tox_group_self_get_public_key(self._tox_pointer, group_number,
key, byref(error)) key, byref(error))
if error.value: if error.value:
LOG_ERROR(f" {error.value}") LOG_ERROR(f" {TOX_ERR_FRIEND_GET_PUBLIC_KEY[error.value]}")
raise RuntimeError(f" {error.value}") raise ToxError(f"{TOX_ERR_FRIEND_GET_PUBLIC_KEY[error.value]}")
return bin_to_string(key, TOX_GROUP_PEER_PUBLIC_KEY_SIZE) 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)) result = Tox.libtoxcore.tox_group_peer_get_name_size(self._tox_pointer, group_number, peer_id, byref(error))
if error.value: if error.value:
LOG_ERROR(f" {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") LOG_TRACE(f"tox_group_peer_get_name_size")
return result 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)) result = Tox.libtoxcore.tox_group_peer_get_name(self._tox_pointer, group_number, peer_id, name, byref(error))
if error.value: if error.value:
LOG_ERROR(f" {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') sRet = str(name[:], 'utf-8', errors='ignore')
return sRet 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)) result = Tox.libtoxcore.tox_group_peer_get_status(self._tox_pointer, group_number, peer_id, byref(error))
if error.value: if error.value:
LOG_ERROR(f" {error.value}") LOG_ERROR(f" {error.value}")
raise RuntimeError(f" {error.value}") raise ToxError(f" {error.value}")
return result return result
def group_peer_get_role(self, group_number, peer_id): 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)) result = Tox.libtoxcore.tox_group_peer_get_role(self._tox_pointer, group_number, peer_id, byref(error))
if error.value: if error.value:
LOG_ERROR(f" {error.value}") LOG_ERROR(f" {error.value}")
raise RuntimeError(f" {error.value}") raise ToxError(f" {error.value}")
return result return result
def group_peer_get_public_key(self, group_number, peer_id): def group_peer_get_public_key(self, group_number, peer_id):
@ -2234,7 +2260,7 @@ class Tox:
key, byref(error)) key, byref(error))
if error.value: if error.value:
LOG_ERROR(f" {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) return bin_to_string(key, TOX_GROUP_PEER_PUBLIC_KEY_SIZE)
def callback_group_peer_name(self, callback, user_data): def callback_group_peer_name(self, callback, user_data):
@ -2300,7 +2326,7 @@ class Tox:
else: else:
if error.value: if error.value:
LOG_ERROR(f"group_set_topic {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 return result
def group_get_topic_size(self, group_number): def group_get_topic_size(self, group_number):
@ -2313,8 +2339,8 @@ class Tox:
""" """
error = c_int() error = c_int()
LOG_TRACE(f"tox_group_get_topic_size")
try: try:
LOG_DEBUG(f"tox_group_get_topic_size")
result = Tox.libtoxcore.tox_group_get_topic_size(self._tox_pointer, group_number, byref(error)) result = Tox.libtoxcore.tox_group_get_topic_size(self._tox_pointer, group_number, byref(error))
except Exception as e: except Exception as e:
LOG_WARN(f" Exception {e}") LOG_WARN(f" Exception {e}")
@ -2322,8 +2348,7 @@ class Tox:
else: else:
if error.value: if error.value:
LOG_ERROR(f" {error.value}") LOG_ERROR(f" {error.value}")
raise RuntimeError(f" {error.value}") raise ToxError(f" {error.value}")
LOG_DEBUG(f"tox_group_get_topic_size")
return result return result
def group_get_topic(self, group_number): 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)) result = Tox.libtoxcore.tox_group_get_topic(self._tox_pointer, group_number, topic, byref(error))
if error.value: if error.value:
LOG_ERROR(f" {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') return str(topic[:size], 'utf-8', errors='ignore')
def group_get_name_size(self, group_number): def group_get_name_size(self, group_number):
@ -2352,11 +2377,10 @@ class Tox:
return value is unspecified. return value is unspecified.
""" """
error = c_int() 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)) result = Tox.libtoxcore.tox_group_get_name_size(self._tox_pointer, group_number, byref(error))
if error.value: if error.value:
LOG_ERROR(f" {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") LOG_TRACE(f"tox_group_get_name_size")
return int(result) return int(result)
@ -2375,7 +2399,7 @@ class Tox:
name, byref(error)) name, byref(error))
if error.value: if error.value:
LOG_ERROR(f" {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') return str(name[:size], 'utf-8', errors='ignore')
def group_get_chat_id(self, group_number): def group_get_chat_id(self, group_number):
@ -2385,14 +2409,23 @@ class Tox:
:return chat id. :return chat id.
""" """
LOG_INFO(f"tox_group_get_id group_number={group_number}")
error = c_int() error = c_int()
buff = create_string_buffer(TOX_GROUP_CHAT_ID_SIZE) buff = create_string_buffer(TOX_GROUP_CHAT_ID_SIZE)
result = Tox.libtoxcore.tox_group_get_chat_id(self._tox_pointer, result = Tox.libtoxcore.tox_group_get_chat_id(self._tox_pointer,
group_number, group_number,
buff, byref(error)) buff, byref(error))
if error.value: if error.value:
LOG_ERROR(f"tox_group_get_chat_id {error.value}") if error.value == 1:
raise RuntimeError(f" {error.value}") 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") LOG_TRACE(f"tox_group_get_chat_id")
return bin_to_string(buff, TOX_GROUP_CHAT_ID_SIZE) return bin_to_string(buff, TOX_GROUP_CHAT_ID_SIZE)
@ -2409,12 +2442,13 @@ class Tox:
return result return result
def groups_get_list(self): def groups_get_list(self):
groups_list_size = self.group_get_number_groups() raise NotImplementedError('tox_groups_get_list')
groups_list = create_string_buffer(sizeof(c_uint32) * groups_list_size) # groups_list_size = self.group_get_number_groups()
groups_list = POINTER(c_uint32)(groups_list) # groups_list = create_string_buffer(sizeof(c_uint32) * groups_list_size)
LOG_DEBUG(f"tox_groups_get_list") # groups_list = POINTER(c_uint32)(groups_list)
Tox.libtoxcore.tox_groups_get_list(self._tox_pointer, groups_list) # LOG_DEBUG(f"tox_groups_get_list")
return groups_list[0:groups_list_size] # 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): 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)) result = Tox.libtoxcore.tox_group_get_privacy_state(self._tox_pointer, group_number, byref(error))
if error.value: if error.value:
LOG_ERROR(f" {error.value}") LOG_ERROR(f" {error.value}")
raise RuntimeError(f" {error.value}") raise ToxError(f" {error.value}")
return result return result
def group_get_peer_limit(self, group_number): 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)) result = Tox.libtoxcore.tox_group_get_peer_limit(self._tox_pointer, group_number, byref(error))
if error.value: if error.value:
LOG_ERROR(f" {error.value}") LOG_ERROR(f" {error.value}")
raise RuntimeError(f" {error.value}") raise ToxError(f" {error.value}")
return result return result
def group_get_password_size(self, group_number): def group_get_password_size(self, group_number):
@ -2461,11 +2495,11 @@ class Tox:
""" """
error = c_int() 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)) result = Tox.libtoxcore.tox_group_get_password_size(self._tox_pointer, group_number, byref(error))
if error.value: if error.value:
LOG_ERROR(f" {error.value}") LOG_ERROR(f" {error.value}")
raise RuntimeError(f" {error.value}") raise ToxError(f" {error.value}")
return result return result
def group_get_password(self, group_number): def group_get_password(self, group_number):
@ -2490,7 +2524,7 @@ class Tox:
password, byref(error)) password, byref(error))
if error.value: if error.value:
LOG_ERROR(f" {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') return str(password[:size], 'utf-8', errors='ignore')
def callback_group_topic(self, callback, user_data): def callback_group_topic(self, callback, user_data):
@ -2603,7 +2637,7 @@ class Tox:
len(data), byref(error)) len(data), byref(error))
if error.value: if error.value:
LOG_ERROR(f" {error.value}") LOG_ERROR(f" {error.value}")
raise RuntimeError(f" {error.value}") raise ToxError(f" {error.value}")
return result return result
def group_send_private_message(self, group_number, peer_id, message_type, message): def group_send_private_message(self, group_number, peer_id, message_type, message):
@ -2630,11 +2664,11 @@ class Tox:
message_type, message, message_type, message,
len(message), byref(error)) len(message), byref(error))
if error.value: if error.value:
LOG_ERROR(f" {error.value}") LOG_ERROR(f"group_send_private_message {TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE[error.value]}")
raise RuntimeError(f" {error.value}") raise ToxError(f"group_send_private_message {TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE[error.value]}")
return result 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. Send a text chat message to the group.
@ -2646,7 +2680,7 @@ class Tox:
then reassemble the fragments. Messages may not be empty. then reassemble the fragments. Messages may not be empty.
:param group_number: The group number of the group the message is intended for. :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. :param message: A non-NULL pointer to the first element of a byte array containing the message text.
:return True on success. :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) # 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, result = Tox.libtoxcore.tox_group_send_message(self._tox_pointer,
group_number, group_number,
type, type_,
message, message,
len(message), len(message),
# dunno # dunno
@ -2668,7 +2702,7 @@ class Tox:
byref(error)) byref(error))
if error.value: if error.value:
LOG_ERROR(f" {error.value}") LOG_ERROR(f" {error.value}")
raise RuntimeError(f" {error.value}") raise ToxError(f" {error.value}")
return result return result
# ----------------------------------------------------------------------------------------------------------------- # -----------------------------------------------------------------------------------------------------------------
@ -2755,10 +2789,10 @@ class Tox:
if error.value: if error.value:
s = sGetError(error.value, TOX_ERR_GROUP_INVITE_FRIEND) s = sGetError(error.value, TOX_ERR_GROUP_INVITE_FRIEND)
LOG_ERROR(f"group_invite_friend {error.value} {s}") 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 return result
# API change # API change - this no longer exists
# @staticmethod # @staticmethod
# def group_self_peer_info_new(): # def group_self_peer_info_new():
# error = c_int() # error = c_int()
@ -2767,7 +2801,8 @@ class Tox:
# result = f(byref(error)) # result = f(byref(error))
# return result # 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 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. is only valid while the inviter is present in the group.
@ -2780,21 +2815,38 @@ class Tox:
error = c_int() error = c_int()
f = Tox.libtoxcore.tox_group_invite_accept f = Tox.libtoxcore.tox_group_invite_accept
f.restype = c_uint32 f.restype = c_uint32
try:
nick = bytes(nick, 'utf-8') nick = bytes(nick, 'utf-8')
invite_data = bytes(invite_data, '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 if False: # API change
peer_info = self.group_self_peer_info_new() peer_info = self.group_self_peer_info_new()
peer_info.contents.nick = c_char_p(nick) peer_info.contents.nick = c_char_p(nick)
peer_info.contents.nick_length = len(nick) peer_info.contents.nick_length = len(nick)
peer_info.contents.user_status = status peer_info.contents.user_status = status
result = f(self._tox_pointer, c_uint32(friend_number), invite_data, len(invite_data), LOG_INFO(f"group_invite_accept friend_number={friend_number} nick={nick} {invite_data}")
nick, len(nick), try:
password, len(password) if password is not None else 0, 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)) 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: if error.value:
LOG_ERROR(f" {error.value}") # The invite data is not in the expected format.
raise RuntimeError(f" {error.value}") 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 return result
def callback_group_invite(self, callback, user_data): 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)()) Tox.libtoxcore.tox_callback_group_invite(self._tox_pointer, POINTER(None)())
self.group_invite_cb = None self.group_invite_cb = None
return return
LOG_DEBUG(f"tox_callback_group_invite")
c_callback = CFUNCTYPE(None, c_void_p, c_uint32, POINTER(c_uint8), c_size_t, c_callback = CFUNCTYPE(None, c_void_p, c_uint32, POINTER(c_uint8), c_size_t,
POINTER(c_uint8), c_size_t, c_void_p) POINTER(c_uint8), c_size_t, c_void_p)
self.group_invite_cb = c_callback(callback) self.group_invite_cb = c_callback(callback)
@ -2953,7 +3004,7 @@ class Tox:
len(password), byref(error)) len(password), byref(error))
if error.value: if error.value:
LOG_ERROR(f" {error.value}") LOG_ERROR(f" {error.value}")
raise RuntimeError(f" {error.value}") raise ToxError(f" {error.value}")
return result return result
def group_founder_set_privacy_state(self, group_number, privacy_state): def group_founder_set_privacy_state(self, group_number, privacy_state):
@ -2978,7 +3029,7 @@ class Tox:
byref(error)) byref(error))
if error.value: if error.value:
LOG_ERROR(f" {error.value}") LOG_ERROR(f" {error.value}")
raise RuntimeError(f" {error.value}") raise ToxError(f" {error.value}")
return result return result
def group_founder_set_peer_limit(self, group_number, max_peers): def group_founder_set_peer_limit(self, group_number, max_peers):
@ -3002,7 +3053,7 @@ class Tox:
byref(error)) byref(error))
if error.value: if error.value:
LOG_ERROR(f" {error.value}") LOG_ERROR(f" {error.value}")
raise RuntimeError(f" {error.value}") raise ToxError(f" {error.value}")
return result 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)) result = Tox.libtoxcore.tox_group_mod_set_role(self._tox_pointer, group_number, peer_id, role, byref(error))
if error.value: if error.value:
LOG_ERROR(f" {error.value}") LOG_ERROR(f" {error.value}")
raise RuntimeError(f" {error.value}") raise ToxError(f" {error.value}")
return result return result
def callback_group_moderation(self, callback, user_data): 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. 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. 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: if callback is None:
self.group_moderation_cb = None self.group_moderation_cb = None
LOG_DEBUG(f"tox_callback_group_moderation") LOG_DEBUG(f"tox_callback_group_moderation")
@ -3056,6 +3110,9 @@ class Tox:
LOG_DEBUG(f"tox_callback_group_moderation") LOG_DEBUG(f"tox_callback_group_moderation")
def group_toggle_set_ignore(self, group_number, peer_id, ignore): 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. Ignore or unignore a peer.
@ -3067,12 +3124,9 @@ class Tox:
""" """
error = c_int() error = c_int()
LOG_DEBUG(f"tox_group_toggle_set_ignore") LOG_DEBUG(f"tox_group_set_ignore")
result = Tox.libtoxcore.tox_group_toggle_set_ignore(self._tox_pointer, group_number, peer_id, ignore, byref(error)) result = Tox.libtoxcore.tox_group_set_ignore(self._tox_pointer, group_number, peer_id, ignore, byref(error))
if error.value: if error.value:
LOG_ERROR(f"tox_group_toggle_set_ignore {error.value}") LOG_ERROR(f"tox_group_set_ignore {error.value}")
raise RuntimeError("tox_group_toggle_set_ignore {error.value}") raise ToxError("tox_group_set_ignore {error.value}")
return result 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 -*- # -*- 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 (CFUNCTYPE, POINTER, ArgumentError, byref, c_bool, c_char_p,
from ctypes import c_char_p, c_int32, c_bool, cast c_int, c_int32, c_size_t, c_uint8, c_uint16, c_uint32,
c_void_p, cast)
from wrapper.libtox import LibToxAV from wrapper.libtox import LibToxAV
from wrapper.toxav_enums import * from wrapper.toxav_enums import *
def LOG_ERROR(a): print('EROR> '+a) def LOG_ERROR(a): print('EROR> '+a)
def LOG_WARN(a): print('WARN> '+a) def LOG_WARN(a): print('WARN> '+a)
def LOG_INFO(a): print('INFO> '+a) def LOG_INFO(a): print('INFO> '+a)
@ -262,7 +264,7 @@ class ToxAV:
24000, or 48000. 24000, or 48000.
""" """
toxav_err_send_frame = c_int() 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] assert sampling_rate in [8000, 12000, 16000, 24000, 48000]
result = self.libtoxav.toxav_audio_send_frame(self._toxav_pointer, result = self.libtoxav.toxav_audio_send_frame(self._toxav_pointer,
c_uint32(friend_number), c_uint32(friend_number),
@ -305,7 +307,7 @@ class ToxAV:
:param v: V (Chroma) plane data. :param v: V (Chroma) plane data.
""" """
toxav_err_send_frame = c_int() 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), 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), c_uint16(height), c_char_p(y), c_char_p(u), c_char_p(v),
byref(toxav_err_send_frame)) byref(toxav_err_send_frame))

View file

@ -1,7 +1,5 @@
# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- # -*- 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: try:
from wrapper import libtox from wrapper import libtox
from wrapper.toxencryptsave_enums_and_consts import * from wrapper.toxencryptsave_enums_and_consts import *
@ -9,6 +7,10 @@ except:
import libtox import libtox
from toxencryptsave_enums_and_consts import * 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: class ToxEncryptSave:
def __init__(self): def __init__(self):